feat: introduce role based access control

This commit is contained in:
Ali BARIN
2023-06-22 22:20:10 +00:00
parent a5c7da331a
commit 69582ff83d
25 changed files with 376 additions and 19 deletions

View File

@@ -40,6 +40,9 @@ class Connection extends Base {
userId: { type: 'string', format: 'uuid' },
verified: { type: 'boolean', default: false },
draft: { type: 'boolean' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -31,6 +31,9 @@ class ExecutionStep extends Base {
dataOut: { type: ['object', 'null'] },
status: { type: 'string', enum: ['success', 'failure'] },
errorDetails: { type: ['object', 'null'] },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -22,6 +22,9 @@ class Execution extends Base {
flowId: { type: 'string', format: 'uuid' },
testRun: { type: 'boolean', default: false },
internalId: { type: 'string' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -19,7 +19,7 @@ class Flow extends Base {
status: 'paused' | 'published' | 'draft';
steps: Step[];
triggerStep: Step;
published_at: string;
publishedAt: string;
remoteWebhookId: string;
executions?: Execution[];
lastExecution?: Execution;
@@ -37,6 +37,10 @@ class Flow extends Base {
userId: { type: 'string', format: 'uuid' },
remoteWebhookId: { type: 'string' },
active: { type: 'boolean' },
publishedAt: { type: 'string' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -0,0 +1,24 @@
import Base from './base';
class Permission extends Base {
id: string;
action: string;
subject: string;
static tableName = 'permissions';
static jsonSchema = {
type: 'object',
required: ['action', 'subject'],
properties: {
id: { type: 'string', format: 'uuid' },
action: { type: 'string', minLength: 1 },
subject: { type: 'string', minLength: 1 },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
}
export default Permission;

View File

@@ -15,7 +15,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => {
modelClass
);
qb.onBuild((builder) => {
if (!builder.context().withSoftDeleted) {
if (!builder.context().withSoftDeleted && qb.modelClass().jsonSchema.properties.deletedAt) {
builder.whereNull(
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
);

View File

@@ -0,0 +1,39 @@
import Base from './base';
import User from './user';
class Role extends Base {
id!: string;
name!: string;
key: string;
description: string;
users?: User[];
static tableName = 'roles';
static jsonSchema = {
type: 'object',
required: ['name', 'key'],
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 },
key: { type: 'string', minLength: 1 },
description: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
static relationMappings = () => ({
users: {
relation: Base.HasManyRelation,
modelClass: User,
join: {
from: 'roles.id',
to: 'users.role_id',
},
},
});
}
export default Role;

View File

@@ -46,6 +46,9 @@ class Step extends Base {
position: { type: 'integer' },
parameters: { type: 'object' },
webhookPath: { type: ['string', 'null'] },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -46,6 +46,9 @@ class Subscription extends Base {
nextBillDate: { type: 'string' },
lastBillDate: { type: 'string' },
cancellationEffectiveDate: { type: 'string' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
@@ -84,7 +87,7 @@ class Subscription extends Base {
return (
this.status === 'deleted' &&
Number(this.cancellationEffectiveDate) >
DateTime.now().startOf('day').toMillis()
DateTime.now().startOf('day').toMillis()
);
}

View File

@@ -24,6 +24,9 @@ class UsageData extends Base {
subscriptionId: { type: 'string', format: 'uuid' },
consumedTaskCount: { type: 'integer' },
nextResetAt: { type: 'string' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};

View File

@@ -1,13 +1,18 @@
import crypto from 'node:crypto';
import { QueryContext, ModelOptions } from 'objection';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import { DateTime } from 'luxon';
import { Ability } from '@casl/ability';
import type { Subject } from '@casl/ability';
import appConfig from '../config/app';
import Base from './base';
import ExtendedQueryBuilder from './query-builder';
import Connection from './connection';
import Flow from './flow';
import Step from './step';
import Role from './role';
import Permission from './permission';
import Execution from './execution';
import UsageData from './usage-data.ee';
import Subscription from './subscription.ee';
@@ -16,8 +21,8 @@ class User extends Base {
id!: string;
fullName!: string;
email!: string;
roleId: string;
password!: string;
role: string;
resetPasswordToken: string;
resetPasswordTokenSentAt: string;
trialExpiryDate: string;
@@ -29,6 +34,8 @@ class User extends Base {
currentUsageData?: UsageData;
subscriptions?: Subscription[];
currentSubscription?: Subscription;
role: Role;
permissions: Permission[];
static tableName = 'users';
@@ -41,7 +48,13 @@ class User extends Base {
fullName: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
password: { type: 'string', minLength: 1, maxLength: 255 },
role: { type: 'string', enum: ['admin', 'user'] },
resetPasswordToken: { type: 'string' },
resetPasswordTokenSentAt: { type: 'string' },
trialExpiryDate: { type: 'string' },
roleId: { type: 'string', format: 'uuid' },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
@@ -124,6 +137,26 @@ class User extends Base {
builder.orderBy('created_at', 'desc').limit(1).first();
},
},
role: {
relation: Base.HasOneRelation,
modelClass: Role,
join: {
from: 'roles.id',
to: 'users.role_id',
},
},
permissions: {
relation: Base.ManyToManyRelation,
modelClass: Permission,
join: {
from: 'users.role_id',
through: {
from: 'roles_permissions.role_id',
to: 'roles_permissions.permission_id',
},
to: 'permissions.id',
},
},
});
login(password: string) {
@@ -248,6 +281,30 @@ class User extends Base {
});
}
}
get ability() {
if (!this.permissions) {
throw new Error('User.permissions must be fetched!');
}
return new Ability(this.permissions);
}
can(action: string, subject: Subject) {
const can = this.ability.can(action, subject);
if (!can) throw new Error('Not authorized!');
return can;
}
cannot(action: string, subject: Subject) {
const cannot = this.ability.cannot(action, subject);
if (cannot) throw new Error('Not authorized!');
return cannot;
}
}
export default User;