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:
bircni
2026-08-17 22:30:24 +02:00
committed by GitHub
parent 1cf904f101
commit ed4a23e893
43 changed files with 305 additions and 375 deletions
+43 -67
View File
@@ -6,6 +6,7 @@ package org
import (
"errors"
"maps"
"net/http"
activities_model "gitea.dev/models/activities"
@@ -17,6 +18,7 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
@@ -150,42 +152,36 @@ func GetTeam(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiTeam)
}
func attachTeamUnits(team *organization.Team, defaultAccessMode perm.AccessMode, units []string) {
unitTypes, _ := unit_model.FindUnitTypes(units...)
team.Units = make([]*organization.TeamUnit, 0, len(units))
for _, tp := range unitTypes {
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: tp,
AccessMode: defaultAccessMode,
})
// assignTeamPermissionUnits sets authorize + team_unit rows.
func assignTeamPermissionUnits(team *organization.Team, permission string, units []string, unitsMap map[string]string) (changed bool, _ error) {
if len(units) > 0 && len(unitsMap) > 0 {
return false, util.NewInvalidArgumentErrorf("only one of units or units_map can be set")
}
}
func attachTeamUnitsMap(team *organization.Team, unitsMap map[string]string) {
team.Units = make([]*organization.TeamUnit, 0, len(unitsMap))
for unitKey, p := range unitsMap {
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: unit_model.TypeFromKey(unitKey),
AccessMode: perm.ParseAccessMode(p),
})
}
}
func attachAdminTeamUnits(team *organization.Team) {
team.Units = make([]*organization.TeamUnit, 0, len(unit_model.AllRepoUnitTypes))
for _, ut := range unit_model.AllRepoUnitTypes {
up := perm.AccessModeAdmin
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
up = perm.AccessModeRead
if len(units) > 0 {
unitsMap = map[string]string{}
for _, unit := range units {
unitsMap[unit] = permission
}
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: ut,
AccessMode: up,
})
}
oldAccessMode := team.AccessMode
oldUnitPerms := team.GetUnitsMap()
if len(unitsMap) > 0 {
team.Units = make([]*organization.TeamUnit, 0, len(unitsMap))
for unitKey, p := range unitsMap {
unitType, unitPerm := unit_model.TypeFromKey(unitKey), perm.ParseAccessMode(p)
team.Units = append(team.Units, &organization.TeamUnit{OrgID: team.OrgID, Type: unitType, AccessMode: unitPerm})
}
} else {
requested := perm.ParseAccessMode(permission, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
if requested == perm.AccessModeNone {
return false, util.NewInvalidArgumentErrorf("no permission specified")
}
team.AccessMode, team.Units = requested, nil
}
changed = oldAccessMode != team.AccessMode || !maps.Equal(oldUnitPerms, team.GetUnitsMap())
return changed, nil
}
// CreateTeam api for create a team
@@ -215,29 +211,18 @@ func CreateTeam(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm[*api.CreateTeamOption](ctx)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
team := &organization.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
Description: form.Description,
IncludesAllRepositories: form.IncludesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
AccessMode: teamPermission,
Visibility: organization.NormalizeTeamVisibility(form.Visibility),
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
} else {
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
return
}
} else {
attachAdminTeamUnits(team)
_, err := assignTeamPermissionUnits(team, string(form.Permission), form.Units, form.UnitsMap)
if err != nil {
ctx.APIErrorAuto(err)
return
}
if err := org_service.NewTeam(ctx, team); err != nil {
@@ -307,28 +292,19 @@ func EditTeam(ctx *context.APIContext) {
isAuthChanged := false
isIncludeAllChanged := false
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
if team.AccessMode != teamPermission {
isAuthChanged = true
team.AccessMode = teamPermission
}
if form.IncludesAllRepositories != nil {
isIncludeAllChanged = true
team.IncludesAllRepositories = *form.IncludesAllRepositories
hasPermFields := form.Permission != "" || len(form.Units) > 0 || len(form.UnitsMap) > 0
if !team.IsOwnerTeam() && hasPermFields {
var err error
isAuthChanged, err = assignTeamPermissionUnits(team, string(form.Permission), form.Units, form.UnitsMap)
if err != nil {
ctx.APIErrorAuto(err)
return
}
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
}
} else {
attachAdminTeamUnits(team)
if !team.IsOwnerTeam() && form.IncludesAllRepositories != nil {
isIncludeAllChanged = true
team.IncludesAllRepositories = *form.IncludesAllRepositories
}
if err := org_service.UpdateTeam(ctx, team, isAuthChanged, isIncludeAllChanged); err != nil {