Skip to content

Commit

Permalink
Merge pull request #13 from sourcefuse/RFIT-188new
Browse files Browse the repository at this point in the history
feat(loopback4-authorization): RFIT-188 Casbin integration
  • Loading branch information
samarpan-b authored Oct 1, 2020
2 parents 5d25c41 + 027efc3 commit 675a0bf
Show file tree
Hide file tree
Showing 13 changed files with 4,173 additions and 1,925 deletions.
212 changes: 211 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ It provides three ways of integration
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.
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.

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


Refer to the usage section below for details on integration

## Install
Expand Down Expand Up @@ -116,7 +119,6 @@ export class User extends Entity implements UserPermissionsOverride<string> {
}
}
```

- 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

```ts
Expand Down Expand Up @@ -267,6 +269,214 @@ export const enum PermissionKey {
}
```

# Extension enhancement using CASBIN authorisation

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

## Usage

In order to use this enhacement into your LoopBack application, please follow below steps.

- Add providers to implement casbin authorisation along with authorisation component.

```ts
this.bind(AuthorizationBindings.CONFIG).to({
allowAlwaysPaths: ['/explorer'],
});
this.component(AuthorizationComponent);

this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
CasbinEnforcerConfigProvider,
);

this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
CasbinResValModifierProvider,
);
```
- Implement the **Casbin Resource value modifier provider**. Customise the resource value based on business logic using route arguments parameter in the provider.

```ts
import {Getter, inject, Provider} from '@loopback/context';
import {HttpErrors} from '@loopback/rest';
import {
AuthorizationBindings,
AuthorizationMetadata,
CasbinResourceModifierFn
} from 'loopback4-authorization';

export class CasbinResValModifierProvider
implements Provider<CasbinResourceModifierFn> {
constructor(
@inject.getter(AuthorizationBindings.METADATA)
private readonly getCasbinMetadata: Getter<AuthorizationMetadata>,
) {}

value(): CasbinResourceModifierFn {
return (pathParams: string[]) => this.action(pathParams);
}

async action(pathParams: string[]): Promise<string> {
const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
if (!metadata) {
throw new HttpErrors.InternalServerError(`Metadata object not found`);
}
const res = metadata.resource;

// Now modify the resource parameter using on path params, as per business logic.
// Returning resource value as such for default case.

return `${res}`;
}
}

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

```ts
import {Provider} from '@loopback/context';
import {CasbinConfig, CasbinEnforcerConfigGetterFn, IAuthUserWithPermissions} from 'loopback4-authorization';
import * as path from 'path';

export class CasbinEnforcerConfigProvider
implements Provider<CasbinEnforcerConfigGetterFn> {
constructor() {}

value(): CasbinEnforcerConfigGetterFn {
return (authUser: IAuthUserWithPermissions, resource: string, isCasbinPolicy?: boolean) =>
this.action(authUser, resource, isCasbinPolicy);
}

async action(authUser: IAuthUserWithPermissions, resource: string, isCasbinPolicy?: boolean): Promise<CasbinConfig> {
const model = path.resolve(
__dirname,
'./../../fixtures/casbin/model.conf',
);

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

const policy = path.resolve(__dirname, './../../fixtures/casbin/policy.csv');

const result: CasbinConfig = {
model,
//allowedRes,
policy
}
return result;
}
}
```

- Add the dependency injections for resource value modifer provider, and casbin authorisation function in the sequence.ts

```ts
@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
protected checkAuthorisation: CasbinAuthorizeFn,
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
protected casbinResModifierFn: CasbinResourceModifierFn,
```

- Add a step in custom sequence to check for authorization whenever any end
point is hit.

```ts
import {inject} from '@loopback/context';
import {
FindRoute,
HttpErrors,
InvokeMethod,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
import {
AuthorizationBindings,
AuthorizeErrorKeys,
AuthorizeFn,
UserPermissionsFn,
} from 'loopback4-authorization';

import {AuthClient} from './models/auth-client.model';
import {User} from './models/user.model';

const SequenceActions = RestBindings.SequenceActions;

export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.USER_AUTH_ACTION)
protected authenticateRequest: AuthenticateFn<AuthUser>,
@inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
protected authenticateRequestClient: AuthenticateFn<AuthClient>,
@inject(AuthorizationBindings.AUTHORIZE_ACTION)
protected checkAuthorisation: AuthorizeFn,
@inject(AuthorizationBindings.USER_PERMISSIONS)
private readonly getUserPermissions: UserPermissionsFn<string>,
) {}

async handle(context: RequestContext) {
const requestTime = Date.now();
try {
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
request.body = args[args.length - 1];
await this.authenticateRequestClient(request);
const authUser: User = await this.authenticateRequest(request);

// Invoke Resource value modifier
const resVal = await this.casbinResModifierFn(args);

// Check authorisation
const isAccessAllowed: boolean = await this.checkAuthorisation(
authUser,
resVal,
request,
);
// Checking access to route here
if (!isAccessAllowed) {
throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
}

const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
this.reject(context, err);
}
}
}
```

- Now we can add access permission keys to the controller methods using authorize
decorator as below. Set isCasbinPolicy parameter to use casbin default policy format. Default is false.

```ts
@authorize({permissions: ['CreateRole'], resource:'role', isCasbinPolicy: true})
@post(rolesPath, {
responses: {
[STATUS_CODE.OK]: {
description: 'Role model instance',
content: {
[CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
},
},
},
})
async create(@requestBody() role: Role): Promise<Role> {
return await this.roleRepository.create(role);
}
```

## Feedback

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.
Expand Down
Loading

0 comments on commit 675a0bf

Please sign in to comment.