Browse Source

Merge 39ce160ea2 into 13f012fb81

pull/4408/merge
Corentin Pitrel 4 days ago committed by GitHub
parent
commit
05328bcf0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 146
      connector/gitlab/gitlab.go
  2. 199
      connector/gitlab/gitlab_test.go

146
connector/gitlab/gitlab.go

@ -26,6 +26,8 @@ const (
// used to retrieve groups from /oauth/userinfo
// https://docs.gitlab.com/ee/integration/openid_connect_provider.html
scopeOpenID = "openid"
// read operations of the /api/v4/groups endpoint
scopeReadApi = "read_api"
)
// Config holds configuration options for gitlab logins.
@ -110,7 +112,7 @@ type gitlabConnector struct {
func (c *gitlabConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
gitlabScopes := []string{scopeUser}
if c.groupsRequired(scopes.Groups) {
gitlabScopes = []string{scopeUser, scopeOpenID}
gitlabScopes = []string{scopeUser, scopeOpenID, scopeReadApi}
}
gitlabEndpoint := oauth2.Endpoint{AuthURL: c.baseURL + "/oauth/authorize", TokenURL: c.baseURL + "/oauth/token"}
@ -189,7 +191,7 @@ func (c *gitlabConnector) identity(ctx context.Context, s connector.Scopes, toke
}
if c.groupsRequired(s.Groups) {
groups, err := c.getGroups(ctx, client, s.Groups, user.Username)
groups, err := c.getGroups(ctx, client, s.Groups, user.Username, user.ID)
if err != nil {
return identity, fmt.Errorf("gitlab: get groups: %v", err)
}
@ -306,19 +308,68 @@ func (c *gitlabConnector) user(ctx context.Context, client *http.Client) (gitlab
return u, nil
}
type userInfo struct {
Groups []string `json:"groups"`
OwnerPermission []string `json:"https://gitlab.org/claims/groups/owner"`
MaintainerPermission []string `json:"https://gitlab.org/claims/groups/maintainer"`
DeveloperPermission []string `json:"https://gitlab.org/claims/groups/developer"`
type group struct {
ID int `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
FullName string `json:"full_name"`
FullPath string `json:"full_path"`
}
// userGroups queries the GitLab API for group membership.
//
// The HTTP passed client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func (c *gitlabConnector) userGroups(ctx context.Context, client *http.Client) ([]string, error) {
req, err := http.NewRequest("GET", c.baseURL+"/oauth/userinfo", nil)
func (c *gitlabConnector) userGroups(ctx context.Context, client *http.Client, userId int) ([]string, error) {
groupsRaw, err := c.getGitlabGroups(ctx, client)
if err != nil {
return []string{}, err
}
groups := []string{}
for _, group := range groupsRaw {
groupMembership, notMember, err := c.getGroupsMembership(ctx, client, group.ID, userId)
if err != nil {
return []string{}, fmt.Errorf("gitlab: get group membership: %v", err)
}
if _, ok := groupMembership["access_level"]; notMember || !ok {
continue
}
groups = append(groups, group.FullPath)
if !c.getGroupsPermission {
continue
}
switch groupMembership["access_level"].(float64) {
case 10:
groups = append(groups, fmt.Sprintf("%s:guest", group.FullPath))
break
case 20:
groups = append(groups, fmt.Sprintf("%s:reporter", group.FullPath))
break
case 30:
groups = append(groups, fmt.Sprintf("%s:developer", group.FullPath))
break
case 40:
groups = append(groups, fmt.Sprintf("%s:maintainer", group.FullPath))
break
case 50:
groups = append(groups, fmt.Sprintf("%s:owner", group.FullPath))
break
case 60:
groups = append(groups, fmt.Sprintf("%s:admin", group.FullPath))
break
}
}
return groups, nil
}
func (c *gitlabConnector) getGitlabGroups(ctx context.Context, client *http.Client) ([]group, error) {
var group []group
req, err := http.NewRequest("GET", c.baseURL+"/api/v4/groups", nil)
if err != nil {
return nil, fmt.Errorf("gitlab: new req: %v", err)
}
@ -336,69 +387,36 @@ func (c *gitlabConnector) userGroups(ctx context.Context, client *http.Client) (
}
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var u userInfo
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
if err = json.NewDecoder(resp.Body).Decode(&group); err != nil {
return nil, fmt.Errorf("failed to decode response: %v", err)
}
if c.getGroupsPermission {
groups := c.setGroupsPermission(u)
return groups, nil
}
return u.Groups, nil
return group, nil
}
func (c *gitlabConnector) setGroupsPermission(u userInfo) []string {
groups := u.Groups
L1:
for _, g := range groups {
for _, op := range u.OwnerPermission {
if g == op {
groups = append(groups, fmt.Sprintf("%s:owner", g))
continue L1
}
if len(g) > len(op) {
if g[0:len(op)] == op && string(g[len(op)]) == "/" {
groups = append(groups, fmt.Sprintf("%s:owner", g))
continue L1
}
}
}
for _, mp := range u.MaintainerPermission {
if g == mp {
groups = append(groups, fmt.Sprintf("%s:maintainer", g))
continue L1
}
if len(g) > len(mp) {
if g[0:len(mp)] == mp && string(g[len(mp)]) == "/" {
groups = append(groups, fmt.Sprintf("%s:maintainer", g))
continue L1
}
}
}
for _, dp := range u.DeveloperPermission {
if g == dp {
groups = append(groups, fmt.Sprintf("%s:developer", g))
continue L1
}
if len(g) > len(dp) {
if g[0:len(dp)] == dp && string(g[len(dp)]) == "/" {
groups = append(groups, fmt.Sprintf("%s:developer", g))
continue L1
}
}
}
func (c *gitlabConnector) getGroupsMembership(ctx context.Context, client *http.Client, groupId int, userId int) (map[string]interface{}, bool, error) {
var groupMembership map[string]interface{}
req, err := http.NewRequest("GET", c.baseURL+fmt.Sprintf("/api/v4/groups/%d/members/all/%d", groupId, userId), nil)
if err != nil {
return map[string]interface{}{}, false, fmt.Errorf("gitlab: new req: %v", err)
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return map[string]interface{}{}, false, fmt.Errorf("gitlab: get URL %v", err)
}
defer resp.Body.Close()
return groups
if resp.StatusCode != http.StatusOK {
return map[string]interface{}{}, true, nil
}
if err = json.NewDecoder(resp.Body).Decode(&groupMembership); err != nil {
return map[string]interface{}{}, false, fmt.Errorf("failed to decode response: %v", err)
}
return groupMembership, false, nil
}
func (c *gitlabConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) {
gitlabGroups, err := c.userGroups(ctx, client)
func (c *gitlabConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string, userId int) ([]string, error) {
gitlabGroups, err := c.userGroups(ctx, client, userId)
if err != nil {
return nil, err
}

199
connector/gitlab/gitlab_test.go

@ -178,14 +178,33 @@ func TestHandleCallbackWithoutRootCADataFailsTLS(t *testing.T) {
func TestUserGroups(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/oauth/userinfo": userInfo{
Groups: []string{"team-1", "team-2"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
{
ID: 2,
Name: "team-2",
FullName: "team-2",
Path: "team-2",
FullPath: "team-2",
},
},
"/api/v4/groups/1/members/all/1": map[string]interface{}{
"access_level": 50,
},
"/api/v4/groups/2/members/all/1": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
c := gitlabConnector{baseURL: s.URL}
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs")
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs", 1)
expectNil(t, err)
expectEquals(t, groups, []string{
@ -196,14 +215,33 @@ func TestUserGroups(t *testing.T) {
func TestUserGroupsWithFiltering(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/oauth/userinfo": userInfo{
Groups: []string{"team-1", "team-2"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
{
ID: 2,
Name: "team-2",
FullName: "team-2",
Path: "team-2",
FullPath: "team-2",
},
},
"/api/v4/groups/1/members/all/1": map[string]interface{}{
"access_level": 50,
},
"/api/v4/groups/2/members/all/1": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
c := gitlabConnector{baseURL: s.URL, groups: []string{"team-1"}}
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs")
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs", 1)
expectNil(t, err)
expectEquals(t, groups, []string{
@ -213,14 +251,12 @@ func TestUserGroupsWithFiltering(t *testing.T) {
func TestUserGroupsWithoutOrgs(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/oauth/userinfo": userInfo{
Groups: []string{},
},
"/api/v4/groups": []group{},
})
defer s.Close()
c := gitlabConnector{baseURL: s.URL}
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs")
groups, err := c.getGroups(context.Background(), newClient(), true, "joebloggs", 1)
expectNil(t, err)
expectEquals(t, len(groups), 0)
@ -234,8 +270,17 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -270,8 +315,17 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/1": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -297,8 +351,17 @@ func TestLoginWithTeamWhitelisted(t *testing.T) {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -324,8 +387,17 @@ func TestLoginWithTeamNonWhitelisted(t *testing.T) {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -351,8 +423,17 @@ func TestRefresh(t *testing.T) {
"refresh_token": "oRzxVjCnohYRHEYEhZshkmakKmoyVoTjfUGC",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -392,8 +473,17 @@ func TestRefreshWithEmptyConnectorData(t *testing.T) {
"refresh_token": "oRzxVjCnohYRHEYEhZshkmakKmoyVoTjfUGC",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"team-1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "team-1",
FullName: "team-1",
Path: "team-1",
FullPath: "team-1",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
})
defer s.Close()
@ -419,11 +509,57 @@ func TestGroupsWithPermission(t *testing.T) {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
"/oauth/userinfo": userInfo{
Groups: []string{"ops", "dev", "ops-test", "ops/project", "dev/project1", "dev/project2"},
OwnerPermission: []string{"ops"},
DeveloperPermission: []string{"dev"},
MaintainerPermission: []string{"dev/project1"},
"/api/v4/groups": []group{
{
ID: 1,
Name: "ops",
FullName: "ops",
Path: "ops",
FullPath: "ops",
},
{
ID: 2,
Name: "dev",
FullName: "dev",
Path: "dev",
FullPath: "dev",
},
{
ID: 3,
Name: "ops/project",
FullName: "ops/project",
Path: "ops/project",
FullPath: "ops/project",
},
{
ID: 4,
Name: "dev/project1",
FullName: "dev/project1",
Path: "dev/project1",
FullPath: "dev/project1",
},
{
ID: 5,
Name: "dev/project2",
FullName: "dev/project2",
Path: "dev/project2",
FullPath: "dev/project2",
},
},
"/api/v4/groups/1/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
"/api/v4/groups/2/members/all/12345678": map[string]interface{}{
"access_level": 30,
},
"/api/v4/groups/3/members/all/12345678": map[string]interface{}{
"access_level": 50,
},
"/api/v4/groups/4/members/all/12345678": map[string]interface{}{
"access_level": 40,
},
"/api/v4/groups/5/members/all/12345678": map[string]interface{}{
"access_level": 30,
},
})
defer s.Close()
@ -440,15 +576,14 @@ func TestGroupsWithPermission(t *testing.T) {
expectEquals(t, identity.Groups, []string{
"ops",
"dev",
"ops-test",
"ops/project",
"dev/project1",
"dev/project2",
"ops:owner",
"dev",
"dev:developer",
"ops/project",
"ops/project:owner",
"dev/project1",
"dev/project1:maintainer",
"dev/project2",
"dev/project2:developer",
})
}

Loading…
Cancel
Save