mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-27 03:21:25 +01:00
fix(org,team): guard the owner team rename, show permission-only team access and tidy member lookups
Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
10 files changed
+124
-12
No files matched your search
@@ -163,7 +163,7 @@ func (s Organizations) Create(ctx context.Context, opts OrganizationOptions) (Or
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return Organization{}, Classify(err, "organization", "", "")
|
||||
return Organization{}, Classify(err, "name", strconv.Quote(deref(opts.Name)), "")
|
||||
}
|
||||
return organizationFrom(org), nil
|
||||
}
|
||||
@@ -285,7 +285,15 @@ func (s Organizations) IsPublicMember(ctx context.Context, name, username string
|
||||
// check runs a membership check that answers no with a 404.
|
||||
func (s Organizations) check(ctx context.Context, op func(context.Context, *gitea.Client) (*gitea.Response, error)) (bool, error) {
|
||||
err := do(ctx, s.Client, op)
|
||||
// Gitea redirects callers who are not members, and tea does not follow the redirect.
|
||||
if failure, ok := errors.AsType[*transport.APIError](err); ok && failure.Status == http.StatusSeeOther {
|
||||
return false, &transport.APIError{Status: http.StatusForbidden, Method: failure.Method, Endpoint: failure.Endpoint, Message: "only organization members can check memberships"}
|
||||
}
|
||||
if IsNotFound(err) {
|
||||
// A mistyped user is not a missing membership.
|
||||
if missing, ok := errors.AsType[*NotFoundError](Classify(err, "", "", "")); ok && missing.Kind == "user" {
|
||||
return false, missing
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
|
||||
@@ -108,6 +108,38 @@ func TestOrganizationCreateSendsSuppliedPreferences(t *testing.T) {
|
||||
require.Equal(t, Organization{ID: 5, Name: "acme", Visibility: "limited", RepoAdminChangeTeamAccess: true}, item)
|
||||
}
|
||||
|
||||
func TestOrganizationMembershipChecksReportWhatGiteaRefuses(t *testing.T) {
|
||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/orgs/acme/members/bob":
|
||||
http.Redirect(w, r, "/org/acme/public_members", http.StatusSeeOther)
|
||||
case "/api/v1/orgs/acme/members/bbo":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"user does not exist [uid: 0, name: bbo]"}`))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
service := Organizations{Client: client}
|
||||
_, err := service.IsMember(context.Background(), "acme", "bob")
|
||||
require.True(t, IsForbidden(err), "%v", err)
|
||||
_, err = service.IsMember(context.Background(), "acme", "bbo")
|
||||
require.EqualError(t, err, `user "bbo" not found`)
|
||||
member, err := service.IsMember(context.Background(), "acme", "carol")
|
||||
require.NoError(t, err)
|
||||
require.False(t, member)
|
||||
}
|
||||
|
||||
func TestOrganizationCreateNamesTheTakenName(t *testing.T) {
|
||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
_, _ = w.Write([]byte(`{"message":"user already exists [name: bob]"}`))
|
||||
}))
|
||||
name := "bob"
|
||||
_, err := (Organizations{Client: client}).Create(context.Background(), OrganizationOptions{Name: &name})
|
||||
require.EqualError(t, err, `name "bob" already exists`)
|
||||
}
|
||||
|
||||
func TestOrganizationMembershipRequests(t *testing.T) {
|
||||
var requests []string
|
||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -67,8 +67,8 @@ func (t Team) Access() string {
|
||||
var level string
|
||||
var granted []string
|
||||
for _, unit := range TeamUnits {
|
||||
access := t.Units[unit]
|
||||
if access == "" || access == "none" {
|
||||
access := t.UnitLevel(unit)
|
||||
if access == "none" {
|
||||
continue
|
||||
}
|
||||
if level != "" && access != level {
|
||||
@@ -86,6 +86,22 @@ func (t Team) Access() string {
|
||||
return level + " (" + strings.Join(granted, ", ") + ")"
|
||||
}
|
||||
|
||||
// UnitLevel is the access the team has to unit: the higher of the unit's own level and the team
|
||||
// permission, which Gitea grants on every unit, capped at the levels the unit allows.
|
||||
func (t Team) UnitLevel(unit string) string {
|
||||
levels := UnitAccess(unit)
|
||||
level := "none"
|
||||
for _, candidate := range []string{t.Units[unit], t.Permission} {
|
||||
if candidate == "write" && !slices.Contains(levels, "write") {
|
||||
candidate = "read"
|
||||
}
|
||||
if slices.Index(levels, candidate) > slices.Index(levels, level) {
|
||||
level = candidate
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
// Slug names the team as ORG/TEAM, or by name alone when its organization is unknown.
|
||||
func (t Team) Slug() string {
|
||||
if t.Organization == "" {
|
||||
@@ -273,12 +289,23 @@ func (s Teams) Delete(ctx context.Context, id int64) error {
|
||||
})
|
||||
}
|
||||
|
||||
// TeamMember identifies a team member without exposing private profile fields.
|
||||
type TeamMember struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
|
||||
func teamMemberFrom(user *gitea.User) TeamMember {
|
||||
return TeamMember{ID: user.ID, Username: user.UserName, FullName: user.FullName}
|
||||
}
|
||||
|
||||
// Members lists members of a team.
|
||||
func (s Teams) Members(ctx context.Context, id int64, limit, page int) (Page[OrganizationMember], error) {
|
||||
func (s Teams) Members(ctx context.Context, id int64, limit, page int) (Page[TeamMember], error) {
|
||||
users, err := callPage(ctx, s.Client, limit, page, func(ctx context.Context, c *gitea.Client, list gitea.ListOptions) ([]*gitea.User, *gitea.Response, error) {
|
||||
return c.Organizations.ListTeamMembers(ctx, id, gitea.ListTeamMembersOptions{ListOptions: list})
|
||||
})
|
||||
return convertPage(users, memberFrom), err
|
||||
return convertPage(users, teamMemberFrom), err
|
||||
}
|
||||
|
||||
// SetMember adds or removes one team membership.
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestTeamAccessRequests(t *testing.T) {
|
||||
service := Teams{Client: client}
|
||||
members, err := service.Members(context.Background(), 3, 5, 2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []OrganizationMember{{ID: 2, Username: "bob"}}, members.Items)
|
||||
require.Equal(t, []TeamMember{{ID: 2, Username: "bob"}}, members.Items)
|
||||
repos, err := service.Repositories(context.Background(), 3, 5, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://gitea.test/acme/project", repos.Items[0].URL)
|
||||
@@ -119,6 +119,15 @@ func TestTeamFromKeepsAbsentValuesEmpty(t *testing.T) {
|
||||
require.Equal(t, Team{ID: 1, Name: "Owners", Permission: "owner", Units: map[string]string{}}, item)
|
||||
}
|
||||
|
||||
func TestTeamUnitLevelAppliesThePermissionToEveryUnit(t *testing.T) {
|
||||
team := Team{Permission: "write", Units: map[string]string{"repo.wiki": "none"}}
|
||||
require.Equal(t, "write", team.UnitLevel("repo.code"))
|
||||
require.Equal(t, "write", team.UnitLevel("repo.wiki"))
|
||||
require.Equal(t, "read", team.UnitLevel("repo.ext_wiki"))
|
||||
require.Equal(t, "write", Team{Permission: "none", Units: map[string]string{"repo.code": "write"}}.UnitLevel("repo.code"))
|
||||
require.Equal(t, "none", Team{Permission: "none"}.UnitLevel("repo.code"))
|
||||
}
|
||||
|
||||
func TestTeamAccessSummarizesUnits(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
permission string
|
||||
@@ -132,6 +141,11 @@ func TestTeamAccessSummarizesUnits(t *testing.T) {
|
||||
{"none", map[string]string{"repo.code": "write", "repo.issues": "write", "repo.pulls": "write", "repo.wiki": "none", "repo.ext_wiki": "read"}, "write (code, issues, pulls)"},
|
||||
{"none", map[string]string{"repo.code": "write", "repo.wiki": "read"}, "custom"},
|
||||
{"none", map[string]string{"repo.ext_issues": "read"}, "none"},
|
||||
// Gitea grants the team permission on every unit when the team has no unit rows.
|
||||
{"write", nil, "write"},
|
||||
{"read", map[string]string{}, "read"},
|
||||
{"write", map[string]string{"repo.wiki": "read"}, "write"},
|
||||
{"read", map[string]string{"repo.code": "write"}, "custom"},
|
||||
} {
|
||||
require.Equal(t, tc.want, Team{Permission: tc.permission, Units: tc.units}.Access(), "%v", tc.units)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user