Merge pull request #20145 from matteosilv/master

email field and external uid annotation for GitlabOrgDiscoveryEntityProvider Enterprise users
This commit is contained in:
Fredrik Adelöw
2023-10-09 14:26:53 +02:00
committed by GitHub
6 changed files with 125 additions and 56 deletions
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/plugin-catalog-backend-module-gitlab': patch
---
fix: use REST API to get root group memberships for GitLab SaaS users listing
This API is the only one that shows `email` field for enterprise users and
allows to filter out bot users not using a license using the `is_using_seat`
field.
We also added the annotation `gitlab.com/saml-external-uid` taking the value
of `group_saml_identity.extern_uid` of the `groups/:group-id/members` endpoint
response. This is useful in case you want to create a `SignInResolver` that
references the user with the id of your identity provider (e.g. OneLogin).
ref:
https://docs.gitlab.com/ee/user/enterprise_user/#get-users-email-addresses-through-the-api
https://docs.gitlab.com/ee/api/members.html#limitations
@@ -421,7 +421,7 @@ describe('GitLabClient', () => {
user: {
id: 'gid://gitlab/User/1',
username: 'user1',
commitEmail: 'user1@example.com',
publicEmail: 'user1@example.com',
name: 'user1',
state: 'active',
webUrl: 'user1.com',
@@ -525,7 +525,7 @@ describe('GitLabClient', () => {
user: {
id: 'gid://gitlab/User/1',
username: 'user1',
commitEmail: 'user1@example.com',
publicEmail: 'user1@example.com',
name: 'user1',
state: 'active',
webUrl: 'user1.com',
@@ -538,7 +538,7 @@ describe('GitLabClient', () => {
user: {
id: 'gid://gitlab/User/2',
username: 'user2',
commitEmail: 'user2@example.com',
publicEmail: 'user2@example.com',
name: 'user2',
state: 'active',
webUrl: 'user2.com',
@@ -772,7 +772,7 @@ describe('GitLabClient', () => {
user: {
id: 'gid://gitlab/User/1',
username: 'user1',
commitEmail: 'user1@example.com',
publicEmail: 'user1@example.com',
name: 'user1',
state: 'active',
webUrl: 'user1.com',
@@ -91,6 +91,22 @@ export class GitLabClient {
});
}
async listSaaSUsers(
groupPath: string,
options?: CommonListOptions,
): Promise<PagedResponse<GitLabUser>> {
return this.pagedRequest(
`/groups/${encodeURIComponent(groupPath)}/members`,
{
...options,
show_seat_info: true,
},
).then(resp => {
resp.items = resp.items.filter(user => user.is_using_seat);
return resp;
});
}
async listGroups(
options?: CommonListOptions,
): Promise<PagedResponse<GitLabGroup>> {
@@ -205,7 +221,7 @@ export class GitLabClient {
user {
id
username
commitEmail
publicEmail
name
state
webUrl
@@ -240,7 +256,7 @@ export class GitLabClient {
const formattedUserResponse = {
id: Number(userItem.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
username: userItem.user.username,
email: userItem.user.commitEmail,
email: userItem.user.publicEmail,
name: userItem.user.name,
state: userItem.user.state,
web_url: userItem.user.webUrl,
@@ -39,12 +39,17 @@ export type GitLabProject = {
export type GitLabUser = {
id: number;
username: string;
email: string;
email?: string;
name: string;
state: string;
web_url: string;
avatar_url: string;
groups?: GitLabGroup[];
group_saml_identity?: GitLabGroupSamlIdentity;
};
export type GitLabGroupSamlIdentity = {
extern_uid: string;
};
export type GitLabGroup = {
@@ -64,7 +69,7 @@ export type GitLabGroupMembersResponse = {
user: {
id: string;
username: string;
commitEmail: string;
publicEmail: string;
name: string;
state: string;
webUrl: string;
@@ -548,46 +548,6 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
}),
),
),
graphql
.link('https://gitlab.com/api/graphql')
.query('getGroupMembers', async (_, res, ctx) =>
res(
ctx.data({
group: {
groupMembers: {
nodes: [
{
user: {
id: 'gid://gitlab/User/12',
username: 'testuser1',
commitEmail: 'testuser1@example.com',
state: 'active',
name: 'Test User 1',
webUrl: 'https://gitlab.com/testuser1',
avatarUrl: 'https://secure.gravatar.com/',
},
},
{
user: {
id: 'gid://gitlab/User/34',
username: 'testuser2',
commitEmail: 'testuser2@example.com',
state: 'active',
name: 'Test User 2',
webUrl: 'https://gitlab.com/testuser2',
avatarUrl: 'https://secure.gravatar.com/',
},
},
],
pageInfo: {
endCursor: 'end',
hasNextPage: false,
},
},
},
}),
),
),
graphql
.link('https://gitlab.com/api/graphql')
.query('getGroupMembers', async (req, res, ctx) =>
@@ -608,6 +568,67 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
}),
),
),
rest.get(
`https://gitlab.com/api/v4/groups/group1/members`,
(_req, res, ctx) => {
const response = [
{
access_level: 30,
created_at: '2023-07-17T08:58:34.984Z',
expires_at: null,
id: 12,
username: 'testuser1',
name: 'Test User 1',
state: 'active',
avatar_url: 'https://secure.gravatar.com/',
web_url: 'https://gitlab.com/testuser1',
email: 'testuser1@example.com',
group_saml_identity: {
provider: 'group_saml',
extern_uid: '51',
saml_provider_id: 1,
},
is_using_seat: true,
membership_state: 'active',
},
{
access_level: 30,
created_at: '2023-07-19T08:58:34.984Z',
expires_at: null,
id: 34,
username: 'testuser2',
name: 'Test User 2',
state: 'active',
avatar_url: 'https://secure.gravatar.com/',
web_url: 'https://gitlab.com/testuser2',
email: 'testuser2@example.com',
group_saml_identity: {
provider: 'group_saml',
extern_uid: '52',
saml_provider_id: 1,
},
is_using_seat: true,
membership_state: 'active',
},
{
access_level: 50,
created_at: '2023-07-15T08:58:34.984Z',
expires_at: '2023-10-26',
id: 54,
username: 'group_100_bot_23dc8057bef66e05181f39be4652577c',
name: 'Token Bot',
state: 'active',
avatar_url: 'https://secure.gravatar.com/',
web_url:
'https://gitlab.com/group_100_bot_23dc8057bef66e05181f39be4652577c',
group_saml_identity: null,
is_using_seat: false,
membership_state: 'active',
},
];
return res(ctx.json(response));
},
),
);
await provider.connect(entityProviderConnection);
@@ -630,11 +651,12 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
'backstage.io/managed-by-origin-location':
'url:https://gitlab.com/testuser1',
'gitlab.com/user-login': 'https://gitlab.com/testuser1',
'gitlab.com/saml-external-uid': '51',
},
name: 'testuser1',
},
spec: {
memberOf: ['group2', 'group3'],
memberOf: ['group2'],
profile: {
displayName: 'Test User 1',
email: 'testuser1@example.com',
@@ -655,11 +677,12 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
'backstage.io/managed-by-origin-location':
'url:https://gitlab.com/testuser2',
'gitlab.com/user-login': 'https://gitlab.com/testuser2',
'gitlab.com/saml-external-uid': '52',
},
name: 'testuser2',
},
spec: {
memberOf: ['group2', 'group3'],
memberOf: ['group3'],
profile: {
displayName: 'Test User 2',
email: 'testuser2@example.com',
@@ -193,12 +193,14 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
});
} else {
groups = (await client.listDescendantGroups(this.config.group)).items;
users = (
await client.getGroupMembers(this.config.group.split('/')[0], [
'DIRECT',
'DESCENDANTS',
])
).items;
const rootGroup = this.config.group.split('/')[0];
users = paginated<GitLabUser>(
options => client.listSaaSUsers(rootGroup, options),
{
page: 1,
per_page: 100,
},
);
}
const idMappedUser: { [userId: number]: GitLabUser } = {};
@@ -332,6 +334,10 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
const annotations: { [annotationName: string]: string } = {};
annotations[`${host}/user-login`] = user.web_url;
if (user?.group_saml_identity?.extern_uid) {
annotations[`${host}/saml-external-uid`] =
user.group_saml_identity.extern_uid;
}
const entity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',