Skip to content

Commit 4dca869

Browse files
Tomeamiszencak-icawxiaoguang
authored
Allow admins and org owners to change org member public status (#28294)
Allows admins and org owners to change org member public status. Before, this would return `Error 403: Cannot publicize another member` despite the fact that the same user could make the same change through the GUI. Fixes #28372 --------- Co-authored-by: Tomáš Ženčák <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent d0688cb commit 4dca869

File tree

3 files changed

+171
-132
lines changed

3 files changed

+171
-132
lines changed

Diff for: routers/api/v1/org/member.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/url"
99

1010
"code.gitea.io/gitea/models/organization"
11+
user_model "code.gitea.io/gitea/models/user"
1112
"code.gitea.io/gitea/modules/setting"
1213
api "code.gitea.io/gitea/modules/structs"
1314
"code.gitea.io/gitea/routers/api/v1/user"
@@ -210,6 +211,20 @@ func IsPublicMember(ctx *context.APIContext) {
210211
}
211212
}
212213

214+
func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model.User) {
215+
// allow user themselves to change their status, and allow admins to change any user
216+
if targetUser.ID == ctx.Doer.ID || ctx.Doer.IsAdmin {
217+
return
218+
}
219+
// allow org owners to change status of members
220+
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
221+
if err != nil {
222+
ctx.APIError(http.StatusInternalServerError, err)
223+
} else if !isOwner {
224+
ctx.APIError(http.StatusForbidden, "Cannot change member visibility")
225+
}
226+
}
227+
213228
// PublicizeMember make a member's membership public
214229
func PublicizeMember(ctx *context.APIContext) {
215230
// swagger:operation PUT /orgs/{org}/public_members/{username} organization orgPublicizeMember
@@ -240,8 +255,8 @@ func PublicizeMember(ctx *context.APIContext) {
240255
if ctx.Written() {
241256
return
242257
}
243-
if userToPublicize.ID != ctx.Doer.ID {
244-
ctx.APIError(http.StatusForbidden, "Cannot publicize another member")
258+
checkCanChangeOrgUserStatus(ctx, userToPublicize)
259+
if ctx.Written() {
245260
return
246261
}
247262
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToPublicize.ID, true)
@@ -282,8 +297,8 @@ func ConcealMember(ctx *context.APIContext) {
282297
if ctx.Written() {
283298
return
284299
}
285-
if userToConceal.ID != ctx.Doer.ID {
286-
ctx.APIError(http.StatusForbidden, "Cannot conceal another member")
300+
checkCanChangeOrgUserStatus(ctx, userToConceal)
301+
if ctx.Written() {
287302
return
288303
}
289304
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToConceal.ID, false)

Diff for: tests/integration/api_org_test.go

+125-103
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"code.gitea.io/gitea/tests"
2323

2424
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
2526
)
2627

2728
func TestAPIOrgCreateRename(t *testing.T) {
@@ -110,121 +111,142 @@ func TestAPIOrgCreateRename(t *testing.T) {
110111
})
111112
}
112113

113-
func TestAPIOrgEdit(t *testing.T) {
114+
func TestAPIOrgGeneral(t *testing.T) {
114115
defer tests.PrepareTestEnv(t)()
115-
session := loginUser(t, "user1")
116-
117-
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
118-
org := api.EditOrgOption{
119-
FullName: "Org3 organization new full name",
120-
Description: "A new description",
121-
Website: "https://try.gitea.io/new",
122-
Location: "Beijing",
123-
Visibility: "private",
124-
}
125-
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
126-
AddTokenAuth(token)
127-
resp := MakeRequest(t, req, http.StatusOK)
116+
user1Session := loginUser(t, "user1")
117+
user1Token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
118+
119+
t.Run("OrgGetAll", func(t *testing.T) {
120+
// accessing with a token will return all orgs
121+
req := NewRequest(t, "GET", "/api/v1/orgs").AddTokenAuth(user1Token)
122+
resp := MakeRequest(t, req, http.StatusOK)
123+
var apiOrgList []*api.Organization
124+
125+
DecodeJSON(t, resp, &apiOrgList)
126+
assert.Len(t, apiOrgList, 13)
127+
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
128+
assert.Equal(t, "limited", apiOrgList[1].Visibility)
129+
130+
// accessing without a token will return only public orgs
131+
req = NewRequest(t, "GET", "/api/v1/orgs")
132+
resp = MakeRequest(t, req, http.StatusOK)
128133

129-
var apiOrg api.Organization
130-
DecodeJSON(t, resp, &apiOrg)
134+
DecodeJSON(t, resp, &apiOrgList)
135+
assert.Len(t, apiOrgList, 9)
136+
assert.Equal(t, "org 17", apiOrgList[0].FullName)
137+
assert.Equal(t, "public", apiOrgList[0].Visibility)
138+
})
131139

132-
assert.Equal(t, "org3", apiOrg.Name)
133-
assert.Equal(t, org.FullName, apiOrg.FullName)
134-
assert.Equal(t, org.Description, apiOrg.Description)
135-
assert.Equal(t, org.Website, apiOrg.Website)
136-
assert.Equal(t, org.Location, apiOrg.Location)
137-
assert.Equal(t, org.Visibility, apiOrg.Visibility)
138-
}
140+
t.Run("OrgEdit", func(t *testing.T) {
141+
org := api.EditOrgOption{
142+
FullName: "Org3 organization new full name",
143+
Description: "A new description",
144+
Website: "https://try.gitea.io/new",
145+
Location: "Beijing",
146+
Visibility: "private",
147+
}
148+
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
149+
resp := MakeRequest(t, req, http.StatusOK)
150+
151+
var apiOrg api.Organization
152+
DecodeJSON(t, resp, &apiOrg)
153+
154+
assert.Equal(t, "org3", apiOrg.Name)
155+
assert.Equal(t, org.FullName, apiOrg.FullName)
156+
assert.Equal(t, org.Description, apiOrg.Description)
157+
assert.Equal(t, org.Website, apiOrg.Website)
158+
assert.Equal(t, org.Location, apiOrg.Location)
159+
assert.Equal(t, org.Visibility, apiOrg.Visibility)
160+
})
139161

140-
func TestAPIOrgEditBadVisibility(t *testing.T) {
141-
defer tests.PrepareTestEnv(t)()
142-
session := loginUser(t, "user1")
143-
144-
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
145-
org := api.EditOrgOption{
146-
FullName: "Org3 organization new full name",
147-
Description: "A new description",
148-
Website: "https://try.gitea.io/new",
149-
Location: "Beijing",
150-
Visibility: "badvisibility",
151-
}
152-
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
153-
AddTokenAuth(token)
154-
MakeRequest(t, req, http.StatusUnprocessableEntity)
155-
}
162+
t.Run("OrgEditBadVisibility", func(t *testing.T) {
163+
org := api.EditOrgOption{
164+
FullName: "Org3 organization new full name",
165+
Description: "A new description",
166+
Website: "https://try.gitea.io/new",
167+
Location: "Beijing",
168+
Visibility: "badvisibility",
169+
}
170+
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
171+
MakeRequest(t, req, http.StatusUnprocessableEntity)
172+
})
156173

157-
func TestAPIOrgDeny(t *testing.T) {
158-
defer tests.PrepareTestEnv(t)()
159-
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
174+
t.Run("OrgDeny", func(t *testing.T) {
175+
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
160176

161-
orgName := "user1_org"
162-
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
163-
MakeRequest(t, req, http.StatusNotFound)
177+
orgName := "user1_org"
178+
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
179+
MakeRequest(t, req, http.StatusNotFound)
164180

165-
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
166-
MakeRequest(t, req, http.StatusNotFound)
181+
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
182+
MakeRequest(t, req, http.StatusNotFound)
167183

168-
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
169-
MakeRequest(t, req, http.StatusNotFound)
170-
}
184+
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
185+
MakeRequest(t, req, http.StatusNotFound)
186+
})
171187

172-
func TestAPIGetAll(t *testing.T) {
173-
defer tests.PrepareTestEnv(t)()
174-
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization)
188+
t.Run("OrgSearchEmptyTeam", func(t *testing.T) {
189+
orgName := "org_with_empty_team"
190+
// create org
191+
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
192+
UserName: orgName,
193+
}).AddTokenAuth(user1Token)
194+
MakeRequest(t, req, http.StatusCreated)
195+
196+
// create team with no member
197+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
198+
Name: "Empty",
199+
IncludesAllRepositories: true,
200+
Permission: "read",
201+
Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
202+
}).AddTokenAuth(user1Token)
203+
MakeRequest(t, req, http.StatusCreated)
204+
205+
// case-insensitive search for teams that have no members
206+
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
207+
AddTokenAuth(user1Token)
208+
resp := MakeRequest(t, req, http.StatusOK)
209+
data := struct {
210+
Ok bool
211+
Data []*api.Team
212+
}{}
213+
DecodeJSON(t, resp, &data)
214+
assert.True(t, data.Ok)
215+
if assert.Len(t, data.Data, 1) {
216+
assert.Equal(t, "Empty", data.Data[0].Name)
217+
}
218+
})
175219

