From 3e00d33449ed56b52e23490cf7730aad36e203eb Mon Sep 17 00:00:00 2001
From: Vladimir <31961982+zvlb@users.noreply.github.com>
Date: Thu, 24 Oct 2024 23:18:24 +0300
Subject: [PATCH] GitLab connector: add GitLab additional group with role 
 (#2941)

Signed-off-by: zvlb <vl.zemtsov@gmail.com>
Signed-off-by: Maksim Nabokikh <maksim.nabokikh@flant.com>
Signed-off-by: Maksim Nabokikh <max.nabokih@gmail.com>
Co-authored-by: Maksim Nabokikh <maksim.nabokikh@flant.com>
Co-authored-by: Maksim Nabokikh <max.nabokih@gmail.com>
---
 connector/gitlab/gitlab.go      | 89 +++++++++++++++++++++++++++------
 connector/gitlab/gitlab_test.go | 41 +++++++++++++++
 2 files changed, 116 insertions(+), 14 deletions(-)

diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go
index fdb2c482..7aa44398 100644
--- a/connector/gitlab/gitlab.go
+++ b/connector/gitlab/gitlab.go
@@ -28,12 +28,13 @@ const (
 
 // Config holds configuration options for gitlab logins.
 type Config struct {
-	BaseURL      string   `json:"baseURL"`
-	ClientID     string   `json:"clientID"`
-	ClientSecret string   `json:"clientSecret"`
-	RedirectURI  string   `json:"redirectURI"`
-	Groups       []string `json:"groups"`
-	UseLoginAsID bool     `json:"useLoginAsID"`
+	BaseURL             string   `json:"baseURL"`
+	ClientID            string   `json:"clientID"`
+	ClientSecret        string   `json:"clientSecret"`
+	RedirectURI         string   `json:"redirectURI"`
+	Groups              []string `json:"groups"`
+	UseLoginAsID        bool     `json:"useLoginAsID"`
+	GetGroupsPermission bool     `json:"getGroupsPermission"`
 }
 
 type gitlabUser struct {
@@ -51,13 +52,14 @@ func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, erro
 		c.BaseURL = "https://gitlab.com"
 	}
 	return &gitlabConnector{
-		baseURL:      c.BaseURL,
-		redirectURI:  c.RedirectURI,
-		clientID:     c.ClientID,
-		clientSecret: c.ClientSecret,
-		logger:       logger.With(slog.Group("connector", "type", "gitlab", "id", id)),
-		groups:       c.Groups,
-		useLoginAsID: c.UseLoginAsID,
+		baseURL:             c.BaseURL,
+		redirectURI:         c.RedirectURI,
+		clientID:            c.ClientID,
+		clientSecret:        c.ClientSecret,
+		logger:              logger.With(slog.Group("connector", "type", "gitlab", "id", id)),
+		groups:              c.Groups,
+		useLoginAsID:        c.UseLoginAsID,
+		getGroupsPermission: c.GetGroupsPermission,
 	}, nil
 }
 
@@ -82,6 +84,9 @@ type gitlabConnector struct {
 	httpClient   *http.Client
 	// if set to true will use the user's handle rather than their numeric id as the ID
 	useLoginAsID bool
+
+	// if set to true permissions will be added to list of groups
+	getGroupsPermission bool
 }
 
 func (c *gitlabConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
@@ -256,7 +261,10 @@ func (c *gitlabConnector) user(ctx context.Context, client *http.Client) (gitlab
 }
 
 type userInfo struct {
-	Groups []string
+	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"`
 }
 
 // userGroups queries the GitLab API for group membership.
@@ -287,9 +295,62 @@ func (c *gitlabConnector) userGroups(ctx context.Context, client *http.Client) (
 		return nil, fmt.Errorf("failed to decode response: %v", err)
 	}
 
+	if c.getGroupsPermission {
+		groups := c.setGroupsPermission(u)
+		return groups, nil
+	}
+
 	return u.Groups, 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
+				}
+			}
+		}
+	}
+
+	return groups
+}
+
 func (c *gitlabConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) {
 	gitlabGroups, err := c.userGroups(ctx, client)
 	if err != nil {
diff --git a/connector/gitlab/gitlab_test.go b/connector/gitlab/gitlab_test.go
index d828b8bd..b67b30c0 100644
--- a/connector/gitlab/gitlab_test.go
+++ b/connector/gitlab/gitlab_test.go
@@ -249,6 +249,47 @@ func TestRefreshWithEmptyConnectorData(t *testing.T) {
 	expectEquals(t, emptyIdentity, identity)
 }
 
+func TestGroupsWithPermission(t *testing.T) {
+	s := newTestServer(map[string]interface{}{
+		"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
+		"/oauth/token": map[string]interface{}{
+			"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"},
+		},
+	})
+	defer s.Close()
+
+	hostURL, err := url.Parse(s.URL)
+	expectNil(t, err)
+
+	req, err := http.NewRequest("GET", hostURL.String(), nil)
+	expectNil(t, err)
+
+	c := gitlabConnector{baseURL: s.URL, httpClient: newClient(), getGroupsPermission: true}
+	identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req)
+	expectNil(t, err)
+
+	expectEquals(t, identity.Groups, []string{
+		"ops",
+		"dev",
+		"ops-test",
+		"ops/project",
+		"dev/project1",
+		"dev/project2",
+		"ops:owner",
+		"dev:developer",
+		"ops/project:owner",
+		"dev/project1:maintainer",
+		"dev/project2:developer",
+	})
+}
+
 func newTestServer(responses map[string]interface{}) *httptest.Server {
 	return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		response := responses[r.RequestURI]
-- 
GitLab