Reputation: 8459
I'm using experimental typescript decorators to manage access control in express.
class AccountController extends Controller {
login(req: Request, res: Response) {
const { email, password } = req.body;
const token = await this.model.login(email, password);
return res.json({
token
});
}
@hasRole('ADMIN')
list(req: Request, res: Response) {
res.json({
data: await this.model.findAll()
});
}
}
hasRole
method decorator is working fine and I'm happy with it.
The Controller
implements REST methods:
class Controller {
list(req: Request, res: Response) { // impl }
get(req: Request, res: Response) { // impl }
create(req: Request, res: Response) { // impl }
update(req: Request, res: Response) { // impl }
delete(req: Request, res: Response) { // impl }
}
The problem is, I have to apply the same decorators to most of other controllers and I find it very repetitive. For example, StockController
should allow access to only MERCHANT
role, and I have to do something like the following:
class StockController extends Controller {
@hasRole('MERCHANT')
list(req: Request, res: Response) {
return super.list(req, res);
}
@hasRole('MERCHANT')
get(req: Request, res: Response) {
return super.get(req, res);
}
@hasRole('MERCHANT')
create(req: Request, res: Response) {
return super.create(req, res);
}
@hasRole('MERCHANT')
update(req: Request, res: Response) {
return super.update(req, res);
}
@hasRole('MERCHANT')
delete(req: Request, res: Response) {
return super.delete(req, res);
}
}
This approach is not only dull and repetitive but also unsafe, because if I add a method to Controller
and accidentally forget to add the method to child controllers, they will allow unwanted access.
I'd like to deal this problem with class decorator and use something like the following:
@requireRole('MERCHANT')
class StockController extends Controller {}
However, from what I see in the doc:
The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition.
As far as I understand it, I cannot implement "method hook" in class decorators. Any suggestions?
For your information, hasRole
decorator looks like the following:
export function hasRole(role: string) {
return function(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(req: Request, res: Response) {
const user = res.locals.user;
if (user && user.hasRole(role)) {
originalMethod.apply(this, [req, res]);
} else {
res.status(403).json({});
}
}
}
}
Upvotes: 5
Views: 4175
Reputation: 1045
This is possible by overriding class methods
function AttachToAllClassDecorator<T>(someParam: string) {
return function(target: new (...params: any[]) => T) {
for (const key of Object.getOwnPropertyNames(target.prototype)) {
// maybe blacklist methods here
let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
if (descriptor) {
descriptor = someDecorator(someParam)(key, descriptor);
Object.defineProperty(target.prototype, key, descriptor);
}
}
}
}
Basically going through all methods (maybe add some logic around it for whitelisting/blacklisting some methods) and overriding with new method that has method decorator wrapped.
Here is basic example for method decorator.
function someDecorator(someParam: string): (methodName: string, descriptor: PropertyDescriptor) => PropertyDescriptor {
return (methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
let method = descriptor.value;
descriptor.value = function(...args: any[]) {
console.warn(`Here for descriptor ${methodName} with param ${someParam}`);
return method.apply(this, args);
}
return descriptor;
}
}
Upvotes: 7