Skip to content

Commit

Permalink
refactor(authorization): improve relation creation and resolve orderi…
Browse files Browse the repository at this point in the history
…ng issues (#685)
  • Loading branch information
kkopanidis authored Oct 4, 2023
1 parent 45b757e commit ca755c6
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 42 deletions.
2 changes: 1 addition & 1 deletion modules/authorization/src/Authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export default class Authorization extends ManagedModule<Config> {
async onServerStart() {
await this.grpcSdk.waitForExistence('database');
this.database = this.grpcSdk.database!;
await runMigrations(this.grpcSdk);
}

async onConfig() {
Expand All @@ -83,6 +82,7 @@ export default class Authorization extends ManagedModule<Config> {
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,
Expand Down
172 changes: 161 additions & 11 deletions modules/authorization/src/controllers/index.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,66 @@ 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],
});
}
}

async constructRelationIndex(subject: string, relation: string, object: string) {
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],
});
}
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand All @@ -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), '*'] },
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down
41 changes: 14 additions & 27 deletions modules/authorization/src/controllers/relations.controller.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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],
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions modules/authorization/src/migrations/actorIndex.migration.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
};
11 changes: 10 additions & 1 deletion modules/authorization/src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -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),
]);
}
Loading

0 comments on commit ca755c6

Please sign in to comment.