Browse Source

feat(gitlab): implement TokenIdentity method (#4606)

Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
pull/4612/head
Maksim Nabokikh 2 weeks ago committed by GitHub
parent
commit
a11b3cd2ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 29
      connector/gitlab/gitlab.go
  2. 85
      connector/gitlab/gitlab_test.go

29
connector/gitlab/gitlab.go

@ -89,6 +89,7 @@ type connectorData struct {
var ( var (
_ connector.CallbackConnector = (*gitlabConnector)(nil) _ connector.CallbackConnector = (*gitlabConnector)(nil)
_ connector.RefreshConnector = (*gitlabConnector)(nil) _ connector.RefreshConnector = (*gitlabConnector)(nil)
_ connector.TokenIdentityConnector = (*gitlabConnector)(nil)
) )
type gitlabConnector struct { type gitlabConnector struct {
@ -243,6 +244,34 @@ func (c *gitlabConnector) Refresh(ctx context.Context, s connector.Scopes, ident
} }
} }
// TokenIdentity is used for token exchange, verifying a GitLab access token
// and returning the associated user identity. This enables direct authentication
// with Dex using an existing GitLab token without going through the OAuth flow.
//
// Note: The connector decides whether to fetch groups based on its configuration
// (groups filter, getGroupsPermission), not on the scopes from the token exchange request.
// The server will then decide whether to include groups in the final token based on
// the requested scopes. This matches the behavior of other connectors (e.g., OIDC).
func (c *gitlabConnector) TokenIdentity(ctx context.Context, _, subjectToken string) (connector.Identity, error) {
if c.httpClient != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
}
token := &oauth2.Token{
AccessToken: subjectToken,
TokenType: "Bearer", // GitLab tokens are typically Bearer tokens even if the type is not explicitly provided.
}
// For token exchange, we determine if groups should be fetched based on connector configuration.
// If the connector has groups filter or getGroupsPermission enabled, we fetch groups.
scopes := connector.Scopes{
// Scopes are not provided in token exchange, so we request groups every time and return only if configured.
Groups: true,
}
return c.identity(ctx, scopes, token)
}
func (c *gitlabConnector) groupsRequired(groupScope bool) bool { func (c *gitlabConnector) groupsRequired(groupScope bool) bool {
return len(c.groups) > 0 || groupScope return len(c.groups) > 0 || groupScope
} }

85
connector/gitlab/gitlab_test.go

@ -485,3 +485,88 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) {
t.Errorf("Expected %+v to equal %+v", a, b) t.Errorf("Expected %+v to equal %+v", a, b)
} }
} }
func TestTokenIdentity(t *testing.T) {
// Note: These tests verify that the connector returns groups based on its configuration.
// The actual inclusion of groups in the final Dex token depends on the 'groups' scope
// in the token exchange request, which is handled by the Dex server, not the connector.
tests := []struct {
name string
userInfo userInfo
groups []string
getGroupsPermission bool
useLoginAsID bool
expectUserID string
expectGroups []string
}{
{
name: "without groups config",
expectUserID: "12345678",
expectGroups: nil,
},
{
name: "with groups filter",
userInfo: userInfo{
Groups: []string{"team-1", "team-2"},
},
groups: []string{"team-1"},
expectUserID: "12345678",
expectGroups: []string{"team-1"},
},
{
name: "with groups permission",
userInfo: userInfo{
Groups: []string{"ops", "dev"},
OwnerPermission: []string{"ops"},
DeveloperPermission: []string{"dev"},
MaintainerPermission: []string{},
},
getGroupsPermission: true,
expectUserID: "12345678",
expectGroups: []string{"ops", "dev", "ops:owner", "dev:developer"},
},
{
name: "with useLoginAsID",
useLoginAsID: true,
expectUserID: "joebloggs",
expectGroups: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
responses := map[string]interface{}{
"/api/v4/user": gitlabUser{
Email: "some@email.com",
ID: 12345678,
Name: "Joe Bloggs",
Username: "joebloggs",
},
"/oauth/userinfo": tc.userInfo,
}
s := newTestServer(responses)
defer s.Close()
c := gitlabConnector{
baseURL: s.URL,
httpClient: newClient(),
groups: tc.groups,
getGroupsPermission: tc.getGroupsPermission,
useLoginAsID: tc.useLoginAsID,
}
accessToken := "test-access-token"
ctx := context.Background()
identity, err := c.TokenIdentity(ctx, "urn:ietf:params:oauth:token-type:access_token", accessToken)
expectNil(t, err)
expectEquals(t, identity.UserID, tc.expectUserID)
expectEquals(t, identity.Username, "Joe Bloggs")
expectEquals(t, identity.PreferredUsername, "joebloggs")
expectEquals(t, identity.Email, "some@email.com")
expectEquals(t, identity.EmailVerified, true)
expectEquals(t, identity.Groups, tc.expectGroups)
})
}
}

Loading…
Cancel
Save