@backstage-community/plugin-rbac-backend 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +671 -0
- package/README.md +220 -0
- package/config.d.ts +68 -0
- package/dist/admin-permissions/admin-creation.cjs.js +117 -0
- package/dist/admin-permissions/admin-creation.cjs.js.map +1 -0
- package/dist/audit-log/audit-logger.cjs.js +108 -0
- package/dist/audit-log/audit-logger.cjs.js.map +1 -0
- package/dist/audit-log/rest-errors-interceptor.cjs.js +100 -0
- package/dist/audit-log/rest-errors-interceptor.cjs.js.map +1 -0
- package/dist/conditional-aliases/alias-resolver.cjs.js +76 -0
- package/dist/conditional-aliases/alias-resolver.cjs.js.map +1 -0
- package/dist/database/casbin-adapter-factory.cjs.js +87 -0
- package/dist/database/casbin-adapter-factory.cjs.js.map +1 -0
- package/dist/database/conditional-storage.cjs.js +172 -0
- package/dist/database/conditional-storage.cjs.js.map +1 -0
- package/dist/database/migration.cjs.js +21 -0
- package/dist/database/migration.cjs.js.map +1 -0
- package/dist/database/role-metadata.cjs.js +89 -0
- package/dist/database/role-metadata.cjs.js.map +1 -0
- package/dist/file-permissions/csv-file-watcher.cjs.js +407 -0
- package/dist/file-permissions/csv-file-watcher.cjs.js.map +1 -0
- package/dist/file-permissions/file-watcher.cjs.js +46 -0
- package/dist/file-permissions/file-watcher.cjs.js.map +1 -0
- package/dist/file-permissions/yaml-conditional-file-watcher.cjs.js +208 -0
- package/dist/file-permissions/yaml-conditional-file-watcher.cjs.js.map +1 -0
- package/dist/helper.cjs.js +171 -0
- package/dist/helper.cjs.js.map +1 -0
- package/dist/index.cjs.js +14 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/plugin.cjs.js +79 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/policies/allow-all-policy.cjs.js +12 -0
- package/dist/policies/allow-all-policy.cjs.js.map +1 -0
- package/dist/policies/permission-policy.cjs.js +243 -0
- package/dist/policies/permission-policy.cjs.js.map +1 -0
- package/dist/providers/connect-providers.cjs.js +211 -0
- package/dist/providers/connect-providers.cjs.js.map +1 -0
- package/dist/role-manager/ancestor-search-memo.cjs.js +159 -0
- package/dist/role-manager/ancestor-search-memo.cjs.js.map +1 -0
- package/dist/role-manager/member-list.cjs.js +101 -0
- package/dist/role-manager/member-list.cjs.js.map +1 -0
- package/dist/role-manager/role-manager.cjs.js +281 -0
- package/dist/role-manager/role-manager.cjs.js.map +1 -0
- package/dist/service/enforcer-delegate.cjs.js +353 -0
- package/dist/service/enforcer-delegate.cjs.js.map +1 -0
- package/dist/service/permission-model.cjs.js +21 -0
- package/dist/service/permission-model.cjs.js.map +1 -0
- package/dist/service/plugin-endpoints.cjs.js +121 -0
- package/dist/service/plugin-endpoints.cjs.js.map +1 -0
- package/dist/service/policies-rest-api.cjs.js +949 -0
- package/dist/service/policies-rest-api.cjs.js.map +1 -0
- package/dist/service/policy-builder.cjs.js +134 -0
- package/dist/service/policy-builder.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +24 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/dist/validation/condition-validation.cjs.js +107 -0
- package/dist/validation/condition-validation.cjs.js.map +1 -0
- package/dist/validation/policies-validation.cjs.js +194 -0
- package/dist/validation/policies-validation.cjs.js.map +1 -0
- package/migrations/20231015161232_migrations.js +41 -0
- package/migrations/20231212224526_migrations.js +84 -0
- package/migrations/20231221113214_migrations.js +60 -0
- package/migrations/20240201144429_migrations.js +37 -0
- package/migrations/20240215154456_migrations.js +143 -0
- package/migrations/20240308134410_migrations.js +31 -0
- package/migrations/20240308134941_migrations.js +43 -0
- package/migrations/20240404111242_migrations.js +53 -0
- package/migrations/20240611092136_migrations.js +29 -0
- package/package.json +98 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var graphlib = require('@dagrejs/graphlib');
|
|
4
|
+
|
|
5
|
+
class AncestorSearchMemo {
|
|
6
|
+
graph;
|
|
7
|
+
catalogApi;
|
|
8
|
+
catalogDBClient;
|
|
9
|
+
auth;
|
|
10
|
+
userEntityRef;
|
|
11
|
+
maxDepth;
|
|
12
|
+
constructor(userEntityRef, catalogApi, catalogDBClient, auth, maxDepth) {
|
|
13
|
+
this.graph = new graphlib.Graph({ directed: true });
|
|
14
|
+
this.userEntityRef = userEntityRef;
|
|
15
|
+
this.catalogApi = catalogApi;
|
|
16
|
+
this.catalogDBClient = catalogDBClient;
|
|
17
|
+
this.auth = auth;
|
|
18
|
+
this.maxDepth = maxDepth;
|
|
19
|
+
}
|
|
20
|
+
isAcyclic() {
|
|
21
|
+
return graphlib.alg.isAcyclic(this.graph);
|
|
22
|
+
}
|
|
23
|
+
findCycles() {
|
|
24
|
+
return graphlib.alg.findCycles(this.graph);
|
|
25
|
+
}
|
|
26
|
+
setEdge(parentEntityRef, childEntityRef) {
|
|
27
|
+
this.graph.setEdge(parentEntityRef, childEntityRef);
|
|
28
|
+
}
|
|
29
|
+
setNode(entityRef) {
|
|
30
|
+
this.graph.setNode(entityRef);
|
|
31
|
+
}
|
|
32
|
+
hasEntityRef(groupRef) {
|
|
33
|
+
return this.graph.hasNode(groupRef);
|
|
34
|
+
}
|
|
35
|
+
debugNodesAndEdges(logger, userEntity) {
|
|
36
|
+
logger.debug(
|
|
37
|
+
`SubGraph edges: ${JSON.stringify(this.graph.edges())} for ${userEntity}`
|
|
38
|
+
);
|
|
39
|
+
logger.debug(
|
|
40
|
+
`SubGraph nodes: ${JSON.stringify(this.graph.nodes())} for ${userEntity}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
getNodes() {
|
|
44
|
+
return this.graph.nodes();
|
|
45
|
+
}
|
|
46
|
+
async doesRelationTableExist() {
|
|
47
|
+
try {
|
|
48
|
+
return await this.catalogDBClient.schema.hasTable("relations");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async getAllGroups() {
|
|
54
|
+
const { token } = await this.auth.getPluginRequestToken({
|
|
55
|
+
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
56
|
+
targetPluginId: "catalog"
|
|
57
|
+
});
|
|
58
|
+
const { items } = await this.catalogApi.getEntities(
|
|
59
|
+
{
|
|
60
|
+
filter: { kind: "Group" },
|
|
61
|
+
fields: ["metadata.name", "metadata.namespace", "spec.parent"]
|
|
62
|
+
},
|
|
63
|
+
{ token }
|
|
64
|
+
);
|
|
65
|
+
return items;
|
|
66
|
+
}
|
|
67
|
+
async getAllRelations() {
|
|
68
|
+
try {
|
|
69
|
+
const rows = await this.catalogDBClient("relations").select("source_entity_ref", "target_entity_ref").where("type", "childOf");
|
|
70
|
+
return rows;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async getUserGroups() {
|
|
76
|
+
const { token } = await this.auth.getPluginRequestToken({
|
|
77
|
+
onBehalfOf: await this.auth.getOwnServiceCredentials(),
|
|
78
|
+
targetPluginId: "catalog"
|
|
79
|
+
});
|
|
80
|
+
const { items } = await this.catalogApi.getEntities(
|
|
81
|
+
{
|
|
82
|
+
filter: { kind: "Group", "relations.hasMember": this.userEntityRef },
|
|
83
|
+
fields: ["metadata.name", "metadata.namespace", "spec.parent"]
|
|
84
|
+
},
|
|
85
|
+
{ token }
|
|
86
|
+
);
|
|
87
|
+
return items;
|
|
88
|
+
}
|
|
89
|
+
async getUserRelations() {
|
|
90
|
+
try {
|
|
91
|
+
const rows = await this.catalogDBClient("relations").select("source_entity_ref", "target_entity_ref").where({ type: "memberOf", source_entity_ref: this.userEntityRef });
|
|
92
|
+
return rows;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
traverseGroups(memo, group, allGroups, current_depth) {
|
|
98
|
+
const groupName = `group:${group.metadata.namespace?.toLocaleLowerCase(
|
|
99
|
+
"en-US"
|
|
100
|
+
)}/${group.metadata.name.toLocaleLowerCase("en-US")}`;
|
|
101
|
+
if (!memo.hasEntityRef(groupName)) {
|
|
102
|
+
memo.setNode(groupName);
|
|
103
|
+
}
|
|
104
|
+
if (this.maxDepth !== void 0 && current_depth >= this.maxDepth) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const depth = current_depth + 1;
|
|
108
|
+
const parent = group.spec?.parent;
|
|
109
|
+
const parentGroup = allGroups.find((g) => g.metadata.name === parent);
|
|
110
|
+
if (parentGroup) {
|
|
111
|
+
const parentName = `group:${group.metadata.namespace?.toLocaleLowerCase(
|
|
112
|
+
"en-US"
|
|
113
|
+
)}/${parentGroup.metadata.name.toLocaleLowerCase("en-US")}`;
|
|
114
|
+
memo.setEdge(parentName, groupName);
|
|
115
|
+
if (memo.isAcyclic()) {
|
|
116
|
+
this.traverseGroups(memo, parentGroup, allGroups, depth);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
traverseRelations(memo, relation, allRelations, current_depth) {
|
|
121
|
+
if (this.maxDepth !== void 0 && current_depth >= this.maxDepth + 1) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const depth = current_depth + 1;
|
|
125
|
+
if (!memo.hasEntityRef(relation.source_entity_ref)) {
|
|
126
|
+
memo.setNode(relation.source_entity_ref);
|
|
127
|
+
}
|
|
128
|
+
memo.setEdge(relation.target_entity_ref, relation.source_entity_ref);
|
|
129
|
+
const parentGroup = allRelations.find(
|
|
130
|
+
(g) => g.source_entity_ref === relation.target_entity_ref
|
|
131
|
+
);
|
|
132
|
+
if (parentGroup && memo.isAcyclic()) {
|
|
133
|
+
this.traverseRelations(memo, parentGroup, allRelations, depth);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async buildUserGraph(memo) {
|
|
137
|
+
if (await this.doesRelationTableExist()) {
|
|
138
|
+
const userRelations = await this.getUserRelations();
|
|
139
|
+
const allRelations = await this.getAllRelations();
|
|
140
|
+
userRelations.forEach(
|
|
141
|
+
(group) => this.traverseRelations(
|
|
142
|
+
memo,
|
|
143
|
+
group,
|
|
144
|
+
allRelations,
|
|
145
|
+
0
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
const userGroups = await this.getUserGroups();
|
|
150
|
+
const allGroups = await this.getAllGroups();
|
|
151
|
+
userGroups.forEach(
|
|
152
|
+
(group) => this.traverseGroups(memo, group, allGroups, 0)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
exports.AncestorSearchMemo = AncestorSearchMemo;
|
|
159
|
+
//# sourceMappingURL=ancestor-search-memo.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ancestor-search-memo.cjs.js","sources":["../../src/role-manager/ancestor-search-memo.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport type { CatalogApi } from '@backstage/catalog-client';\nimport type { Entity } from '@backstage/catalog-model';\n\nimport { alg, Graph } from '@dagrejs/graphlib';\nimport { Knex } from 'knex';\n\nexport interface Relation {\n source_entity_ref: string;\n target_entity_ref: string;\n}\n\nexport type ASMGroup = Relation | Entity;\n\n// AncestorSearchMemo - should be used to build group hierarchy graph for User entity reference.\n// It supports search group entity reference link in the graph.\n// Also AncestorSearchMemo supports detection cycle dependencies between groups in the graph.\n//\nexport class AncestorSearchMemo {\n private graph: Graph;\n\n private catalogApi: CatalogApi;\n private catalogDBClient: Knex;\n private auth: AuthService;\n\n private userEntityRef: string;\n private maxDepth?: number;\n\n constructor(\n userEntityRef: string,\n catalogApi: CatalogApi,\n catalogDBClient: Knex,\n auth: AuthService,\n maxDepth?: number,\n ) {\n this.graph = new Graph({ directed: true });\n this.userEntityRef = userEntityRef;\n this.catalogApi = catalogApi;\n this.catalogDBClient = catalogDBClient;\n this.auth = auth;\n this.maxDepth = maxDepth;\n }\n\n isAcyclic(): boolean {\n return alg.isAcyclic(this.graph);\n }\n\n findCycles(): string[][] {\n return alg.findCycles(this.graph);\n }\n\n setEdge(parentEntityRef: string, childEntityRef: string) {\n this.graph.setEdge(parentEntityRef, childEntityRef);\n }\n\n setNode(entityRef: string): void {\n this.graph.setNode(entityRef);\n }\n\n hasEntityRef(groupRef: string): boolean {\n return this.graph.hasNode(groupRef);\n }\n\n debugNodesAndEdges(logger: LoggerService, userEntity: string): void {\n logger.debug(\n `SubGraph edges: ${JSON.stringify(this.graph.edges())} for ${userEntity}`,\n );\n logger.debug(\n `SubGraph nodes: ${JSON.stringify(this.graph.nodes())} for ${userEntity}`,\n );\n }\n\n getNodes(): string[] {\n return this.graph.nodes();\n }\n\n async doesRelationTableExist(): Promise<boolean> {\n try {\n return await this.catalogDBClient.schema.hasTable('relations');\n } catch (error) {\n return false;\n }\n }\n\n async getAllGroups(): Promise<ASMGroup[]> {\n const { token } = await this.auth.getPluginRequestToken({\n onBehalfOf: await this.auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n\n const { items } = await this.catalogApi.getEntities(\n {\n filter: { kind: 'Group' },\n fields: ['metadata.name', 'metadata.namespace', 'spec.parent'],\n },\n { token },\n );\n return items;\n }\n\n async getAllRelations(): Promise<ASMGroup[]> {\n try {\n const rows = await this.catalogDBClient('relations')\n .select('source_entity_ref', 'target_entity_ref')\n .where('type', 'childOf');\n return rows;\n } catch (error) {\n return [];\n }\n }\n\n async getUserGroups(): Promise<ASMGroup[]> {\n const { token } = await this.auth.getPluginRequestToken({\n onBehalfOf: await this.auth.getOwnServiceCredentials(),\n targetPluginId: 'catalog',\n });\n const { items } = await this.catalogApi.getEntities(\n {\n filter: { kind: 'Group', 'relations.hasMember': this.userEntityRef },\n fields: ['metadata.name', 'metadata.namespace', 'spec.parent'],\n },\n { token },\n );\n return items;\n }\n\n async getUserRelations(): Promise<ASMGroup[]> {\n try {\n const rows = await this.catalogDBClient('relations')\n .select('source_entity_ref', 'target_entity_ref')\n .where({ type: 'memberOf', source_entity_ref: this.userEntityRef });\n return rows;\n } catch (error) {\n return [];\n }\n }\n\n traverseGroups(\n memo: AncestorSearchMemo,\n group: Entity,\n allGroups: Entity[],\n current_depth: number,\n ) {\n const groupName = `group:${group.metadata.namespace?.toLocaleLowerCase(\n 'en-US',\n )}/${group.metadata.name.toLocaleLowerCase('en-US')}`;\n if (!memo.hasEntityRef(groupName)) {\n memo.setNode(groupName);\n }\n\n if (this.maxDepth !== undefined && current_depth >= this.maxDepth) {\n return;\n }\n const depth = current_depth + 1;\n\n const parent = group.spec?.parent as string;\n const parentGroup = allGroups.find(g => g.metadata.name === parent);\n\n if (parentGroup) {\n const parentName = `group:${group.metadata.namespace?.toLocaleLowerCase(\n 'en-US',\n )}/${parentGroup.metadata.name.toLocaleLowerCase('en-US')}`;\n memo.setEdge(parentName, groupName);\n\n if (memo.isAcyclic()) {\n this.traverseGroups(memo, parentGroup, allGroups, depth);\n }\n }\n }\n\n traverseRelations(\n memo: AncestorSearchMemo,\n relation: Relation,\n allRelations: Relation[],\n current_depth: number,\n ) {\n // We add one to the maxDepth here because the user is considered the starting node\n if (this.maxDepth !== undefined && current_depth >= this.maxDepth + 1) {\n return;\n }\n const depth = current_depth + 1;\n\n if (!memo.hasEntityRef(relation.source_entity_ref)) {\n memo.setNode(relation.source_entity_ref);\n }\n\n memo.setEdge(relation.target_entity_ref, relation.source_entity_ref);\n\n const parentGroup = allRelations.find(\n g => g.source_entity_ref === relation.target_entity_ref,\n );\n\n if (parentGroup && memo.isAcyclic()) {\n this.traverseRelations(memo, parentGroup, allRelations, depth);\n }\n }\n\n async buildUserGraph(memo: AncestorSearchMemo) {\n if (await this.doesRelationTableExist()) {\n const userRelations = await this.getUserRelations();\n const allRelations = await this.getAllRelations();\n userRelations.forEach(group =>\n this.traverseRelations(\n memo,\n group as Relation,\n allRelations as Relation[],\n 0,\n ),\n );\n } else {\n const userGroups = await this.getUserGroups();\n const allGroups = await this.getAllGroups();\n userGroups.forEach(group =>\n this.traverseGroups(memo, group as Entity, allGroups as Entity[], 0),\n );\n }\n }\n}\n"],"names":["Graph","alg"],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class RoleMemberList {
|
|
4
|
+
name;
|
|
5
|
+
members;
|
|
6
|
+
roles;
|
|
7
|
+
constructor(name) {
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.members = [];
|
|
10
|
+
this.roles = [];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* addMembers will add members to the RoleMemberList
|
|
14
|
+
* @param members The members to be added.
|
|
15
|
+
*/
|
|
16
|
+
addMembers(members) {
|
|
17
|
+
this.members = members;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* addMember will add a single member to the RoleMemberList, skips adding the user in the
|
|
21
|
+
* event that they already exist in the members array.
|
|
22
|
+
* @param member The member to be added.
|
|
23
|
+
*/
|
|
24
|
+
addMember(member) {
|
|
25
|
+
if (this.members.some((n) => n === member)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.members.push(member);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* hasMember will check if a particular member exists in the members array.
|
|
32
|
+
* @param name The member to be checked for.
|
|
33
|
+
*/
|
|
34
|
+
hasMember(name) {
|
|
35
|
+
return this.members.includes(name);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* deleteMember will remove a user from the members array.
|
|
39
|
+
* @param member The member to be removed.
|
|
40
|
+
*/
|
|
41
|
+
deleteMember(member) {
|
|
42
|
+
this.members = this.members.filter((n) => n !== member);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* buildMembers will query the `casbin_rule` database table to ensure that the role
|
|
46
|
+
* that we have cached is up to date.
|
|
47
|
+
* This is important in multi node scenarios where the cached roles in role manager can become
|
|
48
|
+
* out of sync with the database.
|
|
49
|
+
* @param roleMemberList The RoleMemberList to be updated.
|
|
50
|
+
* @param client The database client.
|
|
51
|
+
*/
|
|
52
|
+
async buildMembers(roleMemberList, client) {
|
|
53
|
+
try {
|
|
54
|
+
const members = await client.table("casbin_rule").where("v1", this.name).pluck("v0").distinct();
|
|
55
|
+
roleMemberList.addMembers(members);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unable to find members for the role ${this.name}. Cause: ${error}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* getMembers will return the members of the RoleMemberList
|
|
64
|
+
* @returns The members.
|
|
65
|
+
*/
|
|
66
|
+
getMembers() {
|
|
67
|
+
return this.members;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* addRoles will add roles to the RoleMemberList
|
|
71
|
+
* @param roles The roles to be added.
|
|
72
|
+
*/
|
|
73
|
+
addRoles(roles) {
|
|
74
|
+
this.roles = roles;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* buildRoles will query the `casbin_rule` database table to quickly grab all of the
|
|
78
|
+
* roles that a particular user is attached to.
|
|
79
|
+
* @param roleMemberList The RoleMemberList to be updated.
|
|
80
|
+
* @param userAndGroups The user and groups to query with.
|
|
81
|
+
* @param client The database client.
|
|
82
|
+
*/
|
|
83
|
+
async buildRoles(roleMemberList, userAndGroups, client) {
|
|
84
|
+
try {
|
|
85
|
+
const roles = await client.table("casbin_rule").whereIn("v0", userAndGroups).pluck("v1").distinct();
|
|
86
|
+
roleMemberList.addRoles(roles);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`Unable to find all roles. Cause: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* getRoles will return the roles of the RoleMemberList.
|
|
93
|
+
* @returns The roles.
|
|
94
|
+
*/
|
|
95
|
+
getRoles() {
|
|
96
|
+
return this.roles;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.RoleMemberList = RoleMemberList;
|
|
101
|
+
//# sourceMappingURL=member-list.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"member-list.cjs.js","sources":["../../src/role-manager/member-list.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Knex } from 'knex';\n\nexport class RoleMemberList {\n public name: string;\n\n private members: string[];\n private roles: string[];\n\n public constructor(name: string) {\n this.name = name;\n this.members = [];\n this.roles = [];\n }\n\n /**\n * addMembers will add members to the RoleMemberList\n * @param members The members to be added.\n */\n public addMembers(members: string[]): void {\n this.members = members;\n }\n\n /**\n * addMember will add a single member to the RoleMemberList, skips adding the user in the\n * event that they already exist in the members array.\n * @param member The member to be added.\n */\n public addMember(member: string): void {\n if (this.members.some(n => n === member)) {\n return;\n }\n this.members.push(member);\n }\n\n /**\n * hasMember will check if a particular member exists in the members array.\n * @param name The member to be checked for.\n */\n public hasMember(name: string): boolean {\n return this.members.includes(name);\n }\n\n /**\n * deleteMember will remove a user from the members array.\n * @param member The member to be removed.\n */\n public deleteMember(member: string): void {\n this.members = this.members.filter(n => n !== member);\n }\n\n /**\n * buildMembers will query the `casbin_rule` database table to ensure that the role\n * that we have cached is up to date.\n * This is important in multi node scenarios where the cached roles in role manager can become\n * out of sync with the database.\n * @param roleMemberList The RoleMemberList to be updated.\n * @param client The database client.\n */\n public async buildMembers(\n roleMemberList: RoleMemberList,\n client: Knex,\n ): Promise<void> {\n try {\n const members: string[] = await client\n .table('casbin_rule')\n .where('v1', this.name)\n .pluck('v0')\n .distinct();\n\n roleMemberList.addMembers(members);\n } catch (error) {\n throw new Error(\n `Unable to find members for the role ${this.name}. Cause: ${error}`,\n );\n }\n }\n\n /**\n * getMembers will return the members of the RoleMemberList\n * @returns The members.\n */\n getMembers(): string[] {\n return this.members;\n }\n\n /**\n * addRoles will add roles to the RoleMemberList\n * @param roles The roles to be added.\n */\n public addRoles(roles: string[]): void {\n this.roles = roles;\n }\n\n /**\n * buildRoles will query the `casbin_rule` database table to quickly grab all of the\n * roles that a particular user is attached to.\n * @param roleMemberList The RoleMemberList to be updated.\n * @param userAndGroups The user and groups to query with.\n * @param client The database client.\n */\n public async buildRoles(\n roleMemberList: RoleMemberList,\n userAndGroups: string[],\n client: Knex,\n ): Promise<void> {\n try {\n const roles: string[] = await client\n .table('casbin_rule')\n .whereIn('v0', userAndGroups)\n .pluck('v1')\n .distinct();\n\n roleMemberList.addRoles(roles);\n } catch (error) {\n throw new Error(`Unable to find all roles. Cause: ${error}`);\n }\n }\n\n /**\n * getRoles will return the roles of the RoleMemberList.\n * @returns The roles.\n */\n getRoles(): string[] {\n return this.roles;\n }\n}\n"],"names":[],"mappings":";;AAiBO,MAAM,cAAe,CAAA;AAAA,EACnB,IAAA,CAAA;AAAA,EAEC,OAAA,CAAA;AAAA,EACA,KAAA,CAAA;AAAA,EAED,YAAY,IAAc,EAAA;AAC/B,IAAA,IAAA,CAAK,IAAO,GAAA,IAAA,CAAA;AACZ,IAAA,IAAA,CAAK,UAAU,EAAC,CAAA;AAChB,IAAA,IAAA,CAAK,QAAQ,EAAC,CAAA;AAAA,GAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,WAAW,OAAyB,EAAA;AACzC,IAAA,IAAA,CAAK,OAAU,GAAA,OAAA,CAAA;AAAA,GACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOO,UAAU,MAAsB,EAAA;AACrC,IAAA,IAAI,KAAK,OAAQ,CAAA,IAAA,CAAK,CAAK,CAAA,KAAA,CAAA,KAAM,MAAM,CAAG,EAAA;AACxC,MAAA,OAAA;AAAA,KACF;AACA,IAAK,IAAA,CAAA,OAAA,CAAQ,KAAK,MAAM,CAAA,CAAA;AAAA,GAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,UAAU,IAAuB,EAAA;AACtC,IAAO,OAAA,IAAA,CAAK,OAAQ,CAAA,QAAA,CAAS,IAAI,CAAA,CAAA;AAAA,GACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,aAAa,MAAsB,EAAA;AACxC,IAAA,IAAA,CAAK,UAAU,IAAK,CAAA,OAAA,CAAQ,MAAO,CAAA,CAAA,CAAA,KAAK,MAAM,MAAM,CAAA,CAAA;AAAA,GACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAa,YACX,CAAA,cAAA,EACA,MACe,EAAA;AACf,IAAI,IAAA;AACF,MAAA,MAAM,OAAoB,GAAA,MAAM,MAC7B,CAAA,KAAA,CAAM,aAAa,CACnB,CAAA,KAAA,CAAM,IAAM,EAAA,IAAA,CAAK,IAAI,CAAA,CACrB,KAAM,CAAA,IAAI,EACV,QAAS,EAAA,CAAA;AAEZ,MAAA,cAAA,CAAe,WAAW,OAAO,CAAA,CAAA;AAAA,aAC1B,KAAO,EAAA;AACd,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAuC,oCAAA,EAAA,IAAA,CAAK,IAAI,CAAA,SAAA,EAAY,KAAK,CAAA,CAAA;AAAA,OACnE,CAAA;AAAA,KACF;AAAA,GACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAuB,GAAA;AACrB,IAAA,OAAO,IAAK,CAAA,OAAA,CAAA;AAAA,GACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,SAAS,KAAuB,EAAA;AACrC,IAAA,IAAA,CAAK,KAAQ,GAAA,KAAA,CAAA;AAAA,GACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAa,UAAA,CACX,cACA,EAAA,aAAA,EACA,MACe,EAAA;AACf,IAAI,IAAA;AACF,MAAA,MAAM,KAAkB,GAAA,MAAM,MAC3B,CAAA,KAAA,CAAM,aAAa,CAAA,CACnB,OAAQ,CAAA,IAAA,EAAM,aAAa,CAAA,CAC3B,KAAM,CAAA,IAAI,EACV,QAAS,EAAA,CAAA;AAEZ,MAAA,cAAA,CAAe,SAAS,KAAK,CAAA,CAAA;AAAA,aACtB,KAAO,EAAA;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAoC,iCAAA,EAAA,KAAK,CAAE,CAAA,CAAA,CAAA;AAAA,KAC7D;AAAA,GACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAqB,GAAA;AACnB,IAAA,OAAO,IAAK,CAAA,KAAA,CAAA;AAAA,GACd;AACF;;;;"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var catalogModel = require('@backstage/catalog-model');
|
|
4
|
+
var ancestorSearchMemo = require('./ancestor-search-memo.cjs.js');
|
|
5
|
+
var memberList = require('./member-list.cjs.js');
|
|
6
|
+
|
|
7
|
+
class BackstageRoleManager {
|
|
8
|
+
constructor(catalogApi, logger, catalogDBClient, rbacDBClient, config, auth) {
|
|
9
|
+
this.catalogApi = catalogApi;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.catalogDBClient = catalogDBClient;
|
|
12
|
+
this.rbacDBClient = rbacDBClient;
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.auth = auth;
|
|
15
|
+
this.allRoles = /* @__PURE__ */ new Map();
|
|
16
|
+
const rbacConfig = this.config.getOptionalConfig("permission.rbac");
|
|
17
|
+
this.maxDepth = rbacConfig?.getOptionalNumber("maxDepth");
|
|
18
|
+
if (this.maxDepth !== void 0 && this.maxDepth < 0) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Max Depth for RBAC group hierarchy must be greater than or equal to zero"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
allRoles;
|
|
25
|
+
maxDepth;
|
|
26
|
+
/**
|
|
27
|
+
* clear clears all stored data and resets the role manager to the initial state.
|
|
28
|
+
*/
|
|
29
|
+
async clear() {
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* addLink adds the inheritance link between name1 and role: name2.
|
|
33
|
+
* aka name1 inherits role: name2.
|
|
34
|
+
* The link that is established is based on the defined grouping policies that are added by the enforcer.
|
|
35
|
+
*
|
|
36
|
+
* ex. `g, name1, name2`.
|
|
37
|
+
* @param name1 User or group that will be assigned to a role.
|
|
38
|
+
* @param name2 The role that will be created or updated.
|
|
39
|
+
* @param _domain Unimplemented prefix to the role.
|
|
40
|
+
*/
|
|
41
|
+
async addLink(name1, name2, ..._domain) {
|
|
42
|
+
if (!this.isPGClient()) {
|
|
43
|
+
const role1 = this.getOrCreateRole(name2);
|
|
44
|
+
role1.addMember(name1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* deleteLink deletes the inheritance link between name1 and role: name2.
|
|
49
|
+
* aka name1 does not inherit role: name2 any more.
|
|
50
|
+
* The link that is deleted is based on the defined grouping policies that are removed by the enforcer.
|
|
51
|
+
*
|
|
52
|
+
* ex. `g, name1, name2`.
|
|
53
|
+
* @param name1 User or group that will be removed from assignment of a role.
|
|
54
|
+
* @param name2 The role that will be deleted or updated.
|
|
55
|
+
* @param _domain Unimplemented.
|
|
56
|
+
*/
|
|
57
|
+
async deleteLink(name1, name2, ..._domain) {
|
|
58
|
+
if (!this.isPGClient()) {
|
|
59
|
+
const role1 = this.getOrCreateRole(name2);
|
|
60
|
+
role1.deleteMember(name1);
|
|
61
|
+
if (role1.getMembers().length === 0) {
|
|
62
|
+
this.allRoles.delete(name2);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* hasLink determines whether name1 inherits role: name2.
|
|
68
|
+
* During this check we build the group hierarchy graph to determine if the particular user is directly or indirectly
|
|
69
|
+
* attached to the role that we are receiving.
|
|
70
|
+
* In the event that there is a postgres database connection, we will attempt to query the roles from the database.
|
|
71
|
+
* Otherwise we will use the cached allRoles to determine if there is a link.
|
|
72
|
+
* @param name1 The user that we are authorizing.
|
|
73
|
+
* @param name2 The name of the role that we are checking against.
|
|
74
|
+
* @param domain Unimplemented.
|
|
75
|
+
* @returns True if the user is directly or indirectly attached to the role.
|
|
76
|
+
*/
|
|
77
|
+
async hasLink(name1, name2, ...domain) {
|
|
78
|
+
let currentRole;
|
|
79
|
+
if (domain.length > 0) {
|
|
80
|
+
throw new Error("domain argument is not supported.");
|
|
81
|
+
}
|
|
82
|
+
if (name2.length === 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (name1 === name2) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (this.isPGClient()) {
|
|
89
|
+
currentRole = new memberList.RoleMemberList(name2);
|
|
90
|
+
await currentRole.buildMembers(currentRole, this.rbacDBClient);
|
|
91
|
+
} else {
|
|
92
|
+
currentRole = this.allRoles.get(name2);
|
|
93
|
+
}
|
|
94
|
+
const directDeclaration = await this.checkForUserToRole(
|
|
95
|
+
name1,
|
|
96
|
+
name2,
|
|
97
|
+
currentRole
|
|
98
|
+
);
|
|
99
|
+
if (directDeclaration) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const { kind } = catalogModel.parseEntityRef(name2);
|
|
103
|
+
if (kind.toLocaleLowerCase() === "user") {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const memo = new ancestorSearchMemo.AncestorSearchMemo(
|
|
107
|
+
name1,
|
|
108
|
+
this.catalogApi,
|
|
109
|
+
this.catalogDBClient,
|
|
110
|
+
this.auth,
|
|
111
|
+
this.maxDepth
|
|
112
|
+
);
|
|
113
|
+
await memo.buildUserGraph(memo);
|
|
114
|
+
memo.debugNodesAndEdges(this.logger, name1);
|
|
115
|
+
if (!memo.isAcyclic()) {
|
|
116
|
+
const cycles = memo.findCycles();
|
|
117
|
+
this.logger.warn(
|
|
118
|
+
`Detected cycle dependencies in the Group graph: ${JSON.stringify(
|
|
119
|
+
cycles
|
|
120
|
+
)}. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: ${JSON.stringify(
|
|
121
|
+
cycles
|
|
122
|
+
)}`
|
|
123
|
+
);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (this.parseEntityKind(name2) === "role" && this.hasMember(currentRole, memo)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return memo.hasEntityRef(name2);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* syncedHasLink determines whether role: name1 inherits role: name2.
|
|
133
|
+
* domain is a prefix to the roles.
|
|
134
|
+
*/
|
|
135
|
+
syncedHasLink(_name1, _name2, ..._domain) {
|
|
136
|
+
throw new Error('Method "syncedHasLink" not implemented.');
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* getRoles gets the roles that a subject inherits.
|
|
140
|
+
*
|
|
141
|
+
* name - is a string entity reference, for example: user:default/tom, role:default/dev,
|
|
142
|
+
* so format is <kind>:<namespace>/<entity-name>.
|
|
143
|
+
* GetRoles method supports only two kind values: 'user' and 'role'.
|
|
144
|
+
*
|
|
145
|
+
* domain - is a prefix to the roles, unused parameter.
|
|
146
|
+
*
|
|
147
|
+
* If name's kind === 'user' we return all inherited roles from groups and roles directly assigned to the user.
|
|
148
|
+
* if name's kind === 'role' we return empty array, because we don't support role inheritance.
|
|
149
|
+
* Case kind === 'group' - should not happen, because:
|
|
150
|
+
* 1) Method getRoles returns only role entity references, so casbin engine doesn't call this
|
|
151
|
+
* method again to ask about name with kind "group".
|
|
152
|
+
* 2) We implemented getRoles method only to use:
|
|
153
|
+
* 'await enforcer.getImplicitPermissionsForUser(userEntityRef)',
|
|
154
|
+
* so name argument can be only with kind 'user' or 'role'.
|
|
155
|
+
*
|
|
156
|
+
* Info: when we call 'await enforcer.getImplicitPermissionsForUser(userEntityRef)',
|
|
157
|
+
* then casbin engine executes 'getRoles' method few times.
|
|
158
|
+
* Firstly casbin asks about roles for 'userEntityRef'.
|
|
159
|
+
* Let's imagine, that 'getRoles' returned two roles for userEntityRef.
|
|
160
|
+
* Then casbin calls 'getRoles' two more times to
|
|
161
|
+
* find parent roles. But we return empty array for each such call,
|
|
162
|
+
* because we don't support role inheritance and we notify casbin about end of the role sub-tree.
|
|
163
|
+
*/
|
|
164
|
+
async getRoles(name, ..._domain) {
|
|
165
|
+
const { kind } = catalogModel.parseEntityRef(name);
|
|
166
|
+
if (kind === "user") {
|
|
167
|
+
const memo = new ancestorSearchMemo.AncestorSearchMemo(
|
|
168
|
+
name,
|
|
169
|
+
this.catalogApi,
|
|
170
|
+
this.catalogDBClient,
|
|
171
|
+
this.auth,
|
|
172
|
+
this.maxDepth
|
|
173
|
+
);
|
|
174
|
+
await memo.buildUserGraph(memo);
|
|
175
|
+
memo.debugNodesAndEdges(this.logger, name);
|
|
176
|
+
if (this.isPGClient()) {
|
|
177
|
+
const currentRole = new memberList.RoleMemberList(name);
|
|
178
|
+
await currentRole.buildRoles(
|
|
179
|
+
currentRole,
|
|
180
|
+
memo.getNodes(),
|
|
181
|
+
this.rbacDBClient
|
|
182
|
+
);
|
|
183
|
+
return Promise.resolve(currentRole.getRoles());
|
|
184
|
+
}
|
|
185
|
+
const allRoles = [];
|
|
186
|
+
memo.setNode(name);
|
|
187
|
+
for (const value of this.allRoles.values()) {
|
|
188
|
+
if (this.hasMember(value, memo)) {
|
|
189
|
+
allRoles.push(value.name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return Promise.resolve(allRoles);
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* getUsers gets the users that inherits a subject.
|
|
198
|
+
* domain is an unreferenced parameter here, may be used in other implementations.
|
|
199
|
+
*/
|
|
200
|
+
async getUsers(_name, ..._domain) {
|
|
201
|
+
throw new Error('Method "getUsers" not implemented.');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* printRoles prints all the roles to log.
|
|
205
|
+
*/
|
|
206
|
+
async printRoles() {
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* getOrCreateRole will get a role if it has already been cached
|
|
210
|
+
* or it will create a new role to be cached.
|
|
211
|
+
* This cache is a simple tree that is used to quickly compare
|
|
212
|
+
* users and groups to roles.
|
|
213
|
+
* @param name The user or group whose cache we will be getting / creating.
|
|
214
|
+
* @returns The cached role as a RoleList.
|
|
215
|
+
*/
|
|
216
|
+
getOrCreateRole(name) {
|
|
217
|
+
const role = this.allRoles.get(name);
|
|
218
|
+
if (role) {
|
|
219
|
+
return role;
|
|
220
|
+
}
|
|
221
|
+
const newRole = new memberList.RoleMemberList(name);
|
|
222
|
+
this.allRoles.set(name, newRole);
|
|
223
|
+
return newRole;
|
|
224
|
+
}
|
|
225
|
+
// parse the entity to find out if it is a user / group / or role
|
|
226
|
+
parseEntityKind(name) {
|
|
227
|
+
const parsed = name.split(":");
|
|
228
|
+
return parsed[0];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* isPGClient checks what the current database client is at them time.
|
|
232
|
+
* This is to ensure that we are querying the database in the event of postgres
|
|
233
|
+
* or using in memory cache for better sqlite3.
|
|
234
|
+
* @returns True if the database client is pg.
|
|
235
|
+
*/
|
|
236
|
+
isPGClient() {
|
|
237
|
+
const client = this.rbacDBClient.client.config.client;
|
|
238
|
+
return client === "pg";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* checkForUserToRole checks if there exists a direct declaration of a user to a role. Used to exit out of
|
|
242
|
+
* hasLink faster in the event to reduce the time it would take to build the user graph.
|
|
243
|
+
* @param name1 The user that we are checking for.
|
|
244
|
+
* @param name2 The role that we are checking for.
|
|
245
|
+
* @returns True if there is a user that is directly attached to a particular role.
|
|
246
|
+
*/
|
|
247
|
+
async checkForUserToRole(name1, name2, currentRole) {
|
|
248
|
+
const tempRole = this.getOrCreateRole(name2);
|
|
249
|
+
if (this.parseEntityKind(name2) === "role" && tempRole.hasMember(name1)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (tempRole.getMembers().length === 0) {
|
|
253
|
+
this.allRoles.delete(name2);
|
|
254
|
+
}
|
|
255
|
+
if (currentRole && currentRole.hasMember(name1)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* hasMember checks if the members from a particular role is associated with the user
|
|
262
|
+
* that the AncestorSearchMemo graph is built for.
|
|
263
|
+
* @param role The role that we are getting the members from.
|
|
264
|
+
* @param memo The user graph that we are comparing members with.
|
|
265
|
+
* @returns True if a member from the role is also associated with the user.
|
|
266
|
+
*/
|
|
267
|
+
hasMember(role, memo) {
|
|
268
|
+
if (role === void 0) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
for (const member of role.getMembers()) {
|
|
272
|
+
if (memo.hasEntityRef(member)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
exports.BackstageRoleManager = BackstageRoleManager;
|
|
281
|
+
//# sourceMappingURL=role-manager.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-manager.cjs.js","sources":["../../src/role-manager/role-manager.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { AuthService, LoggerService } from '@backstage/backend-plugin-api';\nimport type { CatalogApi } from '@backstage/catalog-client';\nimport { parseEntityRef } from '@backstage/catalog-model';\nimport type { Config } from '@backstage/config';\n\nimport { RoleManager } from 'casbin';\nimport { Knex } from 'knex';\n\nimport { AncestorSearchMemo } from './ancestor-search-memo';\nimport { RoleMemberList } from './member-list';\n\nexport class BackstageRoleManager implements RoleManager {\n private allRoles: Map<string, RoleMemberList>;\n private maxDepth?: number;\n constructor(\n private readonly catalogApi: CatalogApi,\n private readonly logger: LoggerService,\n private readonly catalogDBClient: Knex,\n private readonly rbacDBClient: Knex,\n private readonly config: Config,\n private readonly auth: AuthService,\n ) {\n this.allRoles = new Map<string, RoleMemberList>();\n const rbacConfig = this.config.getOptionalConfig('permission.rbac');\n this.maxDepth = rbacConfig?.getOptionalNumber('maxDepth');\n if (this.maxDepth !== undefined && this.maxDepth! < 0) {\n throw new Error(\n 'Max Depth for RBAC group hierarchy must be greater than or equal to zero',\n );\n }\n }\n\n /**\n * clear clears all stored data and resets the role manager to the initial state.\n */\n async clear(): Promise<void> {\n // do nothing\n }\n\n /**\n * addLink adds the inheritance link between name1 and role: name2.\n * aka name1 inherits role: name2.\n * The link that is established is based on the defined grouping policies that are added by the enforcer.\n *\n * ex. `g, name1, name2`.\n * @param name1 User or group that will be assigned to a role.\n * @param name2 The role that will be created or updated.\n * @param _domain Unimplemented prefix to the role.\n */\n async addLink(\n name1: string,\n name2: string,\n ..._domain: string[]\n ): Promise<void> {\n if (!this.isPGClient()) {\n const role1 = this.getOrCreateRole(name2);\n role1.addMember(name1);\n }\n }\n\n /**\n * deleteLink deletes the inheritance link between name1 and role: name2.\n * aka name1 does not inherit role: name2 any more.\n * The link that is deleted is based on the defined grouping policies that are removed by the enforcer.\n *\n * ex. `g, name1, name2`.\n * @param name1 User or group that will be removed from assignment of a role.\n * @param name2 The role that will be deleted or updated.\n * @param _domain Unimplemented.\n */\n async deleteLink(\n name1: string,\n name2: string,\n ..._domain: string[]\n ): Promise<void> {\n if (!this.isPGClient()) {\n const role1 = this.getOrCreateRole(name2);\n role1.deleteMember(name1);\n\n // Clean up in the event that there are no more members in the role\n if (role1.getMembers().length === 0) {\n this.allRoles.delete(name2);\n }\n }\n }\n\n /**\n * hasLink determines whether name1 inherits role: name2.\n * During this check we build the group hierarchy graph to determine if the particular user is directly or indirectly\n * attached to the role that we are receiving.\n * In the event that there is a postgres database connection, we will attempt to query the roles from the database.\n * Otherwise we will use the cached allRoles to determine if there is a link.\n * @param name1 The user that we are authorizing.\n * @param name2 The name of the role that we are checking against.\n * @param domain Unimplemented.\n * @returns True if the user is directly or indirectly attached to the role.\n */\n async hasLink(\n name1: string,\n name2: string,\n ...domain: string[]\n ): Promise<boolean> {\n let currentRole: RoleMemberList;\n if (domain.length > 0) {\n throw new Error('domain argument is not supported.');\n }\n\n // Name2 can be an empty string in the event that there is not a role associated with the user\n // This happens because of the filtering of the roles reduces the number of roles that we iterate through.\n if (name2.length === 0) {\n return false;\n }\n\n if (name1 === name2) {\n return true;\n }\n\n if (this.isPGClient()) {\n currentRole = new RoleMemberList(name2);\n await currentRole.buildMembers(currentRole, this.rbacDBClient);\n } else {\n currentRole = this.allRoles.get(name2)!;\n }\n\n // Check for direct declaration of user to role\n const directDeclaration = await this.checkForUserToRole(\n name1,\n name2,\n currentRole,\n );\n if (directDeclaration) {\n return true;\n }\n\n // name1 is always user in our case.\n // name2 is user or group.\n // user(name1) couldn't inherit user(name2).\n // We can use this fact for optimization.\n const { kind } = parseEntityRef(name2);\n if (kind.toLocaleLowerCase() === 'user') {\n return false;\n }\n\n const memo = new AncestorSearchMemo(\n name1,\n this.catalogApi,\n this.catalogDBClient,\n this.auth,\n this.maxDepth,\n );\n await memo.buildUserGraph(memo);\n\n memo.debugNodesAndEdges(this.logger, name1);\n if (!memo.isAcyclic()) {\n const cycles = memo.findCycles();\n\n this.logger.warn(\n `Detected cycle dependencies in the Group graph: ${JSON.stringify(\n cycles,\n )}. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: ${JSON.stringify(\n cycles,\n )}`,\n );\n\n return false;\n }\n\n if (\n this.parseEntityKind(name2) === 'role' &&\n this.hasMember(currentRole, memo)\n ) {\n return true;\n }\n return memo.hasEntityRef(name2);\n }\n\n /**\n * syncedHasLink determines whether role: name1 inherits role: name2.\n * domain is a prefix to the roles.\n */\n syncedHasLink?(\n _name1: string,\n _name2: string,\n ..._domain: string[]\n ): boolean {\n throw new Error('Method \"syncedHasLink\" not implemented.');\n }\n\n /**\n * getRoles gets the roles that a subject inherits.\n *\n * name - is a string entity reference, for example: user:default/tom, role:default/dev,\n * so format is <kind>:<namespace>/<entity-name>.\n * GetRoles method supports only two kind values: 'user' and 'role'.\n *\n * domain - is a prefix to the roles, unused parameter.\n *\n * If name's kind === 'user' we return all inherited roles from groups and roles directly assigned to the user.\n * if name's kind === 'role' we return empty array, because we don't support role inheritance.\n * Case kind === 'group' - should not happen, because:\n * 1) Method getRoles returns only role entity references, so casbin engine doesn't call this\n * method again to ask about name with kind \"group\".\n * 2) We implemented getRoles method only to use:\n * 'await enforcer.getImplicitPermissionsForUser(userEntityRef)',\n * so name argument can be only with kind 'user' or 'role'.\n *\n * Info: when we call 'await enforcer.getImplicitPermissionsForUser(userEntityRef)',\n * then casbin engine executes 'getRoles' method few times.\n * Firstly casbin asks about roles for 'userEntityRef'.\n * Let's imagine, that 'getRoles' returned two roles for userEntityRef.\n * Then casbin calls 'getRoles' two more times to\n * find parent roles. But we return empty array for each such call,\n * because we don't support role inheritance and we notify casbin about end of the role sub-tree.\n */\n async getRoles(name: string, ..._domain: string[]): Promise<string[]> {\n const { kind } = parseEntityRef(name);\n if (kind === 'user') {\n const memo = new AncestorSearchMemo(\n name,\n this.catalogApi,\n this.catalogDBClient,\n this.auth,\n this.maxDepth,\n );\n await memo.buildUserGraph(memo);\n memo.debugNodesAndEdges(this.logger, name);\n\n if (this.isPGClient()) {\n const currentRole = new RoleMemberList(name);\n await currentRole.buildRoles(\n currentRole,\n memo.getNodes(),\n this.rbacDBClient,\n );\n return Promise.resolve(currentRole.getRoles());\n }\n\n const allRoles: string[] = [];\n // Account for the user not being in the graph\n memo.setNode(name);\n for (const value of this.allRoles.values()) {\n if (this.hasMember(value, memo)) {\n allRoles.push(value.name);\n }\n }\n\n return Promise.resolve(allRoles);\n }\n\n return [];\n }\n\n /**\n * getUsers gets the users that inherits a subject.\n * domain is an unreferenced parameter here, may be used in other implementations.\n */\n async getUsers(_name: string, ..._domain: string[]): Promise<string[]> {\n throw new Error('Method \"getUsers\" not implemented.');\n }\n\n /**\n * printRoles prints all the roles to log.\n */\n async printRoles(): Promise<void> {\n // do nothing\n }\n\n /**\n * getOrCreateRole will get a role if it has already been cached\n * or it will create a new role to be cached.\n * This cache is a simple tree that is used to quickly compare\n * users and groups to roles.\n * @param name The user or group whose cache we will be getting / creating.\n * @returns The cached role as a RoleList.\n */\n private getOrCreateRole(name: string): RoleMemberList {\n const role = this.allRoles.get(name);\n if (role) {\n return role;\n }\n const newRole = new RoleMemberList(name);\n this.allRoles.set(name, newRole);\n\n return newRole;\n }\n\n // parse the entity to find out if it is a user / group / or role\n private parseEntityKind(name: string): string {\n const parsed = name.split(':');\n return parsed[0];\n }\n\n /**\n * isPGClient checks what the current database client is at them time.\n * This is to ensure that we are querying the database in the event of postgres\n * or using in memory cache for better sqlite3.\n * @returns True if the database client is pg.\n */\n isPGClient(): boolean {\n const client = this.rbacDBClient.client.config.client;\n return client === 'pg';\n }\n\n /**\n * checkForUserToRole checks if there exists a direct declaration of a user to a role. Used to exit out of\n * hasLink faster in the event to reduce the time it would take to build the user graph.\n * @param name1 The user that we are checking for.\n * @param name2 The role that we are checking for.\n * @returns True if there is a user that is directly attached to a particular role.\n */\n private async checkForUserToRole(\n name1: string,\n name2: string,\n currentRole: RoleMemberList | undefined,\n ): Promise<boolean | undefined> {\n const tempRole = this.getOrCreateRole(name2);\n\n // Immediately check if the our temporary role has a link with the role that we are comparing it to\n if (this.parseEntityKind(name2) === 'role' && tempRole.hasMember(name1)) {\n return true;\n }\n\n // Clean up the temp role\n if (tempRole.getMembers().length === 0) {\n this.allRoles.delete(name2);\n }\n\n if (currentRole && currentRole.hasMember(name1)) {\n return true;\n }\n\n return undefined;\n }\n\n /**\n * hasMember checks if the members from a particular role is associated with the user\n * that the AncestorSearchMemo graph is built for.\n * @param role The role that we are getting the members from.\n * @param memo The user graph that we are comparing members with.\n * @returns True if a member from the role is also associated with the user.\n */\n private hasMember(\n role: RoleMemberList | undefined,\n memo: AncestorSearchMemo,\n ): boolean {\n if (role === undefined) {\n return false;\n }\n\n for (const member of role.getMembers()) {\n if (memo.hasEntityRef(member)) {\n return true;\n }\n }\n return false;\n }\n}\n"],"names":["RoleMemberList","parseEntityRef","AncestorSearchMemo"],"mappings":"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"}
|