Skip to content

Commit 027efc3

Browse files
author
vineet-suri
committed
RFIT-188 code review comments fixed
1 parent 1e8a1bb commit 027efc3

File tree

3 files changed

+181
-74
lines changed

3 files changed

+181
-74
lines changed

README.md

Lines changed: 179 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ It provides three ways of integration
2121
2. **Role based permissions** - Permissions are associated to roles and users have a specific role attached. This actually reduces redundancy in DB a lot, as most of the time, users will have many common permissions. If that is not the case for you, then, use method #1 above.
2222
3. **Role based permissions with user level override** - This is the most flexible architecture. In this case, method #2 is implemented as is. On top of it, we also add user-level permissions override, allow/deny permissions over role permissions. So, say there is user who can perform all admin role actions except he cannot remove users from the system. So, DeleteUser permission can be denied at user level and role can be set as Admin for the user.
2323

24-
As a further enhancement to these methods, we are using [casbin library!](https://casbin.org/docs/en/overview) to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
25-
1. **Using default casbin policy document** - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
26-
2. **Defining custom logic to form dynamic policies** - Implement dynamic permissions based on app logic in casbin-enforcer-config provider. Authorisation extension will dynamically create casbin policy using this business logic to give the authorisation decisions.
24+
[Extension enhancement using CASBIN authorisation](#Extension-enhancement-using-CASBIN-authorisation)
25+
2726

2827
Refer to the usage section below for details on integration
2928

@@ -41,21 +40,13 @@ For a quick starter guide, you can refer to our [loopback 4 starter](https://git
4140

4241
In order to use this component into your LoopBack application, please follow below steps.
4342

44-
- Add component to application. Also add providers to implement casbin authorisation.
43+
- Add component to application.
4544

4645
```ts
4746
this.bind(AuthorizationBindings.CONFIG).to({
4847
allowAlwaysPaths: ['/explorer'],
4948
});
5049
this.component(AuthorizationComponent);
51-
52-
this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
53-
CasbinEnforcerConfigProvider,
54-
);
55-
56-
this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
57-
CasbinResValModifierProvider,
58-
);
5950
```
6051

6152
- If using method #1 from above, implement Permissions interface in User model and add permissions array.
@@ -128,7 +119,182 @@ export class User extends Entity implements UserPermissionsOverride<string> {
128119
}
129120
}
130121
```
122+
- For method #3, we also provide a simple provider function [_AuthorizationBindings.USER_PERMISSIONS_](<[./src/providers/user-permissions.provider.ts](https://github.com/sourcefuse/loopback4-authorization/blob/master/src/providers/user-permissions.provider.ts)>) to evaluate the user permissions based on its role permissions and user-level overrides. Just inject it
123+
124+
```ts
125+
@inject(AuthorizationBindings.USER_PERMISSIONS)
126+
private readonly getUserPermissions: UserPermissionsFn<string>,
127+
```
128+
129+
and invoke it
130+
131+
```ts
132+
const permissions = this.getUserPermissions(user.permissions, role.permissions);
133+
```
134+
135+
- Add a step in custom sequence to check for authorization whenever any end
136+
point is hit.
137+
138+
```ts
139+
import {inject} from '@loopback/context';
140+
import {
141+
FindRoute,
142+
HttpErrors,
143+
InvokeMethod,
144+
ParseParams,
145+
Reject,
146+
RequestContext,
147+
RestBindings,
148+
Send,
149+
SequenceHandler,
150+
} from '@loopback/rest';
151+
import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
152+
import {
153+
AuthorizationBindings,
154+
AuthorizeErrorKeys,
155+
AuthorizeFn,
156+
UserPermissionsFn,
157+
} from 'loopback4-authorization';
158+
159+
import {AuthClient} from './models/auth-client.model';
160+
import {User} from './models/user.model';
131161

162+
const SequenceActions = RestBindings.SequenceActions;
163+
164+
export class MySequence implements SequenceHandler {
165+
constructor(
166+
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
167+
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
168+
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
169+
@inject(SequenceActions.SEND) public send: Send,
170+
@inject(SequenceActions.REJECT) public reject: Reject,
171+
@inject(AuthenticationBindings.USER_AUTH_ACTION)
172+
protected authenticateRequest: AuthenticateFn<AuthUser>,
173+
@inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
174+
protected authenticateRequestClient: AuthenticateFn<AuthClient>,
175+
@inject(AuthorizationBindings.AUTHORIZE_ACTION)
176+
protected checkAuthorisation: AuthorizeFn,
177+
@inject(AuthorizationBindings.USER_PERMISSIONS)
178+
private readonly getUserPermissions: UserPermissionsFn<string>,
179+
) {}
180+
181+
async handle(context: RequestContext) {
182+
const requestTime = Date.now();
183+
try {
184+
const {request, response} = context;
185+
const route = this.findRoute(request);
186+
const args = await this.parseParams(request, route);
187+
request.body = args[args.length - 1];
188+
await this.authenticateRequestClient(request);
189+
const authUser: User = await this.authenticateRequest(request);
190+
191+
// Do ths if you are using method #3
192+
const permissions = this.getUserPermissions(
193+
authUser.permissions,
194+
authUser.role.permissions,
195+
);
196+
// This is the important line added for authorization. Needed for all 3 methods
197+
const isAccessAllowed: boolean = await this.checkAuthorisation(
198+
permissions, // do authUser.permissions if using method #1
199+
request,
200+
);
201+
// Checking access to route here
202+
if (!isAccessAllowed) {
203+
throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
204+
}
205+
206+
const result = await this.invoke(route, args);
207+
this.send(response, result);
208+
} catch (err) {
209+
this.reject(context, err);
210+
}
211+
}
212+
}
213+
```
214+
215+
The above sequence also contains user authentication using [loopback4-authentication](https://github.com/sourcefuse/loopback4-authentication) package. You can refer to the documentation for the same for more details.
216+
217+
- Now we can add access permission keys to the controller methods using authorize
218+
decorator as below.
219+
220+
```ts
221+
@authorize(['CreateRole'])
222+
@post(rolesPath, {
223+
responses: {
224+
[STATUS_CODE.OK]: {
225+
description: 'Role model instance',
226+
content: {
227+
[CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
228+
},
229+
},
230+
},
231+
})
232+
async create(@requestBody() role: Role): Promise<Role> {
233+
return await this.roleRepository.create(role);
234+
}
235+
```
236+
237+
This endpoint will only be accessible if logged in user has permission
238+
'CreateRole'.
239+
240+
A good practice is to keep all permission strings in a separate enum file like this.
241+
242+
```ts
243+
export const enum PermissionKey {
244+
ViewOwnUser = 'ViewOwnUser',
245+
ViewAnyUser = 'ViewAnyUser',
246+
ViewTenantUser = 'ViewTenantUser',
247+
CreateAnyUser = 'CreateAnyUser',
248+
CreateTenantUser = 'CreateTenantUser',
249+
UpdateOwnUser = 'UpdateOwnUser',
250+
UpdateTenantUser = 'UpdateTenantUser',
251+
UpdateAnyUser = 'UpdateAnyUser',
252+
DeleteTenantUser = 'DeleteTenantUser',
253+
DeleteAnyUser = 'DeleteAnyUser',
254+
255+
ViewTenant = 'ViewTenant',
256+
CreateTenant = 'CreateTenant',
257+
UpdateTenant = 'UpdateTenant',
258+
DeleteTenant = 'DeleteTenant',
259+
260+
ViewRole = 'ViewRole',
261+
CreateRole = 'CreateRole',
262+
UpdateRole = 'UpdateRole',
263+
DeleteRole = 'DeleteRole',
264+
265+
ViewAudit = 'ViewAudit',
266+
CreateAudit = 'CreateAudit',
267+
UpdateAudit = 'UpdateAudit',
268+
DeleteAudit = 'DeleteAudit',
269+
}
270+
```
271+
272+
# Extension enhancement using CASBIN authorisation
273+
274+
As a further enhancement to these methods, we are using [casbin library!](https://casbin.org/docs/en/overview) to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
275+
1. **Using default casbin policy document** - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
276+
2. **Defining custom logic to form dynamic policies** - Implement dynamic permissions based on app logic in casbin-enforcer-config provider. Authorisation extension will dynamically create casbin policy using this business logic to give the authorisation decisions.
277+
278+
## Usage
279+
280+
In order to use this enhacement into your LoopBack application, please follow below steps.
281+
282+
- Add providers to implement casbin authorisation along with authorisation component.
283+
284+
```ts
285+
this.bind(AuthorizationBindings.CONFIG).to({
286+
allowAlwaysPaths: ['/explorer'],
287+
});
288+
this.component(AuthorizationComponent);
289+
290+
this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
291+
CasbinEnforcerConfigProvider,
292+
);
293+
294+
this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
295+
CasbinResValModifierProvider,
296+
);
297+
```
132298
- Implement the **Casbin Resource value modifier provider**. Customise the resource value based on business logic using route arguments parameter in the provider.
133299

134300
```ts
@@ -203,29 +369,14 @@ export class CasbinEnforcerConfigProvider
203369
}
204370
```
205371

206-
207-
- For method #3, we also provide a simple provider function [_AuthorizationBindings.USER_PERMISSIONS_](<[./src/providers/user-permissions.provider.ts](https://github.com/sourcefuse/loopback4-authorization/blob/master/src/providers/user-permissions.provider.ts)>) to evaluate the user permissions based on its role permissions and user-level overrides. Just inject it
208-
209-
```ts
210-
@inject(AuthorizationBindings.USER_PERMISSIONS)
211-
private readonly getUserPermissions: UserPermissionsFn<string>,
212-
```
213-
214-
and invoke it
215-
216-
```ts
217-
const permissions = this.getUserPermissions(user.permissions, role.permissions);
218-
```
219-
220-
221372
- Add the dependency injections for resource value modifer provider, and casbin authorisation function in the sequence.ts
222373

223374
```ts
224375
@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
225376
protected checkAuthorisation: CasbinAuthorizeFn,
226377
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
227378
protected casbinResModifierFn: CasbinResourceModifierFn,
228-
```
379+
```
229380

