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 20:30:24 +00:00
committed by GitHub
co-authored by wxiaoguang
parent 1cf904f101
commit ed4a23e893
43 changed files with 305 additions and 375 deletions
+17 -9
View File
@@ -11,6 +11,7 @@ import (
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
@@ -287,8 +288,8 @@ func applyConditions(sess db.Session, opts *IssuesOptions) {
}
}
// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond {
// teamUnitsRepoReaderCond returns query condition for those repo id in the special org team with special units access
func teamUnitsRepoReaderCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond {
return builder.In(id,
builder.Select("repo_id").From("team_repo").Where(
builder.Eq{
@@ -316,12 +317,19 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
}),
),
)).And(
builder.In(
"team_id", builder.Select("team_id").From("team_unit").Where(
builder.Eq{
"`team_unit`.org_id": orgID,
}.And(
builder.In("`team_unit`.type", units),
builder.Or(
builder.In(
"team_id", builder.Select("id").From("team").Where(
builder.Eq{"id": teamID}.And(builder.Gt{"authorize": perm.AccessModeNone}),
),
),
builder.In(
"team_id", builder.Select("team_id").From("team_unit").Where(
builder.Eq{
"`team_unit`.org_id": orgID,
}.And(
builder.In("`team_unit`.type", units),
),
),
),
),
@@ -338,7 +346,7 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
}
if owner != nil && owner.IsOrganization() {
if team != nil {
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
cond = cond.And(teamUnitsRepoReaderCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
} else {
cond = cond.And(
builder.Or(
+1 -10
View File
@@ -626,16 +626,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
unittype = unit.TypePullRequests
}
for _, team := range teams {
if team.HasAdminAccess() {
checked = append(checked, team.ID)
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
continue
}
has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype})
if err != nil {
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
}
if has {
if team.UnitEnabled(ctx, unittype) {
checked = append(checked, team.ID)
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
}
+3 -3
View File
@@ -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
View File
@@ -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
+3 -9
View File
@@ -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
}
+20
View File
@@ -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))
}
-1
View File
@@ -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 {
+27
View File
@@ -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)
}
+11 -6
View File
@@ -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
}
+5 -5
View File
@@ -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)
}
+1 -4
View File
@@ -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)},
))
}
+12 -16
View File
@@ -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))
}
-3
View File
@@ -33,9 +33,6 @@ const (
TypeProjects // 8 Projects
TypePackages // 9 Packages
TypeActions // 10 Actions
// FIXME: TEAM-UNIT-PERMISSION: the team unit "admin" permission's design is not right, when a new unit is added in the future,
// admin team won't inherit the correct admin permission for the new unit, need to have a complete fix before adding any new unit.
)
// Value returns integer value for unit type (used by template)
+8 -1
View File
@@ -27,6 +27,7 @@ type TestingT interface {
require.TestingT
assert.TestingT
Context() context.Context
Helper()
}
type testCond struct {
@@ -77,6 +78,7 @@ func GetBean[T any](t TestingT, bean T, conditions ...any) (ret T) {
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
func AssertExistsAndLoadBean[T any](t TestingT, bean T, conditions ...any) T {
t.Helper()
exists, err := getBeanIfExists(t, bean, conditions...)
require.NoError(t, err)
require.True(t, exists,
@@ -87,6 +89,7 @@ func AssertExistsAndLoadBean[T any](t TestingT, bean T, conditions ...any) T {
// AssertExistsAndLoadMap assert that a row exists and load it from the test database
func AssertExistsAndLoadMap(t TestingT, table string, conditions ...any) map[string]string {
t.Helper()
e := db.GetEngine(t.Context()).Table(table)
res, err := whereOrderConditions(e, conditions).Query()
assert.NoError(t, err)
@@ -123,6 +126,7 @@ func GetCount(t TestingT, bean any, conditions ...any) int {
// AssertNotExistsBean assert that a bean does not exist in the test database
func AssertNotExistsBean(t TestingT, bean any, conditions ...any) {
t.Helper()
exists, err := getBeanIfExists(t, bean, conditions...)
assert.NoError(t, err)
assert.False(t, exists)
@@ -130,17 +134,20 @@ func AssertNotExistsBean(t TestingT, bean any, conditions ...any) {
// AssertCount assert the count of a bean
func AssertCount(t TestingT, bean, expected any) bool {
t.Helper()
return assert.EqualValues(t, expected, GetCount(t, bean))
}
// AssertInt64InRange assert value is in range [low, high]
func AssertInt64InRange(t assert.TestingT, low, high, value int64) {
func AssertInt64InRange(t TestingT, low, high, value int64) {
t.Helper()
assert.True(t, value >= low && value <= high,
"Expected value in range [%d, %d], found %d", low, high, value)
}
// GetCountByCond get the count of database entries matching bean
func GetCountByCond(t TestingT, tableName string, cond builder.Cond) int64 {
t.Helper()
e := db.GetEngine(t.Context())
count, err := e.Table(tableName).Where(cond).Count()
assert.NoError(t, err)
+6 -4
View File
@@ -52,8 +52,9 @@ type CreateTeamOption struct {
// The description of the team
Description string `json:"description" binding:"MaxSize(255)"`
// Whether the team has access to all repositories in the organization
IncludesAllRepositories bool `json:"includes_all_repositories"`
Permission RepoWritePermission `json:"permission"`
IncludesAllRepositories bool `json:"includes_all_repositories"`
// All units have this permission (read/write/admin)
Permission RepoWritePermission `json:"permission"`
// example: ["repo.actions","repo.packages","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
@@ -72,8 +73,9 @@ type EditTeamOption struct {
// The description of the team
Description *string `json:"description" binding:"MaxSize(255)"`
// Whether the team has access to all repositories in the organization
IncludesAllRepositories *bool `json:"includes_all_repositories"`
Permission RepoWritePermission `json:"permission"`
IncludesAllRepositories *bool `json:"includes_all_repositories"`
// All units have this permission (read/write/admin)
Permission RepoWritePermission `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
+2 -1
View File
@@ -2845,8 +2845,9 @@
"org.teams.read_access_helper": "Members can view and clone team repositories.",
"org.teams.write_access": "Write",
"org.teams.write_access_helper": "Members can read and push to team repositories.",
"org.teams.write_access_all_helper": "Members get write access for all units to the team repositories.",
"org.teams.admin_access": "Administrator Access",
"org.teams.admin_access_helper": "Members can pull and push to team repositories and add collaborators to them.",
"org.teams.admin_access_helper": "Members get administrator access to the team repositories.",
"org.teams.no_desc": "This team has no description",
"org.teams.settings": "Settings",
"org.teams.owners_permission_desc": "Owners have full access to <strong>all repositories</strong> and have <strong>administrator access</strong> to the organization.",
+1 -1
View File
@@ -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
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 {
+1 -1
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
}
+7 -7
View File
@@ -31,12 +31,12 @@ type Organization struct {
Teams []*organization.Team
}
func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool {
return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite
func (org *Organization) CanWriteAnyRepoUnit(ctx *Context, unitType unit.Type) bool {
return org.Organization.AnyRepoUnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite
}
func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool {
return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead
func (org *Organization) CanReadAnyRepoUnit(ctx *Context, unitType unit.Type) bool {
return org.Organization.AnyRepoUnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead
}
func GetOrganizationByParams(ctx *Context) {
@@ -247,9 +247,9 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
}
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
ctx.Data["CanReadProjects"] = ctx.Org.CanReadAnyRepoUnit(ctx, unit.TypeProjects)
ctx.Data["CanReadPackages"] = ctx.Org.CanReadAnyRepoUnit(ctx, unit.TypePackages)
ctx.Data["CanReadCode"] = ctx.Org.CanReadAnyRepoUnit(ctx, unit.TypeCode)
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
if len(ctx.ContextUser.Description) != 0 {
+4 -1
View File
@@ -9,8 +9,10 @@ import (
"context"
"errors"
"fmt"
"maps"
"net/url"
"path"
"slices"
"strconv"
"time"
@@ -894,6 +896,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
return nil, err
}
unitsMap := t.GetUnitsMap()
apiTeam := &api.Team{
ID: t.ID,
Name: t.Name,
@@ -901,7 +904,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
IncludesAllRepositories: t.IncludesAllRepositories,
CanCreateOrgRepo: t.CanCreateOrgRepo,
Permission: api.AccessLevelName(t.AccessMode.ToString()),
Units: t.GetUnitNames(),
Units: slices.Collect(maps.Keys(unitsMap)),
UnitsMap: t.GetUnitsMap(),
Visibility: api.TeamVisibility(t.Visibility.String()),
}
+1 -1
View File
@@ -41,7 +41,7 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository,
}
isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID)
return repo_model.HasAccessToRepoCodeUnit(ctx, repo, user.ID)
}
_ = asymkey_model.CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
+7 -7
View File
@@ -114,8 +114,8 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
return fmt.Errorf("update: %w", err)
}
// update units for team
if len(t.Units) > 0 {
if authChanged {
// update units for team
for _, unit := range t.Units {
unit.TeamID = t.ID
}
@@ -125,13 +125,13 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
Delete(new(organization.TeamUnit)); err != nil {
return err
}
if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil {
return err
if len(t.Units) > 0 {
if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil {
return err
}
}
}
// Update access for team members if needed.
if authChanged {
// Update access for team members if needed.
repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
TeamID: t.ID,
})
+1 -1
View File
@@ -69,7 +69,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit
canAssignProject := canCreate
if !canCreate {
canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
canCreate, err := repo_model.HasAccessToRepoCodeUnit(ctx, repo, issue.Poster.ID)
if err != nil {
return err
}
+1 -1
View File
@@ -130,7 +130,7 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_
c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit)
_ = asymkey_model.CalculateTrustStatus(c.Verification, repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, repository, user.ID)
return repo_model.HasAccessToRepoCodeUnit(ctx, repository, user.ID)
}, &keyMap)
statuses, err := git_model.GetLatestCommitStatus(ctx, repository.ID, c.Commit.ID.String(), db.ListOptionsAll)
+9 -2
View File
@@ -95,11 +95,18 @@
<br>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="permission" value="read" {{if or .PageIsOrgTeamsNew (eq .Team.AccessMode 0) (eq .Team.AccessMode 1) (eq .Team.AccessMode 2)}}checked{{end}}>
<input type="radio" name="permission" value="none" {{if or .PageIsOrgTeamsNew (eq .Team.AccessMode 0) (eq .Team.AccessMode 1)}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.general_access"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.general_access_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="permission" value="write" {{if eq .Team.AccessMode 2}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.write_access"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.write_access_all_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="permission" value="admin" {{if eq .Team.AccessMode 3}}checked{{end}}>
@@ -110,7 +117,7 @@
</div>
<div class="divider"></div>
<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}tw-hidden{{end}}">
<div class="team-units required grouped field {{if ge .Team.AccessMode 2}}tw-hidden{{end}}">
<label>{{ctx.Locale.Tr "org.team_unit_desc"}}</label>
<table class="ui celled table">
<thead>
+2 -4
View File
@@ -50,12 +50,10 @@
<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
{{end}}
</ul>
{{/* the AccessMode should be either none or admin/owner, the real permissions are provided by each team unit */}}
{{if false}}{{/*(eq .Team.AccessMode 2)*/}}
{{if (eq .Team.AccessMode 2)}}
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
{{ctx.Locale.Tr "org.teams.write_permission_desc"}}
{{else if (eq .Team.AccessMode 3)}}
{{/* FIXME: here might not right, see "FIXME: TEAM-UNIT-PERMISSION", new units might not have correct admin permission*/}}
{{else if (ge .Team.AccessMode 3)}}
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
{{else}}
+8 -11
View File
@@ -62,23 +62,20 @@
{{.Name}}
</a>
<div class="item-body flex-text-block">
{{/*FIXME: TEAM-UNIT-PERMISSION this display is not right, search the fixme keyword to see more details */}}
{{svg "octicon-shield-lock"}}
{{if eq .AccessMode 0}}
{{ctx.Locale.Tr "repo.settings.collaboration.per_unit"}}
{{else if eq .AccessMode 1}}
{{ctx.Locale.Tr "repo.settings.collaboration.read"}}
{{if ge .AccessMode 4}}
{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}
{{else if ge .AccessMode 3}}
{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}
{{else if eq .AccessMode 2}}
{{ctx.Locale.Tr "repo.settings.collaboration.write"}}
{{else if eq .AccessMode 3}}
{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}
{{else if eq .AccessMode 4}}
{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}
{{else if eq .AccessMode 1}}
{{ctx.Locale.Tr "repo.settings.collaboration.read"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}
{{ctx.Locale.Tr "repo.settings.collaboration.per_unit"}}
{{end}}
</div>
{{if or (eq .AccessMode 0) (eq .AccessMode 1) (eq .AccessMode 2)}}
{{if lt .AccessMode 2}}
{{$first := true}}
<div class="item-body" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.change_team_permission_tip"}}">
Units:
+12 -2
View File
@@ -4879,7 +4879,12 @@
"x-go-name": "Name"
},
"permission": {
"$ref": "#/components/schemas/RepoWritePermission"
"allOf": [
{
"$ref": "#/components/schemas/RepoWritePermission"
}
],
"description": "All units have this permission (read/write/admin)"
},
"units": {
"deprecated": true,
@@ -6129,7 +6134,12 @@
"x-go-name": "Name"
},
"permission": {
"$ref": "#/components/schemas/RepoWritePermission"
"allOf": [
{
"$ref": "#/components/schemas/RepoWritePermission"
}
],
"description": "All units have this permission (read/write/admin)"
},
"units": {
"deprecated": true,
+2
View File
@@ -27790,6 +27790,7 @@
"x-go-name": "Name"
},
"permission": {
"description": "All units have this permission (read/write/admin)",
"type": "string",
"enum": [
"read",
@@ -29060,6 +29061,7 @@
"x-go-name": "Name"
},
"permission": {
"description": "All units have this permission (read/write/admin)",
"type": "string",
"enum": [
"read",
+1 -1
View File
@@ -183,7 +183,7 @@ func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadUser)
for _, path := range []string{
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
@@ -57,7 +57,7 @@ func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadUser)
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
+6 -3
View File
@@ -293,7 +293,8 @@ func TestPackageAccess(t *testing.T) {
limitedOrgNoMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
publicOrgNoMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
uploadPackage := func(doer, owner *user_model.User, filename string, expectedStatus int) {
uploadPackage := func(t *testing.T, doer, owner *user_model.User, filename string, expectedStatus int) {
t.Helper()
url := fmt.Sprintf("/api/packages/%s/generic/test-package/1.0/%s.bin", owner.Name, filename)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1}))
if doer != nil {
@@ -395,8 +396,10 @@ func TestPackageAccess(t *testing.T) {
}
for _, c := range cases {
for _, t := range c.Targets {
uploadPackage(c.Doer, t.Owner, c.Filename, t.ExpectedStatus)
for _, target := range c.Targets {
t.Run(fmt.Sprintf("%s-%s", c.Filename, target.Owner.Name), func(t *testing.T) {
uploadPackage(t, c.Doer, target.Owner, c.Filename, target.ExpectedStatus)
})
}
}
})
+3 -4
View File
@@ -38,9 +38,9 @@ func TestAPIProjects(t *testing.T) {
// user2 owns repo1 and is on org3's Owners team, so one token covers all three scopes
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization,
auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
// user5 is signed in but is neither an org member nor a repo1 collaborator, and is scoped
// user8 is signed in but is neither an org member nor a repo1 collaborator, and is scoped
// generously so that permissions rather than token scopes are what denies below
outsider := getUserToken(t, "user5", auth_model.AccessTokenScopeWriteIssue,
outsider := getUserToken(t, "user8", auth_model.AccessTokenScopeWriteIssue,
auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeReadUser)
for _, scope := range []projectScope{
@@ -262,8 +262,7 @@ func testAPIProjectPermissions(t *testing.T, ownerToken, outsiderToken string) {
// fixture project 1 belongs to repo1, so this needs no project of its own
const projectURL = "/api/v1/repos/user2/repo1/projects/1"
title := "hijacked"
req := NewRequestWithJSON(t, "PATCH", projectURL, &api.EditProjectOption{Title: &title}).AddTokenAuth(outsiderToken)
req := NewRequestWithJSON(t, "PATCH", projectURL, &api.EditProjectOption{Title: new("hijacked")}).AddTokenAuth(outsiderToken)
MakeRequest(t, req, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", projectURL).AddTokenAuth(outsiderToken), http.StatusForbidden)
+2 -4
View File
@@ -10,11 +10,9 @@ import (
auth_model "gitea.dev/models/auth"
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"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
@@ -38,13 +36,13 @@ func TestAPIRepoTeams(t *testing.T) {
if assert.Len(t, teams, 2) {
assert.Equal(t, "Owners", teams[0].Name)
assert.True(t, teams[0].CanCreateOrgRepo)
assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), "%v == %v", unit.AllUnitKeyNames(), teams[0].Units)
assert.Equal(t, []string{"repo.issues"}, teams[1].Units) // legacy dirty data, although the team.authorize is "owner", the units are also responded
assert.Equal(t, api.AccessLevelNameOwner, teams[0].Permission)
assert.Equal(t, "test_team", teams[1].Name)
assert.False(t, teams[1].CanCreateOrgRepo)
assert.Equal(t, []string{"repo.issues"}, teams[1].Units)
assert.Equal(t, api.AccessLevelNameWrite, teams[1].Permission)
assert.Equal(t, api.AccessLevelNameWrite, teams[1].Permission) // legacy dirty data, although the team.authorize is "write", the units are also responded
}
// IsTeam
+24 -78
View File
@@ -5,8 +5,9 @@ package integration
import (
"fmt"
"maps"
"net/http"
"sort"
"slices"
"testing"
auth_model "gitea.dev/models/auth"
@@ -14,7 +15,6 @@ import (
"gitea.dev/models/organization"
"gitea.dev/models/perm"
"gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/structs"
@@ -70,17 +70,16 @@ func TestAPITeam(t *testing.T) {
Name: "team1",
Description: "team one",
IncludesAllRepositories: true,
Permission: "write",
Permission: "read",
Units: []string{"repo.code", "repo.issues"},
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", org.Name), teamToCreate).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "CreateTeam1", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
api.AccessLevelNameNone, teamToCreate.Units, nil)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
api.AccessLevelNameNone, teamToCreate.Units, nil)
expectedTeamUnitsMap := map[string]string{"repo.code": "read", "repo.issues": "read"}
checkTeamResponse(t, "CreateTeam1", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, api.AccessLevelNameNone, expectedTeamUnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, api.AccessLevelNameNone, expectedTeamUnitsMap)
teamID := apiTeam.ID
// Edit team.
@@ -91,17 +90,14 @@ func TestAPITeam(t *testing.T) {
Description: &editDescription,
Permission: "admin",
IncludesAllRepositories: &editFalse,
Units: []string{"repo.code", "repo.pulls", "repo.releases"},
}
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", teamID), teamToEdit).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam1", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamResponse(t, "EditTeam1", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelName(teamToEdit.Permission), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelName(teamToEdit.Permission), nil)
// Edit team Description only
editDescription = "first team"
@@ -110,10 +106,8 @@ func TestAPITeam(t *testing.T) {
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam1_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamResponse(t, "EditTeam1_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelName(teamToEdit.Permission), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelName(teamToEdit.Permission), nil)
// Read team.
teamRead := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
@@ -122,8 +116,7 @@ func TestAPITeam(t *testing.T) {
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "ReadTeam1", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
checkTeamResponse(t, "ReadTeam1", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
@@ -131,23 +124,19 @@ func TestAPITeam(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
// create team again via UnitsMap
// Create team.
// create team again via UnitsMap (granular: do not send permission=write).
teamToCreate = &api.CreateTeamOption{
Name: "team2",
Description: "team two",
IncludesAllRepositories: true,
Permission: "write",
UnitsMap: map[string]string{"repo.code": "read", "repo.issues": "write", "repo.wiki": "none"},
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", org.Name), teamToCreate).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "CreateTeam2", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
checkTeamResponse(t, "CreateTeam2", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, api.AccessLevelNameNone, teamToCreate.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, api.AccessLevelNameNone, teamToCreate.UnitsMap)
teamID = apiTeam.ID
// Edit team.
@@ -156,7 +145,6 @@ func TestAPITeam(t *testing.T) {
teamToEdit = &api.EditTeamOption{
Name: "teamtwo",
Description: &editDescription,
Permission: "write",
IncludesAllRepositories: &editFalse,
UnitsMap: map[string]string{"repo.code": "read", "repo.pulls": "read", "repo.releases": "write"},
}
@@ -165,10 +153,8 @@ func TestAPITeam(t *testing.T) {
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam2", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamResponse(t, "EditTeam2", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelNameNone, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelNameNone, teamToEdit.UnitsMap)
// Edit team Description only
editDescription = "second team"
@@ -177,10 +163,8 @@ func TestAPITeam(t *testing.T) {
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam2_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamResponse(t, "EditTeam2_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelNameNone, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, api.AccessLevelNameNone, teamToEdit.UnitsMap)
// Read team.
teamRead = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
@@ -189,39 +173,7 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
assert.NoError(t, teamRead.LoadUnits(t.Context()))
checkTeamResponse(t, "ReadTeam2", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
// Create admin team
teamToCreate = &api.CreateTeamOption{
Name: "teamadmin",
Description: "team admin",
IncludesAllRepositories: true,
Permission: "admin",
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", org.Name), teamToCreate).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
apiTeam = DecodeJSON(t, resp, &api.Team{})
for _, ut := range unit.AllRepoUnitTypes {
up := perm.AccessModeAdmin
if ut == unit.TypeExternalTracker || ut == unit.TypeExternalWiki {
up = perm.AccessModeRead
}
unittest.AssertExistsAndLoadBean(t, &organization.TeamUnit{
OrgID: org.ID,
TeamID: apiTeam.ID,
Type: ut,
AccessMode: up,
})
}
teamID = apiTeam.ID
checkTeamResponse(t, "ReadTeam2", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
@@ -230,29 +182,23 @@ func TestAPITeam(t *testing.T) {
unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
}
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission api.AccessLevelName, unitsMap map[string]string) {
t.Run(testName, func(t *testing.T) {
assert.Equal(t, name, apiTeam.Name, "name")
assert.Equal(t, description, apiTeam.Description, "description")
assert.Equal(t, includesAllRepositories, apiTeam.IncludesAllRepositories, "includesAllRepositories")
assert.Equal(t, permission, apiTeam.Permission, "permission")
if units != nil {
sort.StringSlice(units).Sort()
sort.StringSlice(apiTeam.Units).Sort()
assert.Equal(t, units, apiTeam.Units, "units")
}
if unitsMap != nil {
assert.Equal(t, unitsMap, apiTeam.UnitsMap, "unitsMap")
}
assert.ElementsMatch(t, slices.Collect(maps.Keys(unitsMap)), apiTeam.Units, "unitsMap")
assert.Equal(t, unitsMap, apiTeam.UnitsMap, "unitsMap")
})
}
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission api.AccessLevelName, unitsMap map[string]string) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: id})
assert.NoError(t, team.LoadUnits(t.Context()), "LoadUnits")
apiTeam, err := convert.ToTeam(t.Context(), team)
assert.NoError(t, err)
checkTeamResponse(t, fmt.Sprintf("checkTeamBean/%s_%s", name, description), apiTeam, name, description, includesAllRepositories, permission, units, unitsMap)
checkTeamResponse(t, fmt.Sprintf("checkTeamBean/%s_%s", name, description), apiTeam, name, description, includesAllRepositories, permission, unitsMap)
}
type TeamSearchResults struct {
+2 -1
View File
@@ -6,6 +6,7 @@ package integration
import (
"net/http"
"slices"
"strconv"
"testing"
unit_model "gitea.dev/models/unit"
@@ -50,7 +51,7 @@ func TestOrgProjectAccess(t *testing.T) {
"team_name": "team1",
"repo_access": "specific",
"permission": "read",
"unit_8": "0",
"unit_" + strconv.Itoa(unit_model.TypeProjects.Value()): "0",
})
session.MakeRequest(t, req, http.StatusSeeOther)
+3 -4
View File
@@ -184,10 +184,9 @@ func testOrgRestrictedUser(t *testing.T) {
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
apiTeam := DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "CreateTeam_codereader", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", teamToCreate.Units, nil)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", teamToCreate.Units, nil)
expectedUnitsMap := map[string]string{"repo.code": "read"}
checkTeamResponse(t, "CreateTeam_codereader", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, "none", expectedUnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories, "none", expectedUnitsMap)
// teamID := apiTeam.ID
// Now we need to add the restricted user to the team
+1 -1
View File
@@ -7,7 +7,7 @@ function initOrgTeamSettings() {
queryElems(pageContent, 'input[name=permission]', (el) => el.addEventListener('change', () => {
// Change team access mode
const val = pageContent.querySelector<HTMLInputElement>('input[name=permission]:checked')?.value;
toggleElem(pageContent.querySelectorAll('.team-units'), val !== 'admin');
toggleElem(pageContent.querySelectorAll('.team-units'), val !== 'admin' && val !== 'write');
}));
}