@superawesome/permissions-nestjs
Version:
NestJS Guard & Decorators for @superawesome/permissions, promoting orthogonal fine-grained API access control to resources.
457 lines (392 loc) • 19.7 kB
HTML
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>SuperAwesome Permissions for NestJs (@superawesome/permissions-nestjs)</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="../images/favicon.ico">
<link rel="stylesheet" href="../styles/style.css">
<link rel="stylesheet" href="../styles/postmark.css">
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top visible-xs">
<a href="../" class="navbar-brand">SuperAwesome Permissions for NestJs (@superawesome/permissions-nestjs)</a>
<button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button>
</div>
<div class="xs-menu menu" id="mobile-menu">
<div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu>
</div>
<div class="container-fluid main">
<div class="row main">
<div class="hidden-xs menu">
<compodoc-menu mode="normal"></compodoc-menu>
</div>
<!-- START CONTENT -->
<div class="content miscellaneous-variables">
<div class="content-data">
<ol class="breadcrumb">
<li>Miscellaneous</li>
<li>Variables</li>
</ol>
<section>
<h3 id="index">Index</h3>
<table class="table table-sm table-bordered index-table">
<tbody>
<tr>
<td class="col-md-4">
<ul class="index-list">
<li>
<a href="#createPermissionsGuard" title="src/createPermissionsGuard.ts"><b>createPermissionsGuard</b> (src/.../createPermissionsGuard.ts)</a>
</li>
<li>
<a href="#GetPermit" title="src/GetPermit.decorator.ts"><b>GetPermit</b> (src/.../GetPermit.decorator.ts)</a>
</li>
<li>
<a href="#InjectPermissions" title="src/inject-permissions.decorator.ts"><b>InjectPermissions</b> (src/.../inject-permissions.decorator.ts)</a>
</li>
<li>
<a href="#PERMISSIONS_OWNERSHIP_SERVICE_TOKEN" title="src/types.ts"><b>PERMISSIONS_OWNERSHIP_SERVICE_TOKEN</b> (src/.../types.ts)</a>
</li>
<li>
<a href="#PermitGrant" title="src/PermitGrant.decorator.ts"><b>PermitGrant</b> (src/.../PermitGrant.decorator.ts)</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</section>
<h3>src/createPermissionsGuard.ts</h3>
<section>
<h3></h3> <table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="createPermissionsGuard"></a>
<span class="name">
<b>
createPermissionsGuard</b>
<a href="#createPermissionsGuard"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>(
guardOptions: IGuardOptions = {},
permissionDefinitionStringOwnHooks?:
| IPermissionDefinitionStringOwnHooks
| IPermissionDefinitionStringOwnHooks[],
pdDefaults: PermissionDefinitionDefaults = {}
): typeof AbstractPermissionsGuard => {
@Injectable()
class ConcretePermissionsGuard extends AbstractPermissionsGuard {
constructor(
@InjectPermissions() protected readonly permissions: Permissions<Tid, Tid>,
@Inject(PERMISSIONS_OWNERSHIP_SERVICE_TOKEN)
protected readonly permissionsOwnershipService: any,
protected reflector: Reflector,
@Inject(PERMISSIONS_EXTRACT_USER_FROM_REQUEST_TOKEN)
protected readonly extractUserFromRequest: TExtractUserFromRequest<Tid>,
@Inject(PERMISSIONS_MAP_RESOURCE_ID_TOKEN)
protected readonly defaultProjectResourceId: TProjectTo
) {
super(permissions, permissionsOwnershipService, reflector, extractUserFromRequest);
// Replace owner hooks string method names of PermissionDefinitionStringOwnHooks with the real ones from the service
// @todo: make type-safe injectable if there's a way to pass references
// of the decorated service / controller in decorators
// see:
// https://stackoverflow.com/questions/52106406/in-nest-js-how-to-get-a-service-instance-inside-a-decorator
// https://stackoverflow.com/questions/55560858/in-nest-js-is-it-possible-to-get-service-instance-inside-a-param-decorator
// https://stackoverflow.com/questions/52862644/inject-service-into-guard-in-nest-js
// https://stackoverflow.com/questions/49160973/nest-js-unable-to-inject-service-into-guard-if-used-in-module
// https://github.com/nestjs/nest/issues/2130
// https://github.com/nestjs/nest/issues/1916
// https://github.com/nestjs/nest/issues/1038
// https://stackoverflow.com/questions/55325182/nest-cant-resolve-dependencies-of-guard-wrapped-inside-a-decorator
if (!this.permissions.isBuilt && permissionDefinitionStringOwnHooks) {
if (!_.isArray(permissionDefinitionStringOwnHooks))
permissionDefinitionStringOwnHooks = [permissionDefinitionStringOwnHooks];
const hookNames = ['isOwner', 'listOwned', 'limitOwned'];
const permissionDefinitions: PermissionDefinition[] = permissionDefinitionStringOwnHooks.map(
pdStringOwnHooks => {
const realPD: PermissionDefinition = _.omit(pdStringOwnHooks, hookNames) as any; // only static array works without any;-)
_.each(hookNames, hookName => {
if (_.isString(pdStringOwnHooks[hookName])) {
if (!_.isFunction(this.permissionsOwnershipService[pdStringOwnHooks[hookName]]))
throw new HttpException(
`SA-Permissions NestJS: missing service method for "${hookName}" \`${pdStringOwnHooks[hookName]}\``,
HttpStatus.INTERNAL_SERVER_ERROR
);
realPD[hookName] = this.permissionsOwnershipService[
pdStringOwnHooks[hookName]
].bind(permissionsOwnershipService);
}
});
return realPD;
}
);
this.permissions.addDefinitions(permissionDefinitions, pdDefaults);
}
}
/**
* Perform the actual permissions.grantPermit() call & store Permit object in request
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
let permitGrant = this.reflector.get<PermitGrantArgs>('permitGrant', context.getHandler());
const req = context.switchToHttp().getRequest();
if (permitGrant === false) {
// @PermitGrant(false) means we explicitly DO NOT want to protect this method/endpoint at all.
// So we `canActivate` always true, even for requests with no User present.
// @todo: set a dummy "allow all Permit" instead of just false, to allow all Permit functionality to be used in the app.
// for now we set this to false so @GetPermit fails with the right message.
req.__permissions_permit__ = false;
return true;
}
if (!permitGrant) permitGrant = {};
const resource = permitGrant.resource || guardOptions.resource;
if (!resource)
throw new HttpException(
'SA-Permissions NestJS: `resource` to permit.grantPermit() against is not configured for this route',
HttpStatus.INTERNAL_SERVER_ERROR
);
const user = await this.extractUserFromRequest(req);
const resourceIdKey = permitGrant.resourceIdKey || 'id';
const projectResourceId: TProjectTo =
permitGrant.projectResourceId ||
guardOptions.projectResourceId ||
this.defaultProjectResourceId;
const resourceId = projectResourceId(req.params[resourceIdKey]);
const resourceIdsKey = permitGrant.resourceIdsKey || 'ids';
const resourceIds = req.params[resourceIdsKey];
const grantPermitQuery = {
resource,
user,
resourceId,
action: permitGrant.action || context.getHandler().name,
};
const permit: Permit<Tid, Tid> = await this.permissions.grantPermit(
grantPermitQuery as GrantPermitQuery<Tid, Tid>
);
// check requested `resourceIds` exist in permit.listOwn
if (!permit.anyGranted && _.isArray(resourceIds)) {
let ownResourceIds: Tid[] = [];
let nonOwnResourceIds: Tid[] = [];
// Check if permit.listOwn() is supported (i.e we have found at least one listOwned hook
if (permit.isListOwnSupported()) {
ownResourceIds = await permit.listOwn();
nonOwnResourceIds = _.difference(resourceIds, ownResourceIds);
} else {
// Otherwise (i.e. when `limitOwn` is used), check each for the resourceId using `isOwn()`
for (const resourceIdToCheck of resourceIds)
if (!(await permit.isOwn(resourceIdToCheck))) nonOwnResourceIds.push(resourceIdToCheck); // break if one found to optimise
}
// ignore nonOwnResourceIds if ownResourceIds === null, cause `listOwn()` returning null means "allow all"
if (!_.isEmpty(nonOwnResourceIds) && ownResourceIds !== null) {
throw new HttpException(
`SA-Permissions NestJS: Cant access Non-Own ResourceIds ids: [${nonOwnResourceIds.join(
', '
)}]`,
HttpStatus.FORBIDDEN
);
}
}
req.__permissions_permit__ = permit;
return permit.granted;
}
}
// Background: although we have the `PermissionsGuard` factory that returns a new configured *class* each time we call it,
// the NestJS runtime creates only one instance of this Guard class (whichever happens to come first), UNLESS each Class has a unique class name.
// By changing the name of the class before the factory returns it, we force NestJS to instantiate each of them, and hence it works as expected.
return nameAClass(`ConcretePermissionsGuard_${randomString()}`, ConcretePermissionsGuard);
}</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>The factory function that creates the customised Guard for a Controller.</p>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<h3>src/GetPermit.decorator.ts</h3>
<section>
<h3></h3> <table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="GetPermit"></a>
<span class="name">
<b>
GetPermit</b>
<a href="#GetPermit"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>createParamDecorator(
(data, req): Permit => {
if (!req.__permissions_permit__)
if (req.__permissions_permit__ === false) {
throw new Error(
'SA-Permissions-NestJS: @GetPermit() param decorator failed, ' +
'because you have explicitly decorated your endpoint method with `@PermitGrant(false)`. ' +
'Hence a real Permit object can NOT be present for endpoint (a dummy can, but in the future).'
);
} else {
throw new Error(
'SA-Permissions-NestJS: @GetPermit() param decorator failed, because Permit object is not present for endpoint. ' +
'Did you decorate your controller with `@UseGuards(createPermissionsGuard(...))` ?'
);
}
return req.__permissions_permit__;
}
)</code>
</td>
</tr>
</tbody>
</table>
</section>
<h3>src/inject-permissions.decorator.ts</h3>
<section>
<h3></h3> <table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="InjectPermissions"></a>
<span class="name">
<b>
InjectPermissions</b>
<a href="#InjectPermissions"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>() => Inject(PERMISSIONS_TOKEN)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>Inject the Permissions instance, in case you need it - see <a href="/additional-documentation/reference-&-detailed-example.html">detailed example</a></p>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<h3>src/types.ts</h3>
<section>
<h3></h3> <table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="PERMISSIONS_OWNERSHIP_SERVICE_TOKEN"></a>
<span class="name">
<b>
PERMISSIONS_OWNERSHIP_SERVICE_TOKEN</b>
<a href="#PERMISSIONS_OWNERSHIP_SERVICE_TOKEN"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Type : </i> <code><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/string" target="_blank" >string</a></code>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>'PERMISSIONS_OWNERSHIP_SERVICE_TOKEN'</code>
</td>
</tr>
</tbody>
</table>
</section>
<h3>src/PermitGrant.decorator.ts</h3>
<section>
<h3></h3> <table class="table table-sm table-bordered">
<tbody>
<tr>
<td class="col-md-4">
<a name="PermitGrant"></a>
<span class="name">
<b>
PermitGrant</b>
<a href="#PermitGrant"><span class="icon ion-ios-link"></span></a>
</span>
</td>
</tr>
<tr>
<td class="col-md-4">
<i>Default value : </i><code>(permitGrantArgs: PermitGrantArgs = {}): CustomDecorator =>
SetMetadata('permitGrant', permitGrantArgs)</code>
</td>
</tr>
<tr>
<td class="col-md-4">
<div class="io-description"><p>An optional method/endpoint Guard that allows you to either:</p>
<p>a) turn off the guard for this endpoint, by passing the literal <code>false</code>. This will make the endpoint open to even non authenticated users.</p>
<p>b) Pass an object of type <code>PermitGrantQuery</code> to set one or more of:</p>
<ul>
<li><p>a <code>resource</code> name different than the Controller's <code>createPermissionsGuard</code> default <code>resource</code>.</p>
</li>
<li><p>an <code>action</code> name different than the method's name.</p>
</li>
<li><p>a <code>resourceIdKey</code> name different than just <code>id</code>.</p>
</li>
<li><p>a <code>resourceIdsKey</code> name different than just <code>ids</code>.</p>
</li>
</ul>
<p>See <a href="/additional-documentation/reference-&-detailed-example.html">reference in detailed example</a></p>
</div>
</td>
</tr>
</tbody>
</table>
</section>
</div><div class="search-results">
<div class="has-results">
<h1 class="search-results-title"><span class='search-results-count'></span> result-matching "<span class='search-query'></span>"</h1>
<ul class="search-results-list"></ul>
</div>
<div class="no-results">
<h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1>
</div>
</div>
</div>
<!-- END CONTENT -->
</div>
</div>
<script>
var COMPODOC_CURRENT_PAGE_DEPTH = 1;
var COMPODOC_CURRENT_PAGE_CONTEXT = 'miscellaneous-variables';
var COMPODOC_CURRENT_PAGE_URL = 'variables.html';
</script>
<script src="../js/libs/custom-elements.min.js"></script>
<script src="../js/libs/lit-html.js"></script>
<!-- Required to polyfill modern browsers as code is ES5 for IE... -->
<script src="../js/libs/custom-elements-es5-adapter.js" charset="utf-8" defer></script>
<script src="../js/menu-wc.js" defer></script>
<script src="../js/libs/bootstrap-native.js"></script>
<script src="../js/libs/es6-shim.min.js"></script>
<script src="../js/libs/EventDispatcher.js"></script>
<script src="../js/libs/promise.min.js"></script>
<script src="../js/libs/zepto.min.js"></script>
<script src="../js/compodoc.js"></script>
<script src="../js/tabs.js"></script>
<script src="../js/menu.js"></script>
<script src="../js/libs/clipboard.min.js"></script>
<script src="../js/libs/prism.js"></script>
<script src="../js/sourceCode.js"></script>
<script src="../js/search/search.js"></script>
<script src="../js/search/lunr.min.js"></script>
<script src="../js/search/search-lunr.js"></script>
<script src="../js/search/search_index.js"></script>
<script src="../js/lazy-load-graphs.js"></script>
</body>
</html>