diff --git a/models/user/user.go b/models/user/user.go index b1731021fd..382c6955f7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -421,6 +421,10 @@ func (u *User) IsIndividual() bool { return u.Type == UserTypeIndividual } +func (u *User) IsUser() bool { + return u.Type == UserTypeIndividual || u.Type == UserTypeBot +} + // IsBot returns whether or not the user is of type bot func (u *User) IsBot() bool { return u.Type == UserTypeBot diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index c72f812704..1337ce4569 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -65,6 +65,20 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") return } + + // check if scope only applies to public resources + publicOnly, err := scope.PublicOnly() + if err != nil { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + return + } + + if publicOnly { + if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + return + } + } } } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c65e738715..6e4a97b885 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -274,6 +274,62 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) } } +func checkTokenPublicOnly() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if !ctx.PublicOnly { + return + } + + requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory) + if !ok || len(requiredScopeCategories) == 0 { + return + } + + // public Only permission check + switch { + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization): + if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + return + } + if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser): + if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub): + if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification): + if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications") + return + } + case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage): + if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + return + } + } + } +} + // if a token is being used for auth, we check that it contains the required scope // if a token is not being used, reqToken will enforce other sign in methods func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { @@ -289,9 +345,6 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC return } - ctx.Data["ApiTokenScopePublicRepoOnly"] = false - ctx.Data["ApiTokenScopePublicOrgOnly"] = false - // use the http method to determine the access level requiredScopeLevel := auth_model.Read if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { @@ -300,6 +353,18 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC // get the required scope for the given access level and category requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) + allow, err := scope.HasScope(requiredScopes...) + if err != nil { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) + return + } + + if !allow { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) + return + } + + ctx.Data["requiredScopeCategories"] = requiredScopeCategories // check if scope only applies to public resources publicOnly, err := scope.PublicOnly() @@ -308,21 +373,8 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC return } - // this context is used by the middleware in the specific route - ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository) - ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization) - - allow, err := scope.HasScope(requiredScopes...) - if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) - return - } - - if allow { - return - } - - ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) + // assign to true so that those searching should only filter public repositories/users/organizations + ctx.PublicOnly = publicOnly } } @@ -334,25 +386,6 @@ func reqToken() func(ctx *context.APIContext) { return } - if true == ctx.Data["IsApiToken"] { - publicRepo, pubRepoExists := ctx.Data["ApiTokenScopePublicRepoOnly"] - publicOrg, pubOrgExists := ctx.Data["ApiTokenScopePublicOrgOnly"] - - if pubRepoExists && publicRepo.(bool) && - ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") - return - } - - if pubOrgExists && publicOrg.(bool) && - ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") - return - } - - return - } - if ctx.IsSigned { return } @@ -800,11 +833,11 @@ func Routes() *web.Route { m.Group("/user/{username}", func() { m.Get("", activitypub.Person) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) - }, context.UserAssignmentAPI()) + }, context.UserAssignmentAPI(), checkTokenPublicOnly()) m.Group("/user-id/{user-id}", func() { m.Get("", activitypub.Person) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) - }, context.UserIDAssignmentAPI()) + }, context.UserIDAssignmentAPI(), checkTokenPublicOnly()) m.Group("/actor", func() { m.Get("", activitypub.Actor) m.Post("/inbox", activitypub.ActorInbox) @@ -871,7 +904,7 @@ func Routes() *web.Route { }, reqSelfOrAdmin(), reqBasicOrRevProxyAuth()) m.Get("/activities/feeds", user.ListUserActivityFeeds) - }, context.UserAssignmentAPI(), individualPermsChecker) + }, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser)) // Users (requires user scope) @@ -891,7 +924,7 @@ func Routes() *web.Route { } m.Get("/subscriptions", user.GetWatchedRepos) - }, context.UserAssignmentAPI()) + }, context.UserAssignmentAPI(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Users (requires user scope) @@ -988,7 +1021,7 @@ func Routes() *web.Route { m.Get("", user.IsStarring) m.Put("", user.Star) m.Delete("", user.Unstar) - }, repoAssignment()) + }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) } m.Get("/times", repo.ListMyTrackedTimes) @@ -1019,12 +1052,14 @@ func Routes() *web.Route { // Repositories (requires repo scope, org scope) m.Post("/org/{org}/repos", + // FIXME: we need org in context tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated) // requires repo scope + // FIXME: Don't expose repository id outside of the system m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID) // Repos (requires repo scope) @@ -1305,7 +1340,7 @@ func Routes() *web.Route { m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar) m.Delete("", repo.DeleteAvatar) }, reqAdmin(), reqToken()) - }, repoAssignment()) + }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) // Notifications (requires notifications scope) @@ -1314,7 +1349,7 @@ func Routes() *web.Route { m.Combo("/notifications", reqToken()). Get(notify.ListRepoNotifications). Put(notify.ReadRepoNotifications) - }, repoAssignment()) + }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Issue (requires issue scope) @@ -1428,7 +1463,7 @@ func Routes() *web.Route { Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) }) - }, repoAssignment()) + }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs @@ -1439,14 +1474,14 @@ func Routes() *web.Route { m.Get("/files", reqToken(), packages.ListPackageFiles) }) m.Get("/", reqToken(), packages.ListPackages) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) // Organizations m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly()) m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) m.Group("/orgs/{org}", func() { @@ -1513,7 +1548,7 @@ func Routes() *web.Route { m.Put("/unblock/{username}", org.UnblockUser) }, context.UserAssignmentAPI()) }, reqToken(), reqOrgOwnership()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam). @@ -1533,7 +1568,7 @@ func Routes() *web.Route { Get(reqToken(), org.GetTeamRepo) }) m.Get("/activities/feeds", org.ListTeamActivityFeeds) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) m.Group("/admin", func() { m.Group("/cron", func() { diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 9f9483d4ff..95c8c25d8e 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -192,7 +192,7 @@ func GetAll(ctx *context.APIContext) { // "$ref": "#/responses/OrganizationList" vMode := []api.VisibleType{api.VisibleTypePublic} - if ctx.IsSigned { + if ctx.IsSigned && !ctx.PublicOnly { vMode = append(vMode, api.VisibleTypeLimited) if ctx.Doer.IsAdmin { vMode = append(vMode, api.VisibleTypePrivate) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 22779e38d2..99cd9803c5 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -149,7 +149,7 @@ func SearchIssues(ctx *context.APIContext) { Actor: ctx.Doer, } if ctx.IsSigned { - opts.Private = true + opts.Private = !ctx.PublicOnly opts.AllLimited = true } if ctx.FormString("owner") != "" { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 9f6536b2c5..25427125db 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -130,6 +130,11 @@ func Search(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" + private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")) + if ctx.PublicOnly { + private = false + } + opts := &repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, @@ -139,7 +144,7 @@ func Search(ctx *context.APIContext) { TeamID: ctx.FormInt64("team_id"), TopicOnly: ctx.FormBool("topic"), Collaborate: optional.None[bool](), - Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), + Private: private, Template: optional.None[bool](), StarredByID: ctx.FormInt64("starredBy"), IncludeDescription: ctx.FormBool("includeDesc"), diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 831c1436da..4efcb7cc4f 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -10,6 +10,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -68,11 +69,16 @@ func Search(ctx *context.APIContext) { maxResults = 1 users = []*user_model.User{user_model.NewActionsUser()} default: + var visible []structs.VisibleType + if ctx.PublicOnly { + visible = []structs.VisibleType{structs.VisibleTypePublic} + } users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), UID: uid, Type: user_model.UserTypeIndividual, + Visible: visible, ListOptions: listOptions, }) if err != nil { diff --git a/services/context/api.go b/services/context/api.go index 8e255c8573..52d11f5f6a 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -45,6 +45,7 @@ type APIContext struct { Package *Package QuotaGroup *quota_model.Group QuotaRule *quota_model.Rule + PublicOnly bool // Whether the request is for a public endpoint } func init() { diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 6e60fe72bc..4051f95ed9 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -76,6 +76,34 @@ func TestAPIListIssues(t *testing.T) { } } +func TestAPIListIssuesPublicOnly(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID}) + + session := loginUser(t, owner1.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner1.Name, repo1.Name)) + link.RawQuery = url.Values{"state": {"all"}}.Encode() + req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID}) + + session = loginUser(t, owner2.Name) + token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner2.Name, repo2.Name)) + link.RawQuery = url.Values{"state": {"all"}}.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly) + req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken) + MakeRequest(t, req, http.StatusForbidden) +} + func TestAPICreateIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() const body, title = "apiTestBody", "apiTestTitle" @@ -404,6 +432,12 @@ func TestAPISearchIssues(t *testing.T) { DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, expectedIssueCount) + publicOnlyToken := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly) + req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 15) // 15 public issues + since := "2000-01-01T00:50:01+00:00" // 946687801 before := time.Unix(999307200, 0).Format(time.RFC3339) query.Add("since", since) diff --git a/tests/integration/api_repo_branch_test.go b/tests/integration/api_repo_branch_test.go index 5488eca1dc..83159022ea 100644 --- a/tests/integration/api_repo_branch_test.go +++ b/tests/integration/api_repo_branch_test.go @@ -29,9 +29,13 @@ func TestAPIRepoBranchesPlain(t *testing.T) { repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) session := loginUser(t, user1.LowerName) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + // public only token should be forbidden + publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository) link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo + MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden) + + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -43,6 +47,8 @@ func TestAPIRepoBranchesPlain(t *testing.T) { assert.EqualValues(t, "master", branches[1].Name) link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name)) + MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden) + resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) require.NoError(t, err) @@ -50,6 +56,8 @@ func TestAPIRepoBranchesPlain(t *testing.T) { require.NoError(t, json.Unmarshal(bs, &branch)) assert.EqualValues(t, "test_branch", branch.Name) + MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden) + req := NewRequest(t, "POST", link.String()).AddTokenAuth(token) req.Header.Add("Content-Type", "application/json") req.Body = io.NopCloser(bytes.NewBufferString(`{"new_branch_name":"test_branch2", "old_branch_name": "test_branch", "old_ref_name":"refs/heads/test_branch"}`)) @@ -74,6 +82,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) { link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name)) MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound) + MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden) MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent) require.NoError(t, err) diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go index 2ecd338987..d775a448fa 100644 --- a/tests/integration/api_user_search_test.go +++ b/tests/integration/api_user_search_test.go @@ -40,6 +40,19 @@ func TestAPIUserSearchLoggedIn(t *testing.T) { assert.Contains(t, user.UserName, query) assert.NotEmpty(t, user.Email) } + + publicToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly) + req = NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query). + AddTokenAuth(publicToken) + resp = MakeRequest(t, req, http.StatusOK) + results = SearchResults{} + DecodeJSON(t, resp, &results) + assert.NotEmpty(t, results.Data) + for _, user := range results.Data { + assert.Contains(t, user.UserName, query) + assert.NotEmpty(t, user.Email) + assert.True(t, user.Visibility == "public") + } } func TestAPIUserSearchNotLoggedIn(t *testing.T) {