Skip to main content

Role-Based Access Control (RBAC) in Abimongo Core

Abimongo Core includes a flexible, tenant-aware Role-Based Access Control (RBAC) system built to protect database operations and GraphQL resolvers. It supports declarative permissions, programmatic checks, caching for performance, and integrates with middleware (Express / GraphQL) and multi-tenant flows.


Key features​

  • Declarative string-based permissions (e.g. read, update:own, admin:*)
  • Role-based authorization (assign permissions to roles)
  • GraphQL resolver middleware (enforceRBAC) for automatic enforcement
  • Tenant-aware checks and cache invalidation
  • Redis-backed permission cache for performance (optional)
  • Middleware-friendly for Express and GraphQL servers

Overview — primary APIs​

  • enforceRBAC(handler, requiredPermission) — Wrap resolver/handler and enforce a permission.
  • checkPermission(userRole, requiredPermission) — Programmatic permission check (boolean).
  • invalidateTenantCache(tenantId, role) — Invalidate RBAC cache for a tenant and role.

Each function is designed to work with the project's tenant context helpers (for example, getTenantDB and TenantContext).


enforceRBAC​

Wrap resolvers or route handlers with RBAC enforcement. enforceRBAC checks the caller's permissions (including ownership/scoped rules) and throws a clear error if unauthorized.

Signature:

enforceRBAC<T>(
handler: (parent: any, args: any, context: any, info?: any) => Promise<T>,
requiredPermission: string
): (parent: any, args: any, context: any, info?: any) => Promise<T>

Example (GraphQL resolvers):

import { enforceRBAC } from 'abimongo_core';

const resolvers = {
Query: {
users: enforceRBAC(async (_, args, context) => {
const db = await getTenantDB(context.user.tenantId);
return db.collection('users').find({ tenantId: context.user.tenantId }).toArray();
}, 'read'),

adminUsers: enforceRBAC(async (_, args, context) => {
const db = await getTenantDB(context.user.tenantId);
return db.collection('users').find({ role: 'admin' }).toArray();
}, 'admin:read')
},

Mutation: {
createUser: enforceRBAC(async (_, { input }, context) => {
const db = await getTenantDB(context.user.tenantId);
const result = await db.collection('users').insertOne({ ...input, tenantId: context.user.tenantId });
return result;
}, 'create'),

deleteUser: enforceRBAC(async (_, { id }, context) => {
const db = await getTenantDB(context.user.tenantId);
await db.collection('users').deleteOne({ _id: new ObjectId(id), tenantId: context.user.tenantId });
return true;
}, 'delete')
}
};

Notes:

  • enforceRBAC should receive a context that includes the authenticated user (with role and tenantId).
  • The middleware supports ownership-based semantics (e.g., read:own) by passing resource identifiers and relying on checkPermission or the internal ownership checks.

checkPermission​

Programmatic permission check useful in services or custom middleware.

Signature:

checkPermission(userRole: Role | string, requiredPermission: string): boolean

Example:

import { checkPermission, Role } from 'abimongo_core';

const user = { role: Role.USER, tenantId: 'tenant-123' };

if (checkPermission(user.role, 'read')) {
// allowed
}

if (!checkPermission(user.role, 'admin:manage')) {
// denied
}

Implementation notes:

  • The default role-to-permissions map is configurable; it commonly includes admin overrides (e.g. admin: ['*']).
  • checkPermission may support wildcards and hierarchical checks (e.g., permission report:* or admin:*).

invalidateTenantCache​

Invalidate RBAC caches for a given tenant and role. Useful after permission changes (for a user or tenant configuration).

Signature:

invalidateTenantCache(tenantId: string, role: Role | string): Promise<void>

Example:

import { invalidateTenantCache, Role } from 'abimongo_core';

async function updateUserRole(userId: string, newRole: Role, tenantId: string) {
const db = await getTenantDB(tenantId);
await db.collection('users').updateOne({ _id: new ObjectId(userId) }, { $set: { role: newRole } });

// Invalidate permission cache so changes take effect immediately
await invalidateTenantCache(tenantId, newRole);
}

Roles & default permissions​

A typical set of roles (customizable) might be:

enum Role {
ADMIN = 'admin',
MODERATOR = 'moderator',
EDITOR = 'editor',
USER = 'user',
GUEST = 'guest'
}

Default permissions (example mapping):

const rolePermissions = {
admin: ['*'],
moderator: ['read', 'update', 'create', 'moderate'],
editor: ['read', 'update', 'create:own'],
user: ['read:own', 'update:own', 'create:own'],
guest: ['read:public']
};

Design notes:

  • admin: ['*'] is a convenient default for full access but can be restricted.
  • Permission mappings are often loaded from configuration or a tenant-scoped store for multi-tenant setups.

Permission patterns​

Use these common patterns to express authorization needs:

  1. Basic CRUD permissions (global): read, create, update, delete.
  2. Scoped/resource permissions: admin:read, report:generate, user:create.
  3. Ownership-based permissions: read:own, update:own, delete:own.
  4. Visibility-based permissions: read:public, read:private.

Express integration​

Add small middleware helpers that leverage checkPermission to protect routes.

import express from 'express';
import { checkPermission } from 'abimongo_core';

const app = express();

const rbacMiddleware = (requiredPermission: string) => (req: any, res: any, next: any) => {
const user = req.user;
if (!user) return res.status(401).json({ error: 'Authentication required' });
if (!checkPermission(user.role, requiredPermission)) return res.status(403).json({ error: 'Insufficient permissions' });
next();
};

app.get('/api/users', rbacMiddleware('read'), async (req, res) => {
const users = await getUsersFromDB(req.user.tenantId);
res.json(users);
});

Notes:

  • Middleware should assume req.user contains the authenticated user (role + tenantId).
  • For GraphQL, prefer enforceRBAC on resolvers to maintain consistent behavior.

Multi-tenant RBAC​

RBAC integrates with tenant-aware helpers so permission checks are scoped to tenant context by default.

Example tenant-aware resolver:

interface UserContext { user: { id: string; role: Role; tenantId: string }; db?: Db; collection?: string }

const tenantAwareResolver = enforceRBAC(async (_, args, context: UserContext) => {
const { user } = context;
const db = await getTenantDB(user.tenantId);
return db.collection('documents').find({ tenantId: user.tenantId }).toArray();
}, 'read');

Cache invalidation example (tenant-level permission update):

async function updateTenantPermissions(tenantId: string, updates: any) {
await updateTenantConfig(tenantId, updates);
const roles = [Role.ADMIN, Role.USER, Role.MODERATOR];
await Promise.all(roles.map(role => invalidateTenantCache(tenantId, role)));
}

Advanced patterns​

Conditional/ownership checks​

Sometimes you need to allow users to operate on their own resources but deny access otherwise.

const conditionalResolver = enforceRBAC(async (_, { userId }, context: UserContext) => {
const { user } = context;
const db = await getTenantDB(user.tenantId);
if (userId === user.id || checkPermission(user.role, 'admin:read')) {
return db.collection('users').findOne({ _id: new ObjectId(userId), tenantId: user.tenantId });
}
throw new Error('Unauthorized: Can only access own profile');
}, 'read:own');

Resource-based permissions​

Use resource/action pairs to form permissions dynamically.

function checkResourcePermission(userRole: Role, resource: string, action: string): boolean {
const permission = `${resource}:${action}`;
return checkPermission(userRole, permission);
}

Hierarchical/wildcard permissions​

Support wildcards and hierarchical checks for compact permission sets.

function hasPermissionOrHigher(userRole: Role, basePermission: string): boolean {
if (checkPermission(userRole, basePermission)) return true;
if (checkPermission(userRole, 'admin:*')) return true;
const parts = basePermission.split(':');
if (parts.length > 1) {
const wildcardPermission = `${parts[0]}:*`;
return checkPermission(userRole, wildcardPermission);
}
return false;
}

Operational guidance & best practices​

  • Keep permission definitions small and descriptive (e.g., read:own, report:generate).
  • Favor scope-specific permissions over broad * when possible.
  • In multi-tenant deployments, keep permission mappings tenant-scoped when different tenants require different rules.
  • After changing role or tenant permissions, call invalidateTenantCache to avoid stale checks.
  • Log RBAC denials with context (user id, tenant id, attempted permission) for auditing.

Troubleshooting​

  • If legitimate users are denied, check the effective permission list for their role and tenant.
  • Verify the GraphQL/HTTP context includes user.role and user.tenantId.
  • Ensure cache invalidation runs after permission or role updates.

Where to go next​


Reference​

For implementation details see packages/core/src/lib-core/ (search for rbac, enforceRBAC, and invalidateTenantCache).