From e274a14f4762f02410abdc4e8e7219dc6409efe1 Mon Sep 17 00:00:00 2001 From: Konstantinos Kopanidis Date: Thu, 18 Jan 2024 17:08:35 +0200 Subject: [PATCH] refactor(authorization): rework (#891) * fix(authorization): relation checking on bulk creation * fix(authentication): missing scope construction in oAuth2 native * fix(authorization): bulk relations check not checking all provided resources * refactor(authorization): optimize index-building jobs * refactor(authorization): optimize inherited permissions tree-building * [CodeFactor] Apply fixes * fix(authorization): wildcard (*) permissions not added correctly refactor(authorization): small performance improvement in index building * refactor(authorization): another performance improvement * fix(authorization): ResourceDefinition.name field missing uniqueness constraint * fix(authorization): ResourceDefinition relations/permissions nullable MongoDB fields handling (#896) * fix(grpc-sdk): redis connections not closing on process exit (#901) * fix(authorization): constructRelationIndex not reusing grpc-sdk (#902) * refactor(authorization): remove redundant functions (#903) refactor(authorization): re-index resources when definitions are modified feat(authorization): maintain inheritance tree fix(authorization): inherited permissions not removed when tree branches break --------- Co-authored-by: codefactor-io Co-authored-by: Konstantinos Feretos --- libraries/grpc-sdk/src/utilities/EventBus.ts | 4 + .../grpc-sdk/src/utilities/StateManager.ts | 4 + .../src/handlers/oauth2/OAuth2.ts | 4 +- modules/authorization/src/admin/index.ts | 68 +++- .../src/controllers/index.controller.ts | 379 +++++++----------- .../src/controllers/permissions.controller.ts | 2 +- .../src/controllers/queue.controller.ts | 47 ++- .../src/controllers/relations.controller.ts | 59 ++- .../src/controllers/resource.controller.ts | 24 +- .../src/jobs/constructRelationIndex.ts | 138 +------ .../src/models/ObjectIndex.schema.ts | 10 + .../src/models/ResourceDefinition.schema.ts | 7 +- modules/authorization/src/utils/index.ts | 26 +- 13 files changed, 348 insertions(+), 424 deletions(-) diff --git a/libraries/grpc-sdk/src/utilities/EventBus.ts b/libraries/grpc-sdk/src/utilities/EventBus.ts index d654fc167..df1b30a84 100644 --- a/libraries/grpc-sdk/src/utilities/EventBus.ts +++ b/libraries/grpc-sdk/src/utilities/EventBus.ts @@ -19,6 +19,10 @@ export class EventBus { this._clientSubscriber.on('ready', () => { ConduitGrpcSdk.Logger.log('The Bus is in the station...hehe'); }); + process.on('exit', () => { + this._clientSubscriber.quit(); + this._clientPublisher.quit(); + }); } unsubscribe(subscriberId: string): void { diff --git a/libraries/grpc-sdk/src/utilities/StateManager.ts b/libraries/grpc-sdk/src/utilities/StateManager.ts index a4c9197c7..a7d99e503 100644 --- a/libraries/grpc-sdk/src/utilities/StateManager.ts +++ b/libraries/grpc-sdk/src/utilities/StateManager.ts @@ -31,6 +31,10 @@ export class StateManager { // attempted with the `using` API. automaticExtensionThreshold: 500, // time in ms }); + + process.on('exit', () => { + this.redisClient.quit(); + }); } async acquireLock(resource: string, ttl: number = 5000): Promise { diff --git a/modules/authentication/src/handlers/oauth2/OAuth2.ts b/modules/authentication/src/handlers/oauth2/OAuth2.ts index 3b9ebb8d0..52c456dea 100644 --- a/modules/authentication/src/handlers/oauth2/OAuth2.ts +++ b/modules/authentication/src/handlers/oauth2/OAuth2.ts @@ -176,7 +176,9 @@ export abstract class OAuth2 async authenticate(call: ParsedRouterRequest): Promise { ConduitGrpcSdk.Metrics?.increment('login_requests_total'); - const scopes = call.request.params?.scopes ?? this.defaultScopes; + const scopes = this.constructScopes( + call.request.params?.scopes ?? this.defaultScopes, + ); const payload = await this.connectWithProvider({ accessToken: call.request.params['access_token'], clientId: this.settings.clientId, diff --git a/modules/authorization/src/admin/index.ts b/modules/authorization/src/admin/index.ts index 9d84a7754..6bdaa12a1 100644 --- a/modules/authorization/src/admin/index.ts +++ b/modules/authorization/src/admin/index.ts @@ -1,7 +1,14 @@ -import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import ConduitGrpcSdk, { + ConduitRouteActions, + ConduitRouteReturnDefinition, + ParsedRouterRequest, + UnparsedRouterResponse, +} from '@conduitplatform/grpc-sdk'; import { GrpcServer, RoutingManager } from '@conduitplatform/module-tools'; import { ResourceHandler } from './resources'; import { RelationHandler } from './relations'; +import { ActorIndex, ObjectIndex, Relationship, ResourceDefinition } from '../models'; +import { QueueController } from '../controllers'; export class AdminHandlers { private readonly resourceHandler: ResourceHandler; @@ -18,12 +25,69 @@ export class AdminHandlers { this.registerAdminRoutes(); } - reconstructIndices() { + async reconstructIndices( + call: ParsedRouterRequest, + callback: (response: UnparsedRouterResponse) => void, + ) { // used to trigger an index re-construction + ConduitGrpcSdk.Logger.warn('Reconstructing indices...'); + ConduitGrpcSdk.Logger.warn('Wiping index data...'); + await Promise.all([ + ActorIndex.getInstance().deleteMany({}), + ObjectIndex.getInstance().deleteMany({}), + ]); + callback('ok'); + ConduitGrpcSdk.Logger.warn('Beginning index reconstruction...'); + const resources = await ResourceDefinition.getInstance().findMany({}); + ConduitGrpcSdk.Logger.info(`Found ${resources.length} resources...`); + for (const resource of resources) { + ConduitGrpcSdk.Logger.info(`Reconstructing index for ${resource.name}...`); + const query = { + subjectType: resource.name, + }; + const relationsCount = await Relationship.getInstance().countDocuments(query); + ConduitGrpcSdk.Logger.info( + `Found ${relationsCount} relations for ${resource.name}...`, + ); + let processed = 0; + let relations = await Relationship.getInstance().findMany( + query, + undefined, + processed, + 1000, + ); + while (relations.length > 0) { + ConduitGrpcSdk.Logger.info( + `Reconstructing index for ${resource.name}... ${relations.length} remaining`, + ); + await QueueController.getInstance().addRelationIndexJob( + relations.map(r => { + return { subject: r.subject, relation: r.relation, object: r.resource }; + }), + ); + await QueueController.getInstance().waitForIdle(); + processed += relations.length; + relations = await Relationship.getInstance().findMany( + query, + undefined, + processed, + 1000, + ); + } + } } private registerAdminRoutes() { this.routingManager.clear(); + this.routingManager.route( + { + path: '/indexer/reconstruct', + action: ConduitRouteActions.POST, + description: `Wipes and re-constructs the relation indexes.`, + }, + new ConduitRouteReturnDefinition('IndexReconstruct', 'String'), + this.reconstructIndices.bind(this), + ); this.relationHandler.registerRoutes(this.routingManager); this.resourceHandler.registerRoutes(this.routingManager); this.routingManager.registerRoutes().then(); diff --git a/modules/authorization/src/controllers/index.controller.ts b/modules/authorization/src/controllers/index.controller.ts index 16d96adfc..3608d95c2 100644 --- a/modules/authorization/src/controllers/index.controller.ts +++ b/modules/authorization/src/controllers/index.controller.ts @@ -1,6 +1,7 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; -import { ActorIndex, ObjectIndex, ResourceDefinition } from '../models'; -import { RelationsController } from './relations.controller'; +import { ActorIndex, ObjectIndex, Relationship, ResourceDefinition } from '../models'; +import { constructObjectIndex } from '../utils'; +import _ from 'lodash'; import { QueueController } from './queue.controller'; export class IndexController { @@ -16,6 +17,43 @@ export class IndexController { throw new Error('No grpcSdk instance provided!'); } + async reIndexResource(resource: string) { + ConduitGrpcSdk.Logger.info( + `Resource ${resource} was modified, scheduling re-indexing`, + ); + await ActorIndex.getInstance().deleteMany({ + $or: [ + { + subjectType: resource, + }, + { entityType: resource }, + ], + }); + await ObjectIndex.getInstance().deleteMany({ + $or: [ + { + subjectType: resource, + }, + { entityType: resource }, + ], + }); + const relations = await Relationship.getInstance().findMany({ + $or: [ + { + subjectType: resource, + }, + { resourceType: resource }, + ], + }); + await QueueController.getInstance().addRelationIndexJob( + relations.map(r => ({ + subject: r.subject, + relation: r.relation, + object: r.resource, + })), + ); + } + async createOrUpdateObject(subject: string, entity: string) { const index = await ObjectIndex.getInstance().findOne({ subject, entity }); if (!index) { @@ -25,9 +63,9 @@ export class IndexController { subjectId: subject.split(':')[1].split('#')[0], subjectPermission: subject.split('#')[1], entity, - entityId: entity.split(':')[1].split('#')[0], - entityType: entity.split(':')[0], - relation: entity.split('#')[1], + entityId: entity === '*' ? '*' : entity.split(':')[1].split('#')[0], + entityType: entity === '*' ? '*' : entity.split(':')[0], + relation: entity === '*' ? '*' : entity.split('#')[1], }); } } @@ -36,6 +74,10 @@ export class IndexController { const objectDefinition = (await ResourceDefinition.getInstance().findOne({ name: object.split(':')[0], }))!; + + const subjectDefinition = (await ResourceDefinition.getInstance().findOne({ + name: subject.split(':')[0], + }))!; // relations can only be created between actors and resources // object indexes represent relations between actors and permissions on resources // construct actor index @@ -54,43 +96,71 @@ export class IndexController { relation: relation, }); } + if (!objectDefinition.permissions) return; const permissions = Object.keys(objectDefinition.permissions); + const relatedPermissions: { [key: string]: string[] } = {}; const obj = []; for (const permission of permissions) { const roles = objectDefinition.permissions[permission]; for (const role of roles) { if (role.indexOf('->') === -1) { - obj.push({ - subject: `${object}#${permission}`, - subjectId: object.split(':')[1], - subjectType: `${object}#${permission}`.split(':')[0], - subjectPermission: `${object}#${permission}`.split('#')[1], - entity: `${object}#${role}`, - entityId: object.split(':')[1], - entityType: `${object}#${role}`.split(':')[0], - relation: `${object}#${role}`.split('#')[1], - }); + obj.push(constructObjectIndex(object, permission, role, object, [])); } else if (role !== '*') { const [relatedSubject, action] = role.split('->'); if (relation !== relatedSubject) continue; - const possibleConnections = await ObjectIndex.getInstance().findMany({ - subject: `${subject}#${action}`, - }); - for (const connection of possibleConnections) { - obj.push({ - subject: `${object}#${permission}`, - subjectId: object.split(':')[1], - subjectType: `${object}#${permission}`.split(':')[0], - subjectPermission: `${object}#${permission}`.split('#')[1], - entity: connection.entity, - entityId: connection.entity.split(':')[1].split('#')[0], - entityType: connection.entity.split(':')[0], - relation: connection.entity.split('#')[1], - }); + if (!relatedPermissions[action]) { + relatedPermissions[action] = [permission]; + } else if (!relatedPermissions[action].includes(permission)) { + relatedPermissions[action].push(permission); } } } } + const possibleConnections = await ObjectIndex.getInstance().findMany({ + subject: { $in: Object.keys(relatedPermissions).map(i => `${subject}#${i}`) }, + }); + for (const action in relatedPermissions) { + for (const connection of possibleConnections) { + if (connection.subjectPermission !== action) continue; + for (const permission of relatedPermissions[action]) { + obj.push( + constructObjectIndex( + object, + permission, + connection.entity.split('#')[1], + connection.entity.split('#')[0], + [...connection.inheritanceTree, `${subject}#${relation}@${object}`], + ), + ); + } + } + } + if (subjectDefinition.permissions) { + const subjectPermissions = Object.keys(subjectDefinition.permissions); + for (const action in relatedPermissions) { + if (subjectPermissions.includes(action)) { + for (const role of subjectDefinition.permissions[action]) { + if (role.indexOf('->') === -1) { + for (const permission of relatedPermissions[action]) { + obj.push({ + subject: `${object}#${permission}`, + subjectId: object.split(':')[1], + subjectType: `${object}#${permission}`.split(':')[0], + subjectPermission: `${object}#${permission}`.split('#')[1], + entity: `${subject}#${role}`, + entityId: subject.split(':')[1], + entityType: `${subject}#${role}`.split(':')[0], + entityPermission: action, + relation: `${subject}#${role}`.split('#')[1], + inheritanceTree: [`${subject}#${relation}@${object}`], + }); + } + } + } + } + } + } + const indexes = await ObjectIndex.getInstance().findMany({ $and: [ { subject: { $in: obj.map(i => i.subject) } }, @@ -101,17 +171,37 @@ export class IndexController { i => !indexes.find(j => j.subject === i.subject && j.entity === i.entity), ); await ObjectIndex.getInstance().createMany(toCreate); - const actors = await ActorIndex.getInstance().findMany({ - subject: object, - }); - if (actors.length === 0) return; - await QueueController.getInstance().addRelationIndexJob( - actors.map(actor => ({ - subject: actor.subject, - relation: actor.relation, - object: actor.entity.split('#')[0], - })), - ); + + const achievedPermissions = [...new Set(obj.map(i => i.subjectPermission!))]; + const objectsByPermission: { [key: string]: Partial[] } = {}; + for (const permission of achievedPermissions) { + objectsByPermission[permission] = obj.filter( + i => i.subjectPermission === permission, + ); + } + for (const objectPermission in objectsByPermission) { + const childObj: Partial[] = []; + const childIndexes = await ObjectIndex.getInstance().findMany({ + subject: { $ne: `${object}#${objectPermission}` }, + entityId: object.split(':')[1], + entityType: object.split(':')[0], + entityPermission: objectPermission, + }); + for (const childIndex of childIndexes) { + const copy = _.omit(childIndex, ['_id', 'createdAt', 'updatedAt', '__v']); + for (const childObject of objectsByPermission[objectPermission]) { + if (childObject.entityType === copy.entityType) continue; + childObj.push({ + ...copy, + entity: childObject.entity, + entityId: childObject.entityId, + entityType: childObject.entityType, + entityPermission: childObject.entityPermission, + }); + } + } + await ObjectIndex.getInstance().createMany(childObj); + } } async removeRelation(subject: string, relation: string, object: string) { @@ -120,198 +210,12 @@ export class IndexController { subject: subject, entity: `${object}#${relation}`, }); - } - - async removeGeneralRelation( - subjectResource: string, - relation: string, - objectResource: string, - ) { - // delete applicable actor indexes - await ActorIndex.getInstance().deleteMany({ - $or: [ - { - subject: { - $regex: `${subjectResource}.*`, - $options: 'i', - }, - }, - { entity: { $regex: `${objectResource}.*#${relation}`, $options: 'i' } }, - ], + // delete object indexes that were created due to this relation + await ObjectIndex.getInstance().deleteMany({ + inheritanceTree: `${subject}#${relation}@${object}`, }); } - async _processRemovedPermissions( - removedRoles: string[], - permission: string, - resource: ResourceDefinition, - oldResource: ResourceDefinition, - ) { - // for all roles that are no longer valid for a specific permission - // remove all applicable actor indexes - if (removedRoles.length > 0) { - for (const removedRole of removedRoles) { - if (removedRole.indexOf('->') === -1) { - await ObjectIndex.getInstance().deleteMany({ - subject: { - $regex: `${resource.name}.*#${permission}`, - $options: 'i', - }, - entity: { - $regex: `${resource.name}.*#${removedRole}`, - $options: 'i', - }, - }); - } else { - const [relatedSubject, action] = removedRole.split('->'); - const removedResources = oldResource.relations[relatedSubject]; - for (const removedResource of removedResources) { - await ObjectIndex.getInstance().deleteMany({ - subject: { - $regex: `${resource.name}.*#${permission}`, - $options: 'i', - }, - entity: { - $regex: `${removedResource}.*#${action}`, - $options: 'i', - }, - }); - } - } - } - } - } - - async _processAddPermission( - addedRoles: string[], - permission: string, - resource: ResourceDefinition, - ) { - // for all roles that are newly valid for a specific permission - // add all applicable actor indexes - if (addedRoles.length > 0) { - for (const addedRole of addedRoles) { - if (addedRole.indexOf('->') === -1) { - await this.createOrUpdateObject( - resource.name + '#' + permission, - addedRole === '*' ? `*` : `${resource.name}#${addedRole}`, - ); - } else { - const [relatedSubject, action] = addedRole.split('->'); - const addedResources = resource.relations[relatedSubject]; - - for (const addedResource of addedResources) { - const possibleConnections = await ObjectIndex.getInstance().findMany({ - subject: `${addedResource}.*#${action}`, - }); - const applicableObjects = await ObjectIndex.getInstance().findMany({ - subject: `${resource.name}.*`, - }); - let objectNames: string[] = []; - if (applicableObjects.length > 0) { - objectNames = applicableObjects.map(object => { - return object.subject.split('#')[0]; - }); - } - for (const object of objectNames) { - for (const connection of possibleConnections) { - await this.createOrUpdateObject( - object + '#' + permission, - connection.entity, - ); - } - } - } - } - } - } - } - - async modifyPermission(oldResource: any, resource: any) { - const oldPermissions = oldResource.permissions; - const newPermissions = resource.permissions; - const oldPermissionNames = Object.keys(oldPermissions); - const newPermissionNames = Object.keys(newPermissions); - const removedPermissions = oldPermissionNames.filter( - permission => !newPermissionNames.includes(permission), - ); - const modifiedPermissions = newPermissionNames.filter( - permission => !oldPermissionNames.includes(permission), - ); - // remove all permissions that are no longer present - for (const permission of removedPermissions) { - await ObjectIndex.getInstance().deleteMany({ - subject: { - $regex: `${resource}.*#${permission}`, - $options: 'i', - }, - }); - } - for (const permission of modifiedPermissions) { - // check if any roles are no longer valid for a specific permission - if (oldPermissions[permission] !== newPermissions[permission]) { - let oldRoleNames: string[] = []; - if (oldPermissions[permission]) { - oldRoleNames = Object.keys(oldPermissions[permission]); - } - let newRoleNames: string[] = []; - if (newPermissions[permission]) { - newRoleNames = Object.keys(newPermissions[permission]); - } - const removedRoles = oldRoleNames.filter(role => !newRoleNames.includes(role)); - await this._processRemovedPermissions( - removedRoles, - permission, - resource, - oldResource, - ); - const addedRoles = newRoleNames.filter(role => !oldRoleNames.includes(role)); - await this._processAddPermission(addedRoles, permission, resource); - } - } - } - - async modifyRelations(oldResource: any, resource: any) { - const oldRelations = oldResource.relations; - const newRelations = resource.relations; - const oldRelationNames = Object.keys(oldRelations); - const newRelationNames = Object.keys(newRelations); - const removedRelations = oldRelationNames.filter( - relation => !newRelationNames.includes(relation), - ); - const modifiedRelations = newRelationNames.filter(relation => - oldRelationNames.includes(relation), - ); - // remove all relations that are no longer present - for (const relation of removedRelations) { - for (const relationResource of oldRelations[relation]) { - await this.removeGeneralRelation(relationResource, relation, resource.name); - } - } - for (const relation of modifiedRelations) { - // check if any resources are no longer valid for a specific relation - if (oldRelations[relation] !== newRelations[relation]) { - const oldResourceNames = Object.keys(oldRelations[relation]); - const newResourceNames = Object.keys(newRelations[relation]); - const removedResources = oldResourceNames.filter( - resource => !newResourceNames.includes(resource), - ); - // for all resources that are no longer valid for a specific relation - // remove all applicable actor indexes - if (removedResources.length > 0) { - for (const removedResource of removedResources) { - await this.removeGeneralRelation(removedResource, relation, resource.name); - await RelationsController.getInstance().removeGeneralRelation( - removedResource, - relation, - resource.name, - ); - } - } - } - } - } - async removeResource(resourceName: string) { const query = { $or: [ @@ -323,6 +227,23 @@ export class IndexController { }; await ActorIndex.getInstance().deleteMany(query); await ObjectIndex.getInstance().deleteMany(query); + // delete object indexes that were created due to this relation + await ObjectIndex.getInstance().deleteMany({ + $or: [ + { + inheritanceTree: { + $regex: `${resourceName}.*`, + $options: 'i', + }, + }, + { + inheritanceTree: { + $regex: `.*@${resourceName}.*`, + $options: 'i', + }, + }, + ], + }); } async findIndex(subject: string, action: string, object: string) { diff --git a/modules/authorization/src/controllers/permissions.controller.ts b/modules/authorization/src/controllers/permissions.controller.ts index 551b6abdc..f802bfff5 100644 --- a/modules/authorization/src/controllers/permissions.controller.ts +++ b/modules/authorization/src/controllers/permissions.controller.ts @@ -184,7 +184,7 @@ export class PermissionsController { id_action: { $concat: [`${objectType}:`, { $toString: '$_id' }, `#${action}`], }, - entities: '$actors.entity', + entities: { $concatArrays: ['$actors.entity', ['*']] }, }, pipeline: [ { diff --git a/modules/authorization/src/controllers/queue.controller.ts b/modules/authorization/src/controllers/queue.controller.ts index 75f71c6bb..6479a844f 100644 --- a/modules/authorization/src/controllers/queue.controller.ts +++ b/modules/authorization/src/controllers/queue.controller.ts @@ -2,8 +2,8 @@ import path from 'path'; import { Queue, Worker } from 'bullmq'; import { randomUUID } from 'crypto'; import { status } from '@grpc/grpc-js'; -import ConduitGrpcSdk, { GrpcError } from '@conduitplatform/grpc-sdk'; -import { Redis, Cluster } from 'ioredis'; +import ConduitGrpcSdk, { GrpcError, sleep } from '@conduitplatform/grpc-sdk'; +import { Cluster, Redis } from 'ioredis'; export class QueueController { private static _instance: QueueController; @@ -34,9 +34,13 @@ export class QueueController { path.join(__dirname, '../jobs', 'constructRelationIndex.js'), ); const worker = new Worker('authorization-index-queue', processorFile, { + concurrency: 20, connection: this.redisConnection, // autorun: true, }); + worker.on('active', (job: any) => { + ConduitGrpcSdk.Logger.info(`Job ${job.id} started`); + }); worker.on('completed', (job: any) => { ConduitGrpcSdk.Logger.info(`Job ${job.id} completed`); }); @@ -54,18 +58,33 @@ export class QueueController { 'Missing relations (subject, relation, object)', ); } - await this.authorizationQueue.add( - randomUUID(), - { relations }, - { - removeOnComplete: { - age: 3600, - count: 1000, - }, - removeOnFail: { - age: 24 * 3600, - }, - }, + await this.authorizationQueue.addBulk( + relations.map(r => { + return { + name: randomUUID(), + data: { + relation: r, + }, + opts: { + removeOnComplete: { + age: 3600, + count: 1000, + }, + removeOnFail: { + age: 24 * 3600, + }, + }, + }; + }), ); } + + async waitForIdle() { + let waitingCount = await this.authorizationQueue.count(); + while (waitingCount > 0) { + await sleep(1000); + waitingCount = await this.authorizationQueue.count(); + } + return; + } } diff --git a/modules/authorization/src/controllers/relations.controller.ts b/modules/authorization/src/controllers/relations.controller.ts index abcef27cb..0e306e29d 100644 --- a/modules/authorization/src/controllers/relations.controller.ts +++ b/modules/authorization/src/controllers/relations.controller.ts @@ -34,6 +34,9 @@ export class RelationsController { if (!resourceDefinition) { throw new Error('Object resource definition not found'); } + if (!resourceDefinition.relations) { + throw new Error('Relation not allowed'); + } if (resourceDefinition.relations[relation].indexOf('*') !== -1) return; if ( !resourceDefinition.relations[relation] || @@ -62,7 +65,9 @@ export class RelationsController { } private async checkRelations(subject: string, relation: string, resources: string[]) { - checkRelation(subject, relation, resources[0]); + for (const resource of resources) { + checkRelation(subject, relation, resource); + } const computedTuples = resources.map(r => computeRelationTuple(subject, relation, r)); const relationResources = await Relationship.getInstance().findMany({ computedTuple: { $in: computedTuples }, @@ -72,20 +77,25 @@ export class RelationsController { name: subject.split(':')[0], }); if (!subjectResource) throw new Error('Subject resource not found'); - await ResourceDefinition.getInstance() - .findOne({ name: resources[0].split(':')[0] }) - .then(resourceDefinition => { - if (!resourceDefinition) { - throw new Error('Object resource definition not found'); - } - if (resourceDefinition.relations[relation].indexOf('*') !== -1) return; - if ( - !resourceDefinition.relations[relation] || - resourceDefinition.relations[relation].indexOf(subject.split(':')[0]) === -1 - ) { - throw new Error('Relation not allowed'); - } - }); + const definitions = await ResourceDefinition.getInstance().findMany({ + name: { $in: resources.map(resource => resource.split(':')[0]) }, + }); + for (const resource in resources) { + const resourceDefinition = definitions.find(d => d.name === resource.split(':')[0]); + if (!resourceDefinition) { + throw new Error('Object resource definition not found'); + } + if (!resourceDefinition.relations) { + throw new Error('Relation not allowed'); + } + if (resourceDefinition.relations[relation].indexOf('*') !== -1) return; + if ( + !resourceDefinition.relations[relation] || + resourceDefinition.relations[relation].indexOf(subject.split(':')[0]) === -1 + ) { + throw new Error('Relation not allowed'); + } + } } async createRelations(subject: string, relation: string, resources: string[]) { @@ -168,25 +178,6 @@ export class RelationsController { }); } - async removeGeneralRelation( - subjectResource: string, - relation: string, - objectResource: string, - ) { - // delete all relations that could be associated with resource - await Relationship.getInstance().deleteMany({ - subject: { - $regex: `${subjectResource}.*`, - $options: 'i', - }, - resource: { - $regex: `${objectResource}.*`, - $options: 'i', - }, - relation: relation, - }); - } - async getRelation(subject: string, relation: string, object: string) { checkRelation(subject, relation, object); return await Relationship.getInstance().findOne({ diff --git a/modules/authorization/src/controllers/resource.controller.ts b/modules/authorization/src/controllers/resource.controller.ts index 8c82e7ea7..aa28b1721 100644 --- a/modules/authorization/src/controllers/resource.controller.ts +++ b/modules/authorization/src/controllers/resource.controller.ts @@ -2,7 +2,7 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; import { ResourceDefinition } from '../models'; import { IndexController } from './index.controller'; import { RelationsController } from './relations.controller'; -import { isNil, isEqual, cloneDeep } from 'lodash'; +import { cloneDeep, isEqual, isNil } from 'lodash'; export class ResourceController { private static _instance: ResourceController; @@ -33,7 +33,7 @@ export class ResourceController { } await this.validateResourceRelations(resource.relations, resource.name); await this.validateResourcePermissions(resource); - + await this.indexController.reIndexResource(resource.name); const res = await ResourceDefinition.getInstance().create({ ...resource, version: 0, @@ -128,23 +128,9 @@ export class ResourceController { } return { resourceDefinition, status: 'acknowledged' }; } - - if ( - this.attributeCheck(resource.permissions) && - this.attributeCheck(resourceDefinition.permissions) && - resource.permissions !== resourceDefinition.permissions - ) { - await this.validateResourcePermissions(resource); - await this.indexController.modifyPermission(resourceDefinition, resource); - } - if ( - this.attributeCheck(resource.relations) && - this.attributeCheck(resourceDefinition.relations) && - resource.relations !== resourceDefinition.relations - ) { - await this.validateResourceRelations(resource.relations, resource.name); - await this.indexController.modifyRelations(resourceDefinition, resource); - } + await this.validateResourcePermissions(resource); + await this.validateResourceRelations(resource.relations, resource.name); + await this.indexController.reIndexResource(resource.name); delete resource._id; delete resource.name; const res = (await ResourceDefinition.getInstance().findByIdAndUpdate( diff --git a/modules/authorization/src/jobs/constructRelationIndex.ts b/modules/authorization/src/jobs/constructRelationIndex.ts index 213852fff..874dfed39 100644 --- a/modules/authorization/src/jobs/constructRelationIndex.ts +++ b/modules/authorization/src/jobs/constructRelationIndex.ts @@ -1,130 +1,30 @@ import { SandboxedJob } from 'bullmq'; import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; import { ActorIndex, ObjectIndex, ResourceDefinition } from '../models'; -import { QueueController } from '../controllers'; +import { IndexController } from '../controllers'; + +let grpcSdk: ConduitGrpcSdk | undefined = undefined; type ConstructRelationIndexWorkerData = { - relations: { subject: string; relation: string; object: string }[]; + relation: { subject: string; relation: string; object: string }; }; module.exports = async (job: SandboxedJob) => { - const { relations } = job.data; - if (!process.env.CONDUIT_SERVER) throw new Error('No serverUrl provided!'); - const grpcSdk = new ConduitGrpcSdk(process.env.CONDUIT_SERVER, 'authorization', false); - await grpcSdk.initialize(); - await grpcSdk.initializeEventBus(); - await grpcSdk.waitForExistence('database'); - ObjectIndex.getInstance(grpcSdk.database!); - ActorIndex.getInstance(grpcSdk.database!); - ResourceDefinition.getInstance(grpcSdk.database!); - QueueController.getInstance(grpcSdk); - - const objectNames = relations.map(r => r.object.split(':')[0]); - const objectDefinitions = await ResourceDefinition.getInstance().findMany({ - name: { $in: objectNames }, - }); - const obj = []; - const possibleConnectionSubjects = []; - const actorsToCreate = []; - const relationObjects: string[] = []; - for (const r of relations) { - relationObjects.push(r.object); - const entity = `${r.object}#${r.relation}`; - const objectDefinition = objectDefinitions.find( - o => o.name === r.object.split(':')[0], - )!; - const permissions = Object.keys(objectDefinition.permissions); - for (const permission of permissions) { - const roles = objectDefinition.permissions[permission]; - for (const role of roles) { - if (role.indexOf('->') !== -1 && role !== '*') { - const [relatedSubject, action] = role.split('->'); - if (r.relation === relatedSubject) { - possibleConnectionSubjects.push(`${r.subject}#${action}`); - } - } - } - } - const found = await ActorIndex.getInstance().findMany({ - $and: [{ subject: r.subject }, { entity }], - }); - const exists = found.find(f => f.entity === entity); - if (!exists) { - actorsToCreate.push({ - subject: r.subject, - subjectId: r.subject.split(':')[1].split('#')[0], - subjectType: r.subject.split(':')[0], - entity, - entityId: entity.split(':')[1].split('#')[0], - entityType: entity.split(':')[0], - relation: r.relation, - }); - } + const { relation } = job.data; + if (!grpcSdk) { + if (!process.env.CONDUIT_SERVER) throw new Error('No serverUrl provided!'); + grpcSdk = new ConduitGrpcSdk(process.env.CONDUIT_SERVER, 'authorization', false); + await grpcSdk.initialize(); + await grpcSdk.initializeEventBus(); + await grpcSdk.waitForExistence('database'); + ObjectIndex.getInstance(grpcSdk.database!); + ActorIndex.getInstance(grpcSdk.database!); + ResourceDefinition.getInstance(grpcSdk.database!); + IndexController.getInstance(grpcSdk); } - await ActorIndex.getInstance().createMany(actorsToCreate); - const possibleConnections = await ObjectIndex.getInstance().findMany({ - subject: { $in: possibleConnectionSubjects }, - }); - for (const r of relations) { - const objectDefinition = objectDefinitions.find( - o => o.name === r.object.split(':')[0], - )!; - const permissions = Object.keys(objectDefinition.permissions); - for (const permission of permissions) { - const roles = objectDefinition.permissions[permission]; - for (const role of roles) { - if (role.indexOf('->') === -1) { - obj.push({ - subject: `${r.object}#${permission}`, - subjectId: r.object.split(':')[1], - subjectType: `${r.object}#${permission}`.split(':')[0], - subjectPermission: `${r.object}#${permission}`.split('#')[1], - entity: `${r.object}#${role}`, - entityId: r.object.split(':')[1], - entityType: `${r.object}#${role}`.split(':')[0], - relation: `${r.object}#${role}`.split('#')[1], - }); - } else if (role !== '*') { - const [relatedSubject, action] = role.split('->'); - if (r.relation !== relatedSubject) continue; - const relationConnections = possibleConnections.filter( - connection => connection.subject === `${r.subject}#${action}`, - ); - for (const connection of relationConnections) { - obj.push({ - subject: `${r.object}#${permission}`, - subjectId: r.object.split(':')[1], - subjectType: `${r.object}#${permission}`.split(':')[0], - subjectPermission: `${r.object}#${permission}`.split('#')[1], - entity: connection.entity, - entityId: connection.entity.split(':')[1].split('#')[0], - entityType: connection.entity.split(':')[0], - relation: connection.entity.split('#')[1], - }); - } - } - } - } - } - const indexes = await ObjectIndex.getInstance().findMany({ - $and: [ - { subject: { $in: obj.map(i => i.subject) } }, - { entity: { $in: obj.map(i => i.entity) } }, - ], - }); - const objectsToCreate = obj.filter( - i => !indexes.find(j => j.subject === i.subject && j.entity === i.entity), - ); - await ObjectIndex.getInstance().createMany(objectsToCreate); - const actors = await ActorIndex.getInstance().findMany({ - subject: { $in: relationObjects }, - }); - if (actors.length === 0) return; - await QueueController.getInstance().addRelationIndexJob( - actors.map(actor => ({ - subject: actor.subject, - relation: actor.relation, - object: actor.entity.split('#')[0], - })), + await IndexController.getInstance().constructRelationIndex( + relation.subject, + relation.relation, + relation.object, ); }; diff --git a/modules/authorization/src/models/ObjectIndex.schema.ts b/modules/authorization/src/models/ObjectIndex.schema.ts index 78cf37c53..f3de35ad4 100644 --- a/modules/authorization/src/models/ObjectIndex.schema.ts +++ b/modules/authorization/src/models/ObjectIndex.schema.ts @@ -60,12 +60,20 @@ const schema: ConduitModel = { required: true, default: '', }, + entityPermission: { + type: TYPE.String, + default: '', + }, // member relation: { type: TYPE.String, required: true, default: '', }, + inheritanceTree: { + type: [TYPE.String], + default: [], + }, createdAt: TYPE.Date, updatedAt: TYPE.Date, }; @@ -92,6 +100,8 @@ export class ObjectIndex extends ConduitActiveSchema { entity: string; entityId: string; entityType: string; + entityPermission: string; + inheritanceTree: string[]; relation: string; createdAt: Date; updatedAt: Date; diff --git a/modules/authorization/src/models/ResourceDefinition.schema.ts b/modules/authorization/src/models/ResourceDefinition.schema.ts index 4ebbc02ad..a8642249b 100644 --- a/modules/authorization/src/models/ResourceDefinition.schema.ts +++ b/modules/authorization/src/models/ResourceDefinition.schema.ts @@ -6,6 +6,7 @@ const schema: ConduitModel = { name: { type: TYPE.String, required: true, + unique: true, }, /** * Example: @@ -43,7 +44,7 @@ const schema: ConduitModel = { */ permissions: { type: TYPE.JSON, - required: true, + required: false, }, version: { type: TYPE.Number, @@ -70,8 +71,8 @@ export class ResourceDefinition extends ConduitActiveSchema private static _instance: ResourceDefinition; _id: string; name: string; - relations: { [key: string]: string[] }; - permissions: { [key: string]: string[] }; + relations?: { [key: string]: string[] }; + permissions?: { [key: string]: string[] }; version: number; createdAt: Date; updatedAt: Date; diff --git a/modules/authorization/src/utils/index.ts b/modules/authorization/src/utils/index.ts index 14883e27a..6c13e09be 100644 --- a/modules/authorization/src/utils/index.ts +++ b/modules/authorization/src/utils/index.ts @@ -1,4 +1,6 @@ //can be used both for relation checks and permission checks +import { ObjectIndex } from '../models'; + export const checkRelation = (subject: string, relation: string, object: string) => { if (!subject.includes(':')) { throw new Error('Subject must be a valid resource identifier'); @@ -31,6 +33,26 @@ export const computePermissionTuple = ( return `${subject}#${relation}@${object}`; }; +export const constructObjectIndex = ( + subject: string, + permission: string, + role: string, + object: string, + inheritanceTree: string[], +): Partial => { + return { + subject: `${subject}#${permission}`, + subjectId: subject.split(':')[1], + subjectType: `${subject}#${permission}`.split(':')[0], + subjectPermission: `${object}#${permission}`.split('#')[1], + entity: role === '*' ? '*' : `${object}#${role}`, + entityId: role === '*' ? '*' : object.split(':')[1], + entityType: role === '*' ? '*' : `${object}#${role}`.split(':')[0], + relation: role === '*' ? '*' : `${object}#${role}`.split('#')[1], + inheritanceTree: inheritanceTree, + }; +}; + export function getPostgresAccessListQuery( objectTypeCollection: string, computedTuple: string, @@ -51,7 +73,7 @@ export function getPostgresAccessListQuery( SELECT * FROM "cnd_ObjectIndex" WHERE "subjectType" = '${objectType}' AND "subjectPermission" = '${action}' ) as obj - ON actors.entity = obj.entity + ON actors.entity = obj.entity OR obj.entity = '*' ) UNION ( SELECT "computedTuple" @@ -82,5 +104,5 @@ export function getSQLAccessListQuery( INNER JOIN ( SELECT * FROM cnd_ObjectIndex WHERE "subjectType" = '${objectType}' AND "subjectPermission" = '${action}' - ) objects ON actors.entity = objects.entity;`; + ) objects ON actors.entity = obj.entity OR obj.entity = '*';`; }