diff --git a/plugins/rbac-backend/src/file-permissions/csv.test.ts b/plugins/rbac-backend/src/file-permissions/csv.test.ts index e6bd7342ed..8de15634e0 100644 --- a/plugins/rbac-backend/src/file-permissions/csv.test.ts +++ b/plugins/rbac-backend/src/file-permissions/csv.test.ts @@ -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, diff --git a/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts new file mode 100644 index 0000000000..78732d3621 --- /dev/null +++ b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts @@ -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 { + 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 { + 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(); + 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)); + } +} diff --git a/plugins/rbac-backend/src/role-manager/role-list.ts b/plugins/rbac-backend/src/role-manager/role-list.ts new file mode 100644 index 0000000000..2eaa09e4b1 --- /dev/null +++ b/plugins/rbac-backend/src/role-manager/role-list.ts @@ -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; + } +} diff --git a/plugins/rbac-backend/src/service/role-manager.test.ts b/plugins/rbac-backend/src/role-manager/role-manager.test.ts similarity index 78% rename from plugins/rbac-backend/src/service/role-manager.test.ts rename to plugins/rbac-backend/src/role-manager/role-manager.test.ts index 1221c42cf2..ba6757b00f 100644 --- a/plugins/rbac-backend/src/service/role-manager.test.ts +++ b/plugins/rbac-backend/src/role-manager/role-manager.test.ts @@ -4,7 +4,7 @@ import { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; -import { BackstageRoleManager } from './role-manager'; +import { BackstageRoleManager } from '../role-manager/role-manager'; describe('BackstageRoleManager', () => { const catalogApiMock: any = { @@ -112,15 +112,8 @@ describe('BackstageRoleManager', () => { { filter: { kind: 'Group', - 'relations.hasMember': ['user:default/mike'], }, - fields: [ - 'metadata.name', - 'kind', - 'metadata.namespace', - 'spec.parent', - 'spec.children', - ], + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], }, { token: 'some-token' }, ); @@ -142,15 +135,8 @@ describe('BackstageRoleManager', () => { { filter: { kind: 'Group', - 'relations.hasMember': ['user:default/mike'], }, - fields: [ - 'metadata.name', - 'kind', - 'metadata.namespace', - 'spec.parent', - 'spec.children', - ], + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], }, { token: 'some-token' }, ); @@ -182,7 +168,12 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return true for hasLink when user:default/mike inherits from group:default/somegroup', async () => { - const entityMock = createGroupEntity('somegroup', undefined, []); + const entityMock = createGroupEntity( + 'somegroup', + undefined, + [], + ['mike'], + ); catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); const result = await roleManager.hasLink( @@ -201,7 +192,12 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return true for hasLink when user:default/mike inherits role:default/somerole from group:default/somegroup', async () => { - const entityMock = createGroupEntity('somegroup', undefined, []); + const entityMock = createGroupEntity( + 'somegroup', + undefined, + [], + ['mike'], + ); roleManager.addLink('group:default/somegroup', 'role:default/somerole'); catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); @@ -221,7 +217,9 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink when user:default/mike does not inherits group:default/somegroup', async () => { - const entityMock = createGroupEntity('not-matched-group', undefined, []); + const entityMock = createGroupEntity('not-matched-group', undefined, [ + 'mike', + ]); catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); const result = await roleManager.hasLink( @@ -240,7 +238,9 @@ describe('BackstageRoleManager', () => { // user:default/mike group:default/somegroup // it('should return false for hasLink when user:default/mike does not inherits role:default/somerole', async () => { - const entityMock = createGroupEntity('not-matched-group', undefined, []); + const entityMock = createGroupEntity('not-matched-group', undefined, [ + 'mike', + ]); roleManager.addLink('group:default/somegroup', 'role:default/somerole'); catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); @@ -262,21 +262,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return true for hasLink, when user:default/mike inherits from group:default/team-a', async () => { - const groupMock = createGroupEntity('team-b', 'team-a', []); + const groupMock = createGroupEntity('team-b', 'team-a', [], ['mike']); const groupParentMock = createGroupEntity('team-a', undefined, [ 'team-b', ]); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupParentMock] }; - } - return { items: [] }; + return { items: [groupMock, groupParentMock] }; }); const result = await roleManager.hasLink( @@ -297,21 +293,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return true for hasLink, when user:default/mike inherits role:default/team-a from group:default/team-a', async () => { - const groupMock = createGroupEntity('team-b', 'team-a', []); + const groupMock = createGroupEntity('team-b', 'team-a', [], ['mike']); const groupParentMock = createGroupEntity('team-a', undefined, [ 'team-b', ]); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupParentMock] }; - } - return { items: [] }; + return { items: [groupMock, groupParentMock] }; }); roleManager.addLink('group:default/team-a', 'role:default/team-a'); @@ -339,33 +331,16 @@ describe('BackstageRoleManager', () => { 'team-c', 'team-d', ]); - const groupBMock = createGroupEntity('team-b', 'team-a', []); - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-a', []); + const groupBMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['tom']); + const groupDMock = createGroupEntity('team-d', 'team-a', [], ['john']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupBMock] }; } - if (hasMember && hasMember[0] === 'user:default/tom') { - return { items: [groupCMock] }; - } - if (hasMember && hasMember[0] === 'user:default/john') { - return { items: [groupDMock] }; - } - - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupAMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-d') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); const result = await roleManager.hasLink( @@ -386,38 +361,21 @@ describe('BackstageRoleManager', () => { // user:default/tom user:default/mike user:default:john // it('should return true for hasLink, when user:default/mike inherits role:default/team-b from group:default/team-b', async () => { - const groupAMock = createGroupEntity('team-a', undefined, [ - 'team-b', + const groupBMock = createGroupEntity('team-b', undefined, [ + 'team-a', 'team-c', 'team-d', ]); - const groupBMock = createGroupEntity('team-b', 'team-a', []); - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-a', []); + const groupAMock = createGroupEntity('team-a', 'team-b', [], ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['tom']); + const groupDMock = createGroupEntity('team-d', 'team-a', [], ['john']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { - return { items: [groupBMock] }; - } - if (hasMember && hasMember[0] === 'user:default/tom') { - return { items: [groupCMock] }; - } - if (hasMember && hasMember[0] === 'user:default/john') { - return { items: [groupDMock] }; - } - - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupAMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-d') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupAMock] }; } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); roleManager.addLink('group:default/team-b', 'role:default/team-b'); @@ -440,19 +398,15 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike does not inherits from group:default/team-c', async () => { - const groupBMock = createGroupEntity('team-b', 'team-a', []); + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); const groupAMock = createGroupEntity('team-a', undefined, ['team-b']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupBMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock] }; }); const result = await roleManager.hasLink( @@ -473,19 +427,15 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike does not inherits role:default/team-c from group:default/team-c', async () => { - const groupBMock = createGroupEntity('team-b', 'team-a', []); + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); const groupAMock = createGroupEntity('team-a', undefined, ['team-b']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupBMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock] }; }); roleManager.addLink('group:default/team-c', 'role:default/team-c'); @@ -508,25 +458,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return true for hasLink, when user:default/mike inherits group tree with group:default/team-a', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-b', []); - const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); - const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, [], ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, [], ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); const result = await roleManager.hasLink( @@ -547,25 +489,17 @@ describe('BackstageRoleManager', () => { // |--------user:default/mike -------------------| // it('should return true for hasLink, when user:default/mike inherits role:default/team-a group tree with group:default/team-a', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-b', []); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); roleManager.addLink('group:default/team-a', 'role:default/team-a'); @@ -588,25 +522,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike inherits from group:default/team-e', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-b', []); + const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); const result = await roleManager.hasLink( @@ -627,25 +553,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike inherits role:default/team-e from group:default/team-e', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', []); - const groupDMock = createGroupEntity('team-d', 'team-b', []); + const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; }); roleManager.addLink('group:default/team-e', 'role:default/team-e'); @@ -669,19 +587,15 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike inherits from group:default/team-a and group:default/team-b, but we have cycle dependency', async () => { - const groupBMock = createGroupEntity('team-b', 'team-a', []); + const groupBMock = createGroupEntity('team-b', 'team-a', [], ['mike']); const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupBMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupBMock, groupAMock] }; }); let result = await roleManager.hasLink( @@ -715,19 +629,15 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike inherits role:default/team-b and role:default/team-a from group:default/team-a and group:default/team-b, but we have cycle dependency', async () => { - const groupBMock = createGroupEntity('team-b', 'team-a', []); + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupBMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupBMock, groupAMock] }; }); roleManager.addLink('group:default/team-b', 'role:default/team-b'); @@ -767,22 +677,15 @@ describe('BackstageRoleManager', () => { // it('should return false for hasLink, when user:default/mike inherits from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency', async () => { const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); - const groupBMock = createGroupEntity('team-b', 'team-a', []); - const groupCMock = createGroupEntity('team-c', 'team-b', []); + const groupBMock = createGroupEntity('team-b', 'team-a', ['team-c']); + const groupCMock = createGroupEntity('team-c', 'team-b', [], ['mike']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupBMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock] }; }); let result = await roleManager.hasLink( @@ -828,22 +731,15 @@ describe('BackstageRoleManager', () => { // it('should return false for hasLink, when user:default/mike inherits the roles from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency', async () => { const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); - const groupBMock = createGroupEntity('team-b', 'team-a', []); - const groupCMock = createGroupEntity('team-c', 'team-b', []); + const groupBMock = createGroupEntity('team-b', 'team-a', ['team-c']); + const groupCMock = createGroupEntity('team-c', 'team-b', ['mike']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock] }; } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupBMock] }; - } - if (hasParent && hasParent[0] === 'group:default/team-b') { - return { items: [groupAMock] }; - } - return { items: [] }; + return { items: [groupAMock, groupBMock, groupCMock] }; }); roleManager.addLink('group:default/team-a', 'role:default/team-a'); @@ -890,27 +786,17 @@ describe('BackstageRoleManager', () => { // user:default/mike // it('should return false for hasLink, when user:default/mike inherits group tree with group:default/team-a, but we cycle dependency', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); - const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - // first iteration - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - // second iteration - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupCMock, groupDMock, groupAMock, groupBMock] }; }); const result = await roleManager.hasLink( @@ -935,28 +821,19 @@ describe('BackstageRoleManager', () => { // user:default/mike -------------------| // it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-a, but we cycle dependency', async () => { - const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); - const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - // first iteration - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock, groupDMock] }; } - const hasParent = arg.filter['relations.parentOf']; - // second iteration - if ( - hasParent && - hasParent[0] === 'group:default/team-c' && - hasParent[1] === 'group:default/team-d' - ) { - return { items: [groupAMock, groupBMock] }; - } - return { items: [] }; + return { items: [groupCMock, groupDMock, groupAMock, groupBMock] }; }); + roleManager.addLink('group:default/team-a', 'role:default/team-a'); roleManager.addLink('group:default/team-c', 'role:default/team-c'); @@ -970,6 +847,137 @@ describe('BackstageRoleManager', () => { ); }); + // user:default/mike should inherits role from group:default/team-f, and we have a complex graph, and cycle dependency + // So return false on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b | + // ↓ ↑ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-e', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + roleManager.addLink('group:default/team-g', 'role:default/team-g'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeFalsy(); + expect(loggerMock.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for group: group:default/team-a', + ); + + const test = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-g', + ); + expect(test).toBeFalsy(); + }); + + // user:default/mike should inherits role from group:default/team-e, and we have a complex graph + // So return true on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b | + // ↓ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return true for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-f', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-e', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + roleManager.addLink('group:default/team-g', 'role:default/team-g'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeTruthy(); + }); + // user:default/mike should inherits from group:default/team-a, but we have cycle dependency: team-a -> team-c. // So return false on call hasLink. // @@ -986,43 +994,38 @@ describe('BackstageRoleManager', () => { // ↓ ↓ // user:default/mike user:default/tom // + // This test passes now ? it('should return false for hasLink for user:default/mike and group:default/team-a(cycle dependency), but should be true for user:default/tom and group:default/team-b', async () => { const groupRootMock = createGroupEntity('root', undefined, [ 'team-a', 'team-b', ]); - const groupCMock = createGroupEntity('team-c', 'team-a', ['team-a']); - const groupDMock = createGroupEntity('team-d', 'team-b', []); - const groupAMock = createGroupEntity('team-a', 'root', ['team-c']); + const groupCMock = createGroupEntity( + 'team-c', + 'team-a', + ['team-a'], + ['mike'], + ); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['tom']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); const groupBMock = createGroupEntity('team-b', 'root', ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock] }; - } - if (hasMember && hasMember[0] === 'user:default/tom') { + } else if (hasMember && hasMember === 'user:default/tom') { return { items: [groupDMock] }; } - - const hasParent = arg.filter['relations.parentOf']; - - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupAMock] }; - } - - if (hasParent && hasParent[0] === 'group:default/team-d') { - return { items: [groupBMock] }; - } - - if ( - (hasParent && hasParent[0] === 'group:default/team-b') || - hasParent[0] === 'group:default/team-a' - ) { - return { items: [groupRootMock] }; - } - return { items: [] }; + return { + items: [ + groupRootMock, + groupCMock, + groupDMock, + groupAMock, + groupBMock, + ], + }; }); let result = await roleManager.hasLink( @@ -1056,44 +1059,38 @@ describe('BackstageRoleManager', () => { // group:default/team-c group:default/team-d // ↓ ↓ // user:default/mike user:default/tom - // + // This test passes now ? it('should return false for hasLink for user:default/mike and role:default/team-a(cycle dependency), but should be true for user:default/tom and role:default/team-b', async () => { const groupRootMock = createGroupEntity('root', undefined, [ 'team-a', 'team-b', ]); - const groupCMock = createGroupEntity('team-c', 'team-a', ['team-a']); - const groupDMock = createGroupEntity('team-d', 'team-b', []); - const groupAMock = createGroupEntity('team-a', 'root', ['team-c']); + const groupCMock = createGroupEntity( + 'team-c', + 'team-a', + ['team-a'], + ['mike'], + ); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['tom']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); const groupBMock = createGroupEntity('team-b', 'root', ['team-d']); catalogApiMock.getEntities.mockImplementation((arg: any) => { const hasMember = arg.filter['relations.hasMember']; - - if (hasMember && hasMember[0] === 'user:default/mike') { + if (hasMember && hasMember === 'user:default/mike') { return { items: [groupCMock] }; - } - if (hasMember && hasMember[0] === 'user:default/tom') { + } else if (hasMember && hasMember === 'user:default/tom') { return { items: [groupDMock] }; } - - const hasParent = arg.filter['relations.parentOf']; - - if (hasParent && hasParent[0] === 'group:default/team-c') { - return { items: [groupAMock] }; - } - - if (hasParent && hasParent[0] === 'group:default/team-d') { - return { items: [groupBMock] }; - } - - if ( - (hasParent && hasParent[0] === 'group:default/team-b') || - hasParent[0] === 'group:default/team-a' - ) { - return { items: [groupRootMock] }; - } - return { items: [] }; + return { + items: [ + groupRootMock, + groupCMock, + groupDMock, + groupAMock, + groupBMock, + ], + }; }); roleManager.addLink('group:default/team-a', 'role:default/team-a'); @@ -1154,20 +1151,12 @@ describe('BackstageRoleManager', () => { }); it('getRoles returns role for user inherited from group', async () => { - const teamAGroup = createGroupEntity('team-a', undefined, [ - 'user:default/test', - ]); + const teamAGroup = createGroupEntity('team-a', undefined, [], ['test']); roleManager.addLink('group:default/team-a', 'role:default/rbac_admin'); - catalogApiMock.getEntities.mockImplementation((arg: any) => { - const hasMember = arg.filter['relations.hasMember']; - - if (hasMember && hasMember[0] === 'user:default/test') { - return { items: [teamAGroup] }; - } - - return { items: [] }; + catalogApiMock.getEntities.mockImplementation((_arg: any) => { + return { items: [teamAGroup] }; }); let roles = await roleManager.getRoles('user:default/test'); @@ -1188,6 +1177,7 @@ describe('BackstageRoleManager', () => { name: string, parent?: string, children?: string[], + members?: string[], ): Entity { const entity: Entity = { apiVersion: 'v1', @@ -1203,6 +1193,10 @@ describe('BackstageRoleManager', () => { entity.spec!.children = children; } + if (members) { + entity.spec!.members = members; + } + if (parent) { entity.spec!.parent = parent; } diff --git a/plugins/rbac-backend/src/service/role-manager.ts b/plugins/rbac-backend/src/role-manager/role-manager.ts similarity index 58% rename from plugins/rbac-backend/src/service/role-manager.ts rename to plugins/rbac-backend/src/role-manager/role-manager.ts index 652ae2704a..976e384eec 100644 --- a/plugins/rbac-backend/src/service/role-manager.ts +++ b/plugins/rbac-backend/src/role-manager/role-manager.ts @@ -2,130 +2,20 @@ import { TokenManager } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { parseEntityRef } from '@backstage/catalog-model'; -import { alg, Graph } from '@dagrejs/graphlib'; import { RoleManager } from 'casbin'; import { Logger } from 'winston'; -type FilterRelations = 'relations.hasMember' | 'relations.parentOf'; - -class Role { - public name: string; - - private roles: Role[]; - - public constructor(name: string) { - this.name = name; - this.roles = []; - } - - public addRole(role: Role): void { - if (this.roles.some(n => n.name === role.name)) { - return; - } - this.roles.push(role); - } - - public deleteRole(role: Role): 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(): Role[] { - return this.roles; - } -} - -// 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. -// -// Notice: this class should be used like cache object in the nearest feature. -// This cache can be implemented with time expiration and it can be map: Map. -class AncestorSearchMemo { - private filterEntityRefs: Set; - - private graph: Graph; - private fr: FilterRelations; - - constructor(userEntityRef: string) { - this.filterEntityRefs = new Set([userEntityRef]); - this.graph = new Graph({ directed: true }); - this.fr = 'relations.hasMember'; - } - - setFilterEntityRefs(entityRefs: Set) { - this.filterEntityRefs = entityRefs; - } - - getFilterEntityRefs(): Set { - return this.filterEntityRefs; - } - - setFilterRelations(fr: FilterRelations): void { - this.fr = fr; - } - - getFilterRelations(): FilterRelations { - return this.fr; - } - - 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(); - } -} +import { AncestorSearchMemo } from './ancestor-search-memo'; +import { RoleList } from './role-list'; export class BackstageRoleManager implements RoleManager { - private allRoles: Map; + private allRoles: Map; constructor( private readonly catalogApi: CatalogApi, private readonly log: Logger, private readonly tokenManager: TokenManager, ) { - this.allRoles = new Map(); + this.allRoles = new Map(); } /** @@ -202,8 +92,14 @@ export class BackstageRoleManager implements RoleManager { return false; } - const memo = new AncestorSearchMemo(name1); - await this.findAncestorGroups(memo); + const memo = new AncestorSearchMemo( + name1, + this.tokenManager, + this.catalogApi, + ); + await memo.getAllGroups(); + await memo.buildUserGraph(memo); + memo.debugNodesAndEdges(this.log, name1); if (!memo.isAcyclic()) { const cycles = memo.findCycles(); @@ -272,8 +168,13 @@ export class BackstageRoleManager implements RoleManager { async getRoles(name: string, ..._domain: string[]): Promise { const { kind } = parseEntityRef(name); if (kind === 'user') { - const memo = new AncestorSearchMemo(name); - await this.findAncestorGroups(memo); + const memo = new AncestorSearchMemo( + name, + this.tokenManager, + this.catalogApi, + ); + await memo.getAllGroups(); + await memo.buildUserGraph(memo); memo.debugNodesAndEdges(this.log, name); const userAndParentGroups = memo.getNodes(); userAndParentGroups.push(name); @@ -312,63 +213,12 @@ export class BackstageRoleManager implements RoleManager { // do nothing } - private async findAncestorGroups(memo: AncestorSearchMemo): Promise { - const { token } = await this.tokenManager.getToken(); - const { items } = await this.catalogApi.getEntities( - { - filter: { - kind: 'Group', - [memo.getFilterRelations()]: Array.from(memo.getFilterEntityRefs()), - }, - // Save traffic with only required information for us - fields: [ - 'metadata.name', - 'kind', - 'metadata.namespace', - 'spec.parent', - 'spec.children', - ], - }, - { token }, - ); - - const groupsRefs = new Set(); - for (const item of items) { - const groupRef = `group:default/${item.metadata.name.toLocaleLowerCase()}`; - - memo.setNode(groupRef); - for (const child of (item.spec?.children as string[]) ?? []) { - const childEntityRef = `group:default/${child.toLocaleLowerCase()}`; - if ( - memo.getFilterEntityRefs().has(childEntityRef) || - memo.getFilterRelations() === 'relations.hasMember' - ) { - memo.setEdge(groupRef, childEntityRef); - } - } - - if (item.spec?.parent) { - const parentRef = `group:default/${( - item.spec?.parent as string - ).toLocaleLowerCase()}`; - memo.setEdge(parentRef, groupRef); - groupsRefs.add(groupRef); - } - } - - if (groupsRefs.size > 0 && memo.isAcyclic()) { - memo.setFilterEntityRefs(groupsRefs); - memo.setFilterRelations('relations.parentOf'); - await this.findAncestorGroups(memo); - } - } - - private getOrCreateRole(name: string): Role { + private getOrCreateRole(name: string): RoleList { const role = this.allRoles.get(name); if (role) { return role; } - const newRole = new Role(name); + const newRole = new RoleList(name); this.allRoles.set(name, newRole); return newRole; diff --git a/plugins/rbac-backend/src/service/permission-policy.test.ts b/plugins/rbac-backend/src/service/permission-policy.test.ts index 1f5f233ad1..fadd9fd501 100644 --- a/plugins/rbac-backend/src/service/permission-policy.test.ts +++ b/plugins/rbac-backend/src/service/permission-policy.test.ts @@ -36,10 +36,10 @@ import { PolicyMetadataStorage, } from '../database/policy-metadata-storage'; import { RoleMetadataStorage } from '../database/role-metadata'; +import { BackstageRoleManager } from '../role-manager/role-manager'; import { EnforcerDelegate } from './enforcer-delegate'; import { MODEL } from './permission-model'; import { RBACPermissionPolicy } from './permission-policy'; -import { BackstageRoleManager } from './role-manager'; type PermissionAction = 'create' | 'read' | 'update' | 'delete'; @@ -1019,16 +1019,49 @@ describe('RBACPermissionPolicy Tests', () => { }); // Tests for admin added through app config - it('should allow access to permission resource for admin added through app config', async () => { - const decision = await policy.handle( - newPolicyQueryWithResourcePermission( - 'policy-entity.read', - 'policy-entity', - 'read', - ), - newIdentityResponse('user:default/guest'), - ); - expect(decision.result).toBe(AuthorizeResult.ALLOW); + it('should allow access to permission resources for admin added through app config', async () => { + const adminPerm: { + name: string; + resource: string; + action: PermissionAction; + }[] = [ + { + name: 'policy.entity.read', + resource: 'policy-entity', + action: 'read', + }, + { + name: 'policy.entity.create', + resource: 'policy-entity', + action: 'create', + }, + { + name: 'policy.entity.update', + resource: 'policy-entity', + action: 'update', + }, + { + name: 'policy.entity.delete', + resource: 'policy-entity', + action: 'delete', + }, + { + name: 'catalog.entity.read', + resource: 'catalog-entity', + action: 'read', + }, + ]; + for (const perm of adminPerm) { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + perm.name, + perm.resource, + perm.action, + ), + newIdentityResponse('user:default/guest'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + } }); }); @@ -1367,13 +1400,12 @@ describe('Policy checks for resourced permissions defined by name', () => { name: 'team-a', namespace: 'default', }, + spec: { + members: ['tor'], + }, }; - catalogApi.getEntities.mockImplementation(arg => { - const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/tor') { - return { items: [groupEntityMock] }; - } - return { items: [] }; + catalogApi.getEntities.mockImplementation(_arg => { + return { items: [groupEntityMock] }; }); const policy = await createRBACPolicy(` @@ -1402,6 +1434,7 @@ describe('Policy checks for resourced permissions defined by name', () => { namespace: 'default', }, spec: { + members: ['tor'], parent: 'team-b', }, }; @@ -1413,16 +1446,8 @@ describe('Policy checks for resourced permissions defined by name', () => { namespace: 'default', }, }; - catalogApi.getEntities.mockImplementation(arg => { - const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/tor') { - return { items: [groupEntityMock] }; - } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/team-a') { - return { items: [groupParentMock] }; - } - return { items: [] }; + catalogApi.getEntities.mockImplementation(_arg => { + return { items: [groupParentMock, groupEntityMock] }; }); const policy = await createRBACPolicy(` @@ -1577,6 +1602,9 @@ describe('Policy checks for users and groups', () => { name: 'data_admin', namespace: 'default', }, + spec: { + members: ['alice'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); @@ -1596,6 +1624,9 @@ describe('Policy checks for users and groups', () => { name: 'data_admin', namespace: 'default', }, + spec: { + members: ['akira'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1614,6 +1645,9 @@ describe('Policy checks for users and groups', () => { name: 'data_admin', namespace: 'default', }, + spec: { + members: ['antey'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1651,6 +1685,9 @@ describe('Policy checks for users and groups', () => { name: 'data_read_admin', namespace: 'default', }, + spec: { + members: ['mike'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1669,6 +1706,9 @@ describe('Policy checks for users and groups', () => { name: 'data_read_admin', namespace: 'default', }, + spec: { + members: ['tom'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1688,7 +1728,8 @@ describe('Policy checks for users and groups', () => { namespace: 'default', }, spec: { - parent: 'group:default/data_parent_admin', + members: ['mike'], + parent: 'data_parent_admin', }, }; @@ -1701,16 +1742,8 @@ describe('Policy checks for users and groups', () => { }, }; - catalogApi.getEntities.mockImplementation(arg => { - const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { - return { items: [groupMock] }; - } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/data_read_admin') { - return { items: [groupParentMock] }; - } - return { items: [] }; + catalogApi.getEntities.mockImplementation(_arg => { + return { items: [groupMock, groupParentMock] }; }); const decision = await policy.handle( @@ -1731,6 +1764,9 @@ describe('Policy checks for users and groups', () => { name: 'data_admin', namespace: 'default', }, + spec: { + members: ['alice'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); @@ -1821,6 +1857,9 @@ describe('Policy checks for users and groups', () => { name: 'data_read_admin', namespace: 'default', }, + spec: { + members: ['mike'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1843,6 +1882,9 @@ describe('Policy checks for users and groups', () => { name: 'data_read_admin', namespace: 'default', }, + spec: { + members: ['tom'], + }, }; catalogApi.getEntities.mockReturnValue({ items: [entityMock] }); const decision = await policy.handle( @@ -1866,7 +1908,8 @@ describe('Policy checks for users and groups', () => { namespace: 'default', }, spec: { - parent: 'group:default/data_parent_admin', + members: ['mike'], + parent: 'data_parent_admin', }, }; @@ -1879,16 +1922,8 @@ describe('Policy checks for users and groups', () => { }, }; - catalogApi.getEntities.mockImplementation(arg => { - const hasMember = arg.filter['relations.hasMember']; - if (hasMember && hasMember[0] === 'user:default/mike') { - return { items: [groupMock] }; - } - const hasParent = arg.filter['relations.parentOf']; - if (hasParent && hasParent[0] === 'group:default/data_read_admin') { - return { items: [groupParentMock] }; - } - return { items: [] }; + catalogApi.getEntities.mockImplementation(_arg => { + return { items: [groupParentMock, groupMock] }; }); const decision = await policy.handle( diff --git a/plugins/rbac-backend/src/service/policy-builder.ts b/plugins/rbac-backend/src/service/policy-builder.ts index 98456bb0d9..dd1b84decd 100644 --- a/plugins/rbac-backend/src/service/policy-builder.ts +++ b/plugins/rbac-backend/src/service/policy-builder.ts @@ -20,11 +20,11 @@ import { DataBaseConditionalStorage } from '../database/conditional-storage'; import { migrate } from '../database/migration'; import { DataBasePolicyMetadataStorage } from '../database/policy-metadata-storage'; import { DataBaseRoleMetadataStorage } from '../database/role-metadata'; +import { BackstageRoleManager } from '../role-manager/role-manager'; import { EnforcerDelegate } from './enforcer-delegate'; import { MODEL } from './permission-model'; import { RBACPermissionPolicy } from './permission-policy'; import { PolicesServer } from './policies-rest-api'; -import { BackstageRoleManager } from './role-manager'; export class PolicyBuilder { public static async build(