@superawesome/permissions-nestjs
Version:
NestJS Guard & Decorators for @superawesome/permissions, promoting orthogonal fine-grained API access control to resources.
422 lines (332 loc) • 18.1 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 additional-page">
<div class="content-data">
<p><strong>Important note</strong>: This documentation is generated from integration tests, so the examples execute and are tested against.</p>
<p><strong>DO NOT EDIT THIS .md FILE - Its generated from <code>ts-node document-protected-detailed.controller.md.e2e-spec.ts</code></strong></p>
<h1 id="nestjs-with-superawesome-permissions---reference--detailed-example">NestJS with SuperAwesome Permissions - Reference & Detailed example</h1>
<h2 id="detailed-documents-protected-controller-example">Detailed Documents Protected Controller Example</h2>
<p>Lets now see a slightly more complicated example, based on the <a href="/additional-documentation/how-to-use-simple-example.html">previous simple example</a>.</p>
<p>This example is also acting as a reference, using inline comments. Hence, it has to list every single parameter and option, which most times you will not need to touch.</p>
<div><pre class="line-numbers"><code class="language-js">// file: ../detailed/document-protected-detailed.controller.ts
// omitted imports
/**
The `createPermissionsGuard()` is essential,
if we want to protect our Controller with SuperAwesome Permissions.
It creates an instance of a Permissions NestJS Guard, configured for ALL the endpoints/methods in this controller.
We then pass this instance to nestjs @UseGuards.
*/
@UseGuards(
createPermissionsGuard(
{
/**
The default `resource` (eg "document") this Guard will use for `permissions.grantPermit({resource, ...})` (Optional).
__Notes__ :
- Can be overridden per method with `@PermitGrant()`.
- You're advised to have a default `resource` here, at the controller level.
*/
resource: 'document',
/**
Project (i.e map) a `resourceId` on your QueryParams (by default the param named "id"), from string
to what is needed by the ownership hooks (eg a `number`) for this controller (Optional).
__Notes__ :
- It should be able to tolerate if `resourceId` is missing or null etc and not throw.
- We can also have `projectResourceId` as default at module level at `PermissionsModule.forRoot()`,
i.e working for the whole module/app.
Here we can override for this Guard only, if that's needed (rare).
- We can also override all at `@PermitGrant()` level only for that endpoint (even more rare).
*/
projectResourceId: (id) => parseInt(id, 10),
},
/**
Add any relevant PermissionDefinitions as 2nd argument (Optional).
__Notes__:
- If we 've already added the specific `documentPermissionDefinitions` from './document.permissions'
somewhere (either in our module or another controller) we dont need to add them again
(we'll actually get a warning of "redefining action in a PD with same attributes" if we do).
__Notes__:
- The PDs are of type `IPermissionDefinitionStringOwnHooks`, see below.
*/
documentPermissionDefinitions,
/**
Add any relevant PermissionDefinitionDefaults as 3rd argument,
used as defaults only for the PermissionDefinitions at the 2nd argument (Optional).
*/
{ resource: 'document' }
)
)
@Controller('/documents-protected-detailed')
export class DocumentProtectedDetailedController {
constructor(
/**
Optionally, we can inject the SAPermission instance that PermissionsModule has build for us,
so we can manually permit / deny access to resources or do any other things with it - see `securityHole` method below.
*/
@InjectPermissions() private permissions: Permissions
) {}
/**
The @PermitGrant method decorator is optional, it allows us to configure an endpoint.
All its arguments are also optional.
If omitted, the default @PermitGrant is in place, as all methods using the `@UseGuards(createPermissionsGuard(...))` are protected by default.
Pass `@PermitGrant(false)` to disable this - see below.
*/
@PermitGrant({
/**
If the name of the method is different than the action name, we can override it here (Optional).
Ideally we should not, its great if they are consistent.
*/
action: 'read',
/**
If the @Get param for our `resourceId` is different than the default "id",
we can override it with `resourceIdKey` (Optional).
Ideally we should always go with "id" .
*/
resourceIdKey: 'documentId',
/**
If want to override the "resource" this endpoint is dealing with
(i.e the one configured above at the `createPermissionsGuard`), we can override it here (Optional).
*/
resource: 'document',
/**
If want to override the projection function this endpoint is using
(i.e the one configured above at the `createPermissionsGuard`), we can override it here (Optional).
*/
projectResourceId: Number,
})
@Get('/:documentId')
async single(
@Param('documentId', new ParseIntPipe()) id: number,
/**
The @GetPermit parameter decorator injects this method's Permit object into our method (Optional).
Useful for resource filtering, attribute picking etc.
__Notes__:
- It is already configured with the current user, action, resource & optionally resourceId if it exists.
- If `resourceId` exists, then the Guard checks the User's ownership of the specific
resource item and throws a 403 (FORBIDDEN) before even hitting the method body.
*/
@GetPermit() permit: Permit
): Promise<Partial<IDocument>> {
return await permit.pick(ALL_DOCUMENTS.find((doc) => doc.id === id));
}
/**
An empty `@PermitGrant()` can be omitted, if we don't override anything from its args (i.e `PermitGrantArgs`).
In this case "action" equals the method name "list", so its useless.
__Notes__:
- leaving without @PermitGrant() at all means "use default @PermitGrant()", like in this example.
- The default `@PermitGrant()` has all the information it needs:
- `user` from `extractUserFromRequest`, configured at the module.
- `action` by default is the method's name.
- `resource` by default from `createPermissionsGuard` 1st argument `GuardOptions.resource`
- `resourceId` by default reading the request's prop named 'id'.
*/
@PermitGrant()
@Get()
async list(
@GetPermit() permit: Permit,
@Query('any') any?: string
): Promise<Partial<IDocument>[]> {
/**
Inside our method, we decide based on our rules **if we can allow any resource**
OR if we need to filter only user's own.
__Notes:__
- In this example API call, if it has any='true' in its query params
AND the user has `permit.anyGranted` for this action,
then we allow all resources to pass, otherwise we limit only to their own.
We could have thrown Forbidden if `permit.anyGranted` is false,
or have any other kind of behavior, using `Permit` as our permissions guide.
- In a realistic app, this is how you can restrict your DB or API etc results.
The `permit.limitOwn()` method gives you infinite scalability, since its architecture
is agnostic & hence compatible with any kind of data layer.
- In this simplistic example our `permit.limitOwn()` produces just an Array.filter function.
If we wanted to add restrictive clauses to a WHERE SQL query, it would be as easy as:
`query.andWhere(new Brackets(qb => permit.limitOwn(qb)));`
using TypeORM - read more in [`Permit.limitOwn()` docs](https://permissions.docs.superawesome.com/classes/Permit.html#limitOwn).
- In a realistic app, most of this logic would live inside the Service layer,
to which you just pass the `permit` object around for the duration of the request,
for the actual advanced permissions checks and query building to be taking place.
*/
const allowedDocs =
permit.anyGranted && any === 'true'
? ALL_DOCUMENTS
: ALL_DOCUMENTS.filter(permit.limitOwn());
/** pick allowed attributes, depending on ownership of each resource item. */
return await permit.mapPick(allowedDocs);
}
/**
If we want to completely disable the @PermitGrant check for an endpoint/method
and bypass the Guard, then we **must** pass `false` to `@PermitGrant()`.
*/
@PermitGrant(false)
@Post('/security-hole')
securityHole() {
/**
Anyone can access this method bypassing the Guard, even requests without a user.
But we have the injected permissions instance to the rescue!
We can use permissions.grantPermit() the usual way, manually passing our user, resource, action etc, and get back a Permit object and then manually permit or deny actions.
Or we can just introspect our permissions instance, for example:
*/
return [
'These are all actions, roles, resources and PermissionDefinitions of this Permissions instance.',
this.permissions.getActions(),
this.permissions.getRoles(),
this.permissions.getResources(),
this.permissions.getDefinitions({ resource: 'document' }),
];
}
}
</code></pre></div><h1 id="setting-up-the-nestjs-module">Setting up the NestJS module</h1>
<p>The NestJS module is very simple.</p>
<div><pre class="line-numbers"><code class="language-js">// file: ../detailed/example-detailed.module.ts
import * as _ from 'lodash';
import { Module } from '@nestjs/common';
import { PermissionsOwnershipService } from '../permissions/permissions-ownership.service';
import {
PERMISSIONS_OWNERSHIP_SERVICE_TOKEN,
PermissionsModule,
} from '@superawesome/permissions-nestjs';
import { getUser } from '../permissions/getUser';
import { DocumentUnprotectedController } from '../document-unprotected.controller';
import { DocumentProtectedDetailedController } from './document-protected-detailed.controller';
@Module({
imports: [
/**
Use PermissionsModule.forRoot() to configure Permissions for your module.
*/
PermissionsModule.forRoot({
/**
If your req.user object doesnt already comply with [SuperAwesome Permissions IUser](https://permissions.docs.superawesome.com/interfaces/IUser.html),
here you can extract and transform it (Optional).
@param req an expressjs request object
*/
extractUserFromRequest: async (req) => getUser(),
/**
* Here you can override the default reducer for the [limitOwn](https://permissions.docs.superawesome.com/classes/Permit.html#limitOwn) (Optional).
*
* @param user: IUser the request user passed at runtime
*
* @param limitOwneds: TlimitOwned[] an array all the `limitOwn` ownership hooks for the particular user
*/
limitOwnReduce: ({ user, limitOwneds }) =>
_.overSome(limitOwneds.map((limitOwned) => limitOwned({ user }))),
/**
Project (i.e map) a `resourceId` on your QueryParams from string (by default the param named "id"),
to what is needed by the ownership hooks (eg a `number`) for the whole module (Optional).
__Notes__ :
- It should be able to tolerate if `resourceId` is missing or null etc and not throw.
- We can override this `projectResourceId` at the Controller's Guard level at `createPermissionsGuard()`
- We can also override all at `@PermitGrant()` level only a specific endpoint (even more rare).
*/
projectResourceId: (resourceIdStr: string | void) =>
Number(resourceIdStr),
}),
],
controllers: [
DocumentUnprotectedController,
DocumentProtectedDetailedController,
],
providers: [
/**
Using the special PERMISSIONS_OWNERSHIP_SERVICE_TOKEN, we must provide our
`PermissionsOwnershipService` class where our ownership hook methods live.
See `PermissionsOwnershipService` below on how this looks.
*/
{
provide: PERMISSIONS_OWNERSHIP_SERVICE_TOKEN,
useClass: PermissionsOwnershipService,
},
],
})
export class ExampleDetailedModule {}
</code></pre></div><h1 id="the-permissionsownershipservice">The PermissionsOwnershipService</h1>
<p>You will need an @Injectable (a.k.a a Service) that holds all the ownership hooks as methods, provided via the special <code>PERMISSIONS_OWNERSHIP_SERVICE_TOKEN</code> in your module (see above).</p>
<p>In this simple example we don't have any dependencies to inject, but in the real world in this Service you inject you DB repos and any other services you'll need to lookup the actual ownerships of your current user, against the resources they are trying to access.</p>
<p><strong>Note:</strong> If a method name declared in the special PermissionsDefinitions variant <a href="/interfaces/IPermissionDefinitionStringOwnHooks.html">IPermissionDefinitionStringOwnHooks</a> is missing from this <code>PermissionsOwnershipService</code>, you'll get an exception at the module build time.</p>
<div><pre class="line-numbers"><code class="language-js">// file: ../permissions/permissions-ownership.service.ts
import { Injectable } from '@nestjs/common';
import {
isOwner_isDocCreatedByMeAndMyCompanyUsers,
isOwner_isDocCreatedByMeAndMyManagedUsers,
isOwner_isUserCreatorOfDocument,
limitOwned_DocsOfMeAndMyCompanyUsers,
limitOwned_DocsOfMeAndMyManagedUsers,
limitOwned_listUserCreatedDocuments,
} from '@superawesome/permissions/dist/__tests__/data.fixtures';
@Injectable()
export class PermissionsOwnershipService {
// EMPLOYEE
isOwner_isUserCreatorOfDocument = isOwner_isUserCreatorOfDocument;
limitOwned_listUserCreatedDocuments = limitOwned_listUserCreatedDocuments;
// EMPLOYEE_MANAGER
isOwner_isDocCreatedByMeAndMyManagedUsers = isOwner_isDocCreatedByMeAndMyManagedUsers;
limitOwned_DocsOfMeAndMyManagedUsers = limitOwned_DocsOfMeAndMyManagedUsers;
// COMPANY_ADMIN
isOwner_isDocCreatedByMeAndMyCompanyUsers = isOwner_isDocCreatedByMeAndMyCompanyUsers;
limitOwned_DocsOfMeAndMyCompanyUsers = limitOwned_DocsOfMeAndMyCompanyUsers;
}
</code></pre></div><p>That's it, you have all you need to start using permissions-nestjs!</p>
<p>Happy permitting!</p>
</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 = 'additional-page';
var COMPODOC_CURRENT_PAGE_URL = 'reference-&-detailed-example.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>