Skip to content

Commit

Permalink
feat(authorization, database): create bulk relations in authorization (
Browse files Browse the repository at this point in the history
  • Loading branch information
Renc17 authored Sep 12, 2023
1 parent 1543e07 commit d48d7b3
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 28 deletions.
8 changes: 8 additions & 0 deletions libraries/grpc-sdk/src/modules/authorization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ export class Authorization extends ConduitModule<typeof AuthorizationDefinition>
return this.client!.createRelation(data);
}

createRelations(
subject: string,
relation: string,
resources: string[],
): Promise<Empty> {
return this.client!.createRelations({ subject, relation, resources });
}

findRelation(data: FindRelationRequest): Promise<FindRelationResponse> {
return this.client!.findRelation(data);
}
Expand Down
13 changes: 13 additions & 0 deletions modules/authorization/src/Authorization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ConduitGrpcSdk, {
CreateRelationsRequest,
DatabaseProvider,
GrpcRequest,
GrpcResponse,
Expand Down Expand Up @@ -45,6 +46,7 @@ export default class Authorization extends ManagedModule<Config> {
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),
Expand Down Expand Up @@ -125,6 +127,17 @@ export default class Authorization extends ManagedModule<Config> {
callback(null, {});
}

async createRelations(
call: GrpcRequest<CreateRelationsRequest>,
callback: GrpcResponse<Empty>,
) {
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<Relation>, callback: GrpcResponse<Empty>) {
const { relation, resource, subject } = call.request;
await this.relationsController.deleteRelation(subject, relation, resource);
Expand Down
31 changes: 31 additions & 0 deletions modules/authorization/src/admin/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -95,6 +112,20 @@ export class RelationHandler {
return newRelation;
}

async createRelations(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
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<UnparsedRouterResponse> {
const { id } = call.request.params;
const found = await Relationship.getInstance().findOne({ _id: id });
Expand Down
16 changes: 12 additions & 4 deletions modules/authorization/src/authorization.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand All @@ -43,22 +47,24 @@ message FindRelationRequest {
optional int32 limit = 7;
}


message FindRelationResponse {
repeated Relation relations = 1;
int32 count = 2;
}

message AllowedResourcesRequest {
string subject = 1;
string action = 2;
string resourceType = 5;
int32 skip = 6;
int32 limit = 7;
}

message AllowedResourcesResponse {
repeated string resources = 1;
int32 count = 2;
}

message ResourceAccessListRequest {
string subject = 1;
string action = 2;
Expand All @@ -70,6 +76,7 @@ message PermissionCheck {
repeated string actions = 2;
string resource = 3;
}

message PermissionRequest {
string subject = 1;
string action = 2;
Expand All @@ -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);
Expand Down
34 changes: 16 additions & 18 deletions modules/authorization/src/controllers/index.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,42 +32,40 @@ 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;
const possibleConnections = await ObjectIndex.getInstance().findMany({
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) {
Expand Down
66 changes: 65 additions & 1 deletion 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 { Relationship, ResourceDefinition } from '../models';
import { ActorIndex, Relationship, ResourceDefinition } from '../models';
import { IndexController } from './index.controller';

export class RelationsController {
Expand Down Expand Up @@ -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({
Expand Down
11 changes: 6 additions & 5 deletions modules/database/src/adapters/SchemaAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,15 @@ export abstract class SchemaAdapter<T> {
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}`,
});
}
Expand Down

0 comments on commit d48d7b3

Please sign in to comment.