Skip to content

Commit c2c9e22

Browse files
authored
feat(rbac): query the catalog database when building graph (janus-idp#1298)
* feat(rbac): query the catalog database when building graph * feat(rbac): fix failing tests * feat(rbac): add unit tests for catalog database * feat(rbac): update tests based on review suggestions
1 parent 67b7458 commit c2c9e22

File tree

9 files changed

+475
-34
lines changed

9 files changed

+475
-34
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,15 @@ async function createEnforcer(
112112
log: Logger,
113113
tokenManager: TokenManager,
114114
): Promise<Enforcer> {
115+
const catalogDBClient = Knex.knex({ client: MockClient });
115116
const enf = await newEnforcer(theModel, adapter);
116117

117-
const rm = new BackstageRoleManager(catalogApi, log, tokenManager);
118+
const rm = new BackstageRoleManager(
119+
catalogApi,
120+
log,
121+
tokenManager,
122+
catalogDBClient,
123+
);
118124
enf.setRoleManager(rm);
119125
enf.enableAutoBuildRoleLinks(false);
120126
await enf.buildRoleLinks();
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { Entity } from '@backstage/catalog-model';
2+
3+
import * as Knex from 'knex';
4+
import { createTracker, MockClient, Tracker } from 'knex-mock-client';
5+
6+
import { AncestorSearchMemo, Relation } from './ancestor-search-memo';
7+
8+
describe('ancestor-search-memo', () => {
9+
const userRelations = [
10+
{
11+
source_entity_ref: 'user:default/adam',
12+
target_entity_ref: 'group:default/team-a',
13+
},
14+
];
15+
16+
const allRelations = [
17+
{
18+
source_entity_ref: 'user:default/adam',
19+
target_entity_ref: 'group:default/team-a',
20+
},
21+
{
22+
source_entity_ref: 'group:default/team-a',
23+
target_entity_ref: 'group:default/team-b',
24+
},
25+
{
26+
source_entity_ref: 'group:default/team-b',
27+
target_entity_ref: 'group:default/team-c',
28+
},
29+
{
30+
source_entity_ref: 'user:default/george',
31+
target_entity_ref: 'group:default/team-d',
32+
},
33+
{
34+
source_entity_ref: 'group:default/team-d',
35+
target_entity_ref: 'group:default/team-e',
36+
},
37+
{
38+
source_entity_ref: 'group:default/team-e',
39+
target_entity_ref: 'group:default/team-f',
40+
},
41+
];
42+
43+
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', '', [], []),
60+
];
61+
62+
const catalogApiMock: any = {
63+
getEntities: jest
64+
.fn()
65+
.mockImplementation(() => Promise.resolve({ items: testGroups })),
66+
};
67+
68+
const catalogDBClient = Knex.knex({ client: MockClient });
69+
70+
const tokenManagerMock = {
71+
getToken: jest.fn().mockImplementation(async () => {
72+
return Promise.resolve({ token: 'some-token' });
73+
}),
74+
authenticate: jest.fn().mockImplementation(),
75+
};
76+
77+
const asm = new AncestorSearchMemo(
78+
'user:default/adam',
79+
tokenManagerMock,
80+
catalogApiMock,
81+
catalogDBClient,
82+
);
83+
84+
describe('getAllGroups and getAllRelations', () => {
85+
let tracker: Tracker;
86+
87+
beforeAll(() => {
88+
tracker = createTracker(catalogDBClient);
89+
});
90+
91+
afterEach(() => {
92+
tracker.reset();
93+
});
94+
95+
it('should return all relations', async () => {
96+
tracker.on
97+
.select(
98+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
99+
)
100+
.response(allRelations);
101+
const allRelationsTest = await asm.getAllRelations();
102+
expect(allRelationsTest).toEqual(allRelations);
103+
});
104+
105+
it('should return all groups', async () => {
106+
const allGroupsTest = await asm.getAllGroups();
107+
// @ts-ignore
108+
expect(allGroupsTest).toEqual(testGroups);
109+
});
110+
111+
it('should fail to return anything when there is an error getting all relations', async () => {
112+
const allRelationsTest = await asm.getAllRelations();
113+
expect(allRelationsTest).toEqual([]);
114+
});
115+
});
116+
117+
describe('getUserGroups and getUserRelations', () => {
118+
let tracker: Tracker;
119+
120+
beforeAll(() => {
121+
tracker = createTracker(catalogDBClient);
122+
});
123+
124+
afterEach(() => {
125+
tracker.reset();
126+
});
127+
128+
it('should return all user relations', async () => {
129+
tracker.on
130+
.select(
131+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
132+
)
133+
.response(userRelations);
134+
const relations = await asm.getUserRelations();
135+
136+
expect(relations).toEqual(userRelations);
137+
});
138+
139+
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);
148+
});
149+
150+
it('should fail to return anything when there is an error getting user relations', async () => {
151+
const relations = await asm.getUserRelations();
152+
153+
expect(relations).toEqual([]);
154+
});
155+
});
156+
157+
describe('traverseRelations', () => {
158+
let tracker: Tracker;
159+
160+
beforeAll(() => {
161+
tracker = createTracker(catalogDBClient);
162+
});
163+
164+
afterEach(() => {
165+
tracker.reset();
166+
});
167+
168+
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
169+
it('should build a graph for a particular user', async () => {
170+
tracker.on
171+
.select(
172+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
173+
)
174+
.response(userRelations);
175+
const userRelationsTest = await asm.getUserRelations();
176+
177+
tracker.reset();
178+
tracker.on
179+
.select(
180+
/select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/,
181+
)
182+
.response(allRelations);
183+
const allRelationsTest = await asm.getAllRelations();
184+
185+
userRelationsTest.forEach(relation =>
186+
asm.traverseRelations(
187+
asm,
188+
relation as Relation,
189+
allRelationsTest as Relation[],
190+
),
191+
);
192+
193+
expect(asm.hasEntityRef('user:default/adam')).toBeTruthy();
194+
expect(asm.hasEntityRef('group:default/team-a')).toBeTruthy();
195+
expect(asm.hasEntityRef('group:default/team-b')).toBeTruthy();
196+
expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy();
197+
expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy();
198+
});
199+
});
200+
201+
describe('buildUserGraph', () => {
202+
let tracker: Tracker;
203+
204+
const asmUserGraph = new AncestorSearchMemo(
205+
'user:default/adam',
206+
tokenManagerMock,
207+
catalogApiMock,
208+
catalogDBClient,
209+
);
210+
211+
const asmDBSpy = jest
212+
.spyOn(asmUserGraph, 'doesRelationTableExist')
213+
.mockImplementation(() => Promise.resolve(true));
214+
215+
beforeAll(() => {
216+
tracker = createTracker(catalogDBClient);
217+
});
218+
219+
afterEach(() => {
220+
tracker.reset();
221+
});
222+
223+
// user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c
224+
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);
236+
await asmUserGraph.buildUserGraph(asmUserGraph);
237+
238+
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();
244+
});
245+
});
246+
247+
function createGroupEntity(
248+
name: string,
249+
parent?: string,
250+
children?: string[],
251+
members?: string[],
252+
): Entity {
253+
const entity: Entity = {
254+
apiVersion: 'v1',
255+
kind: 'Group',
256+
metadata: {
257+
name,
258+
namespace: 'default',
259+
},
260+
spec: {},
261+
};
262+
263+
if (children) {
264+
entity.spec!.children = children;
265+
}
266+
267+
if (members) {
268+
entity.spec!.members = members;
269+
}
270+
271+
if (parent) {
272+
entity.spec!.parent = parent;
273+
}
274+
275+
return entity;
276+
}
277+
});

0 commit comments

Comments
 (0)