UNPKG

@backstage-community/plugin-rbac-backend

Version:
263 lines (259 loc) 9.65 kB
'use strict'; var catalogModel = require('@backstage/catalog-model'); var memberList = require('./member-list.cjs.js'); var ancestorSearchFactory = require('./ancestor-search-factory.cjs.js'); class BackstageRoleManager { constructor(catalogApi, logger, catalogDBClient, rbacDBClient, config, auth) { this.catalogApi = catalogApi; this.logger = logger; this.catalogDBClient = catalogDBClient; this.rbacDBClient = rbacDBClient; this.config = config; this.auth = auth; this.allRoles = /* @__PURE__ */ new Map(); const rbacConfig = this.config.getOptionalConfig("permission.rbac"); this.maxDepth = rbacConfig?.getOptionalNumber("maxDepth"); if (this.maxDepth !== undefined && this.maxDepth < 0) { throw new Error( "Max Depth for RBAC group hierarchy must be greater than or equal to zero" ); } } allRoles; maxDepth; /** * clear clears all stored data and resets the role manager to the initial state. */ async clear() { } /** * addLink adds the inheritance link between name1 and role: name2. * aka name1 inherits role: name2. * The link that is established is based on the defined grouping policies that are added by the enforcer. * * ex. `g, name1, name2`. * @param name1 User or group that will be assigned to a role. * @param name2 The role that will be created or updated. * @param _domain Unimplemented prefix to the role. */ async addLink(name1, name2, ..._domain) { if (!this.isPGClient()) { const role1 = this.getOrCreateRole(name2); role1.addMember(name1); } } /** * deleteLink deletes the inheritance link between name1 and role: name2. * aka name1 does not inherit role: name2 any more. * The link that is deleted is based on the defined grouping policies that are removed by the enforcer. * * ex. `g, name1, name2`. * @param name1 User or group that will be removed from assignment of a role. * @param name2 The role that will be deleted or updated. * @param _domain Unimplemented. */ async deleteLink(name1, name2, ..._domain) { if (!this.isPGClient()) { const role1 = this.getOrCreateRole(name2); role1.deleteMember(name1); if (role1.getMembers().length === 0) { this.allRoles.delete(name2); } } } /** * hasLink determines whether name1 inherits role: name2. * Before this check is called in the background by the enforcer, * we filter out all roles that the user is not connected to * directly or indirectly through the use of retrieving roles through * enforcer.getRolesForUser and apply those roles to a tempEnforcer. * * This means that hasLink will almost always be true in the event that a user * is assigned to a role (either directly or indirectly) * * In the event that a user or group is not assigned to a role and instead * are assigned directly to permissions, then name2 will become either that * user or group through the filtering. In this case we will build the graph * if necessary for name2 group presence or evaulate based on the names matching. * @param name1 The user that we are authorizing. * @param name2 The name of the role that we are checking against. * @param domain Unimplemented. * @returns True if the user is directly or indirectly attached to the role. */ async hasLink(name1, name2, ...domain) { if (domain.length > 0) { throw new Error("domain argument is not supported."); } if (name2.length === 0) { return false; } if (name1 === name2) { return true; } const { kind } = catalogModel.parseEntityRef(name2); if (kind.toLocaleLowerCase() === "user") { return false; } if (kind.toLocaleLowerCase() === "group") { const memo = await ancestorSearchFactory.AncestorSearchFactory.createAncestorSearchMemo( name1, this.config, this.catalogApi, this.catalogDBClient, this.auth, this.maxDepth ); await memo.buildUserGraph(); memo.debugNodesAndEdges(this.logger, name1); if (!memo.isAcyclic()) { const cycles = memo.findCycles(); this.logger.warn( `Detected cycle dependencies in the Group graph: ${JSON.stringify( cycles )}. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: ${JSON.stringify( cycles )}` ); return false; } return memo.hasEntityRef(name2); } return true; } /** * syncedHasLink determines whether role: name1 inherits role: name2. * domain is a prefix to the roles. */ syncedHasLink(_name1, _name2, ..._domain) { throw new Error('Method "syncedHasLink" not implemented.'); } /** * getRoles gets the roles that a subject inherits. * * name - is a string entity reference, for example: user:default/tom, role:default/dev, * so format is <kind>:<namespace>/<entity-name>. * GetRoles method supports only two kind values: 'user' and 'role'. * * domain - is a prefix to the roles, unused parameter. * * If name's kind === 'user' we return all inherited roles from groups and roles directly assigned to the user. * if name's kind === 'role' we return empty array, because we don't support role inheritance. * Case kind === 'group' - should not happen, because: * 1) Method getRoles returns only role entity references, so casbin engine doesn't call this * method again to ask about name with kind "group". * 2) We implemented getRoles method only to use: * 'await enforcer.getImplicitPermissionsForUser(userEntityRef)', * so name argument can be only with kind 'user' or 'role'. * * Info: when we call 'await enforcer.getImplicitPermissionsForUser(userEntityRef)', * then casbin engine executes 'getRoles' method few times. * Firstly casbin asks about roles for 'userEntityRef'. * Let's imagine, that 'getRoles' returned two roles for userEntityRef. * Then casbin calls 'getRoles' two more times to * find parent roles. But we return empty array for each such call, * because we don't support role inheritance and we notify casbin about end of the role sub-tree. */ async getRoles(name, ..._domain) { const { kind } = catalogModel.parseEntityRef(name); if (kind === "user") { const memo = await ancestorSearchFactory.AncestorSearchFactory.createAncestorSearchMemo( name, this.config, this.catalogApi, this.catalogDBClient, this.auth, this.maxDepth ); await memo.buildUserGraph(); memo.debugNodesAndEdges(this.logger, name); memo.setNode(name); if (!memo.isAcyclic()) { const cycles = memo.findCycles(); this.logger.warn( `Detected cycle dependencies in the Group graph: ${JSON.stringify( cycles )}. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: ${JSON.stringify( cycles )}` ); return Promise.resolve([]); } if (this.isPGClient()) { const currentRole = new memberList.RoleMemberList(name); await currentRole.buildRoles( currentRole, memo.getNodes(), this.rbacDBClient ); return Promise.resolve(currentRole.getRoles()); } const allRoles = []; for (const value of this.allRoles.values()) { if (this.hasMember(value, memo)) { allRoles.push(value.name); } } return Promise.resolve(allRoles); } return []; } /** * getUsers gets the users that inherits a subject. * domain is an unreferenced parameter here, may be used in other implementations. */ async getUsers(_name, ..._domain) { throw new Error('Method "getUsers" not implemented.'); } /** * printRoles prints all the roles to log. */ async printRoles() { } /** * getOrCreateRole will get a role if it has already been cached * or it will create a new role to be cached. * This cache is a simple tree that is used to quickly compare * users and groups to roles. * @param name The user or group whose cache we will be getting / creating. * @returns The cached role as a RoleList. */ getOrCreateRole(name) { const role = this.allRoles.get(name); if (role) { return role; } const newRole = new memberList.RoleMemberList(name); this.allRoles.set(name, newRole); return newRole; } /** * isPGClient checks what the current database client is at them time. * This is to ensure that we are querying the database in the event of postgres * or using in memory cache for better sqlite3. * @returns True if the database client is pg. */ isPGClient() { const client = this.rbacDBClient.client.config.client; return client === "pg"; } /** * hasMember checks if the members from a particular role is associated with the user * that the AncestorSearchMemo graph is built for. * @param role The role that we are getting the members from. * @param memo The user graph that we are comparing members with. * @returns True if a member from the role is also associated with the user. */ hasMember(role, memo) { if (role === undefined) { return false; } for (const member of role.getMembers()) { if (memo.hasEntityRef(member)) { return true; } } return false; } } exports.BackstageRoleManager = BackstageRoleManager; //# sourceMappingURL=role-manager.cjs.js.map