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:
Lukas 2024-02-06 00:10:23 +01:00 committed by GitHub
parent c7467b9828
commit db4a50951c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 94 additions and 46 deletions

View file

@ -31,7 +31,7 @@ const (
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, "/")
owner := strings.Join(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),
IsSCMPrivate: !_repo.Public,
Perm: &model.Perm{
Pull: isRead(_repo),
Push: isWrite(_repo),
Admin: isAdmin(_repo),
Pull: isRead(_repo, projectMember),
Push: isWrite(projectMember),
Admin: isAdmin(projectMember),
},
PREnabled: _repo.MergeRequestsEnabled,
}

View file

@ -246,6 +246,20 @@ func (g *GitLab) getProject(ctx context.Context, client *gitlab.Client, forgeRem
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.
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)
@ -258,7 +272,17 @@ func (g *GitLab) Repo(ctx context.Context, user *model.User, remoteID model.Forg
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.
@ -276,6 +300,10 @@ func (g *GitLab) Repos(ctx context.Context, user *model.User) ([]*model.Repo, er
if g.HideArchives {
opts.Archived = gitlab.Ptr(false)
}
intUserID, err := strconv.Atoi(string(user.ForgeRemoteID))
if err != nil {
return nil, err
}
for i := 1; true; 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 {
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 {
return nil, err
}

View file

@ -58,8 +58,9 @@ func Test_GitLab(t *testing.T) {
client := load(env)
user := model.User{
Login: "test_user",
Token: "e3b0c44298fc1c149afbf4c8996fb",
Login: "test_user",
Token: "e3b0c44298fc1c149afbf4c8996fb",
ForgeRemoteID: "3",
}
repo := model.Repo{
@ -102,6 +103,12 @@ func Test_GitLab(t *testing.T) {
_, err := client.Repo(ctx, &user, "0", "not-existed", "not-existed")
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

View file

@ -39,50 +39,18 @@ func newClient(url, accessToken string, skipVerify bool) (*gitlab.Client, error)
// isRead is a helper function that returns true if the
// user has Read-only access to the repository.
func isRead(proj *gitlab.Project) bool {
user := proj.Permissions.ProjectAccess
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
}
func isRead(proj *gitlab.Project, projectMember *gitlab.ProjectMember) bool {
return proj.Public || projectMember != nil && projectMember.AccessLevel >= gitlab.ReporterPermissions
}
// isWrite is a helper function that returns true if the
// user has Read-Write access to the repository.
func isWrite(proj *gitlab.Project) bool {
user := proj.Permissions.ProjectAccess
group := proj.Permissions.GroupAccess
switch {
case user != nil && user.AccessLevel >= 30:
return true
case group != nil && group.AccessLevel >= 30:
return true
default:
return false
}
func isWrite(projectMember *gitlab.ProjectMember) bool {
return projectMember != nil && projectMember.AccessLevel >= gitlab.DeveloperPermissions
}
// isAdmin is a helper function that returns true if the
// user has Admin access to the repository.
func isAdmin(proj *gitlab.Project) bool {
user := proj.Permissions.ProjectAccess
group := proj.Permissions.GroupAccess
switch {
case user != nil && user.AccessLevel >= 40:
return true
case group != nil && group.AccessLevel >= 40:
return true
default:
return false
}
func isAdmin(projectMember *gitlab.ProjectMember) bool {
return projectMember != nil && projectMember.AccessLevel >= gitlab.MaintainerPermissions
}

View file

@ -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
}
`)

View file

@ -46,6 +46,7 @@ func NewServer(t *testing.T) *httptest.Server {
w.Write(project4Payload)
return
case "/api/v4/projects/brightbox/puppet":
case "/api/v4/projects/6":
w.Write(project6Payload)
return
case "/api/v4/projects/4/hooks":
@ -60,6 +61,15 @@ func NewServer(t *testing.T) *httptest.Server {
case "/api/v4/projects/4/hooks/10717088":
w.WriteHeader(201)
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":
w.Write(accessTokenPayload)
return