diff --git a/lib/access-control.ts b/lib/access-control.ts new file mode 100644 index 0000000..3deea09 --- /dev/null +++ b/lib/access-control.ts @@ -0,0 +1,35 @@ +import * as mongoose from 'mongoose'; +import roleSchema, { IRole } from './models/role'; +import permissionsByRolePipeline from './pipelines/permissionsByRole'; +import permissionSchema, { IPermission } from './models/permission'; + +class AccessControl { + private RoleModel: mongoose.Model; + private PermissionModel: mongoose.Model; + + constructor(conn: mongoose.Connection) { + this.RoleModel = conn.model('roles', roleSchema); + this.PermissionModel = conn.model( + 'permissions', + permissionSchema, + ); + } + + public async getPermissionsByRole(role: string): Promise { + try { + const results = await this.RoleModel.aggregate( + permissionsByRolePipeline(role), + ); + + if (results && results[0] && results[0].permissions) { + return results[0].permissions; + } else { + return []; + } + } catch (err) { + throw new Error(err.stack); + } + } +} + +export default AccessControl; diff --git a/lib/auth.ts b/lib/auth.ts index 0c759ff..2343138 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -2,12 +2,14 @@ import * as mongoose from 'mongoose'; import * as bcrypt from 'bcrypt'; import * as jwt from 'jsonwebtoken'; import * as speakeasy from 'speakeasy'; +import AccessControl from './access-control'; import userSchema, { IUser } from './models/user'; export interface AuthOptions { mongodbUri: string; secret: string; requireTwoFA?: boolean; + useAccessControl?: boolean; tokenExpiry?: number | string; } @@ -15,16 +17,25 @@ class Auth { private connectPromise: Promise; private readonly secret: string; private readonly requireTwoFA: boolean; + private readonly useAccessControl: boolean; + private accessControl: AccessControl; private readonly tokenExpiry: number | string; private UserModel: mongoose.Model; - constructor({ mongodbUri, requireTwoFA, secret, tokenExpiry }: AuthOptions) { - this.connectPromise = this.connectToMongo(mongodbUri); - + constructor({ + mongodbUri, + secret, + requireTwoFA, + tokenExpiry, + useAccessControl, + }: AuthOptions) { this.secret = secret; this.requireTwoFA = requireTwoFA; this.tokenExpiry = tokenExpiry || '30d'; + this.useAccessControl = useAccessControl || false; + + this.connectPromise = this.connectToMongo(mongodbUri); } private connectToMongo(connectionString: string): Promise { @@ -32,18 +43,30 @@ class Auth { const conn = mongoose.createConnection(connectionString); this.UserModel = conn.model('users', userSchema); + if (this.useAccessControl) { + this.accessControl = new AccessControl(conn); + } + conn.once('open', () => resolve()); conn.on('error', err => reject(err)); }); } - private createToken(user: IUser, authorized: boolean) { + private async createToken(user: IUser, authorized: boolean) { const payload = { ...user.toObject(), authorized, }; delete payload.password; + if (this.accessControl) { + const permissions = await this.accessControl.getPermissionsByRole( + user.role, + ); + payload.permissions = + permissions && permissions.length ? permissions : []; + } + const token = jwt.sign(payload, this.secret, { expiresIn: this.tokenExpiry, }); diff --git a/lib/models/permission.ts b/lib/models/permission.ts new file mode 100644 index 0000000..e31b80a --- /dev/null +++ b/lib/models/permission.ts @@ -0,0 +1,13 @@ +import * as mongoose from 'mongoose'; + +export interface IPermission extends mongoose.Document { + name: string; + meta: object; +} + +const permissionSchema = new mongoose.Schema({ + name: { index: true, required: true, type: String, unique: true }, + meta: Object, +}); + +export default permissionSchema; diff --git a/lib/models/role.ts b/lib/models/role.ts new file mode 100644 index 0000000..2afbc35 --- /dev/null +++ b/lib/models/role.ts @@ -0,0 +1,18 @@ +import * as mongoose from 'mongoose'; +import { ObjectId } from 'bson'; + +export interface IRole extends mongoose.Document { + name: string; + permissions: string[]; + inherits: string[]; + meta: object; +} + +const roleSchema = new mongoose.Schema({ + name: { index: true, required: true, type: String, unique: true }, + permissions: [{ type: ObjectId, ref: 'permissions' }], + inherits: [{ type: ObjectId, ref: 'roles' }], + meta: Object, +}); + +export default roleSchema; diff --git a/lib/pipelines/permissionsByRole.ts b/lib/pipelines/permissionsByRole.ts new file mode 100644 index 0000000..fc6ebd4 --- /dev/null +++ b/lib/pipelines/permissionsByRole.ts @@ -0,0 +1,69 @@ +export default function(role: string) { + return [ + { + $match: { + name: role, + }, + }, + { + $graphLookup: { + from: 'roles', + startWith: '$inherits', + connectFromField: 'inherits', + connectToField: 'id', + as: 'inheritedRoles', + }, + }, + { + $unwind: { + path: '$inheritedRoles', + }, + }, + { + $project: { + permissionIds: { + $concatArrays: ['$permissions', '$inheritedRoles.permissions'], + }, + }, + }, + { + $unwind: { + path: '$permissionIds', + }, + }, + { + $group: { + _id: '$permissionIds', + permissionId: { + $first: '$permissionIds', + }, + }, + }, + { + $lookup: { + from: 'permissions', + localField: 'permissionId', + foreignField: 'id', + as: 'permissionObj', + }, + }, + { + $unwind: { + path: '$permissionObj', + }, + }, + { + $project: { + name: '$permissionObj.name', + }, + }, + { + $group: { + _id: '', + permissions: { + $addToSet: '$name', + }, + }, + }, + ]; +} diff --git a/package.json b/package.json index 056b7f1..ba0606b 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "build": "run-s format:fix fix lint tsc", "tsc": "node node_modules/typescript/bin/tsc", - "lint": "node_modules/tslint/bin/tslint -c tslint.json -p tsconfig.json --force", - "fix": "node_modules/tslint/bin/tslint -c tslint.json -p tsconfig.json --fix --force", + "lint": "tslint -c tslint.json -p tsconfig.json --force", + "fix": "tslint -c tslint.json -p tsconfig.json --fix --force", "format:fix": "pretty-quick --staged", "format:all": "prettier --config ./.prettierrc --write \"app/**/*{.ts,.js,.json,.css,.scss}\"" },