mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-20 12:48:00 +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:
@@ -816,7 +816,7 @@ func reqProjectsUnitAccess(accessMode perm.AccessMode) func(ctx *context.APICont
|
||||
}
|
||||
// individual visibility is handled by individualPermsChecker
|
||||
if ctx.ContextUser.IsOrganization() &&
|
||||
organization.OrgFromUser(ctx.ContextUser).UnitPermission(ctx, ctx.Doer, unit.TypeProjects) < accessMode {
|
||||
organization.OrgFromUser(ctx.ContextUser).AnyRepoUnitPermission(ctx, ctx.Doer, unit.TypeProjects) < accessMode {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
+43
-67
@@ -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 {
|
||||
|
||||
@@ -117,7 +117,7 @@ func Projects(ctx *context.Context) {
|
||||
|
||||
func canWriteProjects(ctx *context.Context) bool {
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects)
|
||||
return ctx.Org.CanWriteAnyRepoUnit(ctx, unit.TypeProjects)
|
||||
}
|
||||
return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
|
||||
}
|
||||
|
||||
+30
-73
@@ -6,7 +6,7 @@ package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -327,51 +327,25 @@ func NewTeam(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
}
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
|
||||
// The existing teams won't inherit the correct admin permission for the new unit.
|
||||
// The full history is like this:
|
||||
// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
|
||||
// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
|
||||
// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
|
||||
// - Sometimes, "team unit" is used not really used and "team unit" is used.
|
||||
// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
|
||||
//
|
||||
// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
|
||||
// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
|
||||
//
|
||||
// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
|
||||
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
|
||||
unitPerms := make(map[unit_model.Type]perm.AccessMode)
|
||||
func paresFormTeamUnits(orgID int64, forms url.Values) (units []*org_model.TeamUnit) {
|
||||
for _, ut := range unit_model.AllRepoUnitTypes {
|
||||
// Default access mode is none
|
||||
unitPerms[ut] = perm.AccessModeNone
|
||||
|
||||
v, ok := forms[fmt.Sprintf("unit_%d", ut)]
|
||||
if ok {
|
||||
vv, _ := strconv.Atoi(v[0])
|
||||
if teamPermission >= perm.AccessModeAdmin {
|
||||
unitPerms[ut] = teamPermission
|
||||
// Don't allow `TypeExternal{Tracker,Wiki}` to influence this as they can only be set to READ perms.
|
||||
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
|
||||
unitPerms[ut] = perm.AccessModeRead
|
||||
}
|
||||
} else {
|
||||
unitPerms[ut] = perm.AccessMode(vv)
|
||||
if unitPerms[ut] >= perm.AccessModeAdmin {
|
||||
unitPerms[ut] = perm.AccessModeWrite
|
||||
}
|
||||
}
|
||||
v, ok := forms["unit_"+strconv.Itoa(ut.Value())]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
vv, _ := strconv.Atoi(v[0])
|
||||
mode := perm.AccessMode(vv)
|
||||
mode = min(mode, perm.AccessModeWrite, unit_model.Units[ut].MaxPerm())
|
||||
units = append(units, &org_model.TeamUnit{OrgID: orgID, Type: ut, AccessMode: mode})
|
||||
}
|
||||
return unitPerms
|
||||
return units
|
||||
}
|
||||
|
||||
// NewTeamPost response for create new team
|
||||
func NewTeamPost(ctx *context.Context) {
|
||||
form := web.GetForm[*forms.CreateTeamForm](ctx)
|
||||
includesAllRepositories := form.RepoAccess == "all"
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeWrite, perm.AccessModeAdmin)
|
||||
|
||||
t := &org_model.Team{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
@@ -383,16 +357,6 @@ func NewTeamPost(ctx *context.Context) {
|
||||
Visibility: org_model.NormalizeTeamVisibility(form.Visibility),
|
||||
}
|
||||
|
||||
units := make([]*org_model.TeamUnit, 0, len(unitPerms))
|
||||
for tp, perm := range unitPerms {
|
||||
units = append(units, &org_model.TeamUnit{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Type: tp,
|
||||
AccessMode: perm,
|
||||
})
|
||||
}
|
||||
t.Units = units
|
||||
|
||||
ctx.Data["Title"] = ctx.Org.Organization.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
ctx.Data["PageIsOrgTeamsNew"] = true
|
||||
@@ -404,9 +368,12 @@ func NewTeamPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
if t.AccessMode == perm.AccessModeNone {
|
||||
t.Units = paresFormTeamUnits(ctx.Org.Organization.ID, ctx.Req.Form)
|
||||
if len(t.Units) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := org_service.NewTeam(ctx, t); err != nil {
|
||||
@@ -545,10 +512,9 @@ func EditTeam(ctx *context.Context) {
|
||||
// EditTeamPost response for modify team information
|
||||
func EditTeamPost(ctx *context.Context) {
|
||||
form := web.GetForm[*forms.CreateTeamForm](ctx)
|
||||
|
||||
t := ctx.Org.Team
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
|
||||
isAuthChanged := false
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeWrite, perm.AccessModeAdmin)
|
||||
isIncludeAllChanged := false
|
||||
includesAllRepositories := form.RepoAccess == "all"
|
||||
|
||||
@@ -557,13 +523,10 @@ func EditTeamPost(ctx *context.Context) {
|
||||
ctx.Data["Team"] = t
|
||||
ctx.Data["Units"] = unit_model.Units
|
||||
|
||||
oldTeamAccessMode := t.AccessMode
|
||||
if !t.IsOwnerTeam() {
|
||||
t.Name = form.TeamName
|
||||
if t.AccessMode != teamPermission {
|
||||
isAuthChanged = true
|
||||
t.AccessMode = teamPermission
|
||||
}
|
||||
|
||||
t.AccessMode = teamPermission
|
||||
if t.IncludesAllRepositories != includesAllRepositories {
|
||||
isIncludeAllChanged = true
|
||||
t.IncludesAllRepositories = includesAllRepositories
|
||||
@@ -576,28 +539,22 @@ func EditTeamPost(ctx *context.Context) {
|
||||
t.Visibility = structs.VisibleTypeLimited
|
||||
}
|
||||
|
||||
t.Description = form.Description
|
||||
units := make([]*org_model.TeamUnit, 0, len(unitPerms))
|
||||
for tp, perm := range unitPerms {
|
||||
units = append(units, &org_model.TeamUnit{
|
||||
OrgID: t.OrgID,
|
||||
TeamID: t.ID,
|
||||
Type: tp,
|
||||
AccessMode: perm,
|
||||
})
|
||||
oldTeamUnitsMap := t.GetUnitsMap()
|
||||
if t.AccessMode == perm.AccessModeNone {
|
||||
t.Units = paresFormTeamUnits(ctx.Org.Organization.ID, ctx.Req.Form)
|
||||
if len(t.Units) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Units = units
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
return
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
isAuthChanged := oldTeamAccessMode != t.AccessMode || !maps.Equal(oldTeamUnitsMap, t.GetUnitsMap())
|
||||
t.Description = form.Description
|
||||
if err := org_service.UpdateTeam(ctx, t, isAuthChanged, isIncludeAllChanged); err != nil {
|
||||
ctx.Data["Err_TeamName"] = true
|
||||
switch {
|
||||
|
||||
@@ -403,7 +403,7 @@ func Diff(ctx *context.Context) {
|
||||
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
||||
|
||||
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
|
||||
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
|
||||
return repo_model.HasAccessToRepoCodeUnit(ctx, ctx.Repo.Repository, user.ID)
|
||||
}, nil); err != nil {
|
||||
ctx.ServerError("CalculateTrustStatus", err)
|
||||
return
|
||||
|
||||
@@ -128,7 +128,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
||||
verification := asymkey_service.ParseCommitWithSignature(ctx, latestCommit)
|
||||
|
||||
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
|
||||
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
|
||||
return repo_model.HasAccessToRepoCodeUnit(ctx, ctx.Repo.Repository, user.ID)
|
||||
}, nil); err != nil {
|
||||
ctx.ServerError("CalculateTrustStatus", err)
|
||||
return false
|
||||
|
||||
+1
-1
@@ -453,7 +453,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}
|
||||
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode {
|
||||
if ctx.Org.Organization.AnyRepoUnitPermission(ctx, ctx.Doer, unitType) < accessMode {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user