Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions lib/access-control.ts
Original file line number Diff line number Diff line change
@@ -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<IRole>;
private PermissionModel: mongoose.Model<IPermission>;

constructor(conn: mongoose.Connection) {
this.RoleModel = conn.model<IRole>('roles', roleSchema);
this.PermissionModel = conn.model<IPermission>(
'permissions',
permissionSchema,
);
}

public async getPermissionsByRole(role: string): Promise<string[]> {
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;
31 changes: 27 additions & 4 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,71 @@ 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;
}

class Auth {
private connectPromise: Promise<void>;
private readonly secret: string;
private readonly requireTwoFA: boolean;
private readonly useAccessControl: boolean;
private accessControl: AccessControl;
private readonly tokenExpiry: number | string;

private UserModel: mongoose.Model<IUser>;

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<void> {
return new Promise((resolve, reject) => {
const conn = mongoose.createConnection(connectionString);
this.UserModel = conn.model<IUser>('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,
});
Expand Down
13 changes: 13 additions & 0 deletions lib/models/permission.ts
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions lib/models/role.ts
Original file line number Diff line number Diff line change
@@ -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;
69 changes: 69 additions & 0 deletions lib/pipelines/permissionsByRole.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
},
];
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}\""
},
Expand Down