Skip to content

Commit d95febf

Browse files
authored
fix(loopback4-authorization): CasbinResourceModifierFn definition and readme update (#15)
* fix(loopback4-authorization): CasbinResourceModifierFn definition and readme update SFO-154 SFO-160 * docs(loopback4-authorization): adding inline comments SFO-154 * feat(loopback4-authorization): changes to load model from string or via code SFO-160 * docs(loopback4-authorization): update readme for model initilization methods SFO-160 * docs(loopback4-authorization): model-policy formats documentation SFO-160
1 parent 1f827b6 commit d95febf

File tree

3 files changed

+88
-30
lines changed

3 files changed

+88
-30
lines changed

README.md

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ It provides three ways of integration
2323

2424
[Extension enhancement using CASBIN authorisation](#Extension-enhancement-using-CASBIN-authorisation)
2525

26-
2726
Refer to the usage section below for details on integration
2827

2928
## Install
@@ -119,6 +118,7 @@ export class User extends Entity implements UserPermissionsOverride<string> {
119118
}
120119
}
121120
```
121+
122122
- 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
123123

124124
```ts
@@ -271,7 +271,8 @@ export const enum PermissionKey {
271271

272272
# Extension enhancement using CASBIN authorisation
273273

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:
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+
275276
1. **Using default casbin policy document** - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
276277
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.
277278

@@ -295,6 +296,7 @@ this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
295296
CasbinResValModifierProvider,
296297
);
297298
```
299+
298300
- Implement the **Casbin Resource value modifier provider**. Customise the resource value based on business logic using route arguments parameter in the provider.
299301

300302
```ts
@@ -303,22 +305,32 @@ import {HttpErrors} from '@loopback/rest';
303305
import {
304306
AuthorizationBindings,
305307
AuthorizationMetadata,
306-
CasbinResourceModifierFn
308+
CasbinResourceModifierFn,
307309
} from 'loopback4-authorization';
308310

309311
export class CasbinResValModifierProvider
310312
implements Provider<CasbinResourceModifierFn> {
311313
constructor(
312314
@inject.getter(AuthorizationBindings.METADATA)
313315
private readonly getCasbinMetadata: Getter<AuthorizationMetadata>,
316+
@inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
317+
private readonly allowAlwaysPath: string[],
314318
) {}
315319

316320
value(): CasbinResourceModifierFn {
317-
return (pathParams: string[]) => this.action(pathParams);
321+
return (pathParams: string[], req: Request) => this.action(pathParams, req);
318322
}
319323

320-
async action(pathParams: string[]): Promise<string> {
324+
async action(pathParams: string[], req: Request): Promise<string> {
321325
const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
326+
327+
if (
328+
!metadata &&
329+
!!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0)
330+
) {
331+
return '';
332+
}
333+
322334
if (!metadata) {
323335
throw new HttpErrors.InternalServerError(`Metadata object not found`);
324336
}
@@ -330,40 +342,77 @@ export class CasbinResValModifierProvider
330342
return `${res}`;
331343
}
332344
}
333-
334345
```
335-
- Implement the **casbin enforcer config provider** . Provide the casbin model path. In case 1 of using [default casbin format policy!](https://casbin.org/docs/en/how-it-works), provide the casbin policy path. In other case of creating dynamic policy, provide the array of Resource-Permission objects for a given user, based on business logic.
346+
347+
- Implement the **casbin enforcer config provider** . Provide the casbin model path. Model definition can be initialized from [.CONF file, from code, or from a string](https://casbin.org/docs/en/model-storage).
348+
In the case of policy creation being handled by extension (isCasbinPolicy parameter is false), provide the array of Resource-Permission objects for a given user, based on business logic.
349+
In other case, provide the policy from file or as CSV string or from [casbin Adapters](https://casbin.org/docs/en/adapters).
350+
**NOTE**: In the second case, if model is initialized from .CONF file, then any of the above formats can be used for policy. But if model is being initialised from code or string, then policy should be provided as [casbin adapter](https://casbin.org/docs/en/adapters) only.
336351

337352
```ts
338353
import {Provider} from '@loopback/context';
339-
import {CasbinConfig, CasbinEnforcerConfigGetterFn, IAuthUserWithPermissions} from 'loopback4-authorization';
354+
import {
355+
CasbinConfig,
356+
CasbinEnforcerConfigGetterFn,
357+
IAuthUserWithPermissions,
358+
} from 'loopback4-authorization';
340359
import * as path from 'path';
341360

342361
export class CasbinEnforcerConfigProvider
343362
implements Provider<CasbinEnforcerConfigGetterFn> {
344363
constructor() {}
345364

346365
value(): CasbinEnforcerConfigGetterFn {
347-
return (authUser: IAuthUserWithPermissions, resource: string, isCasbinPolicy?: boolean) =>
348-
this.action(authUser, resource, isCasbinPolicy);
366+
return (
367+
authUser: IAuthUserWithPermissions,
368+
resource: string,
369+
isCasbinPolicy?: boolean,
370+
) => this.action(authUser, resource, isCasbinPolicy);
349371
}
350372

351-
async action(authUser: IAuthUserWithPermissions, resource: string, isCasbinPolicy?: boolean): Promise<CasbinConfig> {
352-
const model = path.resolve(
353-
__dirname,
354-
'./../../fixtures/casbin/model.conf',
355-
);
373+
async action(
374+
authUser: IAuthUserWithPermissions,
375+
resource: string,
376+
isCasbinPolicy?: boolean,
377+
): Promise<CasbinConfig> {
378+
const model = path.resolve(__dirname, './../../fixtures/casbin/model.conf'); // Model initialization from file path
379+
/**
380+
* import * as casbin from 'casbin';
381+
*
382+
* To initialize model from code, use
383+
* let m = new casbin.Model();
384+
* m.addDef('r', 'r', 'sub, obj, act'); and so on...
385+
*
386+
* To initialize model from string, use
387+
* const text = `
388+
* [request_definition]
389+
* r = sub, obj, act
390+
*
391+
* [policy_definition]
392+
* p = sub, obj, act
393+
*
394+
* [policy_effect]
395+
* e = some(where (p.eft == allow))
396+
*
397+
* [matchers]
398+
* m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
399+
* `;
400+
* const model = casbin.newModelFromString(text);
401+
*/
356402

357403
// Write business logic to find out the allowed resource-permission sets for this user. Below is a dummy value.
358404
//const allowedRes = [{resource: 'session', permission: "CreateMeetingSession"}];
359405

360-
const policy = path.resolve(__dirname, './../../fixtures/casbin/policy.csv');
406+
const policy = path.resolve(
407+
__dirname,
408+
'./../../fixtures/casbin/policy.csv',
409+
);
361410

362411
const result: CasbinConfig = {
363412
model,
364413
//allowedRes,
365-
policy
366-
}
414+
policy,
415+
};
367416
return result;
368417
}
369418
}
@@ -418,10 +467,10 @@ export class MySequence implements SequenceHandler {
418467
protected authenticateRequest: AuthenticateFn<AuthUser>,
419468
@inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
420469
protected authenticateRequestClient: AuthenticateFn<AuthClient>,
421-
@inject(AuthorizationBindings.AUTHORIZE_ACTION)
422-
protected checkAuthorisation: AuthorizeFn,
423-
@inject(AuthorizationBindings.USER_PERMISSIONS)
424-
private readonly getUserPermissions: UserPermissionsFn<string>,
470+
@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
471+
protected checkAuthorisation: CasbinAuthorizeFn,
472+
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
473+
protected casbinResModifierFn: CasbinResourceModifierFn,
425474
) {}
426475

427476
async handle(context: RequestContext) {

src/providers/casbin-authorization-action.provider.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,28 @@ export class CasbinAuthorizationProvider
5454
);
5555
}
5656