176-
// accessing with a token will return all orgs
177-
req := NewRequest(t, "GET", "/api/v1/orgs").
178-
AddTokenAuth(token)
179-
resp := MakeRequest(t, req, http.StatusOK)
180-
var apiOrgList []*api.Organization
220+
t.Run("User2ChangeStatus", func(t *testing.T) {
221+
user2Session := loginUser(t, "user2")
222+
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteOrganization)
181223

182-
DecodeJSON(t, resp, &apiOrgList)
183-
assert.Len(t, apiOrgList, 13)
184-
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
185-
assert.Equal(t, "limited", apiOrgList[1].Visibility)
224+
req := NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user2").AddTokenAuth(user2Token)
225+
MakeRequest(t, req, http.StatusNoContent)
226+
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user2").AddTokenAuth(user2Token)
227+
MakeRequest(t, req, http.StatusNoContent)
186228

187-
// accessing without a token will return only public orgs
188-
req = NewRequest(t, "GET", "/api/v1/orgs")
189-
resp = MakeRequest(t, req, http.StatusOK)
229+
// non admin but org owner could also change other member's status
230+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
231+
require.False(t, user2.IsAdmin)
232+
req = NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user2Token)
233+
MakeRequest(t, req, http.StatusNoContent)
234+
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user2Token)
235+
MakeRequest(t, req, http.StatusNoContent)
236+
})
190237

