Skip to content

Commit

Permalink
docs: add more details as per review feedback
Browse files Browse the repository at this point in the history
Added more details like user->role->permissions relation, authentication considerations, etc., to
make it more meaningful as per @raymondfeng feedback
  • Loading branch information
samarpanB committed Apr 17, 2019
1 parent 1de37cb commit 1f6c644
Showing 1 changed file with 217 additions and 13 deletions.
230 changes: 217 additions & 13 deletions docs/site/Loopback-component-authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ This can be part of your main REST Application project or can be created as a
Loopback extension for reuse in multiple projects. Latter is the better option
for obvious reasons - reusability.

**Note: Loopback team is working on making authorization an out-of-the-box
feature in Loopback 4. It is a work in progress and will soon be there. Until
then, this implementation guide can be followed.**

## The requirement

1. Every protected API end point needs to be restricted by specific permissions.
Expand All @@ -25,6 +29,24 @@ for obvious reasons - reusability.
4. Publicly accessible APIs must be accessible regardless of user permissions.
5. Every user has a set of permissions. These permissions may be associated via
role attached to the user or directly to the user.
6. A user can be provided additional permissions or denied some permissions over
an above its role permissions. This is considered explicit allow/deny and
always takes precedence while calculating permissions.

## Considerations

There are a few considerations that are taken into account before this
implementation can be done.

