diff --git a/modules/authorization/src/Authorization.ts b/modules/authorization/src/Authorization.ts index c2eeec27c..7b548ba70 100644 --- a/modules/authorization/src/Authorization.ts +++ b/modules/authorization/src/Authorization.ts @@ -74,7 +74,6 @@ export default class Authorization extends ManagedModule { async onServerStart() { await this.grpcSdk.waitForExistence('database'); this.database = this.grpcSdk.database!; - await runMigrations(this.grpcSdk); } async onConfig() { @@ -83,6 +82,7 @@ export default class Authorization extends ManagedModule { this.updateHealth(HealthCheckStatus.NOT_SERVING); } else { await this.registerSchemas(); + await runMigrations(this.grpcSdk); this.indexController = IndexController.getInstance(this.grpcSdk); this.relationsController = RelationsController.getInstance( this.grpcSdk, diff --git a/modules/authorization/src/controllers/index.controller.ts b/modules/authorization/src/controllers/index.controller.ts index 6c68ace7a..c09e947a8 100644 --- a/modules/authorization/src/controllers/index.controller.ts +++ b/modules/authorization/src/controllers/index.controller.ts @@ -24,7 +24,14 @@ export class IndexController { async createOrUpdateObject(subject: string, entity: string) { const index = await ObjectIndex.getInstance().findOne({ subject, entity }); if (!index) { - await ObjectIndex.getInstance().create({ subject, entity }); + await ObjectIndex.getInstance().create({ + subject, + subjectType: subject.split(':')[0], + subjectPermission: subject.split('#')[1], + entity, + entityType: entity.split(':')[0], + relation: entity.split('#')[1], + }); } } @@ -32,26 +39,51 @@ 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, + subjectType: subject.split(':')[0], + entity: `${object}#${relation}`, + entityType: object.split(':')[0], + relation: 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) { + if (role.indexOf('->') === -1) { obj.push({ - subject: object + '#' + permission, - entity: role === '*' ? `*` : `${object}#${role}`, + subject: `${object}#${permission}`, + subjectType: `${object}#${permission}`.split(':')[0], + subjectPermission: `${object}#${permission}`.split('#')[1], + entity: `${object}#${role}`, + entityType: `${object}#${role}`.split(':')[0], + relation: `${object}#${role}`.split('#')[1], }); - } else { + } 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, entity: connection.entity }); + obj.push({ + subject: `${object}#${permission}`, + subjectType: `${object}#${permission}`.split(':')[0], + subjectPermission: `${object}#${permission}`.split('#')[1], + entity: connection.entity, + entityType: connection.entity.split(':')[0], + relation: connection.entity.split('#')[1], + }); } } } @@ -66,6 +98,124 @@ 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 this.constructRelationIndexes( + actors.map(actor => ({ + subject: actor.subject, + relation: actor.relation, + object: actor.entity.split('#')[0], + })), + ); + } + + async constructRelationIndexes( + relations: { subject: string; relation: string; object: string }[], + ) { + 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, + subjectType: r.subject.split(':')[0], + entity, + entityType: entity.split(':')[0], + relation: r.relation, + }); + } + } + 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}`, + subjectType: `${r.object}#${permission}`.split(':')[0], + subjectPermission: `${r.object}#${permission}`.split('#')[1], + entity: `${r.object}#${role}`, + 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}`, + subjectType: `${r.object}#${permission}`.split(':')[0], + subjectPermission: `${r.object}#${permission}`.split('#')[1], + entity: connection.entity, + 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 this.constructRelationIndexes( + actors.map(actor => ({ + subject: actor.subject, + relation: actor.relation, + object: actor.entity.split('#')[0], + })), + ); } async removeRelation(subject: string, relation: string, object: string) { @@ -298,7 +448,7 @@ export class IndexController { const objectDefinition = await ObjectIndex.getInstance().findOne({ subject: object + '#' + action, - entity: { $in: [...subjectDefinition?.map(index => index.entity), '*'] }, + entity: { $in: [...subjectDefinition.map(index => index.entity), '*'] }, }); return !!objectDefinition; } @@ -317,7 +467,7 @@ export class IndexController { const objectDefinition = await ObjectIndex.getInstance().findMany( { subject: { $like: `${objectType}:%#${action}` }, - entity: { $in: [...subjectDefinition?.map(index => index.entity), '*'] }, + entity: { $in: [...subjectDefinition.map(index => index.entity), '*'] }, }, undefined, skip, @@ -333,7 +483,7 @@ export class IndexController { return await ObjectIndex.getInstance().countDocuments({ subject: { $like: `${objectType}:%#${action}` }, - entity: { $in: [...subjectDefinition?.map(index => index.entity), '*'] }, + entity: { $in: [...subjectDefinition.map(index => index.entity), '*'] }, }); } } diff --git a/modules/authorization/src/controllers/permissions.controller.ts b/modules/authorization/src/controllers/permissions.controller.ts index dd5368d55..1a93f6974 100644 --- a/modules/authorization/src/controllers/permissions.controller.ts +++ b/modules/authorization/src/controllers/permissions.controller.ts @@ -33,8 +33,10 @@ export class PermissionsController { const computedTuple = computePermissionTuple(subject, action, resource); await Permission.getInstance().create({ subject, + subjectType: subject.split(':')[0], permission: action, resource, + resourceType: resource.split(':')[0], computedTuple, }); } diff --git a/modules/authorization/src/controllers/relations.controller.ts b/modules/authorization/src/controllers/relations.controller.ts index a7ff4c098..a38b1bbfc 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 { ActorIndex, Relationship, ResourceDefinition } from '../models'; +import { Relationship, ResourceDefinition } from '../models'; import { IndexController } from './index.controller'; export class RelationsController { @@ -27,7 +27,7 @@ export class RelationsController { let relationResource = await Relationship.getInstance().findOne({ computedTuple: computeRelationTuple(subject, relation, object), }); - if (relationResource) throw new Error('Relation already exists'); + if (relationResource) return relationResource; const subjectResource = await ResourceDefinition.getInstance().findOne({ name: subject.split(':')[0], @@ -50,8 +50,10 @@ export class RelationsController { relationResource = await Relationship.getInstance().create({ subject: subject, + subjectType: subject.split(':')[0], relation: relation, resource: object, + resourceType: object.split(':')[0], computedTuple: computeRelationTuple(subject, relation, object), }); await this.indexController.constructRelationIndex(subject, relation, object); @@ -87,39 +89,24 @@ export class RelationsController { 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, + subjectType: subject.split(':')[0], relation, resource: r, + resourceType: r.split(':')[0], 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; + const relationDocs = await Relationship.getInstance().createMany(relations); + const relationEntries = relations.map(r => ({ + subject: r.subject, + relation: r.relation, + object: r.resource, + })); + await this.indexController.constructRelationIndexes(relationEntries); + return relationDocs; } async deleteRelation(subject: string, relation: string, object: string) { diff --git a/modules/authorization/src/migrations/actorIndex.migration.ts b/modules/authorization/src/migrations/actorIndex.migration.ts new file mode 100644 index 000000000..c1057cebb --- /dev/null +++ b/modules/authorization/src/migrations/actorIndex.migration.ts @@ -0,0 +1,37 @@ +import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { ActorIndex } from '../models'; + +export const migrateActorIndex = async (grpcSdk: ConduitGrpcSdk) => { + const count = await ActorIndex.getInstance().countDocuments({ + entityType: '', + }); + if (count === 0) { + return; + } + let actorIndexes = await ActorIndex.getInstance().findMany( + { + entityType: '', + }, + undefined, + 0, + 100, + ); + let iterator = 0; + while (actorIndexes.length > 0) { + for (const actorIndex of actorIndexes) { + await ActorIndex.getInstance().findByIdAndUpdate(actorIndex._id, { + entityType: actorIndex.entity.split(':')[0], + subjectType: actorIndex.subject.split(':')[0], + relation: actorIndex.subject.split('#')[1], + }); + } + actorIndexes = await ActorIndex.getInstance().findMany( + { + entityType: '', + }, + undefined, + ++iterator * 100, + 100, + ); + } +}; diff --git a/modules/authorization/src/migrations/index.ts b/modules/authorization/src/migrations/index.ts index 801771f5e..8d8a0026a 100644 --- a/modules/authorization/src/migrations/index.ts +++ b/modules/authorization/src/migrations/index.ts @@ -1,5 +1,14 @@ import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { migrateObjectIndex } from './objectIndex.migration'; +import { migrateActorIndex } from './actorIndex.migration'; +import { migrateRelationships } from './relationship.migration'; +import { migratePermission } from './permission.migration'; export async function runMigrations(grpcSdk: ConduitGrpcSdk) { - // ... + await Promise.all([ + migrateObjectIndex(grpcSdk), + migrateActorIndex(grpcSdk), + migrateRelationships(grpcSdk), + migratePermission(grpcSdk), + ]); } diff --git a/modules/authorization/src/migrations/objectIndex.migration.ts b/modules/authorization/src/migrations/objectIndex.migration.ts new file mode 100644 index 000000000..77111d580 --- /dev/null +++ b/modules/authorization/src/migrations/objectIndex.migration.ts @@ -0,0 +1,38 @@ +import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { ObjectIndex } from '../models'; + +export const migrateObjectIndex = async (grpcSdk: ConduitGrpcSdk) => { + const count = await ObjectIndex.getInstance().countDocuments({ + entityType: '', + }); + if (count === 0) { + return; + } + let objectIndexes = await ObjectIndex.getInstance().findMany( + { + entityType: '', + }, + undefined, + 0, + 100, + ); + let iterator = 0; + while (objectIndexes.length > 0) { + for (const objectIndex of objectIndexes) { + await ObjectIndex.getInstance().findByIdAndUpdate(objectIndex._id, { + subjectType: objectIndex.subject.split(':')[0], + subjectPermission: objectIndex.subject.split('#')[1], + entityType: objectIndex.entity.split(':')[0], + relation: objectIndex.subject.split('#')[1], + }); + } + objectIndexes = await ObjectIndex.getInstance().findMany( + { + entityType: '', + }, + undefined, + ++iterator * 100, + 100, + ); + } +}; diff --git a/modules/authorization/src/migrations/permission.migration.ts b/modules/authorization/src/migrations/permission.migration.ts new file mode 100644 index 000000000..63ed2b315 --- /dev/null +++ b/modules/authorization/src/migrations/permission.migration.ts @@ -0,0 +1,36 @@ +import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { Permission } from '../models'; + +export const migratePermission = async (grpcSdk: ConduitGrpcSdk) => { + const count = await Permission.getInstance().countDocuments({ + resourceType: '', + }); + if (count === 0) { + return; + } + let permissions = await Permission.getInstance().findMany( + { + resourceType: '', + }, + undefined, + 0, + 100, + ); + let iterator = 0; + while (permissions.length > 0) { + for (const permission of permissions) { + await Permission.getInstance().findByIdAndUpdate(permission._id, { + subjectType: permission.subject.split(':')[0], + resourceType: permission.resource.split(':')[0], + }); + } + permissions = await Permission.getInstance().findMany( + { + resourceType: '', + }, + undefined, + ++iterator * 100, + 100, + ); + } +}; diff --git a/modules/authorization/src/migrations/relationship.migration.ts b/modules/authorization/src/migrations/relationship.migration.ts new file mode 100644 index 000000000..373b4663b --- /dev/null +++ b/modules/authorization/src/migrations/relationship.migration.ts @@ -0,0 +1,36 @@ +import ConduitGrpcSdk from '@conduitplatform/grpc-sdk'; +import { Relationship } from '../models'; + +export const migrateRelationships = async (grpcSdk: ConduitGrpcSdk) => { + const count = await Relationship.getInstance().countDocuments({ + resourceType: '', + }); + if (count === 0) { + return; + } + let relationships = await Relationship.getInstance().findMany( + { + resourceType: '', + }, + undefined, + 0, + 100, + ); + let iterator = 0; + while (relationships.length > 0) { + for (const objectIndex of relationships) { + await Relationship.getInstance().findByIdAndUpdate(objectIndex._id, { + subjectType: objectIndex.subject.split(':')[0], + resourceType: objectIndex.resource.split(':')[0], + }); + } + relationships = await Relationship.getInstance().findMany( + { + resourceType: '', + }, + undefined, + ++iterator * 100, + 100, + ); + } +}; diff --git a/modules/authorization/src/models/ActorIndex.schema.ts b/modules/authorization/src/models/ActorIndex.schema.ts index 169dc8c87..34120859f 100644 --- a/modules/authorization/src/models/ActorIndex.schema.ts +++ b/modules/authorization/src/models/ActorIndex.schema.ts @@ -9,6 +9,7 @@ const schema: ConduitModel = { _id: TYPE.ObjectId, /** * { + * subject entity * "user:12312312": "organization:123123#member" * } */ @@ -16,10 +17,28 @@ const schema: ConduitModel = { type: TYPE.String, required: true, }, + // user + subjectType: { + type: TYPE.String, + required: true, + default: '', + }, entity: { type: TYPE.String, required: true, }, + // organization + entityType: { + type: TYPE.String, + required: true, + default: '', + }, + // member + relation: { + type: TYPE.String, + required: true, + default: '', + }, createdAt: TYPE.Date, updatedAt: TYPE.Date, }; @@ -40,7 +59,10 @@ export class ActorIndex extends ConduitActiveSchema { private static _instance: ActorIndex; _id: string; subject: string; + subjectType: string; entity: string; + entityType: string; + relation: string; createdAt: Date; updatedAt: Date; diff --git a/modules/authorization/src/models/ObjectIndex.schema.ts b/modules/authorization/src/models/ObjectIndex.schema.ts index 204e9d3c8..3f25c78d2 100644 --- a/modules/authorization/src/models/ObjectIndex.schema.ts +++ b/modules/authorization/src/models/ObjectIndex.schema.ts @@ -17,10 +17,34 @@ const schema: ConduitModel = { type: TYPE.String, required: true, }, + // organization + subjectType: { + type: TYPE.String, + required: true, + default: '', + }, + // view + subjectPermission: { + type: TYPE.String, + required: true, + default: '', + }, entity: { type: TYPE.String, required: true, }, + // organization + entityType: { + type: TYPE.String, + required: true, + default: '', + }, + // member + relation: { + type: TYPE.String, + required: true, + default: '', + }, createdAt: TYPE.Date, updatedAt: TYPE.Date, }; @@ -41,7 +65,11 @@ export class ObjectIndex extends ConduitActiveSchema { private static _instance: ObjectIndex; _id: string; subject: string; + subjectType: string; + subjectPermission: string; entity: string; + entityType: string; + relation: string; createdAt: Date; updatedAt: Date; diff --git a/modules/authorization/src/models/Permission.schema.ts b/modules/authorization/src/models/Permission.schema.ts index 2cea9f3f5..8574ab7e7 100644 --- a/modules/authorization/src/models/Permission.schema.ts +++ b/modules/authorization/src/models/Permission.schema.ts @@ -13,12 +13,24 @@ const schema: ConduitModel = { type: TYPE.String, required: true, }, + // organization + resourceType: { + type: TYPE.String, + required: true, + default: '', + }, // user:1adasdas subject: { type: TYPE.String, required: true, }, - // member relation: "owner" + // user + subjectType: { + type: TYPE.String, + required: true, + default: '', + }, + // read permission: { type: TYPE.String, required: true, @@ -48,7 +60,9 @@ export class Permission extends ConduitActiveSchema { private static _instance: Permission; _id: string; resource: string; + resourceType: string; subject: string; + subjectType: string; permission: string; computedTuple: string; createdAt: Date; diff --git a/modules/authorization/src/models/Relationship.schema.ts b/modules/authorization/src/models/Relationship.schema.ts index 06ffd7a1d..d90bc4e74 100644 --- a/modules/authorization/src/models/Relationship.schema.ts +++ b/modules/authorization/src/models/Relationship.schema.ts @@ -12,11 +12,23 @@ const schema: ConduitModel = { type: TYPE.String, required: true, }, + // organization + resourceType: { + type: TYPE.String, + required: true, + default: '', + }, // user:1adasdas subject: { type: TYPE.String, required: true, }, + // user + subjectType: { + type: TYPE.String, + required: true, + default: '', + }, // member relation: "owner" relation: { type: TYPE.String, @@ -47,8 +59,9 @@ export class Relationship extends ConduitActiveSchema { private static _instance: Relationship; _id: string; resource: string; - resourceId: string; + resourceType: string; subject: string; + subjectType: string; relation: string; computedTuple: string; createdAt: Date;