From d48d7b38b33b669311b2e20ead015df750a7675c Mon Sep 17 00:00:00 2001 From: Renc17 <57152951+Renc17@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:23:23 +0300 Subject: [PATCH] feat(authorization, database): create bulk relations in authorization (#681) --- .../src/modules/authorization/index.ts | 8 +++ modules/authorization/src/Authorization.ts | 13 ++++ modules/authorization/src/admin/relations.ts | 31 +++++++++ modules/authorization/src/authorization.proto | 16 +++-- .../src/controllers/index.controller.ts | 34 +++++----- .../src/controllers/relations.controller.ts | 66 ++++++++++++++++++- .../database/src/adapters/SchemaAdapter.ts | 11 ++-- 7 files changed, 151 insertions(+), 28 deletions(-) diff --git a/libraries/grpc-sdk/src/modules/authorization/index.ts b/libraries/grpc-sdk/src/modules/authorization/index.ts index 99fd57341..2c9f0d7c4 100644 --- a/libraries/grpc-sdk/src/modules/authorization/index.ts +++ b/libraries/grpc-sdk/src/modules/authorization/index.ts @@ -45,6 +45,14 @@ export class Authorization extends ConduitModule return this.client!.createRelation(data); } + createRelations( + subject: string, + relation: string, + resources: string[], + ): Promise { + return this.client!.createRelations({ subject, relation, resources }); + } + findRelation(data: FindRelationRequest): Promise { return this.client!.findRelation(data); } diff --git a/modules/authorization/src/Authorization.ts b/modules/authorization/src/Authorization.ts index a3d18699e..c2eeec27c 100644 --- a/modules/authorization/src/Authorization.ts +++ b/modules/authorization/src/Authorization.ts @@ -1,4 +1,5 @@ import ConduitGrpcSdk, { + CreateRelationsRequest, DatabaseProvider, GrpcRequest, GrpcResponse, @@ -45,6 +46,7 @@ export default class Authorization extends ManagedModule { deleteResource: this.deleteResource.bind(this), updateResource: this.updateResource.bind(this), createRelation: this.createRelation.bind(this), + createRelations: this.createRelations.bind(this), grantPermission: this.grantPermission.bind(this), removePermission: this.removePermission.bind(this), deleteRelation: this.deleteRelation.bind(this), @@ -125,6 +127,17 @@ export default class Authorization extends ManagedModule { callback(null, {}); } + async createRelations( + call: GrpcRequest, + callback: GrpcResponse, + ) { + const subject = call.request.subject; + const relation = call.request.relation; + const resources = call.request.resources; + await this.relationsController.createRelations(subject, relation, resources); + callback(null, {}); + } + async deleteRelation(call: GrpcRequest, callback: GrpcResponse) { const { relation, resource, subject } = call.request; await this.relationsController.deleteRelation(subject, relation, resource); diff --git a/modules/authorization/src/admin/relations.ts b/modules/authorization/src/admin/relations.ts index e6b2db0d2..52bbf5855 100644 --- a/modules/authorization/src/admin/relations.ts +++ b/modules/authorization/src/admin/relations.ts @@ -35,6 +35,23 @@ export class RelationHandler { ), this.createRelation.bind(this), ); + routingManager.route( + { + path: '/relations/many', + action: ConduitRouteActions.POST, + description: `Creates many relations.`, + bodyParams: { + subject: ConduitString.Required, + relation: ConduitString.Required, + resources: [ConduitString.Required], + }, + }, + new ConduitRouteReturnDefinition( + 'CreateRelations', + Relationship.getInstance().fields, + ), + this.createRelations.bind(this), + ); routingManager.route( { path: '/relations/:id', @@ -95,6 +112,20 @@ export class RelationHandler { return newRelation; } + async createRelations(call: ParsedRouterRequest): Promise { + const { subject, relation, resources } = call.request.params; + const newRelation = await RelationsController.getInstance().createRelations( + subject, + relation, + resources, + ); + this.grpcSdk.bus?.publish( + 'authentication:create:relation', + JSON.stringify(newRelation), + ); + return newRelation; + } + async getRelation(call: ParsedRouterRequest): Promise { const { id } = call.request.params; const found = await Relationship.getInstance().findOne({ _id: id }); diff --git a/modules/authorization/src/authorization.proto b/modules/authorization/src/authorization.proto index 59853c5c4..8d604ca0a 100644 --- a/modules/authorization/src/authorization.proto +++ b/modules/authorization/src/authorization.proto @@ -6,17 +6,14 @@ message Resource { string name = 1; repeated _Relation relations = 2; repeated _Permission permissions = 3; - message _Relation { string name = 1; repeated string resourceType = 2; } - message _Permission { string name = 1; repeated string roles = 2; } - } message Relation { @@ -25,10 +22,17 @@ message Relation { string resource = 3; } +message CreateRelationsRequest { + string subject = 1; + string relation = 2; + repeated string resources = 3; +} + message DeleteAllRelationsRequest { optional string subject = 1; optional string resource = 2; } + message DeleteResourceRequest { string name = 1; } @@ -43,11 +47,11 @@ message FindRelationRequest { optional int32 limit = 7; } - message FindRelationResponse { repeated Relation relations = 1; int32 count = 2; } + message AllowedResourcesRequest { string subject = 1; string action = 2; @@ -55,10 +59,12 @@ message AllowedResourcesRequest { int32 skip = 6; int32 limit = 7; } + message AllowedResourcesResponse { repeated string resources = 1; int32 count = 2; } + message ResourceAccessListRequest { string subject = 1; string action = 2; @@ -70,6 +76,7 @@ message PermissionCheck { repeated string actions = 2; string resource = 3; } + message PermissionRequest { string subject = 1; string action = 2; @@ -85,6 +92,7 @@ service Authorization { rpc DeleteResource(DeleteResourceRequest) returns (google.protobuf.Empty); rpc UpdateResource(Resource) returns (google.protobuf.Empty); rpc CreateRelation(Relation) returns (google.protobuf.Empty); + rpc CreateRelations(CreateRelationsRequest) returns (google.protobuf.Empty); rpc DeleteRelation(Relation) returns (google.protobuf.Empty); rpc GrantPermission(PermissionRequest) returns (google.protobuf.Empty); rpc RemovePermission(PermissionRequest) returns (google.protobuf.Empty); diff --git a/modules/authorization/src/controllers/index.controller.ts b/modules/authorization/src/controllers/index.controller.ts index 4ce8e3964..6c68ace7a 100644 --- a/modules/authorization/src/controllers/index.controller.ts +++ b/modules/authorization/src/controllers/index.controller.ts @@ -32,30 +32,18 @@ export class IndexController { const objectDefinition = (await ResourceDefinition.getInstance().findOne({ name: object.split(':')[0], }))!; - // relations can only be created between actors and resources - // object indexes represent relations between actors and permissions on resources - // construct actor index - const found = await ActorIndex.getInstance().findOne({ - subject: subject, - entity: `${object}#${relation}`, - }); - if (!found) { - await ActorIndex.getInstance().create({ - subject: subject, - entity: `${object}#${relation}`, - }); - } const permissions = Object.keys(objectDefinition.permissions); + const obj = []; for (const permission of permissions) { const roles = objectDefinition.permissions[permission]; for (const role of roles) { // no index needed for "allowAll" permissions // or for self modification if (role === '*' || role.indexOf('->') === -1) { - await this.createOrUpdateObject( - object + '#' + permission, - role === '*' ? `*` : `${object}#${role}`, - ); + obj.push({ + subject: object + '#' + permission, + entity: role === '*' ? `*` : `${object}#${role}`, + }); } else { const [relatedSubject, action] = role.split('->'); if (relation !== relatedSubject) continue; @@ -63,11 +51,21 @@ export class IndexController { subject: `${subject}#${action}`, }); for (const connection of possibleConnections) { - await this.createOrUpdateObject(object + '#' + permission, connection.entity); + obj.push({ subject: object + '#' + permission, entity: connection.entity }); } } } } + const indexes = await ObjectIndex.getInstance().findMany({ + $and: [ + { subject: { $in: obj.map(i => i.subject) } }, + { entity: { $in: obj.map(i => i.entity) } }, + ], + }); + const toCreate = obj.filter( + i => !indexes.find(j => j.subject === i.subject && j.entity === i.entity), + ); + await ObjectIndex.getInstance().createMany(toCreate); } async removeRelation(subject: string, relation: string, object: string) { diff --git a/modules/authorization/src/controllers/relations.controller.ts b/modules/authorization/src/controllers/relations.controller.ts index 3605f228c..a7ff4c098 100644 --- a/modules/authorization/src/controllers/relations.controller.ts +++ b/modules/authorization/src/controllers/relations.controller.ts @@ -1,6 +1,6 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; import { checkRelation, computeRelationTuple } from '../utils'; -import { Relationship, ResourceDefinition } from '../models'; +import { ActorIndex, Relationship, ResourceDefinition } from '../models'; import { IndexController } from './index.controller'; export class RelationsController { @@ -58,6 +58,70 @@ export class RelationsController { return relationResource; } + private async checkRelations(subject: string, relation: string, resources: string[]) { + checkRelation(subject, relation, resources[0]); + const computedTuples = resources.map(r => computeRelationTuple(subject, relation, r)); + const relationResources = await Relationship.getInstance().findMany({ + computedTuple: { $in: computedTuples }, + }); + if (relationResources.length) throw new Error('Relations already exist'); + const subjectResource = await ResourceDefinition.getInstance().findOne({ + 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'); + } + }); + } + + async createRelations(subject: string, relation: string, resources: string[]) { + await this.checkRelations(subject, relation, resources); + const entities: string[] = []; + const relations = resources.map(r => { + entities.push(`${r}#${relation}`); + return { + subject, + relation, + resource: r, + computedTuple: computeRelationTuple(subject, relation, r), + }; + }); + + const relationResource = await Relationship.getInstance().createMany(relations); + // relations can only be created between actors and resources + // object indexes represent relations between actors and permissions on resources + // construct actor index + const found = await ActorIndex.getInstance().findMany({ + $and: [{ subject: { $eq: subject } }, { entity: { $in: entities } }], + }); + const toCreate = entities.flatMap(e => { + const exists = found.find(f => f.entity === e); + if (exists) return []; + return { + subject, + entity: e, + }; + }); + await ActorIndex.getInstance().createMany(toCreate); + await Promise.all( + relations.map(r => + this.indexController.constructRelationIndex(r.subject, r.relation, r.resource), + ), + ); + return relationResource; + } + async deleteRelation(subject: string, relation: string, object: string) { checkRelation(subject, relation, object); const relationResource = await Relationship.getInstance().findOne({ diff --git a/modules/database/src/adapters/SchemaAdapter.ts b/modules/database/src/adapters/SchemaAdapter.ts index 6bc0f62c9..1e50df57b 100644 --- a/modules/database/src/adapters/SchemaAdapter.ts +++ b/modules/database/src/adapters/SchemaAdapter.ts @@ -205,14 +205,15 @@ export abstract class SchemaAdapter { if (!options || (!options?.userId && options?.scope)) { return; } + const subject = options.scope ?? `User:${options.userId}`; + const relation = 'owner'; if (Array.isArray(data)) { - for (const d of data) { - await this.addPermissionToData(d, options); - } + const resources = data.map(d => `${this.originalSchema.name}:${d._id}`); + await this.grpcSdk.authorization?.createRelations(subject, relation, resources); } else { await this.grpcSdk.authorization?.createRelation({ - subject: options.scope ?? `User:${options.userId}`, - relation: 'owner', + subject, + relation, resource: `${this.originalSchema.name}:${data._id}`, }); }