diff --git a/models/organization/org.go b/models/organization/org.go index 260aaa9c997..fe90a9fae80 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -91,6 +91,14 @@ func (org *Organization) IsOwnedBy(ctx context.Context, uid int64) (bool, error) return IsOrganizationOwner(ctx, org.ID, uid) } +// CanChangeRepoTeamAccess reports whether a repository administrator can change team access. +func (org *Organization) CanChangeRepoTeamAccess(ctx context.Context, doer *user_model.User) (bool, error) { + if org.RepoAdminChangeTeamAccess || doer.IsAdmin { + return true, nil + } + return org.IsOwnedBy(ctx, doer.ID) +} + // IsOrgAdmin returns true if given user is in the owner team or an admin team. func (org *Organization) IsOrgAdmin(ctx context.Context, uid int64) (bool, error) { return IsOrganizationAdmin(ctx, org.ID, uid) diff --git a/models/organization/org_list.go b/models/organization/org_list.go index 73de7db824b..9f9ea69c7c7 100644 --- a/models/organization/org_list.go +++ b/models/organization/org_list.go @@ -89,6 +89,9 @@ func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType { if doer.IsAdmin || doer.ID == other.ID { return structs.VisibleTypePrivate } + if doer.IsRestricted { + return structs.VisibleTypePublic + } return structs.VisibleTypeLimited } diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index a3a8b1e2b05..6c4524ecb5a 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -77,8 +77,14 @@ func testLoadOrgListTeams(t *testing.T) { } func testDoerViewOtherVisibility(t *testing.T) { + viewer := &user_model.User{ID: 1} + other := &user_model.User{ID: 2} + restrictedViewer := &user_model.User{ID: 3, IsRestricted: true} + assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil)) - assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2})) - assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1})) - assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2})) + assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(viewer, other)) + assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(restrictedViewer, other)) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(viewer, viewer)) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(restrictedViewer, restrictedViewer)) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 4, IsAdmin: true, IsRestricted: true}, other)) } diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go index db3288a4be3..e3db4527609 100644 --- a/models/packages/package_blob.go +++ b/models/packages/package_blob.go @@ -126,6 +126,10 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model if user.IsAdmin { return true, nil } + ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic} + if !user.IsRestricted { + ownerVisibilities = append(ownerVisibilities, structs.VisibleTypeLimited) + } maxTeamAuthorize := builder. Select("max(team.authorize)"). @@ -144,7 +148,7 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model // owner = user builder.Eq{"`user`.id": user.ID}. // user can see owner - Or(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}.Or(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})). + Or(builder.In("`user`.visibility", ownerVisibilities)). // owner is an organization and user has access to it Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}. And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))), diff --git a/models/packages/package_blob_test.go b/models/packages/package_blob_test.go index b73328d26b2..55ae037c7e4 100644 --- a/models/packages/package_blob_test.go +++ b/models/packages/package_blob_test.go @@ -7,6 +7,7 @@ import ( "testing" "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,3 +50,25 @@ func TestGetOrInsertBlobConcurrent(t *testing.T) { } assert.Equal(t, numGoroutines-1, existedCount) } + +func TestIsBlobAccessibleForRestrictedUser(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 33}) + pkg, err := TryInsertPackage(t.Context(), &Package{OwnerID: owner.ID, Type: TypeContainer, Name: "limited", LowerName: "limited"}) + require.NoError(t, err) + version, err := GetOrInsertVersion(t.Context(), &PackageVersion{PackageID: pkg.ID, Version: "1", LowerVersion: "1"}) + require.NoError(t, err) + blob, _, err := GetOrInsertBlob(t.Context(), &PackageBlob{Size: 1, HashMD5: "md5", HashSHA1: "sha1", HashSHA256: "sha256", HashSHA512: "sha512"}) + require.NoError(t, err) + _, err = TryInsertFile(t.Context(), &PackageFile{VersionID: version.ID, BlobID: blob.ID, Name: "blob", LowerName: "blob"}) + require.NoError(t, err) + + accessible, err := IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})) + require.NoError(t, err) + assert.True(t, accessible) + + accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})) + require.NoError(t, err) + assert.False(t, accessible) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 427a84a02cf..0238e9a06c0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1701,7 +1701,7 @@ func Routes() *web.Router { m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker) m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) - m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) + m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll) m.Group("/orgs/{org}", func() { m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 5d64a9164aa..54e83c2a98c 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -683,16 +683,13 @@ func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { } func canChangeTeamRepository(ctx *context.APIContext) bool { - if ctx.Org.Organization.RepoAdminChangeTeamAccess { - return true - } - isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID) + canChange, err := ctx.Org.Organization.CanChangeRepoTeamAccess(ctx, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return false } - if !isOwner { - ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner") + if !canChange { + ctx.APIError(http.StatusForbidden, "Must be an organization owner") return false } return true diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index fc39b1c6166..afa8a3caa46 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -137,6 +137,8 @@ func AddTeam(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "422": // "$ref": "#/responses/validationError" // "405": @@ -173,6 +175,8 @@ func DeleteTeam(ctx *context.APIContext) { // responses: // "204": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "422": // "$ref": "#/responses/validationError" // "405": @@ -186,9 +190,9 @@ func DeleteTeam(ctx *context.APIContext) { func changeRepoTeam(ctx *context.APIContext, add bool) { if !ctx.Repo.Owner.IsOrganization() { ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization") + return } - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { - ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner") + if !canChangeRepoTeam(ctx) { return } @@ -220,6 +224,19 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { ctx.Status(http.StatusNoContent) } +func canChangeRepoTeam(ctx *context.APIContext) bool { + canChange, err := organization.OrgFromUser(ctx.Repo.Owner).CanChangeRepoTeamAccess(ctx, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return false + } + if !canChange { + ctx.APIError(http.StatusForbidden, "Must be an organization owner") + return false + } + return true +} + func getTeamByParam(ctx *context.APIContext) *organization.Team { team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) if err != nil { diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go index dd733dca6e7..01a78a7f190 100644 --- a/routers/web/repo/actions/badge.go +++ b/routers/web/repo/actions/badge.go @@ -10,6 +10,7 @@ import ( "strings" actions_model "gitea.dev/models/actions" + auth_model "gitea.dev/models/auth" "gitea.dev/modules/badge" "gitea.dev/modules/git" "gitea.dev/modules/util" @@ -17,6 +18,11 @@ import ( ) func GetWorkflowBadge(ctx *context.Context) { + context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + if ctx.Written() { + return + } + workflowFile := ctx.PathParam("workflow_name") branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch) event := ctx.FormString("event") diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index 0be463ece8b..a1037dbb5fd 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -43,6 +43,13 @@ func Collaboration(ctx *context.Context) { ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName ctx.Data["Org"] = ctx.Repo.Repository.Owner ctx.Data["Units"] = unit_model.Units + if ctx.Repo.Owner.IsOrganization() { + ctx.Data["CanChangeRepoTeamAccess"], err = organization.OrgFromUser(ctx.Repo.Owner).CanChangeRepoTeamAccess(ctx, ctx.Doer) + if err != nil { + ctx.ServerError("CanChangeRepoTeamAccess", err) + return + } + } ctx.HTML(http.StatusOK, tplCollaboration) } @@ -155,9 +162,7 @@ func DeleteCollaboration(ctx *context.Context) { // AddTeamPost response for adding a team to a repository func AddTeamPost(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { - ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + if !canChangeRepoTeamAccess(ctx) { return } @@ -201,9 +206,7 @@ func AddTeamPost(ctx *context.Context) { // DeleteTeam response for deleting a team from a repository func DeleteTeam(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() { - ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + if !canChangeRepoTeamAccess(ctx) { return } @@ -221,3 +224,16 @@ func DeleteTeam(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success")) ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration") } + +func canChangeRepoTeamAccess(ctx *context.Context) bool { + canChange, err := organization.OrgFromUser(ctx.Repo.Owner).CanChangeRepoTeamAccess(ctx, ctx.Doer) + if err != nil { + ctx.ServerError("CanChangeRepoTeamAccess", err) + return false + } + if !canChange { + ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + } + return canChange +} diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index e86527536bb..dda5e13b76e 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -240,40 +240,27 @@ func TestAddTeamPost(t *testing.T) { func TestAddTeamPost_NotAllowed(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "org26/repo43") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) + require.NoError(t, repo.LoadOwner(t.Context())) + adminTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 12}) + targetTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + require.NoError(t, repo_service.TeamAddRepository(t.Context(), adminTeam, repo)) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) + repoContext := &context.Repository{Owner: repo.Owner, Repository: repo} + renderCtx, _ := contexttest.MockContext(t, repo.Link()+"/settings/collaboration") + renderCtx.Repo = repoContext + renderCtx.Doer = doer + Collaboration(renderCtx) + assert.Equal(t, false, renderCtx.Data["CanChangeRepoTeamAccess"]) - ctx.Req.Form.Set("team", "team11") - - org := &user_model.User{ - LowerName: "org26", - Type: user_model.UserTypeOrganization, - } - - team := &organization.Team{ - ID: 11, - OrgID: 26, - } - - re := &repo_model.Repository{ - ID: 43, - Owner: org, - OwnerID: 26, - } - - repo := &context.Repository{ - Owner: &user_model.User{ - ID: 26, - LowerName: "org26", - RepoAdminChangeTeamAccess: false, - }, - Repository: re, - } - - ctx.Repo = repo + ctx, _ := contexttest.MockContext(t, repo.Link()+"/settings/collaboration") + ctx.Req.Form.Set("team", targetTeam.Name) + ctx.Repo = repoContext + ctx.Doer = doer AddTeamPost(ctx) - assert.False(t, repo_service.HasRepository(t.Context(), team, re.ID)) + assert.False(t, repo_service.HasRepository(t.Context(), targetTeam, repo.ID)) assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } diff --git a/services/context/package.go b/services/context/package.go index 7c1e55c8a34..977953cefb4 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -14,7 +14,6 @@ import ( "gitea.dev/models/unit" user_model "gitea.dev/models/user" "gitea.dev/modules/setting" - "gitea.dev/modules/structs" "gitea.dev/modules/templates" ) @@ -155,10 +154,10 @@ func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.Acces // 1. Check if user is package owner if doer.ID == pkgOwner.ID { accessMode = perm.AccessModeOwner - } else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited + } else if pkgOwner.Visibility.IsPublic() || (pkgOwner.Visibility.IsLimited() && !doer.IsRestricted) { // 2. Check if package owner is visible to the doer accessMode = perm.AccessModeRead } - } else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public + } else if pkgOwner.Visibility.IsPublic() { // 3. Check if package owner is public accessMode = perm.AccessModeRead } } diff --git a/services/context/package_test.go b/services/context/package_test.go new file mode 100644 index 00000000000..2b0dfd923e7 --- /dev/null +++ b/services/context/package_test.go @@ -0,0 +1,26 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "testing" + + "gitea.dev/models/perm" + "gitea.dev/models/user" + "gitea.dev/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestDeterminePackageAccessModeForLimitedOwner(t *testing.T) { + owner := &user.User{ID: 1, Visibility: structs.VisibleTypeLimited} + + accessMode, err := determineAccessMode(&Base{}, owner, &user.User{ID: 2, IsActive: true}) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeRead, accessMode) + + accessMode, err = determineAccessMode(&Base{}, owner, &user.User{ID: 3, IsActive: true, IsRestricted: true}) + assert.NoError(t, err) + assert.Equal(t, perm.AccessModeNone, accessMode) +} diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index 48e3f3287e0..752fe7ded15 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -51,7 +51,7 @@

