Skip to content
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

How to make roles in loopback 4 #4291

Closed
vu-cv opened this issue Oct 31, 2019 · 28 comments
Closed

How to make roles in loopback 4 #4291

vu-cv opened this issue Oct 31, 2019 · 28 comments
Assignees

Comments

@vu-cv
Copy link

vu-cv commented Oct 31, 2019

Your docs not detail. help me please!

@agnes512
Copy link
Contributor

@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.

@dhmlau
Copy link
Member

dhmlau commented Nov 22, 2019

@vucv138, we're working on the documentation, see #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 dhmlau closed this as completed Nov 22, 2019
@Riplar
Copy link

Riplar commented Dec 11, 2019

@dhmlau @deepakrkris @jannyHou
I don't think this and #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.

@dhmlau
Copy link
Member

dhmlau commented Dec 11, 2019

@deepakrkris @jannyHou, could you please help? Thanks.

@dhmlau dhmlau reopened this Dec 11, 2019
@dhmlau dhmlau transferred this issue from strongloop/loopback Dec 11, 2019
@mhmnemati
Copy link

mhmnemati commented Dec 24, 2019

You can take a look at this extension, it can help you to use HRBAC authorization model

@Riplar
Copy link

Riplar commented Jan 3, 2020

@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?

@deepakrkris
Copy link
Contributor

deepakrkris commented Jan 3, 2020

@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.

@deepakrkris
Copy link
Contributor

@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)

@deepakrkris
Copy link
Contributor

@RipkensLar other than the doc fix I have mentioned in comment #4291 (comment) 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.

@deepakrkris
Copy link
Contributor

@RipkensLar please review and add your suggestions : #4361

@deepakrkris
Copy link
Contributor

@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.

@Riplar
Copy link

Riplar commented Jan 6, 2020

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.

@deepakrkris
Copy link
Contributor

deepakrkris commented Jan 6, 2020

@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.

@hsluoyz
Copy link

hsluoyz commented Jan 7, 2020

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

@Riplar
Copy link

Riplar commented Jan 7, 2020

@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.

@deepakrkris
Copy link
Contributor

@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.

@Riplar
Copy link

Riplar commented Jan 8, 2020

@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.

@Riplar
Copy link

Riplar commented Jan 8, 2020

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
Copy link
Contributor

@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.

@RipkensLar these are casbin specific questions and not LoopBack related.

@deepakrkris
Copy link
Contributor

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?

@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.

@Riplar
Copy link

Riplar commented Jan 8, 2020

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?

@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.

@deepakrkris
Copy link
Contributor

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?

@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.

@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.

@jannyHou
Copy link
Contributor

jannyHou commented Jan 8, 2020

@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:

https://github.com/strongloop/loopback-next/blob/2bfd18fffc65f79a920ac99e9d0365161cfd4f1b/packages/authorization/src/authorize-interceptor.ts#L82

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:

https://github.com/strongloop/loopback-next/blob/e49b81996161a7f26ca3c92f0c61c1e47ff45a62/packages/authorization/src/authorize-interceptor.ts#L83

is some feature we haven't developed, we now only has an abstraction for Role:

https://github.com/strongloop/loopback-next/blob/2bfd18fffc65f79a920ac99e9d0365161cfd4f1b/packages/security/src/types.ts#L50

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.

dougal83 pushed a commit to dougal83/loopback-next that referenced this issue Jan 8, 2020
@jannyHou
Copy link
Contributor

jannyHou commented Jan 8, 2020

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:

  • 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

https://github.com/strongloop/loopback-next/blob/2bfd18fffc65f79a920ac99e9d0365161cfd4f1b/packages/authorization/src/authorize-interceptor.ts#L98

  • then the authorizer knows "the role of the request sender", and "the roles allowed for visiting the endpoint", and makes decision accordingly.

While the casbin module works in a more complicated way, here is the difference:

  • authorize interceptor converts user profile to principal(roles NOT included, only username included), assign it as authorizationCtx.principal[0]
  • authorize interceptor retrieves scope, resource as metadata provided in the decorator @authorize({resource: 'order', scope: 'create'})
  • authorize interceptor invokes authorizer function
  • then authorizer
    • knows username from request and scope, resource from endpoint metadata
    • makes decision by invoking the casbin enforcer, which calculates based on the defined model and policy

@Riplar
Copy link

Riplar commented Jan 9, 2020

Thank 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.

@jannyHou
Copy link
Contributor

jannyHou commented Jan 10, 2020

FYI - I submitted a PR to improve the basic use example in @loopback/authorization:
#4405, suggestions are welcomed :)

@dhmlau
Copy link
Member

dhmlau commented Feb 9, 2020

Closing as done since PR#4405 has merged.

@dhmlau dhmlau closed this as completed Feb 9, 2020
@ianjenkinsii
Copy link

ianjenkinsii commented Nov 18, 2020

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:

  • 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

https://github.com/strongloop/loopback-next/blob/2bfd18fffc65f79a920ac99e9d0365161cfd4f1b/packages/authorization/src/authorize-interceptor.ts#L98

  • 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants