mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-20 09:18:02 +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:
@@ -284,8 +284,8 @@ func (org *Organization) CustomAvatarRelativePath() string {
|
||||
return org.Avatar
|
||||
}
|
||||
|
||||
// UnitPermission returns unit permission
|
||||
func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
|
||||
func (org *Organization) AnyRepoUnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode {
|
||||
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
|
||||
if doer != nil {
|
||||
teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID)
|
||||
if err != nil {
|
||||
@@ -299,7 +299,7 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us
|
||||
}
|
||||
|
||||
if len(teams) > 0 {
|
||||
return teams.UnitMaxAccess(unitType)
|
||||
return teams.AnyRepoUnitMaxAccess(ctx, unitType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-24
@@ -149,29 +149,14 @@ func (t *Team) LoadUnits(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUnitNames returns the team units names
|
||||
func (t *Team) GetUnitNames() (res []string) {
|
||||
if t.HasAdminAccess() {
|
||||
return unit.AllUnitKeyNames()
|
||||
}
|
||||
|
||||
for _, u := range t.Units {
|
||||
res = append(res, unit.Units[u.Type].NameKey)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// GetUnitsMap returns the team units permissions
|
||||
func (t *Team) GetUnitsMap() map[string]string {
|
||||
if len(t.Units) == 0 {
|
||||
return nil
|
||||
}
|
||||
m := make(map[string]string)
|
||||
if t.HasAdminAccess() {
|
||||
for _, u := range unit.Units {
|
||||
m[u.NameKey] = t.AccessMode.ToString()
|
||||
}
|
||||
} else {
|
||||
for _, u := range t.Units {
|
||||
m[u.Unit().NameKey] = u.AccessMode.ToString()
|
||||
}
|
||||
for _, u := range t.Units {
|
||||
m[u.Unit().NameKey] = u.AccessMode.ToString()
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -214,16 +199,21 @@ func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode
|
||||
return accessMode
|
||||
}
|
||||
|
||||
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) {
|
||||
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (mode perm.AccessMode, exist bool) {
|
||||
if err := t.LoadUnits(ctx); err != nil {
|
||||
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
|
||||
log.Error("Error loading team (ID: %d) units: %v", t.ID, err)
|
||||
}
|
||||
for _, u := range t.Units {
|
||||
if u.Type == tp {
|
||||
return u.AccessMode, true
|
||||
mode, exist = u.AccessMode, true
|
||||
break
|
||||
}
|
||||
}
|
||||
return perm.AccessModeNone, false
|
||||
mode = max(mode, t.AccessMode)
|
||||
if unitDef, ok := unit.Units[tp]; ok {
|
||||
mode = min(mode, unitDef.MaxPerm())
|
||||
}
|
||||
return mode, exist || t.AccessMode > perm.AccessModeNone
|
||||
}
|
||||
|
||||
// IsUsableTeamName tests if a name could be as team name
|
||||
|
||||
@@ -27,20 +27,14 @@ func (t TeamList) LoadUnits(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TeamList) UnitMaxAccess(tp unit.Type) perm.AccessMode {
|
||||
func (t TeamList) AnyRepoUnitMaxAccess(ctx context.Context, tp unit.Type) perm.AccessMode {
|
||||
// FIXME: ORG-TEAM-UNIT-MAX-PERMISSION: this function is not right, team can access repo1's code doesn't mean it can access repo2's code
|
||||
maxAccess := perm.AccessModeNone
|
||||
for _, team := range t {
|
||||
if team.IsOwnerTeam() {
|
||||
return perm.AccessModeOwner
|
||||
}
|
||||
for _, teamUnit := range team.Units {
|
||||
if teamUnit.Type != tp {
|
||||
continue
|
||||
}
|
||||
if teamUnit.AccessMode > maxAccess {
|
||||
maxAccess = teamUnit.AccessMode
|
||||
}
|
||||
}
|
||||
maxAccess = max(maxAccess, team.UnitAccessMode(ctx, tp))
|
||||
}
|
||||
return maxAccess
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
org_model "gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -22,3 +24,21 @@ func Test_GetTeamsByIDs(t *testing.T) {
|
||||
assert.Equal(t, "Owners", teams[1].Name)
|
||||
assert.Equal(t, "team1", teams[2].Name)
|
||||
}
|
||||
|
||||
func TestTeamList_UnitMaxAccess(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
adminTeam := &org_model.Team{AccessMode: perm.AccessModeAdmin, Units: nil}
|
||||
writeTeam := &org_model.Team{AccessMode: perm.AccessModeWrite, Units: nil}
|
||||
granularTeam := &org_model.Team{
|
||||
AccessMode: perm.AccessModeNone,
|
||||
Units: []*org_model.TeamUnit{
|
||||
{Type: unit.TypeCode, AccessMode: perm.AccessModeWrite},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
|
||||
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{writeTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
|
||||
assert.Equal(t, perm.AccessModeWrite, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeCode))
|
||||
assert.Equal(t, perm.AccessModeNone, org_model.TeamList{granularTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
|
||||
assert.Equal(t, perm.AccessModeAdmin, org_model.TeamList{granularTeam, adminTeam}.AnyRepoUnitMaxAccess(ctx, unit.TypeActions))
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
||||
|
||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
@@ -291,3 +293,28 @@ func TestIsUsableTeamName(t *testing.T) {
|
||||
assert.NoError(t, organization.IsUsableTeamName("usable"))
|
||||
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))
|
||||
}
|
||||
|
||||
func TestTeam_UnitAccessModeEx(t *testing.T) {
|
||||
team := &organization.Team{
|
||||
AccessMode: perm.AccessModeWrite, Units: []*organization.TeamUnit{
|
||||
{Type: unit.TypeIssues, AccessMode: perm.AccessModeRead}, // team mode wins
|
||||
{Type: unit.TypeWiki, AccessMode: perm.AccessModeAdmin}, // unit mode wins
|
||||
},
|
||||
}
|
||||
mode, exist := team.UnitAccessModeEx(t.Context(), unit.TypeActions)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, perm.AccessModeWrite, mode)
|
||||
assert.Equal(t, perm.AccessModeWrite, team.UnitAccessMode(t.Context(), unit.TypeIssues))
|
||||
assert.Equal(t, perm.AccessModeAdmin, team.UnitAccessMode(t.Context(), unit.TypeWiki))
|
||||
assert.Equal(t, perm.AccessModeRead, team.UnitAccessMode(t.Context(), unit.TypeExternalWiki)) // limited by unit definition
|
||||
|
||||
team = &organization.Team{AccessMode: perm.AccessModeOwner, Units: []*organization.TeamUnit{}}
|
||||
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypePackages)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, perm.AccessModeAdmin, mode)
|
||||
|
||||
team = &organization.Team{AccessMode: perm.AccessModeNone, Units: []*organization.TeamUnit{}}
|
||||
mode, exist = team.UnitAccessModeEx(t.Context(), unit.TypeActions)
|
||||
assert.False(t, exist)
|
||||
assert.Equal(t, perm.AccessModeNone, mode)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user