
Security News
Node.js TSC Votes to Stop Distributing Corepack
Corepack will be phased out from future Node.js releases following a TSC vote.
loopback4-authorization
Advanced tools
A loopback-next extension for authorization in loopback applications. Its a very simplistic yet powerful and effective implementation using simple string based permissions.
It provides three ways of integration
Extension enhancement using CASBIN authorisation
Refer to the usage section below for details on integration
npm install loopback4-authorization
For a quick starter guide, you can refer to our loopback 4 starter application which utilizes method #3 from the above in a simple multi-tenant application.
In order to use this component into your LoopBack application, please follow below steps.
this.bind(AuthorizationBindings.CONFIG).to({
allowAlwaysPaths: ['/explorer'],
});
this.component(AuthorizationComponent);
@model({
name: 'users',
})
export class User extends Entity implements Permissions<string> {
// .....
// other attributes here
// .....
@property({
type: 'array',
itemType: 'string',
})
permissions: string[];
constructor(data?: Partial<User>) {
super(data);
}
}
@model({
name: 'roles',
})
export class Role extends Entity implements Permissions<string> {
// .....
// other attributes here
// .....
@property({
type: 'array',
itemType: 'string',
})
permissions: string[];
constructor(data?: Partial<Role>) {
super(data);
}
}
@model({
name: 'users',
})
export class User extends Entity implements UserPermissionsOverride<string> {
// .....
// other attributes here
// .....
@property({
type: 'array',
itemType: 'object',
})
permissions: UserPermission<string>[];
constructor(data?: Partial<User>) {
super(data);
}
}
@inject(AuthorizationBindings.USER_PERMISSIONS)
private readonly getUserPermissions: UserPermissionsFn<string>,
and invoke it
const permissions = this.getUserPermissions(user.permissions, role.permissions);
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);
// Do ths if you are using method #3
const permissions = this.getUserPermissions(
authUser.permissions,
authUser.role.permissions,
);
// This is the important line added for authorization. Needed for all 3 methods
const isAccessAllowed: boolean = await this.checkAuthorisation(
permissions, // do authUser.permissions if using method #1
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);
}
}
}
The above sequence also contains user authentication using loopback4-authentication package. You can refer to the documentation for the same for more details.
@authorize(['CreateRole'])
@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);
}
This endpoint will only be accessible if logged in user has permission 'CreateRole'.
A good practice is to keep all permission strings in a separate enum file like this.
export const enum PermissionKey {
ViewOwnUser = 'ViewOwnUser',
ViewAnyUser = 'ViewAnyUser',
ViewTenantUser = 'ViewTenantUser',
CreateAnyUser = 'CreateAnyUser',
CreateTenantUser = 'CreateTenantUser',
UpdateOwnUser = 'UpdateOwnUser',
UpdateTenantUser = 'UpdateTenantUser',
UpdateAnyUser = 'UpdateAnyUser',
DeleteTenantUser = 'DeleteTenantUser',
DeleteAnyUser = 'DeleteAnyUser',
ViewTenant = 'ViewTenant',
CreateTenant = 'CreateTenant',
UpdateTenant = 'UpdateTenant',
DeleteTenant = 'DeleteTenant',
ViewRole = 'ViewRole',
CreateRole = 'CreateRole',
UpdateRole = 'UpdateRole',
DeleteRole = 'DeleteRole',
ViewAudit = 'ViewAudit',
CreateAudit = 'CreateAudit',
UpdateAudit = 'UpdateAudit',
DeleteAudit = 'DeleteAudit',
}
As a further enhancement to these methods, we are using casbin library to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
In order to use this enhacement into your LoopBack application, please follow below steps.
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,
);
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>,
@inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
private readonly allowAlwaysPath: string[],
) {}
value(): CasbinResourceModifierFn {
return (pathParams: string[], req: Request) => this.action(pathParams, req);
}
async action(pathParams: string[], req: Request): Promise<string> {
const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
if (
!metadata &&
!!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0)
) {
return '';
}
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}`;
}
}
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'); // Model initialization from file path
/**
* import * as casbin from 'casbin';
*
* To initialize model from code, use
* let m = new casbin.Model();
* m.addDef('r', 'r', 'sub, obj, act'); and so on...
*
* To initialize model from string, use
* const text = `
* [request_definition]
* r = sub, obj, act
*
* [policy_definition]
* p = sub, obj, act
*
* [policy_effect]
* e = some(where (p.eft == allow))
*
* [matchers]
* m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
* `;
* const model = casbin.newModelFromString(text);
*/
// 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;
}
}
@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
protected checkAuthorisation: CasbinAuthorizeFn,
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
protected casbinResModifierFn: CasbinResourceModifierFn,
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.CASBIN_AUTHORIZE_ACTION)
protected checkAuthorisation: CasbinAuthorizeFn,
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
protected casbinResModifierFn: CasbinResourceModifierFn,
) {}
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);
}
}
}
@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);
}
If you've noticed a bug or have a question or have a feature request, search the issue tracker to see if someone else in the community has already created a ticket. If not, go ahead and make one! All feature requests are welcome. Implementation time may vary. Feel free to contribute the same, if you can. If you think this extension is useful, please star it. Appreciation really helps in keeping this project alive.
Please read CONTRIBUTING.md for details on the process for submitting pull requests to us.
Code of conduct guidelines here.
Release v5.1.2 March 13, 2023
Welcome to the March 13, 2023 release of loopback4-authorization. There are many updates in this version that we hope you will like, the key highlights include:
loopback version update :- chore(deps): loopback version update was commited on March 13, 2023 by Gautam Agarwal
loopback version update
GH-83
Stale Bot missing in the repository :- chore(chore): add github stale bot was commited on February 27, 2023 by yeshamavani
Added stale.yml file to configure stale options
GH-81
Clink on the above links to understand the changes in detail.
FAQs
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Corepack will be phased out from future Node.js releases following a TSC vote.
Research
Security News
Research uncovers Black Basta's plans to exploit package registries for ransomware delivery alongside evidence of similar attacks already targeting open source ecosystems.
Security News
Oxlint's beta release introduces 500+ built-in linting rules while delivering twice the speed of previous versions, with future support planned for custom plugins and improved IDE integration.