Skip to main content

Authorization

Role-based access control (RBAC) and workspace-level permissions.

Authorization Model

┌─────────────────────────────────────────────────────────────────────────┐
│ Authorization Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Request │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Authenticate │ ◀─── Is user logged in? │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │Load Workspace│ ◀─── Does workspace exist? │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ BOLA Check │ ◀─── Is user member or owner? │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Permission │ ◀─── Does user have required permission? │
│ │ Check │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Authorized │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Workspace Roles

RoleDescriptionUse Case
ownerWorkspace creatorFull control
adminAdministratorManage members, settings
memberRegular memberQuery, view sources
viewerRead-onlyQuery only

Permission Flags

const permissions = {
canQuery: Boolean, // Ask questions
canViewSources: Boolean, // View source documents
canInvite: Boolean, // Invite new members
canManageSync: Boolean, // Trigger syncs
canEditSettings: Boolean, // Modify workspace settings
};

Role-Permission Matrix

PermissionOwnerAdminMemberViewer
canQuery
canViewSources
canInvite
canManageSync
canEditSettings
Delete Workspace

BOLA Protection

Broken Object Level Authorization (BOLA) protection ensures users can only access their own workspaces:

// middleware/loadWorkspace.js

export const loadWorkspace = async (req, res, next) => {
const workspaceId = req.headers['x-workspace-id'];

const workspace = await Workspace.findById(workspaceId);

if (!workspace) {
throw new AppError('Workspace not found', 404);
}

// BOLA Check: Verify user belongs to this workspace
const userId = req.user._id.toString();
const isMember = workspace.members?.some(
m => m.user?.toString() === userId && m.status === 'active'
);
const isOwner = workspace.owner?.toString() === userId;

if (!isMember && !isOwner) {
logger.warn('BOLA attempt detected', {
event: 'bola_attempt',
userId: req.user._id,
workspaceId,
ip: req.ip,
});
throw new AppError('Access denied to this workspace', 403);
}

req.workspace = workspace;
next();
};

Permission Middleware

// middleware/workspaceAuth.js

export const requireWorkspaceAccess = (permission = 'canQuery') => {
return async (req, res, next) => {
const workspace = req.workspace;
const userId = req.user._id.toString();

// Owner has all permissions
if (workspace.owner?.toString() === userId) {
return next();
}

// Find member
const member = workspace.members?.find(
m => m.user?.toString() === userId && m.status === 'active'
);

if (!member) {
throw new AppError('Not a workspace member', 403);
}

// Check specific permission
if (!member.permissions?.[permission]) {
logger.warn('Permission denied', {
event: 'permission_denied',
userId: req.user._id,
workspaceId: workspace._id,
permission,
});
throw new AppError(`Permission denied: ${permission}`, 403);
}

next();
};
};

export const requireWorkspaceOwner = async (req, res, next) => {
const workspace = req.workspace;
const userId = req.user._id.toString();

if (workspace.owner?.toString() !== userId) {
throw new AppError('Owner access required', 403);
}

next();
};

export const canInviteMembers = requireWorkspaceAccess('canInvite');
export const canManageSync = requireWorkspaceAccess('canManageSync');

Route Protection Examples

// routes/ragRoutes.js

router.post('/ask',
authenticate, // 1. Must be logged in
loadWorkspace, // 2. Load workspace, BOLA check
requireWorkspaceAccess('canQuery'), // 3. Check canQuery permission
ragController.ask
);

// routes/workspaceRoutes.js

router.delete('/:id',
authenticate,
loadWorkspace,
requireWorkspaceOwner, // Only owner can delete
workspaceController.delete
);

router.post('/:id/members',
authenticate,
loadWorkspace,
requireWorkspaceAccess('canInvite'), // Must have invite permission
workspaceController.addMember
);

Database-Level Tenant Isolation

The platform uses AsyncLocalStorage to automatically filter queries:

// services/tenantIsolation.js

import { AsyncLocalStorage } from 'async_hooks';

const tenantContext = new AsyncLocalStorage();

export function withTenantContext(context, fn) {
return tenantContext.run(context, fn);
}

export function tenantIsolationPlugin(schema) {
// Auto-filter all queries by workspaceId
schema.pre(['find', 'findOne', 'countDocuments'], function() {
const ctx = tenantContext.getStore();
if (ctx?.workspaceId) {
this.where({ workspaceId: ctx.workspaceId });
}
});

// Detect cross-tenant access
schema.post(['find', 'findOne'], function(docs) {
const ctx = tenantContext.getStore();
if (!ctx?.workspaceId) return;

const results = Array.isArray(docs) ? docs : [docs].filter(Boolean);
for (const doc of results) {
if (doc.workspaceId?.toString() !== ctx.workspaceId) {
logger.error('Cross-tenant access detected!', {
event: 'cross_tenant_access',
requestedWorkspace: ctx.workspaceId,
documentWorkspace: doc.workspaceId,
});
throw new Error('Cross-tenant access violation');
}
}
});
}

Vector Store Isolation

Every Qdrant query MUST include workspace filter:

// services/rag/queryRetrieval.js

export function buildQdrantFilter(filters, workspaceId) {
if (!workspaceId) {
throw new Error('workspaceId is required for vector store queries');
}

return {
must: [
{ key: 'metadata.workspaceId', match: { value: workspaceId } },
// Additional filters...
]
};
}

Audit Trail

All authorization events are logged:

logger.info('Authorization event', {
event: 'access_granted',
userId: req.user._id,
workspaceId: req.workspace._id,
permission: 'canQuery',
resource: '/api/v1/rag',
timestamp: new Date(),
});