mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 03:41:01 +00:00
Consider gitlab inherited permissions (#3308)
The gitlab projects endpoint does not include information about permissions granted by namespace memberships. To get this information a separate query to https://docs.gitlab.com/ee/api/members.html#get-a-member-of-a-group-or-project-including-inherited-and-invited-members is necessary.
This commit is contained in:
parent
c7467b9828
commit
db4a50951c
6 changed files with 94 additions and 46 deletions
|
@ -31,7 +31,7 @@ const (
|
||||||
mergeRefs = "refs/merge-requests/%d/head" // merge request merged with base
|
mergeRefs = "refs/merge-requests/%d/head" // merge request merged with base
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *GitLab) convertGitLabRepo(_repo *gitlab.Project) (*model.Repo, error) {
|
func (g *GitLab) convertGitLabRepo(_repo *gitlab.Project, projectMember *gitlab.ProjectMember) (*model.Repo, error) {
|
||||||
parts := strings.Split(_repo.PathWithNamespace, "/")
|
parts := strings.Split(_repo.PathWithNamespace, "/")
|
||||||
owner := strings.Join(parts[:len(parts)-1], "/")
|
owner := strings.Join(parts[:len(parts)-1], "/")
|
||||||
name := parts[len(parts)-1]
|
name := parts[len(parts)-1]
|
||||||
|
@ -48,9 +48,9 @@ func (g *GitLab) convertGitLabRepo(_repo *gitlab.Project) (*model.Repo, error) {
|
||||||
Visibility: model.RepoVisibility(_repo.Visibility),
|
Visibility: model.RepoVisibility(_repo.Visibility),
|
||||||
IsSCMPrivate: !_repo.Public,
|
IsSCMPrivate: !_repo.Public,
|
||||||
Perm: &model.Perm{
|
Perm: &model.Perm{
|
||||||
Pull: isRead(_repo),
|
Pull: isRead(_repo, projectMember),
|
||||||
Push: isWrite(_repo),
|
Push: isWrite(projectMember),
|
||||||
Admin: isAdmin(_repo),
|
Admin: isAdmin(projectMember),
|
||||||
},
|
},
|
||||||
PREnabled: _repo.MergeRequestsEnabled,
|
PREnabled: _repo.MergeRequestsEnabled,
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,6 +246,20 @@ func (g *GitLab) getProject(ctx context.Context, client *gitlab.Client, forgeRem
|
||||||
return repo, err
|
return repo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GitLab) getInheritedProjectMember(ctx context.Context, client *gitlab.Client, forgeRemoteID model.ForgeRemoteID, owner, name string, userID int) (*gitlab.ProjectMember, error) {
|
||||||
|
if forgeRemoteID.IsValid() {
|
||||||
|
intID, err := strconv.Atoi(string(forgeRemoteID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
projectMember, _, err := client.ProjectMembers.GetInheritedProjectMember(intID, userID, gitlab.WithContext(ctx))
|
||||||
|
return projectMember, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectMember, _, err := client.ProjectMembers.GetInheritedProjectMember(fmt.Sprintf("%s/%s", owner, name), userID, gitlab.WithContext(ctx))
|
||||||
|
return projectMember, err
|
||||||
|
}
|
||||||
|
|
||||||
// Repo fetches the repository from the forge.
|
// Repo fetches the repository from the forge.
|
||||||
func (g *GitLab) Repo(ctx context.Context, user *model.User, remoteID model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
|
func (g *GitLab) Repo(ctx context.Context, user *model.User, remoteID model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
|
||||||
client, err := newClient(g.url, user.Token, g.SkipVerify)
|
client, err := newClient(g.url, user.Token, g.SkipVerify)
|
||||||
|
@ -258,7 +272,17 @@ func (g *GitLab) Repo(ctx context.Context, user *model.User, remoteID model.Forg
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.convertGitLabRepo(_repo)
|
intUserID, err := strconv.Atoi(string(user.ForgeRemoteID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectMember, err := g.getInheritedProjectMember(ctx, client, remoteID, owner, name, intUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.convertGitLabRepo(_repo, projectMember)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repos fetches a list of repos from the forge.
|
// Repos fetches a list of repos from the forge.
|
||||||
|
@ -276,6 +300,10 @@ func (g *GitLab) Repos(ctx context.Context, user *model.User) ([]*model.Repo, er
|
||||||
if g.HideArchives {
|
if g.HideArchives {
|
||||||
opts.Archived = gitlab.Ptr(false)
|
opts.Archived = gitlab.Ptr(false)
|
||||||
}
|
}
|
||||||
|
intUserID, err := strconv.Atoi(string(user.ForgeRemoteID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for i := 1; true; i++ {
|
for i := 1; true; i++ {
|
||||||
opts.Page = i
|
opts.Page = i
|
||||||
|
@ -285,7 +313,12 @@ func (g *GitLab) Repos(ctx context.Context, user *model.User) ([]*model.Repo, er
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range batch {
|
for i := range batch {
|
||||||
repo, err := g.convertGitLabRepo(batch[i])
|
projectMember, _, err := client.ProjectMembers.GetInheritedProjectMember(batch[i].ID, intUserID, gitlab.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := g.convertGitLabRepo(batch[i], projectMember)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,9 @@ func Test_GitLab(t *testing.T) {
|
||||||
client := load(env)
|
client := load(env)
|
||||||
|
|
||||||
user := model.User{
|
user := model.User{
|
||||||
Login: "test_user",
|
Login: "test_user",
|
||||||
Token: "e3b0c44298fc1c149afbf4c8996fb",
|
Token: "e3b0c44298fc1c149afbf4c8996fb",
|
||||||
|
ForgeRemoteID: "3",
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := model.Repo{
|
repo := model.Repo{
|
||||||
|
@ -102,6 +103,12 @@ func Test_GitLab(t *testing.T) {
|
||||||
_, err := client.Repo(ctx, &user, "0", "not-existed", "not-existed")
|
_, err := client.Repo(ctx, &user, "0", "not-existed", "not-existed")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
g.It("Should return repo with push access, when user inherits membership from namespace", func() {
|
||||||
|
_repo, err := client.Repo(ctx, &user, "6", "brightbox", "puppet")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, _repo.Perm.Push)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test activate method
|
// Test activate method
|
||||||
|
|
|
@ -39,50 +39,18 @@ func newClient(url, accessToken string, skipVerify bool) (*gitlab.Client, error)
|
||||||
|
|
||||||
// isRead is a helper function that returns true if the
|
// isRead is a helper function that returns true if the
|
||||||
// user has Read-only access to the repository.
|
// user has Read-only access to the repository.
|
||||||
func isRead(proj *gitlab.Project) bool {
|
func isRead(proj *gitlab.Project, projectMember *gitlab.ProjectMember) bool {
|
||||||
user := proj.Permissions.ProjectAccess
|
return proj.Public || projectMember != nil && projectMember.AccessLevel >= gitlab.ReporterPermissions
|
||||||
group := proj.Permissions.GroupAccess
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case proj.Public:
|
|
||||||
return true
|
|
||||||
case user != nil && user.AccessLevel >= 20:
|
|
||||||
return true
|
|
||||||
case group != nil && group.AccessLevel >= 20:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isWrite is a helper function that returns true if the
|
// isWrite is a helper function that returns true if the
|
||||||
// user has Read-Write access to the repository.
|
// user has Read-Write access to the repository.
|
||||||
func isWrite(proj *gitlab.Project) bool {
|
func isWrite(projectMember *gitlab.ProjectMember) bool {
|
||||||
user := proj.Permissions.ProjectAccess
|
return projectMember != nil && projectMember.AccessLevel >= gitlab.DeveloperPermissions
|
||||||
group := proj.Permissions.GroupAccess
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case user != nil && user.AccessLevel >= 30:
|
|
||||||
return true
|
|
||||||
case group != nil && group.AccessLevel >= 30:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAdmin is a helper function that returns true if the
|
// isAdmin is a helper function that returns true if the
|
||||||
// user has Admin access to the repository.
|
// user has Admin access to the repository.
|
||||||
func isAdmin(proj *gitlab.Project) bool {
|
func isAdmin(projectMember *gitlab.ProjectMember) bool {
|
||||||
user := proj.Permissions.ProjectAccess
|
return projectMember != nil && projectMember.AccessLevel >= gitlab.MaintainerPermissions
|
||||||
group := proj.Permissions.GroupAccess
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case user != nil && user.AccessLevel >= 40:
|
|
||||||
return true
|
|
||||||
case group != nil && group.AccessLevel >= 40:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
30
server/forge/gitlab/testdata/projects.go
vendored
30
server/forge/gitlab/testdata/projects.go
vendored
|
@ -304,3 +304,33 @@ var project4PayloadHooks = []byte(`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
var project4PayloadMembers = []byte(`
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"username": "some_user",
|
||||||
|
"name": "Diaspora",
|
||||||
|
"state": "active",
|
||||||
|
"locked": false,
|
||||||
|
"avatar_url": "https://example.com/uploads/-/system/user/avatar/3/avatar.png",
|
||||||
|
"web_url": "https://example.com/some_user",
|
||||||
|
"access_level": 50,
|
||||||
|
"created_at": "2024-01-16T12:39:58.912Z",
|
||||||
|
"expires_at": null
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
var project6PayloadMembers = []byte(`
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"username": "some_user",
|
||||||
|
"name": "Diaspora",
|
||||||
|
"state": "active",
|
||||||
|
"locked": false,
|
||||||
|
"avatar_url": "https://example.com/uploads/-/system/user/avatar/3/avatar.png",
|
||||||
|
"web_url": "https://example.com/some_user",
|
||||||
|
"access_level": 30,
|
||||||
|
"created_at": "2024-01-16T12:39:58.912Z",
|
||||||
|
"expires_at": null
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
10
server/forge/gitlab/testdata/testdata.go
vendored
10
server/forge/gitlab/testdata/testdata.go
vendored
|
@ -46,6 +46,7 @@ func NewServer(t *testing.T) *httptest.Server {
|
||||||
w.Write(project4Payload)
|
w.Write(project4Payload)
|
||||||
return
|
return
|
||||||
case "/api/v4/projects/brightbox/puppet":
|
case "/api/v4/projects/brightbox/puppet":
|
||||||
|
case "/api/v4/projects/6":
|
||||||
w.Write(project6Payload)
|
w.Write(project6Payload)
|
||||||
return
|
return
|
||||||
case "/api/v4/projects/4/hooks":
|
case "/api/v4/projects/4/hooks":
|
||||||
|
@ -60,6 +61,15 @@ func NewServer(t *testing.T) *httptest.Server {
|
||||||
case "/api/v4/projects/4/hooks/10717088":
|
case "/api/v4/projects/4/hooks/10717088":
|
||||||
w.WriteHeader(201)
|
w.WriteHeader(201)
|
||||||
return
|
return
|
||||||
|
case "/api/v4/projects/4/members/all/3":
|
||||||
|
w.Write(project4PayloadMembers)
|
||||||
|
return
|
||||||
|
case "/api/v4/projects/diaspora/diaspora-client/members/all/3":
|
||||||
|
w.Write(project4PayloadMembers)
|
||||||
|
return
|
||||||
|
case "/api/v4/projects/6/members/all/3":
|
||||||
|
w.Write(project6PayloadMembers)
|
||||||
|
return
|
||||||
case "/oauth/token":
|
case "/oauth/token":
|
||||||
w.Write(accessTokenPayload)
|
w.Write(accessTokenPayload)
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue