UNPKG

@superawesome/permissions-nestjs

Version:

NestJS Guard & Decorators for @superawesome/permissions, promoting orthogonal fine-grained API access control to resources.

147 lines 9.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createPermissionsGuard = exports.AbstractPermissionsGuard = void 0; const tslib_1 = require("tslib"); const _ = require("lodash"); // remove lodash - see https://stackoverflow.com/questions/1187518/how-to-get-the-difference-between-two-arrays-in-javascript const common_1 = require("@nestjs/common"); const core_1 = require("@nestjs/core"); const permissions_1 = require("@superawesome/permissions"); const inject_permissions_decorator_1 = require("./inject-permissions.decorator"); const types_1 = require("./types"); const utils_1 = require("./utils"); /** * Acts only as an interface * @internal */ class AbstractPermissionsGuard { constructor( // dummy, used only as interface permissions, permissionsOwnershipService, reflector, extractUserFromRequest) { this.permissions = permissions; this.permissionsOwnershipService = permissionsOwnershipService; this.reflector = reflector; this.extractUserFromRequest = extractUserFromRequest; } } exports.AbstractPermissionsGuard = AbstractPermissionsGuard; /** * The factory function that creates the customised Guard for a Controller. * * @param guardOptions see IGuardOptions * @param permissionDefinitionStringOwnHooks * @param pdDefaults */ exports.createPermissionsGuard = (guardOptions = {}, permissionDefinitionStringOwnHooks, pdDefaults = {}) => { let ConcretePermissionsGuard = /** @class */ (() => { let ConcretePermissionsGuard = class ConcretePermissionsGuard extends AbstractPermissionsGuard { constructor(permissions, permissionsOwnershipService, reflector, extractUserFromRequest, defaultProjectResourceId) { super(permissions, permissionsOwnershipService, reflector, extractUserFromRequest); this.permissions = permissions; this.permissionsOwnershipService = permissionsOwnershipService; this.reflector = reflector; this.extractUserFromRequest = extractUserFromRequest; this.defaultProjectResourceId = defaultProjectResourceId; // 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 = permissionDefinitionStringOwnHooks.map(pdStringOwnHooks => { const realPD = _.omit(pdStringOwnHooks, hookNames); // only static array works without any;-) _.each(hookNames, hookName => { if (_.isString(pdStringOwnHooks[hookName])) { if (!_.isFunction(this.permissionsOwnershipService[pdStringOwnHooks[hookName]])) throw new common_1.HttpException(`SA-Permissions NestJS: missing service method for "${hookName}" \`${pdStringOwnHooks[hookName]}\``, common_1.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) { let permitGrant = this.reflector.get('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 common_1.HttpException('SA-Permissions NestJS: `resource` to permit.grantPermit() against is not configured for this route', common_1.HttpStatus.INTERNAL_SERVER_ERROR); const user = await this.extractUserFromRequest(req); const resourceIdKey = permitGrant.resourceIdKey || 'id'; const projectResourceId = 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 = await this.permissions.grantPermit(grantPermitQuery); // check requested `resourceIds` exist in permit.listOwn if (!permit.anyGranted && _.isArray(resourceIds)) { let ownResourceIds = []; let nonOwnResourceIds = []; // 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 common_1.HttpException(`SA-Permissions NestJS: Cant access Non-Own ResourceIds ids: [${nonOwnResourceIds.join(', ')}]`, common_1.HttpStatus.FORBIDDEN); } } req.__permissions_permit__ = permit; return permit.granted; } }; ConcretePermissionsGuard = tslib_1.__decorate([ common_1.Injectable(), tslib_1.__param(0, inject_permissions_decorator_1.InjectPermissions()), tslib_1.__param(1, common_1.Inject(types_1.PERMISSIONS_OWNERSHIP_SERVICE_TOKEN)), tslib_1.__param(3, common_1.Inject(types_1.PERMISSIONS_EXTRACT_USER_FROM_REQUEST_TOKEN)), tslib_1.__param(4, common_1.Inject(types_1.PERMISSIONS_MAP_RESOURCE_ID_TOKEN)), tslib_1.__metadata("design:paramtypes", [permissions_1.Permissions, Object, core_1.Reflector, Function, Function]) ], ConcretePermissionsGuard); return ConcretePermissionsGuard; })(); // 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 utils_1.nameAClass(`ConcretePermissionsGuard_${utils_1.randomString()}`, ConcretePermissionsGuard); }; //# sourceMappingURL=createPermissionsGuard.js.map