-
Notifications
You must be signed in to change notification settings - Fork 713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Request: Inject middlewares to controllers with decorators #487
Comments
Hi, thanks for this feature request. I believe this has been discussed in the past but I can't remember it 😢 . Let see if other members of the team can remember it... Can you guys please review @codyjs @lholznagel thanks 👍 |
I remember that we wanted to do something like this in citadeljs. I think this would be a good idea. 👍 |
@maxmalov would you like to contribute this feature? I also think that it is a good idea 👍 I'm only wondering one thing... At the moment you can pass middleware as: @Controller(path, [middleware, ...])
// or
@Method(method, path, [middleware, ...])
// or
@SHORTCUT(path, [middleware, ...]) More info here. I was wondering if we need: AAn extra @Controller("/")
@Middleware(Constants.AuthorizeMiddleware)
class MyController {
// ... BOr if we should allow @Controller("/", Constants.AuthorizeMiddleware)
class MyController {
// ... A service identifier can be a class constructor I think option A will be easier to implement. Should we call the decorator What do you guys think? |
@remojansen I'd really like to, but I'm fairly new with inversify, so I will definitely need you help and review at the start. I'm common with current approach of injecting middleware, but have a few concerns about it. Since right not it is possible to inject express request handlers only you cannot control how the middleware is being created. I've done some digging already and come up to the following motivation:
// middleware factory definition
export interface AuthMiddlewareFactory {
(authService: AuthService): express.RequestHandler;
}
function makeAuthMiddleware(authService): express.RequestHandler {
return (req, res, next) => {
// ...
};
} // configuration
container.bind<AuthService>("AuthService").toSelf();
container.bind<AuthMiddlewareFactory>("AuthMiddlewareFactory").toConstantValue(makeAuthMiddleware);
container.bind<interfaces.Factory<express.RequestHandler>>("Factory<AuthMiddleware>").toFactory<express.RequestHandler>((context: interfaces.Context) => {
return () => {
let service = context.container.get<AuthService>("AuthService");
let makeAuthMiddleware = context.container.get<AuthMiddlewareFactory>("AuthMiddlewareFactory");
return makeAuthMiddleware(service);
};
}); Well, this is a lot of code for a single middleware and a lot of manual dependency resolutions which complicates this solution. So this is the point I stuck at, but I guess there should be another way... |
I'm going to separate your comments in two features. A) Being able to stub middleware during testingIf your controller looks as follows: let TYPE = {
AuthorizeMiddleware: Symbol("AuthorizeMiddleware"),
AllowAnonymousMiddleware: Symbol("AllowAnonymousMiddleware")
};
@injectable()
@Controller("/")
@Middleware(TYPE.AuthorizeMiddleware)
class MyController {
@Post("/")
public create() {
// ...
}
@Get("/")
@Middleware(TYPE.AllowAnonymousMiddleware)
public get() {
// ...
}
} And your middleware doesn't have dependencies: let AuthorizeMiddleware = (req, res) => {
// ...
};
let AllowAnonymousMiddleware = (req, res) => {
// ...
}; Declaring the bindings should be fine: let container = new Container();
container.bind<AuthorizeMiddleware>(TYPE.AuthorizeMiddleware)
.toConstantValue(AuthorizeMiddleware);
container.bind<AllowAnonymousMiddleware>(TYPE.AllowAnonymousMiddleware)
.toConstantValue(AllowAnonymousMiddleware);
container.bind<Controller>(TYPE.Controller)
.to(MyController)
.whenTargetNamed("MyController"); At the moment it is not possible to stub the middleware because we hard code a reference to the middleware: @Controller("/", AuthorizeMiddleware) But the @Controller("/")
@Middleware(TYPE.AuthorizeMiddleware) This will allow you to stub your middleware when testing. All you would need to do is to use different bindings for container.bind<AuthorizeMiddleware>(TYPE.AuthorizeMiddleware)
.toConstantValue(function mock(req, res) {
// ...
});
container.bind<AllowAnonymousMiddleware>(TYPE.AllowAnonymousMiddleware)
.toConstantValue(function mock(req, res) {
// ...
}); B) Being able to inject entities to middlewareLet's imagine that you have the same controller: let TYPE = {
AuthorizeMiddleware: Symbol("AuthorizeMiddleware"),
AllowAnonymousMiddleware: Symbol("AllowAnonymousMiddleware")
};
@injectable()
@Controller("/")
@Middleware(TYPE.AuthorizeMiddleware)
class MyController {
@Post("/")
public create() {
// ...
}
@Get("/")
@Middleware(TYPE.AllowAnonymousMiddleware)
public get() {
// ...
}
} But this time the middleware requires a service to be injected: let authorizeMiddlewareFactory = (authService) =>{
return function authorizeMiddleware(req, res) => {
// Use authService here...
}
};
let AllowAnonymousMiddleware = (req, res) => {
// ...
}; The solution you suggested would work but requires a lot of code. I would try with let container = new Container();
container.bind<AutService>(TYPE.AutService).to(AutService);
container.bind<AuthorizeMiddleware>(TYPE.AuthorizeMiddleware)
.toDynamicValue((context: interfaces.Context) => {
let authService = context.container.get(TYPE.AutService);
return authorizeMiddlewareFactory(authService);
});
container.bind<AllowAnonymousMiddleware>(TYPE.AllowAnonymousMiddleware)
.toConstantValue(AllowAnonymousMiddleware);
container.bind<Controller>(TYPE.Controller)
.to(MyController)
.whenTargetNamed("MyController"); If your middleware has more than one dependency: container.bind<AuthorizeMiddleware>(TYPE.AuthorizeMiddleware)
.toDynamicValue((context: interfaces.Context) => {
let authService = context.container.get(TYPE.AutService);
let logginService = context.container.get(TYPE.LogginService);
return authorizeMiddlewareFactory(authService, logginService);
}); It will start becoming ugly. In general, if you see You could create a small helper to solve this problem: function bindMiddleware(container, middlewareId, middlewareFactory, dependencies) {
container.bind<AuthorizeMiddleware>(TYPE.AuthorizeMiddleware)
.toDynamicValue((context: interfaces.Context) => {
let injections = dependencies.map((dependency) => {
context.container.get(dependency);
});
return middlewareFactory(...injections);
});
} You could then use this helper as follows: let authorizeMiddlewareFactory = (authService, logginService) =>{
return function authorizeMiddleware(req, res) => {
// Use authService & logginService here...
}
};
bindMiddleware(
container,
TYPE.AuthorizeMiddleware,
authorizeMiddlewareFactory,
[TYPE.AutService, TYPE.LogginService]
); I think this is something we could document in the recipes but not something that should be part of the framework. The main problem here is that we are dealing with functions not with classes and we can't use decorators on functions. I saw online that there are conversations about adding decorator support for functions but we need to wait until there is more clarity on that... If you want this helper (or something similar) to become a npm module you can always release it yourself. Summary
The |
Wow, @remojansen, thanks for detailed explanation 👍. I'll play around with it and see what I can do. |
@remojansen I've tried different approaches you've described previously (about the way of injecting middlewares). So I put all my current findings and thoughts below: A) Inject middleware via
|
Hi @maxmalov thanks a lot for your work 👍 it looks very good. I like more option one. I only spotted one important required change. The Middleware interface needs to use the import { interfaces } express from "inversify";
export type Middleware = (interfaces.ServiceIdentifier<any> | express.RequestHandler); |
@maxmalov, @remojansen Hey, great work. Is it possible to add this same feature into inversify-restify-utils package? Thanks. |
Hi @mitjarogl at the moment I don't have time because I'm writing a book after working hours. If you want to send a PR I will be happy to merge it. In Express it was implemented by inversify/inversify-express-utils#35 you can use it as reference. |
@remojansen , @maxmalov: sorry in advance if I am not supposed to comment on a closed issue. I am looking to accomplish what you outlined at the very beginning; namely creating the equivalent of the [Authorize] filter/middleware that allows it to receive parameters for the role or roles required to execute the action/operation? After reading this issue and the latest inversify express utils page, I decided to create an implementation of BaseMiddleware that I am passing through to the @httpget|Delete|Apply decorators. It's working but is clunky because I haven't been able to discover how to pass parameters from the decorator to the middleware to allow me to designate which role is required per operation/action. Here's what I was looking to accomplish by using the same middleware shown below as AuthorizationFilter. @httpget('/', AuthorizationFilter('ReviewerRole')) // for methods that don't require Admin role to execute What have I done? I created this middleware but it's hard-coded to with the use of 'AdministratorRole'. This is a problem because what I'd like to do is to reuse decorator and middleware as shown below in the ReportController. class AuthorizationFilter extends BaseMiddleware { @controller('') // Here I would like to pass in 'ReviewerRole' since this is just a read only operation but don't know how to pass this to the AuthorizationFilter (BaseMiddleware from above) // Here I would like to pass in AdministratorRole' since this is a delete but don't know how to pass this to the AuthorizationFilter (BaseMiddleware from above) My intuition is that this is already possible. I'd appreciate your guidance on the correct way to accomplish this. |
@CVANCGCG as far as I can remember this is not currently possible. I would recommend trying a different approach based on auth provider and principals https://github.com/inversify/inversify-express-utils#authprovider Or you can split authorization filter into 2 separate middlewares: the first one will inject the given role inside // this is pretty simple middleware which doesn't need any dependency resolution
const RequredRole = role => (req, res, next) => {
req.requiredRole = role; // or use Symbols instead of public property access
next();
}
// controller class
@httpget('/reports/:id', RequredRole('ReviewerRole'), AuthorizeByRoleFilter)
public async handleGetPortfolioOverviewByManager(
): Promise { ... } Also, request scope services might look very promising https://github.com/inversify/inversify-express-utils#request-scope-services |
@maxmalov, thank you for the detailed recommendation and just as much, I appreciate the quick response. Yes, in fact, I am using the AuthProvider based solution to gain the Principal instance that wraps the user identity object exposed via the details getter. What I've just discovered was how to the middleware as a ServiceIdentifier (string) to associate inject 1:N named instances of the AuthorizationFilter. The 3-part solution looks like this: Part 2: Service Identifier Definition and Container registration
Before:
After:
Part 3: AuthorizationFilter BaseMiddleware state and constructor modification
And for actions that require more elevated permissions to authorize:
|
Yeah, your solution works too, it also can be modified to look like in your initial question. Factories can produce everything, even service identifiers 🙃 const AuthorizationFilter = role => `AuthorizationFilter_${role}`;
// configuration phase
for (const role of ROLES) {
DIContainer.bind<AuthorizationFilter>(AuthorizationFilter(role))
.toDynamicValue(() => {
return new AuthorizationFilter(role);
})
.inRequestScope();
}
// usage
@httpDelete('/reports/:id', AuthorizationFilter('AdministratorRole'))
public async handleGetPortfolioOverviewByManager(
): Promise { but keep in mind this solution works only when the set of roles is predefined and won't change in runtime |
@maxmalov, that's great insight. Your solution is more extensible and in IMO, is more intuitive to understand -- especially for those who are not intimately familiar with Service Identifiers as another way to resolve dependencies in addition to an explicit Middleware type reference. I'll give this a try. Thanks for the additional thought provoking enhancement. Aside: I am still not proud of the fact my solution introduces state into the AuthorizationFilter. This feels like a workaround but maybe it's OK since we're working with classes rather than just a series of chained functions. |
Hi,
I was looking at the example how to inject my middleware into my controllers and a few concers came to my mind.
First one is that every controller in this approach become container aware and it is very easy to start using it as a service locator everywhere within controller class. This can lead to an antipattern being spread across the majority of controllers, so the developer should constantly keep in mind while developing controllers.
Second one is that middleware injection usually is very common operation. From my experience I can say that authorization middleware presents in the majority of routes. So this construction from the example becomes a boilerplate.
I really like how such things implemented in .Net MVC, ex Authorize, AllowAnonymous attributes:
And I'm wondering is this possible with inversify too, in a way like:
Pros I see in this approach:
@Method
decorator can be removed, which plays well with single responsibility principleMiddleware
one.Any ideas, concerns?
Thanks
The text was updated successfully, but these errors were encountered: