Skip to content

Commit

Permalink
fix(rbac): reduce the catalog calls when build graph (janus-idp#1203)
Browse files Browse the repository at this point in the history
* fix(rbac): reduce the catalog calls when build graph

* fix(rbac): update userEntityRef to userName

* fix(rbac): fix minor sonarcloud issue

* fix(rbac): review suggestions

* fix(rbac): add tests on more complex graphs

* fix(rbac): fix tests
  • Loading branch information
PatAKnight authored Feb 20, 2024
1 parent dca5f2e commit e63aac2
Show file tree
Hide file tree
Showing 7 changed files with 533 additions and 493 deletions.
2 changes: 1 addition & 1 deletion plugins/rbac-backend/src/file-permissions/csv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import {
PolicyMetadataStorage,
} from '../database/policy-metadata-storage';
import { RoleMetadataStorage } from '../database/role-metadata';
import { BackstageRoleManager } from '../role-manager/role-manager';
import { EnforcerDelegate } from '../service/enforcer-delegate';
import { MODEL } from '../service/permission-model';
import { BackstageRoleManager } from '../service/role-manager';
import {
addPermissionPoliciesFileData,
loadFilteredGroupingPoliciesFromCSV,
Expand Down
120 changes: 120 additions & 0 deletions plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { TokenManager } from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';

import { alg, Graph } from '@dagrejs/graphlib';
import { Logger } from 'winston';

// AncestorSearchMemo - should be used to build group hierarchy graph for User entity reference.
// It supports search group entity reference link in the graph.
// Also AncestorSearchMemo supports detection cycle dependencies between groups in the graph.
//
export class AncestorSearchMemo {
private graph: Graph;

private tokenManager: TokenManager;
private catalogApi: CatalogApi;

private userEntityRef: string;

private allGroups: Entity[];

constructor(
userEntityRef: string,
tokenManager: TokenManager,
catalogApi: CatalogApi,
) {
this.graph = new Graph({ directed: true });
this.userEntityRef = userEntityRef;
this.tokenManager = tokenManager;
this.catalogApi = catalogApi;
this.allGroups = [];
}

isAcyclic(): boolean {
return alg.isAcyclic(this.graph);
}

findCycles(): string[][] {
return alg.findCycles(this.graph);
}

setEdge(parentEntityRef: string, childEntityRef: string) {
this.graph.setEdge(parentEntityRef, childEntityRef);
}

setNode(entityRef: string): void {
this.graph.setNode(entityRef);
}

hasEntityRef(groupRef: string): boolean {
return this.graph.hasNode(groupRef);
}

debugNodesAndEdges(log: Logger, userEntity: string): void {
log.debug(
`SubGraph edges: ${JSON.stringify(this.graph.edges())} for ${userEntity}`,
);
log.debug(
`SubGraph nodes: ${JSON.stringify(this.graph.nodes())} for ${userEntity}`,
);
}

getNodes(): string[] {
return this.graph.nodes();
}

async getAllGroups(): Promise<void> {
const { token } = await this.tokenManager.getToken();
const { items } = await this.catalogApi.getEntities(
{
filter: { kind: 'Group' },
fields: ['metadata.name', 'metadata.namespace', 'spec.parent'],
},
{ token },
);
this.allGroups = items;
}

async getUserGroups(): Promise<Entity[]> {
const { token } = await this.tokenManager.getToken();
const { items } = await this.catalogApi.getEntities(
{
filter: { kind: 'Group', 'relations.hasMember': this.userEntityRef },
fields: ['metadata.name', 'metadata.namespace', 'spec.parent'],
},
{ token },
);
return items;
}

traverseGroups(memo: AncestorSearchMemo, group: Entity) {
const groupsRefs = new Set<string>();
const groupName = `group:${group.metadata.namespace?.toLocaleLowerCase(
'en-US',
)}/${group.metadata.name.toLocaleLowerCase('en-US')}`;
if (!memo.hasEntityRef(groupName)) {
memo.setNode(groupName);
}

const parent = group.spec?.parent as string;
const parentGroup = this.allGroups.find(g => g.metadata.name === parent);

if (parentGroup) {
const parentName = `group:${group.metadata.namespace?.toLocaleLowerCase(
'en-US',
)}/${parent.toLocaleLowerCase('en-US')}`;
memo.setEdge(parentName, groupName);
groupsRefs.add(parentName);
}

if (groupsRefs.size > 0 && memo.isAcyclic()) {
this.traverseGroups(memo, parentGroup!);
}
}

async buildUserGraph(memo: AncestorSearchMemo) {
const userGroups = await this.getUserGroups();
userGroups.forEach(group => this.traverseGroups(memo, group));
}
}
41 changes: 41 additions & 0 deletions plugins/rbac-backend/src/role-manager/role-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export class RoleList {
public name: string;

private roles: RoleList[];

public constructor(name: string) {
this.name = name;
this.roles = [];
}

public addRole(role: RoleList): void {
if (this.roles.some(n => n.name === role.name)) {
return;
}
this.roles.push(role);
}

public deleteRole(role: RoleList): void {
this.roles = this.roles.filter(n => n.name !== role.name);
}

public hasRole(name: string, hierarchyLevel: number): boolean {
if (this.name === name) {
return true;
}
if (hierarchyLevel <= 0) {
return false;
}
for (const role of this.roles) {
if (role.hasRole(name, hierarchyLevel - 1)) {
return true;
}
}

return false;
}

getRoles(): RoleList[] {
return this.roles;
}
}
Loading

0 comments on commit e63aac2

Please sign in to comment.