Merge pull request #27575 from hghtwr/26554

extend filter logic to include parent group
This commit is contained in:
Fredrik Adelöw
2024-12-10 12:06:53 +01:00
committed by GitHub
7 changed files with 94 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-gitlab': minor
---
Implemented discovery for top-level groups defined in config.group or if undefined global top-level group in Gitlab
+2 -2
View File
@@ -170,7 +170,7 @@ catalog:
yourProviderId:
host: gitlab.com
orgEnabled: true
group: org/teams # Required for gitlab.com when `orgEnabled: true`. Optional for self managed. Must not end with slash. Accepts only groups under the provided path (which will be stripped)
group: org/teams # Required for gitlab.com when `orgEnabled: true`. Optional for self managed. Must not end with slash. Accepts only this group and groups under the provided path (which will be stripped)
relations: # Optional
- INHERITED # Optional. Members of any ancestor groups will also be considered members of the current group.
- DESCENDANTS # Optional. Members of any descendant groups will also be considered members of the current group.
@@ -240,7 +240,7 @@ catalog:
yourProviderId:
host: gitlab.com ## Could also be self hosted.
orgEnabled: true
group: org/teams # Required for gitlab.com when `orgEnabled: true`. Optional for self managed. Must not end with slash. Accepts only groups under the provided path (which will be stripped)
group: org/teams # Required for gitlab.com when `orgEnabled: true`. Optional for self managed. Must not end with slash. Accepts only this group and groups under the provided path (which will be stripped)
restrictUsersToGroup: true # Optional: Backstage will ingest only users directly assigned to org/teams.
includeUsersWithoutSeat: false # Optional: Set to true to include users without paid seat, only applicable for SaaS
```
@@ -136,6 +136,22 @@ const httpHandlers = [
// dynamic handlers
// https://docs.gitlab.com/ee/api/groups.html#list-group-details supports encoded path and id
const httpGroupFindByEncodedPathDynamic = all_groups_response.flatMap(group => [
// Handler for apiBaseUrl
rest.get(`${apiBaseUrl}/groups/${group.full_path}`, (_, res, ctx) => {
return res(
ctx.json(all_groups_response.find(g => g.full_path === group.full_path)),
);
}),
// Handler for apiSaaSBaseUrl
rest.get(`${apiBaseUrlSaas}/groups/${group.full_path}`, (_, res, ctx) => {
return res(
ctx.json(all_groups_response.find(g => g.full_path === group.full_path)),
);
}),
]);
const httpGroupFindByIdDynamic = all_groups_response.map(group => {
return rest.get(`${apiBaseUrl}/groups/${group.id}`, (_, res, ctx) => {
return res(ctx.json(all_groups_response.find(g => g.id === group.id)));
@@ -618,4 +634,5 @@ export const handlers = [
...httpGroupListDescendantProjectsById,
...httpGroupListDescendantProjectsByName,
...graphqlHandlers,
...httpGroupFindByEncodedPathDynamic,
];
@@ -1041,7 +1041,7 @@ export const all_groups_response: GitLabGroup[] = [
{
id: 1,
name: 'group1',
description: '',
description: 'description1',
full_path: 'group1',
},
{
@@ -1804,6 +1804,7 @@ export const expected_group_user_entity: MockObject[] = [
'url:https://example.com/group1',
'example.com/team-path': 'group1',
},
description: 'description1',
name: 'group1',
},
spec: {
@@ -2090,6 +2091,7 @@ export const expected_full_org_scan_entities: MockObject[] = [
'url:https://example.com/group1',
'example.com/team-path': 'group1',
},
description: 'description1',
name: 'group1',
},
spec: {
@@ -2347,7 +2349,7 @@ export const expected_full_members_group_org_scan_entities: MockObject[] = [
name: 'JohnDoe',
},
spec: {
memberOf: ['subgroup1'],
memberOf: ['group1', 'subgroup1'], // since #26554 also config.group needs to be here.
profile: {
displayName: 'John Doe',
email: 'john.doe@company.com',
@@ -2431,6 +2433,30 @@ export const expected_full_members_group_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',
'backstage.io/managed-by-origin-location':
'url:https://example.com/group1',
'example.com/team-path': 'group1',
},
description: 'description1',
name: 'group1',
},
spec: {
children: [],
profile: {
displayName: 'group1',
},
type: 'team',
},
},
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
},
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
@@ -2472,7 +2498,7 @@ export const expected_group_members_group_org_scan_entities: MockObject[] = [
name: 'JohnDoe',
},
spec: {
memberOf: ['subgroup1'],
memberOf: ['subgroup1', 'group1'],
profile: {
displayName: 'John Doe',
email: 'john.doe@company.com',
@@ -2507,6 +2533,30 @@ export const expected_group_members_group_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',
'backstage.io/managed-by-origin-location':
'url:https://example.com/group1',
'example.com/team-path': 'group1',
},
name: 'group1',
description: 'description1',
},
spec: {
children: [],
profile: {
displayName: 'group1',
},
type: 'team',
},
},
locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id',
},
];
export const all_self_hosted_group1_members: MockObject[] = [
@@ -165,6 +165,15 @@ export class GitLabClient {
return this.pagedRequest(`/groups`, options);
}
// https://docs.gitlab.com/ee/api/groups.html#list-group-details
// id can either be group id or encoded full path
async getGroupByPath(
groupPath: string,
options?: CommonListOptions,
): Promise<GitLabGroup> {
return this.nonPagedRequest(`/groups/${groupPath}`, options);
}
async listDescendantGroups(
groupPath: string,
): Promise<PagedResponse<GitLabGroup>> {
@@ -435,7 +435,7 @@ describe('GitlabOrgDiscoveryEntityProvider - refresh', () => {
});
// This should return all members of the self-hosted instance regardless of the group set -> expected_full_members_group_org_scan_entities
// All instance members, but only the group entities below the config.group
// All instance members, but only the group entities of config.group and below (#26554)
it('Self-hosted: should get all instance users when restrictUsersToGroup is not set', async () => {
const config = new ConfigReader(mock.config_org_group_selfHosted);
const schedule = new PersistingTaskRunner();
@@ -366,6 +366,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
if (this.gitLabClient.isSelfManaged() && this.config.restrictUsersToGroup) {
groups = (await this.gitLabClient.listDescendantGroups(this.config.group))
.items;
groups.push(await this.gitLabClient.getGroupByPath(this.config.group)); // adds the parent group for #26554
users = paginated<GitLabUser>(
options =>
this.gitLabClient.listGroupMembers(this.config.group, options), // calls /groups/<groupId>/members
@@ -395,10 +396,14 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
else {
groups = (await this.gitLabClient.listDescendantGroups(this.config.group))
.items;
groups.push(await this.gitLabClient.getGroupByPath(this.config.group)); // adds the parent group for #26554
const rootGroupSplit = this.config.group.split('/');
const rootGroup = this.config.restrictUsersToGroup
? rootGroupSplit[rootGroupSplit.length - 1]
: rootGroupSplit[0];
users = paginated<GitLabUser>(
options =>
this.gitLabClient.listSaaSUsers(
@@ -775,7 +780,8 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
return (
this.config.groupPattern.test(group.full_path) &&
(!this.config.group ||
group.full_path.startsWith(`${this.config.group}/`))
group.full_path.startsWith(`${this.config.group}/`) ||
group.full_path === this.config.group)
);
}