191-
DecodeJSON(t, resp, &apiOrgList)
192-
assert.Len(t, apiOrgList, 9)
193-
assert.Equal(t, "org 17", apiOrgList[0].FullName)
194-
assert.Equal(t, "public", apiOrgList[0].Visibility)
195-
}
238+
t.Run("User4ChangeStatus", func(t *testing.T) {
239+
user4Session := loginUser(t, "user4")
240+
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteOrganization)
196241

197-
func TestAPIOrgSearchEmptyTeam(t *testing.T) {
198-
defer tests.PrepareTestEnv(t)()
199-
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
200-
orgName := "org_with_empty_team"
201-
202-
// create org
203-
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
204-
UserName: orgName,
205-
}).AddTokenAuth(token)
206-
MakeRequest(t, req, http.StatusCreated)
207-
208-
// create team with no member
209-
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
210-
Name: "Empty",
211-
IncludesAllRepositories: true,
212-
Permission: "read",
213-
Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
214-
}).AddTokenAuth(token)
215-
MakeRequest(t, req, http.StatusCreated)
216-
217-
// case-insensitive search for teams that have no members
218-
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
219-
AddTokenAuth(token)
220-
resp := MakeRequest(t, req, http.StatusOK)
221-
data := struct {
222-
Ok bool
223-
Data []*api.Team
224-
}{}
225-
DecodeJSON(t, resp, &data)
226-
assert.True(t, data.Ok)
227-
if assert.Len(t, data.Data, 1) {
228-
assert.Equal(t, "Empty", data.Data[0].Name)
229-
}
242+
// user4 is a normal team member, they could change their own status
243+
req := NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user4").AddTokenAuth(user4Token)
244+
MakeRequest(t, req, http.StatusNoContent)
245+
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user4").AddTokenAuth(user4Token)
246+
MakeRequest(t, req, http.StatusNoContent)
247+
req = NewRequest(t, "PUT", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user4Token)
248+
MakeRequest(t, req, http.StatusForbidden)
249+
req = NewRequest(t, "DELETE", "/api/v1/orgs/org3/public_members/user1").AddTokenAuth(user4Token)
250+
MakeRequest(t, req, http.StatusForbidden)
251+
})
230252
}

Diff for: tests/integration/api_team_user_test.go

+27-25
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,31 @@ import (
2121

2222
func TestAPITeamUser(t *testing.T) {
2323
defer tests.PrepareTestEnv(t)()
24-
25-
normalUsername := "user2"
26-
session := loginUser(t, normalUsername)
27-
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
28-
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1").
29-
AddTokenAuth(token)
30-
MakeRequest(t, req, http.StatusNotFound)
31-
32-
req = NewRequest(t, "GET", "/api/v1/teams/1/members/user2").
33-
AddTokenAuth(token)
34-
resp := MakeRequest(t, req, http.StatusOK)
35-
var user2 *api.User
36-
DecodeJSON(t, resp, &user2)
37-
user2.Created = user2.Created.In(time.Local)
38-
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
39-
40-
expectedUser := convert.ToUser(db.DefaultContext, user, user)
41-
42-
// test time via unix timestamp
43-
assert.Equal(t, expectedUser.LastLogin.Unix(), user2.LastLogin.Unix())
44-
assert.Equal(t, expectedUser.Created.Unix(), user2.Created.Unix())
45-
expectedUser.LastLogin = user2.LastLogin
46-
expectedUser.Created = user2.Created
47-
48-
assert.Equal(t, expectedUser, user2)
24+
user2Session := loginUser(t, "user2")
25+
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteOrganization)
26+
27+
t.Run("User2ReadUser1", func(t *testing.T) {
28+
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1").AddTokenAuth(user2Token)
29+
MakeRequest(t, req, http.StatusNotFound)
30+
})
31+
32+
t.Run("User2ReadSelf", func(t *testing.T) {
33+
// read self user
34+
req := NewRequest(t, "GET", "/api/v1/teams/1/members/user2").AddTokenAuth(user2Token)
35+
resp := MakeRequest(t, req, http.StatusOK)
36+
var user2 *api.User
37+
DecodeJSON(t, resp, &user2)
38+
user2.Created = user2.Created.In(time.Local)
39+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
40+
41+
expectedUser := convert.ToUser(db.DefaultContext, user, user)
42+
43+
// test time via unix timestamp
44+
assert.Equal(t, expectedUser.LastLogin.Unix(), user2.LastLogin.Unix())
45+
assert.Equal(t, expectedUser.Created.Unix(), user2.Created.Unix())
46+
expectedUser.LastLogin = user2.LastLogin
47+
expectedUser.Created = user2.Created
48+
49+
assert.Equal(t, expectedUser, user2)
50+
})
4951
}

0 commit comments

Comments
 (0)