Important note: This documentation is generated from integration tests, so the examples execute and are tested against.
DO NOT EDIT THIS .md FILE - Its generated from ts-node document-protected-detailed.controller.md.e2e-spec.ts
Lets now see a slightly more complicated example, based on the previous simple example.
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.
// 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' }),
];
}
}
The NestJS module is very simple.
// 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 {}
You will need an @Injectable (a.k.a a Service) that holds all the ownership hooks as methods, provided via the special PERMISSIONS_OWNERSHIP_SERVICE_TOKEN in your module (see above).
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.
Note: If a method name declared in the special PermissionsDefinitions variant IPermissionDefinitionStringOwnHooks is missing from this PermissionsOwnershipService, you'll get an exception at the module build time.
// 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;
}
That's it, you have all you need to start using permissions-nestjs!
Happy permitting!