@superawesome/permissions-nestjs
Version:
NestJS Guard & Decorators for @superawesome/permissions, promoting orthogonal fine-grained API access control to resources.
147 lines • 9.68 kB
JavaScript
;
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