From 2537c3f345ecc5b5755036695d1357c42ddad74e Mon Sep 17 00:00:00 2001 From: Hghtwr Date: Thu, 20 Jun 2024 16:32:26 +0200 Subject: [PATCH] Extend tests for subgroups Signed-off-by: Hghtwr --- .yarnrc.yml | 8 +- .../src/__testUtils__/handlers.ts | 63 ++++- .../src/__testUtils__/mocks.ts | 224 +++++++++++++++++- .../GitlabOrgDiscoveryEntityProvider.test.ts | 223 ++++++++++++++++- .../GitlabOrgDiscoveryEntityProvider.ts | 16 +- 5 files changed, 518 insertions(+), 16 deletions(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index 466074db0a..18069eb1a7 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,13 +1,17 @@ enableGlobalCache: true +httpProxy: "http://sia-lb.telekom.de:8080" + httpTimeout: 300000 +httpsProxy: "http://sia-lb.telekom.de:8080" + nodeLinker: node-modules -npmRegistryServer: 'https://registry.npmjs.org/' +npmRegistryServer: "https://registry.npmjs.org/" plugins: - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs - spec: '@yarnpkg/plugin-workspace-tools' + spec: "@yarnpkg/plugin-workspace-tools" yarnPath: .yarn/releases/yarn-3.8.1.cjs diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 80aa0370d3..04a95c4332 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -19,6 +19,7 @@ import { all_groups_response, all_projects_response, all_saas_users_response, + subgroup_saas_users_response, all_users_response, apiBaseUrl, apiBaseUrlSaas, @@ -68,6 +69,13 @@ const httpHandlers = [ return res(ctx.json(all_saas_users_response)); }), + rest.get( + `${apiBaseUrlSaas}/groups/subgroup1/members/all`, + (_req, res, ctx) => { + return res(ctx.json(subgroup_saas_users_response)); // To-DO change + }, + ), + /** * Users REST endpoint mocks */ @@ -186,7 +194,34 @@ const graphqlHandlers = [ .link(graphQlBaseUrl) .query('getGroupMembers', async (req, res, ctx) => { const { group, relations } = req.variables; - + // group is actually full_path + if (group === 'group1/subgroup1' && relations.includes('DIRECT')) { + return res( + ctx.data({ + group: { + groupMembers: { + nodes: [ + { + user: { + id: 'gid://gitlab/User/1', + username: 'user1', + publicEmail: 'user1@example.com', + name: 'user1', + state: 'active', + webUrl: 'user1.com', + avatarUrl: 'user1', + }, + }, + ], + pageInfo: { + endCursor: 'end', + hasNextPage: false, + }, + }, + }, + }), + ); + } if (group === 'group1' && relations.includes('DIRECT')) { return res( ctx.data({ @@ -367,7 +402,31 @@ const graphqlHandlers = [ ]), ); } - + if (group === 'group1') { + return res( + ctx.data({ + group: { + descendantGroups: { + nodes: [ + { + id: 'gid://gitlab/Group/6', + name: 'subgroup1', + description: 'description1', + fullPath: 'group1/subgroup1', + parent: { + id: '123', + }, + }, + ], + pageInfo: { + endCursor: 'end', + hasNextPage: false, + }, + }, + }, + }), + ); + } if (group === 'group-with-parent') { return res( ctx.data({ diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts index e3154eb1ce..b30f80888f 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts @@ -822,6 +822,12 @@ export const all_groups_response: GitLabGroup[] = [ description: '', full_path: 'parent1/nonMatchingGroup', }, + { + id: 6, + name: 'subgroup1', + description: '', + full_path: 'group1/subgroup1', + }, ]; export const group_with_parent: MockObject[] = [ @@ -1708,7 +1714,7 @@ export const expected_full_org_scan_entities: MockObject[] = [ name: 'JohnDoe', }, spec: { - memberOf: ['group1'], + memberOf: ['group1', 'group1-subgroup1'], profile: { displayName: 'John Doe', email: 'john.doe@company.com', @@ -1815,6 +1821,30 @@ export const expected_full_org_scan_entities: MockObject[] = [ }, locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/group1/subgroup1', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/group1/subgroup1', + 'example.com/team-path': 'group1/subgroup1', + }, + name: 'group1-subgroup1', + }, + spec: { + children: [], + profile: { + displayName: 'subgroup1', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, ]; export const expected_full_org_scan_entities_saas: MockObject[] = [ @@ -1871,3 +1901,195 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [ locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', }, ]; + +export const subgroup_saas_users_response: MockObject[] = [ + { + 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: 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', + }, +]; + +export const expected_subgroup_org_scan_entities_saas: MockObject[] = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://gitlab.com/testuser1', + '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: [], + profile: { + displayName: 'Test User 1', + email: 'testuser1@example.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, +]; + +// Simulate return of all users but only with membership of the descendants of config.group +export const expected_group_org_scan_entities: MockObject[] = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:https://example.com/JohnDoe', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/JohnDoe', + 'example.com/user-login': 'https://gitlab.example/john_doe', + }, + name: 'JohnDoe', + }, + spec: { + memberOf: ['subgroup1'], + profile: { + displayName: 'John Doe', + email: 'john.doe@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:https://example.com/JaneDoe', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/JaneDoe', + 'example.com/user-login': 'https://gitlab.example/jane_doe', + }, + name: 'JaneDoe', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Jane Doe', + email: 'jane.doe@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/MarySmith', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/MarySmith', + 'example.com/user-login': 'https://gitlab.example/mary_smith', + }, + name: 'MarySmith', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Mary Smith', + email: 'mary.smith@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/MarioMario', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/MarioMario', + 'example.com/user-login': 'https://gitlab.example/mario_mario', + }, + name: 'MarioMario', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Mario Mario', + email: 'mario.mario-company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/group1/subgroup1', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/group1/subgroup1', + 'example.com/team-path': 'group1/subgroup1', + }, + name: 'subgroup1', + }, + spec: { + children: [], + profile: { + displayName: 'subgroup1', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, +]; diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts index 5a60f1b4b3..8cf046909d 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts @@ -175,6 +175,220 @@ describe('GitlabOrgDiscoveryEntityProvider - configuration', () => { }); }); +// This should return all members of the SaaS Root group (group1) -> expected_full_org_scan_entities_saas +it('SaaS: should get all saas root group users when restrictUsersToGroup is not set', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_full_org_scan_entities_saas, // + }); +}); + +// This should return all members of the SaaS Root group (group1) -> expected_full_org_scan_entities_saas +it('SaaS: should get all saas root group users when restrictUsersToGroup is false', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + restrictUsersToGroup: false, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_full_org_scan_entities_saas, // + }); +}); + +// This should return only members of the SaaS subgroup (group1/subgroup1) -> expected_subgroup_org_scan_entities_saas +it('SaaS: should get only subgroup users when restrictUsersToGroup is true', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1/subgroup1', + restrictUsersToGroup: true, + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_subgroup_org_scan_entities_saas, + }); +}); + +// This should return all members of the self-hosted instance regardless of the group set -> expected_full_org_scan_entities +it('Self-hosted: should get all instance users when restrictUsersToGroup is not set', async () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_group_org_scan_entities, // This should deliver all users but only their membership in subgroups of config.group + }); +}); + describe('GitlabOrgDiscoveryEntityProvider - refresh', () => { it('should apply full update on scheduled execution', async () => { const config = new ConfigReader(mock.config_org_integration); @@ -260,15 +474,16 @@ describe('GitlabOrgDiscoveryEntityProvider - refresh', () => { const taskDef = schedule.getTasks()[0]; await (taskDef.fn as () => Promise)(); - const userEntities = mock.expected_full_org_scan_entities.filter( + const entities = mock.expected_full_org_scan_entities.filter( element => element.entity.metadata.name !== 'MarioMario', ); // filter out user with non matched e-mail - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(userEntities).not.toHaveLength(mock.all_users_response.length); + expect(entities).not.toHaveLength( + mock.expected_full_org_scan_entities.length, + ); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ type: 'full', - entities: userEntities, + entities: entities, }); }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index ce545ebe84..e2043e0393 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -107,7 +107,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { private userTransformer: UserTransformer; private groupEntitiesTransformer: GroupEntitiesTransformer; private groupNameTransformer: GroupNameTransformer; - private readonly gitLabClient: GitLabClient; + public readonly gitLabClient: GitLabClient; static fromConfig( config: Config, @@ -356,13 +356,14 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { let groups; let users; - // Doesn't matter if we are on SaaS or not, we can either get the users from the defined group or from the root group. + // Self-hosted: Fetch the users either from the defined group (restrictUsersToGroup) or fetch all users from the GitLab instance + // SaaS: Fetch the users from the defined group (restrictUsersToGroup) or fetch all users from the root group. if (this.gitLabClient.isSelfManaged() && this.config.restrictUsersToGroup) { groups = (await this.gitLabClient.listDescendantGroups(this.config.group)) .items; users = paginated( options => - this.gitLabClient.listGroupMembers(this.config.group, options), + this.gitLabClient.listGroupMembers(this.config.group, options), // calls /groups//members { page: 1, per_page: 100, @@ -381,17 +382,18 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { }, ); users = paginated( - options => this.gitLabClient.listUsers(options), + options => this.gitLabClient.listUsers(options), // calls /users? { page: 1, per_page: 100, active: true }, ); } - // For SaaS, the only difference is the root group + // SaaS: Fetch the users from the defined group (restrictUsersToGroup) or fetch all users from the root group. else { groups = (await this.gitLabClient.listDescendantGroups(this.config.group)) .items; + const rootGroupSplit = this.config.group.split('/'); const rootGroup = this.config.restrictUsersToGroup - ? this.config.group - : this.config.group.split('/')[0]; + ? rootGroupSplit[rootGroupSplit.length - 1] + : rootGroupSplit[0]; users = paginated( options => this.gitLabClient.listSaaSUsers(rootGroup, options), {