1. User authentication is already implemented. You can refer to the
[@loopback/authentication](https://github.com/strongloop/loopback-next/tree/master/packages/authentication#loopbackauthentication)
guide.
2. As part of authentication, client is sent back a token (JWT or similar) which
client need to pass in every API request headers thereafter.
3. The authenticate action provider parses the token to return AuthResponse
object.
4. AuthResponse contains the logged in user information including associated
role details.

## The implementation

Expand All @@ -33,27 +55,94 @@ First, let's define the types needed for this.
{% include code-caption.html content="/src/authorization/types.ts" %}

```ts
import {PermissionKey} from './permission-key';

/**
* Authorize action method interface
*/
export interface AuthorizeFn {
(userPermissions?: string[]): Promise<boolean>;
// userPermissions - Array of permission keys granted to the user
// This is actually a union of permissions picked up based on role
// attached to the user and allowed permissions at specific user level
(userPermissions: PermissionKey[]): Promise<boolean>;
}

/**
* Authorization metadata interface for the method decorator
*/
export interface AuthorizationMetadata {
// Array of permissions required at the method level.
// User need to have at least one of these to access the API method.
permissions: string[];
}

/**
* User Permission model
* used for explicit allow/deny any permission at user level
*/
export interface UserPermission {
permission: PermissionKey;
is_allowed: boolean;
}

/**
* User permissions manipulation method interface.
*
* This is where we can add our business logic to read and
* union permissions associated to user via role with
* those associated directly to the user.
*
*/
export interface UserPermissionsFn {
(
userPermissions: UserPermission[],
rolePermissions: PermissionKey[],
): PermissionKey[];
}
```

We define two interfaces.
We define four interfaces.

- **_AuthorizeFn_** - This is going to be the interface for authorization action
business logic.
- **_AuthorizationMetadata_** - This interface represents the information to be
passed via decorator for each individual controller method.
- **_UserPermission_** - This is the interface to be used for associating user
level permissions. It is actually doing explicit allow/deny at user level,
over and above role permissions.
- **_UserPermissionsFn_** - This is going to be the interface for user
permissions manipulation, if required any.

The PermissionKey is an enum containing all possible permission keys. Here is a
sample.

{% include code-caption.html content="/src/authorization/permission-key.ts" %}

```ts
export const enum PermissionKey {
// For accessing own (logged in user) profile
ViewOwnUser = 'ViewOwnUser',
// For accessing other users profile.
ViewAnyUser = 'ViewAnyUser',
// For creating a user
CreateAnyUser = 'CreateAnyUser',
// For updating own (logged in user) profile
UpdateOwnUser = 'UpdateOwnUser',
// For updating other users profile
UpdateAnyUser = 'UpdateAnyUser',
// For deleting a user
DeleteAnyUser = 'DeleteAnyUser',

// For accessing a role
ViewRoles = 'ViewRoles',
// For creating a role
CreateRoles = 'CreateRoles',
// For updating a role info
UpdateRoles = 'UpdateRoles',
// For removing a role
DeleteRoles = 'DeleteRoles',
}
```

Next, we create the binding keys for each type and accessor key for method
decorator.
Expand All @@ -63,7 +152,7 @@ decorator.
```ts
import {BindingKey} from '@loopback/context';
import {MetadataAccessor} from '@loopback/metadata';
import {AuthorizeFn, AuthorizationMetadata} from './types';
import {AuthorizeFn, AuthorizationMetadata, UserPermissionsFn} from './types';

/**
* Binding keys used by this component.
Expand All @@ -76,6 +165,10 @@ export namespace AuthorizatonBindings {
export const METADATA = BindingKey.create<AuthorizationMetadata | undefined>(
'userAuthorization.operationMetadata',
);

export const USER_PERMISSIONS = BindingKey.create<UserPermissionsFn>(
'userAuthorization.actions.userPermissions',
);
}

/**
Expand All @@ -87,13 +180,18 @@ export const AUTHORIZATION_METADATA_ACCESSOR = MetadataAccessor.create<
>('userAuthorization.accessor.operationMetadata');
```

Now, we need to create two providers
Now, we need to create three providers

- **_AuthorizationMetadataProvider_** - This will read the decorator metadata
from the controller methods wherever the decorator is used.
- **_AuthorizeActionProvider_** - This holds the business logic for access
validation of the user based upon access permissions allowed at method level
via decorator metadata above.
- **_UserPermissionsProvider_** - This is where we can add our business logic to
read and unify permissions associated to user via role, with those associated
directly to the user. In our case, an explicit allow/deny at user level takes
precendence over role permissions. But this business logic may vary
apllication to application. So, feel free to customize.

{% include code-caption.html content="/src/authorization/providers/authorization-metadata.provider.ts" %}

Expand Down Expand Up @@ -174,24 +272,70 @@ export class AuthorizeActionProvider implements Provider<AuthorizeFn> {
}
```

Below is the user permissions manipulation logic. If there is no requirement of
user level permissions in your application, you can skip the below.

{% include code-caption.html content="/src/authorization/providers/user-permissions.provider.ts" %}

```ts
import {Provider} from '@loopback/context';

import {PermissionKey} from '../permission-key';
import {UserPermission, UserPermissionsFn} from '../types';

export class UserPermissionsProvider implements Provider<UserPermissionsFn> {
constructor() {}

value(): UserPermissionsFn {
return (userPermissions, rolePermissions) =>
this.action(userPermissions, rolePermissions);
}

action(
userPermissions: UserPermission[],
rolePermissions: PermissionKey[],
): PermissionKey[] {
let perms: PermissionKey[] = [];
// First add all permissions associated with role
perms = perms.concat(rolePermissions);
// Now update permissions based on user permissions
userPermissions.forEach((userPerm: UserPermission) => {
if (userPerm.is_allowed && perms.indexOf(userPerm.permission) < 0) {
// Add permission if it is not part of role but allowed to user
perms.push(userPerm.permission);
} else if (
!userPerm.is_allowed &&
perms.indexOf(userPerm.permission) >= 0
) {
// Remove permission if it is disallowed for user
perms.splice(perms.indexOf(userPerm.permission), 1);
}
});
return perms;
}
}
```

Next, we need to expose these providers via Component to be bound to the
context.

{% include code-caption.html content="/src/authorization/component.ts" %}

```ts
import {Component, ProviderMap} from '@loopback/core';
import {AuthorizatonBindings} from './keys';
import {AuthorizeActionProvider} from './providers/authorization-action.provider';
import {AuthorizationMetadataProvider} from './providers/authorization-metadata.provider';
import {AuthorisatonBindings} from './keys';
import {AuthoriseActionProvider} from './providers/authorisation-action.provider';
import {AuthorisationMetadataProvider} from './providers/authorisation-metadata.provider';
import {UserPermissionsProvider} from './providers/user-permissions.provider';

export class AuthorizationComponent implements Component {
export class AuthorisationComponent implements Component {
providers?: ProviderMap;

constructor() {
this.providers = {
[AuthorizatonBindings.AUTHORIZE_ACTION.key]: AuthorizeActionProvider,
[AuthorizatonBindings.METADATA.key]: AuthorizationMetadataProvider,
[AuthorisatonBindings.AUTHORISE_ACTION.key]: AuthoriseActionProvider,
[AuthorisatonBindings.METADATA.key]: AuthorisationMetadataProvider,
[AuthorisatonBindings.USER_PERMISSIONS.key]: UserPermissionsProvider,
};
}
}
Expand Down Expand Up @@ -239,9 +383,11 @@ export * from './component';
export * from './types';
export * from './keys';
export * from './error-keys';
export * from './permission-key';
export * from './decorators/authorize.decorator';
export * from './providers/authorization-metadata.provider';
export * from './providers/authorization-action.provider';
export * from './providers/user-permissions.provider';
```

That is all for the authorization component. You can create all of the above
Expand All @@ -261,6 +407,54 @@ more steps to go.
this.component(AuthenticationComponent);
```

- Add permissions array to the role model.

{% include code-caption.html content="/src/models/role.model.ts" %}

```ts
@model({
name: 'roles',
})
export class Role extends Entity {
// .....
// other attributes here
// .....

@property.array(String, {
required: true,
})
permissions: PermissionKey[];

constructor(data?: Partial<Role>) {
super(data);
}
}
```

- Add user level permissions array to the user model. Do this if there is a use
case of explicit allow/deny of permissions at user-level in the application.
You can skip otherwise.

{% include code-caption.html content="/src/models/user.model.ts" %}

```ts
@model({
name: 'users',
})
export class User extends Entity {
// .....
// other attributes here
// .....

@property.array(String)
permissions: UserPermission[];

constructor(data?: Partial<User>) {
super(data);
}
}
```

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

Expand All @@ -279,6 +473,7 @@ import {
SequenceHandler,
HttpErrors,
} from '@loopback/rest';
import {AuthenticationBindings, AuthenticateFn} from './authenticate';
import {
AuthorizatonBindings,
AuthorizeFn,
Expand All @@ -294,6 +489,10 @@ export class MySequence implements SequenceHandler {
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
@inject(AuthorizatonBindings.USER_PERMISSIONS)
protected fetchUserPermissons: UserPermissionsFn,
@inject(AuthorizatonBindings.AUTHORIZE_ACTION)
protected checkAuthorization: AuthorizeFn,
) {}
Expand All @@ -304,7 +503,12 @@ export class MySequence implements SequenceHandler {
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
// Do authentication of the user and fetch user permissions below
const permissions: string[] = [];
const authUser: AuthResponse = await this.authenticateRequest(request);
// Parse and calculate user permissions based on role and user level
const permissions: PermissionKey[] = this.fetchUserPermissons(
authUser.permissions,
authUser.role.permissions,
);
// This is main line added to sequence
// where we are invoking the authorize action function to check for access
const isAccessAllowed: boolean = await this.checkAuthorization(
Expand All @@ -326,7 +530,7 @@ Now we can add access permission keys to the controller methods using authorize
decorator as below.

```ts
@authorize(['CanCreateRole'])
@authorize([PermissionKey.CreateRoles])
@post(rolesPath, {
responses: {
[STATUS_CODE.OK]: {
Expand All @@ -343,4 +547,4 @@ async create(@requestBody() role: Role): Promise<Role> {
```

This endpoint will only be accessible if logged in user has permission
'CanCreateRole'.
'CreateRoles'.

0 comments on commit 1f6c644

Please sign in to comment.