{{ctx.Locale.Tr "repo.settings.teams"}}

- {{$allowedToChangeTeams := (or (.Org.RepoAdminChangeTeamAccess) (.Permission.IsOwner))}} + {{$allowedToChangeTeams := .CanChangeRepoTeamAccess}} {{if .Teams}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 62e60f88c19..9b2af31f009 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17781,6 +17781,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -17828,6 +17831,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 3566aae6f69..df3cabbac5c 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -29943,6 +29943,9 @@ "204": { "$ref": "#/components/responses/empty" }, + "403": { + "$ref": "#/components/responses/forbidden" + }, "404": { "$ref": "#/components/responses/notFound" }, @@ -30040,6 +30043,9 @@ "204": { "$ref": "#/components/responses/empty" }, + "403": { + "$ref": "#/components/responses/forbidden" + }, "404": { "$ref": "#/components/responses/notFound" }, diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index f0250c5d5a5..4f525b308ab 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -121,6 +121,9 @@ func testAPIOrgGeneral(t *testing.T) { user1Token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization) t.Run("OrgGetAll", func(t *testing.T) { + miscToken := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeReadMisc) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs").AddTokenAuth(miscToken), http.StatusForbidden) + // accessing with a token will return all orgs req := NewRequest(t, "GET", "/api/v1/orgs").AddTokenAuth(user1Token) resp := MakeRequest(t, req, http.StatusOK) @@ -130,6 +133,14 @@ func testAPIOrgGeneral(t *testing.T) { assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName) assert.Equal(t, api.UserVisibilityLimited, apiOrgList[1].Visibility) + publicOnlyToken := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly) + resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs").AddTokenAuth(publicOnlyToken), http.StatusOK) + apiOrgList = DecodeJSON(t, resp, []*api.Organization{}) + assert.Len(t, apiOrgList, 9) + for _, org := range apiOrgList { + assert.Equal(t, api.UserVisibilityPublic, org.Visibility) + } + // accessing without a token will return only public orgs req = NewRequest(t, "GET", "/api/v1/orgs") resp = MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/api_repo_teams_test.go b/tests/integration/api_repo_teams_test.go index 506270e3ac3..b9dca9be73a 100644 --- a/tests/integration/api_repo_teams_test.go +++ b/tests/integration/api_repo_teams_test.go @@ -9,12 +9,14 @@ import ( "testing" auth_model "gitea.dev/models/auth" + "gitea.dev/models/organization" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" "gitea.dev/modules/util" + repo_service "gitea.dev/services/repository" "gitea.dev/tests" "github.com/stretchr/testify/assert" @@ -63,6 +65,19 @@ func TestAPIRepoTeams(t *testing.T) { AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) + adminTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 12}) + targetTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + existingTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}) + assert.NoError(t, repo_service.TeamAddRepository(t.Context(), adminTeam, publicOrgRepo)) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) + token = getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/teams/%s", publicOrgRepo.FullName(), targetTeam.Name)).AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + assert.False(t, repo_service.HasRepository(t.Context(), targetTeam, publicOrgRepo.ID)) + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/teams/%s", publicOrgRepo.FullName(), existingTeam.Name)).AddTokenAuth(token) + MakeRequest(t, req, http.StatusForbidden) + assert.True(t, repo_service.HasRepository(t.Context(), existingTeam, publicOrgRepo.ID)) + // AddTeam with user2 user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session = loginUser(t, user.Name) diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go index 3ad28be1805..8a01c043135 100644 --- a/tests/integration/api_team_test.go +++ b/tests/integration/api_team_test.go @@ -329,6 +329,15 @@ func TestAPIAddRemoveTeamRepositoryRequiresOrgOwnerOrSetting(t *testing.T) { req = NewRequest(t, "DELETE", url).AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: team.ID, RepoID: targetRepo.ID}) + + siteAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + token = getUserToken(t, siteAdmin.Name, auth_model.AccessTokenScopeWriteOrganization) + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: team.ID, RepoID: targetRepo.ID}) + req = NewRequest(t, "PUT", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: team.ID, RepoID: targetRepo.ID}) } func TestAPITeamVisibilityAccess(t *testing.T) { diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go index 54291f42728..381ea43f52d 100644 --- a/tests/integration/api_user_orgs_test.go +++ b/tests/integration/api_user_orgs_test.go @@ -77,6 +77,9 @@ func TestUserOrgs(t *testing.T) { orgs = getUserOrgs(t, unrelatedUsername, privateMemberUsername) assert.Empty(t, orgs) + orgs = getUserOrgs(t, "user29", adminUsername) + assert.Empty(t, orgs) + // not authenticated call should not be allowed testUserOrgsUnauthenticated(t, privateMemberUsername) } diff --git a/tests/integration/repo_home_token_scope_test.go b/tests/integration/repo_home_token_scope_test.go index 7010a44cf8c..7404fde401a 100644 --- a/tests/integration/repo_home_token_scope_test.go +++ b/tests/integration/repo_home_token_scope_test.go @@ -11,30 +11,33 @@ import ( "gitea.dev/tests" ) -// TestRepoHomeContentTokenScopes ensures the web repository home page enforces the -// repository read scope (and public-only confinement) of an API token used via basic -// auth, so a wrongly-scoped token cannot read private repository content. -func TestRepoHomeContentTokenScopes(t *testing.T) { +func TestRepoWebTokenScopes(t *testing.T) { defer tests.PrepareTestEnv(t)() - // user2/repo2 is a private repository owned by user2 - const url = "/user2/repo2" - - // a token without repository scope must be denied miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc) - reqDenied := NewRequest(t, "GET", url) - reqDenied.SetBasicAuth("user2", miscToken) - MakeRequest(t, reqDenied, http.StatusForbidden) - - // a public-only token must be denied on a private repo publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly) - reqPublicOnly := NewRequest(t, "GET", url) - reqPublicOnly.SetBasicAuth("user2", publicOnlyToken) - MakeRequest(t, reqPublicOnly, http.StatusForbidden) + readToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository) - // a token with repository read scope is allowed - ownerReadToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository) - reqAllowed := NewRequest(t, "GET", url) - reqAllowed.SetBasicAuth("user2", ownerReadToken) - MakeRequest(t, reqAllowed, http.StatusOK) + for _, test := range []struct { + name string + url string + }{ + {"repository home", "/user2/repo2"}, + {"workflow badge", "/org3/repo3/actions/workflows/test.yml/badge.svg"}, + } { + t.Run(test.name, func(t *testing.T) { + assertBasicAuthStatus(t, test.url, miscToken, http.StatusForbidden) + assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusForbidden) + assertBasicAuthStatus(t, test.url, readToken, http.StatusOK) + }) + } + + assertBasicAuthStatus(t, "/user2/repo1/actions/workflows/test.yml/badge.svg", publicOnlyToken, http.StatusOK) +} + +func assertBasicAuthStatus(t *testing.T, url, token string, status int) { + t.Helper() + req := NewRequest(t, http.MethodGet, url) + req.SetBasicAuth("user2", token) + MakeRequest(t, req, status) }