57-
const subject = this.getUserName(`${user.id}`);
57+
if (!user.id) {
58+
throw new HttpErrors.Unauthorized(`User not found.`);
59+
}
5860

59-
// const object = resource;
61+
const subject = this.getUserName(`${user.id}`);
6062

6163
let desiredPermissions;
6264

65+
//Fetch permissions to check from decorator metadata
6366
if (metadata.permissions && metadata.permissions.length > 0) {
6467
desiredPermissions = metadata.permissions;
6568
} else {
6669
throw new HttpErrors.Unauthorized(
6770
`Permissions are missing in the decorator.`,
6871
);
6972
}
70-
// Fetch casbin config by invoking casbin-config-getter-provider
7173

74+
// Fetch casbin config by invoking casbin-config-getter-provider
7275
const casbinConfig = await this.getCasbinEnforcerConfig(
7376
user,
7477
metadata.resource,
78+
metadata.isCasbinPolicy,
7579
);
7680

7781
let enforcer: casbin.Enforcer;
@@ -92,17 +96,20 @@ export class CasbinAuthorizationProvider
9296
const baseDir = path.join(__dirname, '../../src/policy.csv');
9397
await fsPromises.writeFile(baseDir, policy);
9498

95-
enforcer = await casbin.newEnforcer(casbinConfig.model, baseDir);
99+
const policyAdapter = new casbin.FileAdapter(baseDir);
100+
101+
enforcer = await casbin.newEnforcer(casbinConfig.model, policyAdapter);
96102
} else {
97103
return false;
98104
}
99105

106+
// Use casbin enforce method to get authorization decision
100107
for (const permission of desiredPermissions) {
101108
const decision = await enforcer.enforce(subject, resource, permission);
102109
authDecision = authDecision || decision;
103110
}
104111
} catch (err) {
105-
throw new HttpErrors.Unauthorized(err);
112+
throw new HttpErrors.Unauthorized(err.message);
106113
}
107114

108115
return authDecision;
@@ -115,6 +122,7 @@ export class CasbinAuthorizationProvider
115122
return `u${id}`;
116123
}
117124

125+
// Create casbin policy for user based on ResourcePermission data provided by extension client
118126
createCasbinPolicy(
119127
resPermObj: ResourcePermissionObject[],
120128
subject: string,

src/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Request} from '@loopback/express';
2+
import {FileAdapter, Model} from 'casbin';
23
import PostgresAdapter from 'casbin-pg-adapter';
34

45
/**
@@ -106,15 +107,15 @@ export interface CasbinEnforcerConfigGetterFn {
106107
* for integration with casbin, as per business logic
107108
*/
108109
export interface CasbinResourceModifierFn {
109-
(pathParams: string[]): Promise<string>;
110+
(pathParams: string[], req: Request): Promise<string>;
110111
}
111112

112113
/**
113114
* Casbin config object
114115
*/
115116
export interface CasbinConfig {
116-
model: string;
117-
policy?: string | PostgresAdapter;
117+
model: string | Model;
118+
policy?: string | PostgresAdapter | FileAdapter;
118119
allowedRes?: ResourcePermissionObject[];
119120
}
120121

0 commit comments

Comments
 (0)