Your docs not detail. help me please!
@vucv138 hi, what do you mean by creating roles? like admin, user... etc.? Are you trying to build an authentication system or?
It would be helpful if you make your question descriptive ( e.g current behavior, expectations). Thanks.
@vucv138, we're working on the documentation, see https://github.com/strongloop/loopback-next/issues/3694. In the meanwhile, could you please check out this blog post series: https://strongloop.com/strongblog/building-an-online-game-with-loopback-4-pt7/?
I'd like to close this issue as duplicate of strongloop/loopback#3694. Please continue the discussion over there if needed. Thanks.
@dhmlau @deepakrkris @jannyHou
I don't think this and https://github.com/strongloop/loopback-next/issues/3694 should be closed. I went through the shopping-example, the documentation and the README from the authorization package and am still unsure how I can implement role based authorization. Your linked blog post series is not using the current authorization package, so this does not help.
The AuthorizationContext interface has a "roles" attribute, and in the authorization package README is a controller method decorated with @authorize({allow: ['ADMIN']}), but the AuthorizationMetadata interface only allows an attribute called "allowedRoles". Additionally to that there is no information on how to define and implement the role "ADMIN" at all.
It is nearly impossible for me to determine what the correct way of implementing role based authorization is. Maybe you can clear some things up for me.
@deepakrkris @jannyHou, could you please help? Thanks.
You can take a look at this extension, it can help you to use HRBAC authorization model
@dhmlau @deepakrkris @jannyHou
May you please help me out? I have to implement the authorization soon to be able to go into production. Is using the extension mentioned by @koliberr136a1 the only way to implement roles?
@RipkensLar sorry about the late reply. Please go through the shopping example , specifically the user order controller , to see how authorization decorator is used ( https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user-order.controller.ts ). The authorization section in the read me of the shopping app, gives details on how authorization has been enforced (https://github.com/strongloop/loopback4-example-shopping/blob/master/README.md#authorization)
That said, there is an error that you have pointed out in the loopback.io documentation example under https://loopback.io/doc/en/lb4/Loopback-component-authorization.html#configuring-api-endpoints ,
@authorize({allow: ['ADMIN']})
export class MyController {
@get('/number-of-views')
numOfViews(): number {
return 100;
}
@authorize.skip()
@get('/hello')
hello(): string {
return 'Hello';
}
}
@authorize({allow: ['ADMIN']}) should be @authorize({allowedRoles: ['ADMIN']})
I will have a doc fix asap.
@RipkensLar to give an easier pointer to start from the example shopping app , please take a look at https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts#L79
this.bind('authorizationProviders.casbin-provider')
.toProvider(CasbinAuthorizationProvider)
.tag(AuthorizationTags.AUTHORIZER);
The above binding tells the loopback authorization package what function will act as an authorizer for every api call. In our example shopping app that function is defined in the provider https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/authorizor.ts
Before doing all this the loopback authorization package itself should be registered (https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts#L75)
@RipkensLar other than the doc fix I have mentioned in comment https://github.com/strongloop/loopback-next/issues/4291#issuecomment-570642815 if you feel the general readability of authorization docs https://loopback.io/doc/en/lb4/Loopback-component-authorization.html is obscure, please suggest improvements and I will have it updated.
@RipkensLar please review and add your suggestions : https://github.com/strongloop/loopback-next/pull/4361
@RipkensLar the extension suggested by @koliberr136a1 is for hierarchical roles and is not a generic implementation. The loopback shopping app has much simpler examples for authorization.
Thanks for your detailed replies @deepakrkris. I understand how I would set the whole thing up, but the part that is missing for me is this right here:
@authorize({allowedRoles: ['ADMIN']})
export class MyController {
@get('/number-of-views')
numOfViews(): number {
return 100;
}
@authorize.skip()
@get('/hello')
hello(): string {
return 'Hello';
}
}
Where do I have to define that a certain user has the role ADMIN? I would think that I would have to create a new property in my user model which contains the role of the certain user.
@RipkensLar , how and where user-role configuration is done is part of the developer's solution and LoopBack does not enforce any details. Please take a look at the acceptance tests in the authorization package, https://github.com/strongloop/loopback-next/tree/master/packages/authorization/src/__tests__/acceptance .
In the casbin authorization test (authorization-casbin.acceptance.ts) user-role configuration is done by the casbin framework and is stored in a rbac_policy.csv file whereas in the basic test (authorization.acceptance.ts) , custom logic is written per user.
Casbin supports to store roles (and permission assignment, we call them policy rules together) into file (rbac_policy.csv) or DB (all supported DB are listed in: https://casbin.org/docs/en/adapters).
You can use either:
https://casbin.org/docs/en/management-api
https://casbin.org/docs/en/rbac-api
@deepakrkris @hsluoyz
That helped quite a bit. So I got the whole authorization set up. Now I am struggeling to configure my rbac model. I don't need accessed resources and access methods. I just want to give user xy one role and check for that in my authorizor.ts:
async authorize(
authorizationCtx: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
const request: AuthorizationRequest = {
subject: authorizationCtx.principals[0].name,
object: metadata.resource ?? authorizationCtx.resource,
action: (metadata.scopes && metadata.scopes[0]) || 'execute',
};
const allow = await this.enforcer.enforce(
request.subject,
request.action,
);
if (allow) return AuthorizationDecision.ALLOW;
else if (allow === false) return AuthorizationDecision.DENY;
return AuthorizationDecision.ABSTAIN;
}
This is my rbac_model.conf:
[request_definition]
r = sub, act
[policy_definition]
p = sub, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.act == p.act
And in my database I have these two rows:
p vn execute
g testUser vn
But when I decorate a method in my controller with another role than vn, for example like this @authorize({allowedRoles: ['idm']}), my authorization still passes, which is not my desired behavior.
@RipkensLar looks like you are missing the object in the enforce() call , which possibly is a necessary param (https://github.com/casbin/node-casbin/blob/9b95d03146ce7dd5d1b58469f3f302df268b4792/README.md#get-started). Casbin is only used as an example in loopback tests. you can go thru their docs and debug to see how the api calls respond.
@deepakrkris ok, I added the object in the enforcer() call and in my casbin rbac_model.conf. But I am struggeling with the policy. In my opinion I would only need something like this:
p vn
g testUser vn
Which says that there is a role called vn and a user testUser who has the role vn. And then I can use the @authorize decorator to define which roles are authorized for each method in my controllers. But that is not working. I thought that would be a super simple authorization use-case because I don't need resources, scopes, etc, but I have no idea what is missing, even after searching through the casbin docs and debugging.
I also tried to access to user's roles through the AuthorizationContext, but the roles are always just an empty array. Same for the scopes. I took a look into the authorization component and the context is set here:
https://github.com/strongloop/loopback-next/blob/e49b81996161a7f26ca3c92f0c61c1e47ff45a62/packages/authorization/src/authorize-interceptor.ts#L83
But I am unable to find another part where it actually gets the roles. Is this the desired behavior?
@deepakrkris ok, I added the
objectin the enforcer() call and in my casbinrbac_model.conf. But I am struggeling with the policy. In my opinion I would only need something like this:p vn g testUser vnWhich says that there is a role called
vnand a usertestUserwho has the rolevn. And then I can use the@authorizedecorator to define which roles are authorized for each method in my controllers. But that is not working. I thought that would be a super simple authorization use-case because I don't need resources, scopes, etc, but I have no idea what is missing, even after searching through the casbin docs and debugging.
@RipkensLar these are casbin specific questions and not LoopBack related.
I also tried to access to user's roles through the AuthorizationContext, but the roles are always just an empty array. Same for the scopes. I took a look into the authorization component and the context is set here:
https://github.com/strongloop/loopback-next/blob/e49b81996161a7f26ca3c92f0c61c1e47ff45a62/packages/authorization/src/authorize-interceptor.ts#L83But I am unable to find another part where it actually gets the roles. Is this the desired behavior?
@RipkensLar if you would like to know what is the role of the user for the incoming request , you can check it in the authorizer function provided by you . for example , in the shopping example
the authorizationCtx in this location (https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/authorizor.ts#L26) gives the role of the incoming request. The configured role of the endpoint is in the AuthorizationMetadata. From here how the LoopBack user checks and enforces policies is entirely their own logic.
I also tried to access to user's roles through the AuthorizationContext, but the roles are always just an empty array. Same for the scopes. I took a look into the authorization component and the context is set here:
https://github.com/strongloop/loopback-next/blob/e49b81996161a7f26ca3c92f0c61c1e47ff45a62/packages/authorization/src/authorize-interceptor.ts#L83But I am unable to find another part where it actually gets the roles. Is this the desired behavior?
@RipkensLar if you would like to know what is the role of the user for the incoming request , you can check it in the authorizer function provided by you . for example , in the shopping example
the authorizationCtx in this location (https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/authorizor.ts#L26) gives the role of the incoming request. The configured role of the endpoint is in the AuthorizationMetadata. From here how the LoopBack user checks and enforces policies is entirely their own logic.
But where does loopback load the roles of the user for the incoming request? In the authorize-interceptor.ts the roles attribute only get an empty array as a value and nothing else.
When I know that I can head over to casbin and set up my model and policy.
I also tried to access to user's roles through the AuthorizationContext, but the roles are always just an empty array. Same for the scopes. I took a look into the authorization component and the context is set here:
https://github.com/strongloop/loopback-next/blob/e49b81996161a7f26ca3c92f0c61c1e47ff45a62/packages/authorization/src/authorize-interceptor.ts#L83But I am unable to find another part where it actually gets the roles. Is this the desired behavior?
@RipkensLar if you would like to know what is the role of the user for the incoming request , you can check it in the authorizer function provided by you . for example , in the shopping example
the authorizationCtx in this location (https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/authorizor.ts#L26) gives the role of the incoming request. The configured role of the endpoint is in the AuthorizationMetadata. From here how the LoopBack user checks and enforces policies is entirely their own logic.But where does loopback load the roles of the user for the incoming request? In the
authorize-interceptor.tsthe roles attribute only get an empty array as a value and nothing else.
When I know that I can head over to casbin and set up my model and policy.
@RipkensLar that is a very valid question. I assumed that would be taken care by the authentication component. But looks like there is an overlap in functionalities between authentication and authorization components. I am checking with the team. I will reply back asap.
@RipkensLar Sorry for the late reply, after reading through all the discussion I share your confusion and all the questions about building an RBAC system using @loopback/authorization. I will write a comprehensive explanation but let me answer the latest question first:
But where does loopback load the roles of the user for the incoming request? In the authorize-interceptor.ts the roles attribute only get an empty array as a value and nothing else.
The role map is defined in the casbin policy file, like what you have as
p vn
g testUser vn
When a request comes in, the user name should be included in the user profile so that authorizationCtx.principals[0] has testUser:
And since your policy says testUser inherits vn(which is the role), your authorizer invokes an enforcer which is able to infer testUser's role as vn. And this is how the authorizer knows the request is from someone with role vn.
The code you pointed out:
is some feature we haven't developed, we now only has an abstraction for Role:
but haven't figured out a concrete plan of applying it in the authorization system. And that's why at this moment, the implementation of concept "Role" relies on your authorizer and 3rd party module like casbin.
About the example @authorize({allowedRoles: ['admin']}):
From my understanding, please do NOT mix it with the design that leverages casbin model&policy.
You can design your app in a way that the roles is an array property of User, the encoded user profile in the token will be sth like
{username: 'alice', roles: ['admin'], email: '[email protected]'}
Then the authorization works in this way:
@loopback/authentication decodes the user profile from token(roles included) then passes it to @loopback/authorizationroles included), assign it as authorizationCtx.principal[0]allowedRoles as metadata provided in the decorator @authorize({allowedRoles: ['admin']}):While the casbin module works in a more complicated way, here is the difference:
roles NOT included, only username included), assign it as authorizationCtx.principal[0]scope, resource as metadata provided in the decorator @authorize({resource: 'order', scope: 'create'})username from request and scope, resource from endpoint metadataThank you @deepakrkris and @jannyHou for your help. Got it working as described:
- A request comes in with the token in header
- @loopback/authentication decodes the user profile from token(roles included) then passes it to @loopback/authorization
- authorize interceptor converts user profile to principal(roles included), assign it as authorizationCtx.principal[0]
- authorize interceptor retrieves allowedRoles as metadata provided in the decorator @authorize({allowedRoles: ['admin']}):
- authorize interceptor invokes authorizer function as
- then the authorizer knows "the role of the request sender", and "the roles allowed for visiting the endpoint", and makes decision accordingly.
FYI - I submitted a PR to improve the basic use example in @loopback/authorization:
https://github.com/strongloop/loopback-next/pull/4405, suggestions are welcomed :)
Closing as done since PR#4405 has merged.
About the example
@authorize({allowedRoles: ['admin']}):
From my understanding, please do NOT mix it with the design that leverages casbin model&policy.You can design your app in a way that the
rolesis an array property ofUser, the encoded user profile in the token will be sth like{username: 'alice', roles: ['admin'], email: '[email protected]'}Then the authorization works in this way:
- A request comes in with the token in header
@loopback/authenticationdecodes the user profile from token(rolesincluded) then passes it to@loopback/authorization- authorize interceptor converts user profile to principal(
rolesincluded), assign it asauthorizationCtx.principal[0]- authorize interceptor retrieves
allowedRolesas metadata provided in the decorator@authorize({allowedRoles: ['admin']}):- authorize interceptor invokes authorizer function as
- then the authorizer knows "the role of the request sender", and "the roles allowed for visiting the endpoint", and makes decision accordingly.
I am unable to get the "role" to be included in the a jwt token and also the principals object does not have it. Below is a snippet of loopback:authorization:interceptor Security context
{
principals: [
{
name: 'john_doe',
id: 'e78f902a-6364-4dc5-9524-1ecd1f7c4262',
type: 'USER',
[Symbol(securityId)]: 'e78f902a-6364-4dc5-9524-1ecd1f7c4262'
}
],
roles: [],
scopes: [],
---- snip ------
Additionally, I am confused as to why the jwt encoding seems to only allow certain fields. For example, if I attempt to encode a userProfile with the kv "username": "john_doe" the loopback4's jwt encoding drops it but if I encode with "name": "john_doe" then it is included.
What parts of my code would you need to see to lend assistance?
Most helpful comment
@dhmlau @deepakrkris @jannyHou
I don't think this and https://github.com/strongloop/loopback-next/issues/3694 should be closed. I went through the shopping-example, the documentation and the README from the authorization package and am still unsure how I can implement role based authorization. Your linked blog post series is not using the current authorization package, so this does not help.
The AuthorizationContext interface has a "roles" attribute, and in the authorization package README is a controller method decorated with
@authorize({allow: ['ADMIN']}), but the AuthorizationMetadata interface only allows an attribute called "allowedRoles". Additionally to that there is no information on how to define and implement the role "ADMIN" at all.It is nearly impossible for me to determine what the correct way of implementing role based authorization is. Maybe you can clear some things up for me.