Skip to content

Commit ea87f34

Browse files
authored
feat(rbac): add the optional maxDepth feature (janus-idp#1486)
* feat(rbac): add the optional maxDepth feature
1 parent 2a567f0 commit ea87f34

File tree

10 files changed

+392
-62
lines changed

10 files changed

+392
-62
lines changed

plugins/rbac-backend/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,16 @@ The RBAC plugin offers the option to store policies in a database. It supports t
175175
- postgres: Recommended for production environments.
176176

177177
Ensure that you have already configured the database backend for your Backstage instance, as the RBAC plugin utilizes the same database configuration.
178+
179+
### Optional maximum depth
180+
181+
The RBAC plugin also includes an option max depth feature for organizations with potentially complex group hierarchy, this configuration value will ensure that the RBAC plugin will stop at a certain depth when building user graphs.
182+
183+
```YAML
184+
permission:
185+
enabled: true
186+
rbac:
187+
maxDepth: 1
188+
```
189+
190+
The maxDepth must be greater than 0 to ensure that the graphs are built correctly. Also the graph will be built with a hierarchy of 1 + maxDepth.

plugins/rbac-backend/config.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export interface Config {
3838
* The RBAC plugin will handle access control for plugins included in this list.
3939
*/
4040
pluginsWithPermission?: string[];
41+
/**
42+
* An optional value that limits the depth when building the hierarchy group graph
43+
* @visibility frontend
44+
*/
45+
maxDepth?: number;
4146
};
4247
};
4348
}

plugins/rbac-backend/src/file-permissions/csv.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TokenManager } from '@backstage/backend-common';
2+
import { ConfigReader } from '@backstage/config';
23

34
import {
45
Adapter,
@@ -115,11 +116,14 @@ async function createEnforcer(
115116
const catalogDBClient = Knex.knex({ client: MockClient });
116117
const enf = await newEnforcer(theModel, adapter);
117118

119+
const config = newConfigReader();
120+
118121
const rm = new BackstageRoleManager(
119122
catalogApi,
120123
log,
121124
tokenManager,
122125
catalogDBClient,
126+
config,
123127
);
124128
enf.setRoleManager(rm);
125129
enf.enableAutoBuildRoleLinks(false);
@@ -1005,3 +1009,34 @@ describe('CSV file', () => {
10051009
});
10061010
});
10071011
});
1012+
1013+
function newConfigReader(
1014+
users?: Array<{ name: string }>,
1015+
superUsers?: Array<{ name: string }>,
1016+
): ConfigReader {
1017+
const testUsers = [
1018+
{
1019+
name: 'user:default/guest',
1020+
},
1021+
{
1022+
name: 'group:default/guests',
1023+
},
1024+
];
1025+
1026+
return new ConfigReader({
1027+
permission: {
1028+
rbac: {
1029+
admin: {
1030+
users: users || testUsers,
1031+
superUsers: superUsers,
1032+
},
1033+
},
1034+
},
1035+
backend: {
1036+
database: {
1037+
client: 'better-sqlite3',
1038+
connection: ':memory:',
1039+
},
1040+
},
1041+
});
1042+
}

plugins/rbac-backend/src/role-manager/ancestor-search-memo.test.ts

Lines changed: 139 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity } from '@backstage/catalog-model';
1+
import { Entity, GroupEntity } from '@backstage/catalog-model';
22

33
import * as Knex from 'knex';
44
import { createTracker, MockClient, Tracker } from 'knex-mock-client';
@@ -41,28 +41,24 @@ describe('ancestor-search-memo', () => {
4141
];
4242

4343
const testGroups = [
44-
createGroupEntity(
45-
'group:default/team-a',
46-
'group:default/team-b',
47-
[],
48-
['adam'],
49-
),
50-
createGroupEntity('group:default/team-b', 'group:default/team-c', [], []),
51-
createGroupEntity('group:default/team-c', '', [], []),
52-
createGroupEntity(
53-
'group:default/team-d',
54-
'group:default/team-e',
55-
[],
56-
['george'],
57-
),
58-
createGroupEntity('group:default/team-e', 'group:default/team-f', [], []),
59-
createGroupEntity('group:default/team-f', '', [], []),
44+
createGroupEntity('team-a', 'team-b', [], ['adam']),
45+
createGroupEntity('team-b', 'team-c', [], []),
46+
createGroupEntity('team-c', '', [], []),
47+
createGroupEntity('team-d', 'team-e', [], ['george']),
48+
createGroupEntity('team-e', 'team-f', [], []),
49+
createGroupEntity('team-f', '', [], []),
6050
];
6151

52+
const testUserGroups = [createGroupEntity('team-a', 'team-b', [], ['adam'])];
53+
6254
const catalogApiMock: any = {
63-
getEntities: jest
64-
.fn()
65-
.mockImplementation(() => Promise.resolve({ items: testGroups })),
55+
getEntities: jest.fn().mockImplementation((arg: any) => {
56+
const hasMember = arg.filter['relations.hasMember'];
57+
if (hasMember && hasMember === 'user:default/adam') {
58+
return { items: testUserGroups };
59+
}
60+
return { items: testGroups };
61+
}),
6662
};
6763

6864
const catalogDBClient = Knex.knex({ client: MockClient });
@@ -74,12 +70,16 @@ describe('ancestor-search-memo', () => {
7470
authenticate: jest.fn().mockImplementation(),
7571
};
7672

77-
const asm = new AncestorSearchMemo(
78-
'user:default/adam',
79-
tokenManagerMock,
80-
catalogApiMock,
81-
catalogDBClient,
82-
);
73+
let asm: AncestorSearchMemo;
74+
75+
beforeEach(() => {
76+
asm = new AncestorSearchMemo(
77+
'user:default/adam',
78+
tokenManagerMock,
79+
catalogApiMock,
80+
catalogDBClient,
81+
);
82+
});
8383

8484
describe('getAllGroups and getAllRelations', () => {
8585
let tracker: Tracker;
@@ -104,7 +104,6 @@ describe('ancestor-search-memo', () => {
104104

105105
it('should return all groups', async () => {
106106
const allGroupsTest = await asm.getAllGroups();
107-
// @ts-ignore
108107
expect(allGroupsTest).toEqual(testGroups);
109108
});
110109

@@ -137,14 +136,8 @@ describe('ancestor-search-memo', () => {
137136
});
138137

139138
it('should return all user groups', async () => {
140-
tracker.on
141-
.select(
142-
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
143-
)
144-
.response(userRelations);
145-
const relations = await asm.getUserRelations();
146-
147-
expect(relations).toEqual(userRelations);
139+
const userGroups = await asm.getUserGroups();
140+
expect(userGroups).toEqual(testUserGroups);
148141
});
149142

150143
it('should fail to return anything when there is an error getting user relations', async () => {
@@ -187,6 +180,7 @@ describe('ancestor-search-memo', () => {
187180
asm,
188181
relation as Relation,
189182
allRelationsTest as Relation[],
183+
0,
190184
),
191185
);
192186

@@ -196,6 +190,103 @@ describe('ancestor-search-memo', () => {
196190
expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy();
197191
expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy();
198192
});
193+
194+
// maxDepth of one stops here
195+
// |
196+
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
197+
it('should build the graph but stop based on the maxDepth', async () => {
198+
const asmMaxDepth = new AncestorSearchMemo(
199+
'user:default/adam',
200+
tokenManagerMock,
201+
catalogApiMock,
202+
catalogDBClient,
203+
1,
204+
);
205+
206+
tracker.on
207+
.select(
208+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
209+
)
210+
.response(userRelations);
211+
const userRelationsTest = await asmMaxDepth.getUserRelations();
212+
213+
tracker.reset();
214+
tracker.on
215+
.select(
216+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
217+
)
218+
.response(allRelations);
219+
const allRelationsTest = await asmMaxDepth.getAllRelations();
220+
221+
userRelationsTest.forEach(relation =>
222+
asmMaxDepth.traverseRelations(
223+
asmMaxDepth,
224+
relation as Relation,
225+
allRelationsTest as Relation[],
226+
0,
227+
),
228+
);
229+
230+
expect(asmMaxDepth.hasEntityRef('user:default/adam')).toBeTruthy();
231+
expect(asmMaxDepth.hasEntityRef('group:default/team-a')).toBeTruthy();
232+
expect(asmMaxDepth.hasEntityRef('group:default/team-b')).toBeTruthy();
233+
expect(asmMaxDepth.hasEntityRef('group:default/team-c')).toBeFalsy();
234+
expect(asmMaxDepth.hasEntityRef('group:default/team-d')).toBeFalsy();
235+
});
236+
});
237+
238+
describe('traverseGroups', () => {
239+
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
240+
it('should build a graph for a particular user', async () => {
241+
const userGroupsTest = await asm.getUserGroups();
242+
243+
const allGroupsTest = await asm.getAllGroups();
244+
245+
userGroupsTest.forEach(group =>
246+
asm.traverseGroups(
247+
asm,
248+
group as GroupEntity,
249+
allGroupsTest as GroupEntity[],
250+
0,
251+
),
252+
);
253+
254+
expect(asm.hasEntityRef('group:default/team-a')).toBeTruthy();
255+
expect(asm.hasEntityRef('group:default/team-b')).toBeTruthy();
256+
expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy();
257+
expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy();
258+
});
259+
260+
// maxDepth of one stops here
261+
// |
262+
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
263+
it('should build the graph but stop based on the maxDepth', async () => {
264+
const asmMaxDepth = new AncestorSearchMemo(
265+
'user:default/adam',
266+
tokenManagerMock,
267+
catalogApiMock,
268+
catalogDBClient,
269+
1,
270+
);
271+
272+
const userGroupsTest = await asmMaxDepth.getUserGroups();
273+
274+
const allGroupsTest = await asmMaxDepth.getAllGroups();
275+
276+
userGroupsTest.forEach(group =>
277+
asmMaxDepth.traverseGroups(
278+
asmMaxDepth,
279+
group as GroupEntity,
280+
allGroupsTest as GroupEntity[],
281+
0,
282+
),
283+
);
284+
285+
expect(asmMaxDepth.hasEntityRef('group:default/team-a')).toBeTruthy();
286+
expect(asmMaxDepth.hasEntityRef('group:default/team-b')).toBeTruthy();
287+
expect(asmMaxDepth.hasEntityRef('group:default/team-c')).toBeFalsy();
288+
expect(asmMaxDepth.hasEntityRef('group:default/team-d')).toBeFalsy();
289+
});
199290
});
200291

201292
describe('buildUserGraph', () => {
@@ -211,6 +302,12 @@ describe('ancestor-search-memo', () => {
211302
const asmDBSpy = jest
212303
.spyOn(asmUserGraph, 'doesRelationTableExist')
213304
.mockImplementation(() => Promise.resolve(true));
305+
const userRelationsSpy = jest
306+
.spyOn(asmUserGraph, 'getUserRelations')
307+
.mockImplementation(() => Promise.resolve(userRelations));
308+
const allRelationsSpy = jest
309+
.spyOn(asmUserGraph, 'getAllRelations')
310+
.mockImplementation(() => Promise.resolve(allRelations));
214311

215312
beforeAll(() => {
216313
tracker = createTracker(catalogDBClient);
@@ -222,25 +319,16 @@ describe('ancestor-search-memo', () => {
222319

223320
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
224321
it('should build the user graph using relations table', async () => {
225-
tracker.on
226-
.select(
227-
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
228-
)
229-
.response(userRelations);
230-
tracker.reset();
231-
tracker.on
232-
.select(
233-
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
234-
)
235-
.response(allRelations);
236322
await asmUserGraph.buildUserGraph(asmUserGraph);
237323

238324
expect(asmDBSpy).toHaveBeenCalled();
239-
expect(asm.hasEntityRef('user:default/adam')).toBeTruthy();
240-
expect(asm.hasEntityRef('group:default/team-a')).toBeTruthy();
241-
expect(asm.hasEntityRef('group:default/team-b')).toBeTruthy();
242-
expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy();
243-
expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy();
325+
expect(userRelationsSpy).toHaveBeenCalled();
326+
expect(allRelationsSpy).toHaveBeenCalled();
327+
expect(asmUserGraph.hasEntityRef('user:default/adam')).toBeTruthy();
328+
expect(asmUserGraph.hasEntityRef('group:default/team-a')).toBeTruthy();
329+
expect(asmUserGraph.hasEntityRef('group:default/team-b')).toBeTruthy();
330+
expect(asmUserGraph.hasEntityRef('group:default/team-c')).toBeTruthy();
331+
expect(asmUserGraph.hasEntityRef('group:default/team-d')).toBeFalsy();
244332
});
245333
});
246334

0 commit comments

Comments
 (0)