230381
- Add a step in custom sequence to check for authorization whenever any end
231382
point is hit.
@@ -306,8 +457,6 @@ export class MySequence implements SequenceHandler {
306457
}
307458
```
308459

309-
The above sequence also contains user authentication using [loopback4-authentication](https://github.com/sourcefuse/loopback4-authentication) package. You can refer to the documentation for the same for more details.
310-
311460
- Now we can add access permission keys to the controller methods using authorize
312461
decorator as below. Set isCasbinPolicy parameter to use casbin default policy format. Default is false.
313462

@@ -328,41 +477,6 @@ async create(@requestBody() role: Role): Promise<Role> {
328477
}
329478
```
330479

331-
This endpoint will only be accessible if logged in user has permission
332-
'CreateRole'.
333-
334-
A good practice is to keep all permission strings in a separate enum file like this.
335-
336-
```ts
337-
export const enum PermissionKey {
338-
ViewOwnUser = 'ViewOwnUser',
339-
ViewAnyUser = 'ViewAnyUser',
340-
ViewTenantUser = 'ViewTenantUser',
341-
CreateAnyUser = 'CreateAnyUser',
342-
CreateTenantUser = 'CreateTenantUser',
343-
UpdateOwnUser = 'UpdateOwnUser',
344-
UpdateTenantUser = 'UpdateTenantUser',
345-
UpdateAnyUser = 'UpdateAnyUser',
346-
DeleteTenantUser = 'DeleteTenantUser',
347-
DeleteAnyUser = 'DeleteAnyUser',
348-
349-
ViewTenant = 'ViewTenant',
350-
CreateTenant = 'CreateTenant',
351-
UpdateTenant = 'UpdateTenant',
352-
DeleteTenant = 'DeleteTenant',
353-
354-
ViewRole = 'ViewRole',
355-
CreateRole = 'CreateRole',
356-
UpdateRole = 'UpdateRole',
357-
DeleteRole = 'DeleteRole',
358-
359-
ViewAudit = 'ViewAudit',
360-
CreateAudit = 'CreateAudit',
361-
UpdateAudit = 'UpdateAudit',
362-
DeleteAudit = 'DeleteAudit',
363-
}
364-
```
365-
366480
## Feedback
367481

