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({});
}
}
}
}
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({});
}
}
}
}
Share
Improve this question
edited Aug 11, 2020 at 12:44
glinda93
asked Aug 11, 2020 at 9:26
glinda93glinda93
8,4996 gold badges50 silver badges87 bronze badges
2
- A method decorator wraps the method into a new function that calls the original method and can also do other things before or after that. A class decorator wraps the constructor. It is possible to write a class decorator that does what you need. – axiac Commented Aug 11, 2020 at 9:43
- @axiac Thanks for suggestion. Please guide me by an example, I'm lost.. – glinda93 Commented Aug 11, 2020 at 9:44
1 Answer
Reset to default 7This 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;
}
}
TS Playground