|
| 1 | +## Abstract |
| 2 | + |
| 3 | +Permissions control is a general task when it comes to development of more or less complex web application. Your application may have various roles and resources you need to protect. |
| 4 | +ACL (access control list) provides a flexible way of configuring "who can do what against what resource". |
| 5 | + |
| 6 | +In this article we configure a common setup when the app has three roles (`guest`, `user` and `moderator`), the roles have different permissions (`view`, `create`, `remove`) |
| 7 | +and the application contains two type of resources that needs to be protected (`news`, `comments`). |
| 8 | + |
| 9 | +## ACL Configuration |
| 10 | + |
| 11 | +Nebular ACL has a simple way of setting it up. When registering a module you can specify a set of ACL rules by simply providing it as a module configuration. |
| 12 | + |
| 13 | +Let's assume that our guest users can only `view` `news` and `comments`, users can do everything as guests, but also can `create` `comments`, and moderators can also `create` and `remove` `news` and `comments`. |
| 14 | +Now, let's convert this into an ACL configuration object which Nebular can understand. Open your `app.module.ts` and change the `NbSecurityModule.forRoot()` call as follows: |
| 15 | + |
| 16 | +```typescript |
| 17 | + |
| 18 | +@NgModule({ |
| 19 | + imports: [ |
| 20 | + // ... |
| 21 | + |
| 22 | + NbSecurityModule.forRoot({ |
| 23 | + accessControl: { |
| 24 | + guest: { |
| 25 | + view: ['news', 'comments'], |
| 26 | + }, |
| 27 | + user: { |
| 28 | + parent: 'guest', |
| 29 | + create: 'comments', |
| 30 | + }, |
| 31 | + moderator: { |
| 32 | + parent: 'user', |
| 33 | + create: 'news', |
| 34 | + remove: '*', |
| 35 | + }, |
| 36 | + }, |
| 37 | + }), |
| 38 | + |
| 39 | + ], |
| 40 | + |
| 41 | +``` |
| 42 | +
|
| 43 | +As you can see the configuration is pretty much straightforward, each role can have a list of permissions (view, create, remove) and resources that are allowed for those permissions. We can also specify a `*` resource, |
| 44 | +which means that we have a permission againts any resource (like moderators can remove both news and comments). |
| 45 | +<hr class="section-end"> |
| 46 | +
|
| 47 | +## Role Configuration |
| 48 | +
|
| 49 | +So far we told Nebular Security what roles-permissions-resources our application has. Now we need to specify how Nebular can determine a role of currently authenticated user. |
| 50 | +To do so we need to create a `RoleProvider` with one simple method `getRole`, which returns an `Observable<string>` of a role. |
| 51 | +In a simplest form we can provide this service directly in the main module: |
| 52 | +
|
| 53 | +
|
| 54 | +```typescript |
| 55 | +// ... |
| 56 | + |
| 57 | +import { of as observableOf } from 'rxjs/observable/of'; |
| 58 | +import { NbSecurityModule, NbRoleProvider } from '@nebular/security'; |
| 59 | + |
| 60 | + |
| 61 | +@NgModule({ |
| 62 | + imports: [ |
| 63 | + // ... |
| 64 | + |
| 65 | + NbSecurityModule.forRoot({ |
| 66 | + // ... |
| 67 | + }), |
| 68 | + |
| 69 | + ], |
| 70 | + providers: [ |
| 71 | + // ... |
| 72 | + { |
| 73 | + provide: NbRoleProvider, |
| 74 | + useValue: { |
| 75 | + getRole: () => { |
| 76 | + return observableOf('guest'); |
| 77 | + }, |
| 78 | + }, |
| 79 | + }, |
| 80 | + ], |
| 81 | +``` |
| 82 | +That's easy we have just provided a role, so that Nebular can determine which user is currently accessing the app. |
| 83 | +The good thing about this configuration is that it's not tightly coupled with the rest of your authentication flow, which gives you a lot of flexibility over it. |
| 84 | +
|
| 85 | +But, in our example the role is "hardcoded", which in the real world app would be dynamic and depend on the current user. |
| 86 | +
|
| 87 | +Assuming that you already have `Nebular Auth` module fully configured and functioning based on `JWT` we will adjust the example to retrieve a role from the user token. |
| 88 | +
|
| 89 | +Let's create a separate `role.provider.ts` service in order not to put a lot of logic into the module itself: |
| 90 | +
|
| 91 | +```typescript |
| 92 | +import { Injectable } from '@angular/core'; |
| 93 | +import { Observable } from 'rxjs/Observable'; |
| 94 | +import { map } from 'rxjs/operators/map'; |
| 95 | + |
| 96 | +import { NbAuthService, NbAuthJWTToken } from '@nebular/auth'; |
| 97 | +import { NbRoleProvider } from '@nebular/security'; |
| 98 | + |
| 99 | + |
| 100 | +@Injectable() |
| 101 | +export class RoleProvider implements NbRoleProvider { |
| 102 | + |
| 103 | + constructor(private authService: NbAuthService) { |
| 104 | + } |
| 105 | + |
| 106 | + getRole(): Observable<string> { |
| 107 | + // ... |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +``` |
| 112 | +
|
| 113 | +Now, let's complete the `getRole` method to extract the role from the token: |
| 114 | +
|
| 115 | +```typescript |
| 116 | +import { Injectable } from '@angular/core'; |
| 117 | +import { Observable } from 'rxjs/Observable'; |
| 118 | +import { map } from 'rxjs/operators/map'; |
| 119 | + |
| 120 | +import { NbAuthService, NbAuthJWTToken } from '@nebular/auth'; |
| 121 | +import { NbRoleProvider } from '@nebular/security'; |
| 122 | + |
| 123 | +@Injectable() |
| 124 | +export class RoleProvider implements NbRoleProvider { |
| 125 | + |
| 126 | + constructor(private authService: NbAuthService) { |
| 127 | + } |
| 128 | + |
| 129 | + getRole(): Observable<string> { |
| 130 | + return this.authService.onTokenChange() |
| 131 | + .pipe( |
| 132 | + map((token: NbAuthJWTToken) => { |
| 133 | + return token ? token.getPayload()['role'] : 'guest'; |
| 134 | + }), |
| 135 | + ); |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | +
|
| 140 | +So we subscribe to the `tokenChange` observable, which will produce a new token each time authentication change occurres. |
| 141 | +Then we simply get a role from a token (for example simplicity, we assume that token payload always has a role value) or return default `guest` value. |
| 142 | +
|
| 143 | +Don't worry if your setup does not use Nebular Auth. You can adjust this code to retrieve a user role from any service of your own. |
| 144 | +
|
| 145 | +
|
| 146 | +And let's provide the service in the app module: |
| 147 | +
|
| 148 | +```typescript |
| 149 | +// ... |
| 150 | + |
| 151 | +import { RoleProvider } from './role.provider'; |
| 152 | +import { NbSecurityModule, NbRoleProvider } from '@nebular/security'; |
| 153 | + |
| 154 | + |
| 155 | +@NgModule({ |
| 156 | + imports: [ |
| 157 | + // ... |
| 158 | + |
| 159 | + NbSecurityModule.forRoot({ |
| 160 | + // ... |
| 161 | + }), |
| 162 | + |
| 163 | + ], |
| 164 | + providers: [ |
| 165 | + // ... |
| 166 | + { provide: NbRoleProvider, useClass: RoleProvider }, // provide the class |
| 167 | + ], |
| 168 | +``` |
| 169 | +
|
| 170 | +
|
| 171 | +## Usage |
| 172 | +
|
| 173 | +Finally, we can move on to the part where we start putting security rules in our app. Let's assume that we have that `Post Comment` button, that should only be shown to authenticated users (with a role `user`). |
| 174 | +So we need to hide the button for guests. |
| 175 | +
|
| 176 | +Nebular Security provides us with a simple `*nbIsGranted` conditional directive, which under the hood works as `*ngIf`, showing or hiding a template block based on a user role: |
| 177 | +
|
| 178 | +```typescript |
| 179 | +@Component({ |
| 180 | + // ... |
| 181 | + template: ` |
| 182 | + <button *nbIsGranted="['create', 'comments']" >Post Comment</button> |
| 183 | + `, |
| 184 | +}) |
| 185 | +export class CommentFormComponent { |
| 186 | +// ... |
| 187 | +``` |
| 188 | +We just need to pass a `permission` and some `resource` in order to control the button visibility. |
| 189 | +
|
| 190 | +For more advanced use cases, we can directly use the `NbAccessChecker` service. It provides you with `isGranted` method , which returns an `Observable<boolean>` of the ACL check result. |
| 191 | +We can adjust our example to utilize it. In your `comment-form.component.ts`, import the `NbAccessChecker` service. |
| 192 | +
|
| 193 | +```typescript |
| 194 | +import { Component } from '@angular/core'; |
| 195 | +import { NbAccessChecker } from '@nebular/security'; |
| 196 | + |
| 197 | +@Component({ |
| 198 | + // ... |
| 199 | +}) |
| 200 | +export class CommentFormComponent { |
| 201 | + |
| 202 | + constructor(public accessChecker: NbAccessChecker) { } |
| 203 | +} |
| 204 | +``` |
| 205 | +
|
| 206 | +And let's add an `if` statement to the `Post Comment` button, so that it is only shown when permitted: |
| 207 | +
|
| 208 | +```typescript |
| 209 | +@Component({ |
| 210 | + // ... |
| 211 | + template: ` |
| 212 | + <button *ngIf="accessChecker.isGranted('create', 'comments') | async" >Post Comment</button> |
| 213 | + `, |
| 214 | +}) |
| 215 | +export class CommentFormComponent { |
| 216 | +// ... |
| 217 | +``` |
| 218 | +We call `isGranted` method, which listens to the currently provided role and checks it permissions against specified in the ACL configuration. |
| 219 | +Moreover, as it listens to the *role change*, it hides the button if authentication gets changed during the app usage. |
| 220 | +
|
| 221 | +Same way we can call the `isGranted` method from any part of the app, including router guards and services, which gives us a transparent and flexibly configurable way to manage user access to various resources. |
0 commit comments