UNPKG

@mamoorali295/rbac

Version:

Complete RBAC (Role-Based Access Control) system for Node.js with Express middleware, NestJS integration, GraphQL support, MongoDB & PostgreSQL support, modern admin dashboard, TypeScript support, and dynamic permission management

190 lines (189 loc) 9.25 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.authDirectiveTransformer = authDirectiveTransformer; const utils_1 = require("@graphql-tools/utils"); const graphql_1 = require("graphql"); const RBAC_1 = require("../../RBAC"); /** * GraphQL directive transformer for RBAC authentication and authorization. * Can auto-infer permissions from field names or use explicit configuration. * * @example * ```graphql * type Query { * # Auto-inferred permissions * billingInvoices: [Invoice] @auth * * # Explicit permissions * adminReset: Boolean @auth(feature: "admin", permission: "sudo") * * # Feature-level permission (infers permission from field name) * billingCreate(input: CreateInput): Invoice @auth(feature: "billing") * } * * type Mutation { * # Auto-inferred: feature="user", permission="create" * createUser(input: CreateUserInput): User @auth * * # Auto-inferred: feature="user", permission="update" * updateUser(id: ID!, input: UpdateUserInput): User @auth * * # Auto-inferred: feature="user", permission="delete" * deleteUser(id: ID!): Boolean @auth * } * ``` */ function authDirectiveTransformer(schema, directiveName = 'auth') { return (0, utils_1.mapSchema)(schema, { [utils_1.MapperKind.OBJECT_FIELD]: (fieldConfig) => { var _a; const authDirective = (_a = (0, utils_1.getDirective)(schema, fieldConfig, directiveName)) === null || _a === void 0 ? void 0 : _a[0]; if (authDirective) { const { resolve = graphql_1.defaultFieldResolver } = fieldConfig; const { feature, permission } = authDirective; fieldConfig.resolve = function (source, args, context, info) { return __awaiter(this, void 0, void 0, function* () { try { if (!RBAC_1.RBAC['config'] || !RBAC_1.RBAC['initialized'] || !RBAC_1.RBAC['dbAdapter']) { throw new graphql_1.GraphQLError('RBAC system not initialized', { extensions: { code: 'UNAUTHENTICATED' } }); } // Extract user identity from GraphQL context const userIdentity = yield getUserIdentityFromContext(context); // Get feature and permission const { feature: resolvedFeature, permission: resolvedPermission } = getFeatureAndPermission(info, feature, permission); // Check permissions const user = yield RBAC_1.RBAC['dbAdapter'].findUserByUserIdWithRole(userIdentity.user_id); if (!user) { throw new graphql_1.GraphQLError('User not found in RBAC system', { extensions: { code: 'UNAUTHENTICATED' } }); } const role = user.role; if (!role || !role.features) { throw new graphql_1.GraphQLError('No role or features assigned', { extensions: { code: 'FORBIDDEN' } }); } const userFeature = role.features.find((f) => f.feature.name === resolvedFeature); if (!userFeature) { throw new graphql_1.GraphQLError(`Access denied to feature: ${resolvedFeature}`, { extensions: { code: 'FORBIDDEN' } }); } const hasPermission = userFeature.permissions.some((p) => p.name === resolvedPermission); if (!hasPermission) { throw new graphql_1.GraphQLError(`Permission denied: ${resolvedPermission} on ${resolvedFeature}`, { extensions: { code: 'FORBIDDEN' } }); } return resolve.call(this, source, args, context, info); } catch (error) { if (error instanceof graphql_1.GraphQLError) { throw error; } throw new graphql_1.GraphQLError('Internal server error during permission check', { extensions: { code: 'INTERNAL_SERVER_ERROR' } }); } }); }; } return fieldConfig; } }); } function getUserIdentityFromContext(context) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; if ((_a = RBAC_1.RBAC['config']) === null || _a === void 0 ? void 0 : _a.authAdapter) { // For GraphQL, we need to adapt the Express-style authAdapter // Assuming context contains request object or user info const request = context.req || context.request || context; return yield RBAC_1.RBAC['config'].authAdapter(request); } // Fallback: extract from context const user_id = ((_b = context.user) === null || _b === void 0 ? void 0 : _b.id) || ((_c = context.user) === null || _c === void 0 ? void 0 : _c.user_id) || context.user_id || context.userId; const email = ((_d = context.user) === null || _d === void 0 ? void 0 : _d.email) || context.email; if (!user_id) { throw new graphql_1.GraphQLError('Unable to determine user identity. Provide authAdapter or attach user info to GraphQL context.', { extensions: { code: 'UNAUTHENTICATED' } }); } return { user_id, email }; }); } function getFeatureAndPermission(info, explicitFeature, explicitPermission) { if (explicitFeature && explicitPermission) { return { feature: explicitFeature, permission: explicitPermission }; } const fieldName = info.fieldName.toLowerCase(); const parentTypeName = info.parentType.name.toLowerCase(); // If feature is explicitly provided, infer permission from field name if (explicitFeature) { const feature = explicitFeature; let permission = 'read'; if (fieldName.startsWith('create') || fieldName.startsWith('add')) { permission = 'create'; } else if (fieldName.startsWith('update') || fieldName.startsWith('edit') || fieldName.startsWith('modify')) { permission = 'update'; } else if (fieldName.startsWith('delete') || fieldName.startsWith('remove')) { permission = 'delete'; } else if (fieldName.includes('sudo') || fieldName.includes('admin')) { permission = 'sudo'; } else if (parentTypeName === 'mutation') { permission = 'create'; // Default for mutations } return { feature, permission }; } // Auto-infer both feature and permission let feature = 'default'; let permission = 'read'; // Extract feature from field name (e.g., billingInvoices -> billing) const featureMatch = fieldName.match(/^([a-z]+)[A-Z]/); if (featureMatch) { feature = featureMatch[1]; } else { // If no camelCase pattern, use first word or entire field name const words = fieldName.split(/(?=[A-Z])/); feature = words[0] || 'default'; } // Infer permission from field name and operation type if (fieldName.includes('sudo') || fieldName.includes('admin')) { permission = 'sudo'; } else if (parentTypeName === 'mutation') { if (fieldName.startsWith('create') || fieldName.startsWith('add')) { permission = 'create'; } else if (fieldName.startsWith('update') || fieldName.startsWith('edit') || fieldName.startsWith('modify')) { permission = 'update'; } else if (fieldName.startsWith('delete') || fieldName.startsWith('remove')) { permission = 'delete'; } else { permission = 'create'; // Default for mutations } } else { permission = 'read'; // Default for queries } return { feature, permission }; }