368482
If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-authorization/issues) to see if someone else in the community has already created a ticket.

src/policy.csv

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +0,0 @@
1-
p, u0851e3b-156b-4b7f-85e9-48f0953a6cc8, session, CreateMeetingSession
2-

src/types.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Request} from '@loopback/express';
1+
import { Request } from '@loopback/express';
22
import PostgresAdapter from 'casbin-pg-adapter';
33

44
/**
@@ -35,7 +35,7 @@ export interface AuthorizationMetadata {
3535
/**
3636
* Boolean flag to determine whether we are using casbin policy format or not
3737
* isCasbinPolicy = true, when we are providing casbin format policy doc in application
38-
* isCasbinPolicy = false, when we are impplementing provider in app to give casbin policy
38+
* isCasbinPolicy = false, when we are implementing provider in app to give casbin policy
3939
*/
4040
isCasbinPolicy?: boolean;
4141
}
@@ -135,8 +135,6 @@ export interface IUserPrefs {
135135

136136
export interface IAuthUserWithPermissions<
137137
ID = string,
138-
TID = string,
139-
UTID = string
140138
> extends IAuthUser {
141139
id?: string;
142140
identifier?: ID;
@@ -148,7 +146,4 @@ export interface IAuthUserWithPermissions<
148146
firstName: string;
149147
lastName: string;
150148
middleName?: string;
151-
tenantId?: TID;
152-
userTenantId?: UTID;
153-
passwordExpiryTime?: Date;
154149
}

0 commit comments

Comments
 (0)