From 0df053f2ae2877d3a7cc4c07840e6e33659c646f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 6 Jul 2025 15:11:20 -0700 Subject: [PATCH] add token scope --- models/auth/access_token_scope.go | 13 +++++++++++++ routers/api/v1/api.go | 23 ++++++++++++++++------- routers/api/v1/projects/project.go | 6 +++--- tests/integration/api_project_test.go | 18 +++++++++--------- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 3eae19b2a53..84bf2db43b1 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -24,6 +24,7 @@ const ( AccessTokenScopeCategoryIssue AccessTokenScopeCategoryRepository AccessTokenScopeCategoryUser + AccessTokenScopeCategoryProject ) // AllAccessTokenScopeCategories contains all access token scope categories @@ -37,6 +38,7 @@ var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{ AccessTokenScopeCategoryIssue, AccessTokenScopeCategoryRepository, AccessTokenScopeCategoryUser, + AccessTokenScopeCategoryProject, } // AccessTokenScopeLevel represents the access levels without a given scope category @@ -82,6 +84,9 @@ const ( AccessTokenScopeReadUser AccessTokenScope = "read:user" AccessTokenScopeWriteUser AccessTokenScope = "write:user" + + AccessTokenScopeReadProject AccessTokenScope = "read:project" + AccessTokenScopeWriteProject AccessTokenScope = "write:project" ) // accessTokenScopeBitmap represents a bitmap of access token scopes. @@ -124,6 +129,9 @@ const ( accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1< 64 scopes, // refactoring the whole implementation in this file (and only this file) is needed. @@ -142,6 +150,7 @@ var allAccessTokenScopes = []AccessTokenScope{ AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue, AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository, AccessTokenScopeWriteUser, AccessTokenScopeReadUser, + AccessTokenScopeWriteProject, AccessTokenScopeReadProject, } // allAccessTokenScopeBits contains all access token scopes. @@ -166,6 +175,8 @@ var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{ AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits, AccessTokenScopeReadUser: accessTokenScopeReadUserBits, AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits, + AccessTokenScopeReadProject: accessTokenScopeReadProjectBits, + AccessTokenScopeWriteProject: accessTokenScopeWriteProjectBits, } // readAccessTokenScopes maps a scope category to the read permission scope @@ -180,6 +191,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue, AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository, AccessTokenScopeCategoryUser: AccessTokenScopeReadUser, + AccessTokenScopeCategoryProject: AccessTokenScopeReadProject, }, Write: { AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub, @@ -191,6 +203,7 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue, AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository, AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser, + AccessTokenScopeCategoryProject: AccessTokenScopeWriteProject, }, } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8297d8755b7..c935789de8d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1042,6 +1042,13 @@ func Routes() *web.Router { m.Get("/subscriptions", user.GetWatchedRepos) }, context.UserAssignmentAPI(), checkTokenPublicOnly()) + + m.Group("/{username}", func() { + m.Group("/projects", func() { + m.Get("", projects.ListUserProjects) + m.Post("", bind(api.NewProjectOption{}), projects.CreateUserProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryProject)) + }, context.UserAssignmentAPI()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Users (requires user scope) @@ -1162,11 +1169,6 @@ func Routes() *web.Router { m.Delete("", user.UnblockUser) }, context.UserAssignmentAPI(), checkTokenPublicOnly()) }) - - m.Group("/projects", func() { - m.Get("", projects.ListUserProjects) - m.Post("", bind(api.NewProjectOption{}), projects.CreateUserProject) - }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1476,7 +1478,7 @@ func Routes() *web.Router { m.Group("/projects", func() { m.Post("", bind(api.NewProjectOption{}), projects.CreateRepoProject) - }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryProject)) }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) @@ -1701,7 +1703,7 @@ func Routes() *web.Router { m.Group("/projects", func() { m.Post("", bind(api.NewProjectOption{}), projects.CreateOrgProject) m.Get("", projects.ListOrgProjects) - }) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryProject)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). @@ -1724,6 +1726,13 @@ func Routes() *web.Router { m.Get("/activities/feeds", org.ListTeamActivityFeeds) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) + // Projects + m.Group("/projects", func() { + m.Get("{project_id}", projects.GetProject) + m.Patch("{project_id}", bind(api.UpdateProjectOption{}), projects.UpdateProject) + m.Delete("{project_id}", projects.DeleteProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryProject), reqToken()) + m.Group("/admin", func() { m.Group("/cron", func() { m.Get("", admin.ListCronTasks) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 3bf2e0fc972..7580d87f58a 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -158,7 +158,7 @@ func GetProject(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("project_id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.APIError(http.StatusNotFound, err) @@ -202,7 +202,7 @@ func UpdateProject(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.UpdateProjectOption) - project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("project_id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.APIError(http.StatusNotFound, err) @@ -249,7 +249,7 @@ func DeleteProject(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64("id")); err != nil { + if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64("project_id")); err != nil { ctx.APIErrorInternal(err) return } diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 24115278af7..66c3baefb20 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -23,9 +23,9 @@ func TestAPICreateUserProject(t *testing.T) { const title, description = "project_name", "project_description" templateType := project_model.TemplateTypeBasicKanban.ToString() - token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteProject, auth_model.AccessTokenScopeWriteUser) - req := NewRequestWithJSON(t, "POST", "/api/v1/user/projects", &api.NewProjectOption{ + req := NewRequestWithJSON(t, "POST", "/api/v1/users/user2/projects", &api.NewProjectOption{ Name: title, Body: description, TemplateType: templateType, @@ -91,7 +91,7 @@ func TestAPIListUserProjects(t *testing.T) { defer tests.PrepareTestEnv(t)() token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) - link, _ := url.Parse("/api/v1/user/projects") + link, _ := url.Parse("/api/v1/users/user2/projects") req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProjects []*api.Project @@ -134,8 +134,8 @@ func TestAPIListRepoProjects(t *testing.T) { func TestAPIGetProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadProject) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 4)) req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProject *api.Project @@ -149,8 +149,8 @@ func TestAPIGetProject(t *testing.T) { func TestAPIUpdateProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteProject) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 4)) req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectOption{Name: "First project updated"}).AddTokenAuth(token) @@ -163,8 +163,8 @@ func TestAPIUpdateProject(t *testing.T) { func TestAPIDeleteProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteProject) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 4)) req := NewRequest(t, "DELETE", link.String()).AddTokenAuth(token)