mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-19 22:48:32 +00:00
enhance: inherit team access for all units (#38938)
Admin and write team authorize now grant that mode on every unit, including units added later, instead of only rows present in `team_unit`. Granular teams keep `authorize=none` and explicit unit rows. Closes the `TEAM-UNIT-PERMISSION` design gap from https://github.com/go-gitea/gitea/pull/34128. Maybe also fix #15962 (actually maybe it had been fixed before, the root cause is out-of-sync "access" table) ## Screenshots only writing selected: <img width="1399" height="1007" alt="image" src="https://github.com/user-attachments/assets/1d1b4c49-a59a-47b6-998f-0464a067395b" /> _Created with the help of AI_ --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -111,16 +111,21 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) {
|
||||
return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID})
|
||||
}
|
||||
|
||||
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
|
||||
func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int64) (bool, error) {
|
||||
func HasAccessToRepoCodeUnit(ctx context.Context, repo *Repository, userID int64) (bool, error) {
|
||||
if repo.OwnerID == userID {
|
||||
return true, nil
|
||||
}
|
||||
teamMember, err := db.GetEngine(ctx).Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
|
||||
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
|
||||
teamMember, err := db.GetEngine(ctx).Table("team_user").
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
|
||||
Join("INNER", "team", "team.id = team_user.team_id").
|
||||
Join("LEFT", "team_unit", "team_unit.team_id = team_user.team_id AND team_unit.`type` = ?", unit.TypeCode).
|
||||
Where("team_repo.repo_id = ?", repo.ID).
|
||||
And("team_unit.`type` = ?", unit.TypeCode).
|
||||
And("team_user.uid = ?", userID).Table("team_user").Exist()
|
||||
And("team_user.uid = ?", userID).
|
||||
And(builder.Or(
|
||||
builder.Gt{"team.authorize": perm.AccessModeNone},
|
||||
builder.Gt{"team_unit.access_mode": perm.AccessModeNone},
|
||||
)).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -73,31 +73,31 @@ func TestRepository_IsOwnerMemberCollaborator(t *testing.T) {
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
|
||||
// Organisation owner.
|
||||
actual, err := repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 2)
|
||||
actual, err := repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, actual)
|
||||
|
||||
// Team member.
|
||||
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 4)
|
||||
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, actual)
|
||||
|
||||
// Normal user.
|
||||
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo1, 1)
|
||||
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, actual)
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
|
||||
// Collaborator.
|
||||
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo2, 4)
|
||||
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo2, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, actual)
|
||||
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15})
|
||||
|
||||
// Repository owner.
|
||||
actual, err = repo_model.IsOwnerMemberCollaborator(t.Context(), repo3, 2)
|
||||
actual, err = repo_model.HasAccessToRepoCodeUnit(t.Context(), repo3, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, actual)
|
||||
}
|
||||
|
||||
@@ -310,15 +310,12 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder {
|
||||
}
|
||||
|
||||
// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit.
|
||||
// A team grants the unit either through an explicit team_unit row (access_mode > none) or by being an
|
||||
// admin/owner team (team.authorize >= admin), which grants every unit regardless of team_unit rows —
|
||||
// mirroring the HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
|
||||
func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder {
|
||||
return userOrgTeamRepoBuilder(userID).
|
||||
Join("INNER", "team", "`team`.id = `team_repo`.team_id").
|
||||
Join("LEFT", "team_unit", builder.Expr("`team_unit`.team_id = `team_repo`.team_id AND `team_unit`.`type` = ?", unitType)).
|
||||
Where(builder.Or(
|
||||
builder.Gte{"`team`.authorize": int(perm.AccessModeAdmin)},
|
||||
builder.Gt{"`team`.authorize": int(perm.AccessModeNone)},
|
||||
builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -486,10 +486,7 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
||||
assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed")
|
||||
}
|
||||
|
||||
// TestUserOrgUnitRepoCondTeamAuthorize pins the team.authorize behavior of userOrgTeamUnitRepoBuilder
|
||||
// (exercised through UserOrgUnitRepoCond): an admin/owner team grants every unit even without an explicit
|
||||
// team_unit row, while a non-admin team only grants a unit it has an explicit row for. This guards both
|
||||
// directions — hiding repos from admin-team members, and over-broadening a plain team's access.
|
||||
// TestUserOrgUnitRepoCondTeamAuthorize pins team.authorize vs team_unit.access_mode
|
||||
func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
@@ -500,17 +497,16 @@ func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) {
|
||||
return ids
|
||||
}
|
||||
|
||||
// Case A: user18 is only on org17's owner team (team5, authorize=owner), linked to the private repo24
|
||||
// but with no Actions team_unit row. The owner authorize must still grant it, mirroring the runtime
|
||||
// HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission.
|
||||
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24),
|
||||
"an owner team grants a unit it has no explicit team_unit row for")
|
||||
// Owner team5 has no Actions team_unit row but still grants via authorize=owner.
|
||||
assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24))
|
||||
|
||||
// Cases B and C share one subject so the team_unit row is the only difference: user4 is only on org3's
|
||||
// write team (team2, authorize=write, non-admin), linked to the private repo3. team2 has an explicit
|
||||
// Projects row but none for Actions.
|
||||
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3),
|
||||
"a non-admin team grants a unit it has an explicit team_unit row for")
|
||||
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3),
|
||||
"a non-admin team must NOT grant a unit it has no team_unit row for")
|
||||
// team2 is "authorize=write" with Projects team_unit but no Actions row.
|
||||
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3))
|
||||
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3))
|
||||
|
||||
// now team2 is "authorize=none", no Actions row.
|
||||
_, err := db.GetEngine(t.Context()).Exec("UPDATE team SET authorize=0 WHERE id=2")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3))
|
||||
assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user