2 * @file Tenant Context and Multi-Tenancy Middleware
3 * @module middleware/tenant
5 * Comprehensive multi-tenancy middleware providing tenant context extraction, isolation,
6 * and Row-Level Security (RLS) enforcement. Extracts tenant from subdomain or JWT,
7 * validates tenant status, and provides query filtering utilities.
10 * - **Tenant extraction**: From subdomain (app.tenantname.ibghub.com) or JWT tenantId claim
11 * - **Status validation**: Ensures tenant is active before allowing requests
12 * - **MSP mode detection**: Identifies Root MSP tenants with cross-tenant access
13 * - **RLS support**: PostgreSQL Row-Level Security integration via app.current_tenant
14 * - **Query filtering**: SQL WHERE clause generators for tenant isolation
17 * - Root MSP (tenant_id=1): Super-tenant with cross-tenant visibility (is_msp=true)
18 * - Regular tenants: Isolated data visibility (is_msp=false)
20 * Middleware execution order (IMPORTANT):
21 * 1. authenticateToken - Extract JWT and user info
22 * 2. setTenantContext - Extract tenant context from subdomain/JWT
23 * 3. requireTenant - Enforce tenant context exists (optional)
24 * 4. requireRoot - Enforce MSP privileges (optional)
25 * 5. applyRLS - Set PostgreSQL session context (optional)
27 * Public route handling:
28 * - /users/login, /users/request-password-reset, etc. bypass tenant checks
29 * - Unauthenticated requests attempt subdomain-only tenant resolution
32 * - Use X-Tenant-Subdomain header to override subdomain (localhost testing)
33 * @requires db - PostgreSQL connection pool from services/db
34 * @see {@link module:middleware/auth} for authentication middleware
35 * @see {@link https://www.postgresql.org/docs/current/ddl-rowsecurity.html} PostgreSQL RLS docs
37 * // Standard tenant-isolated route
38 * const { setTenantContext, getTenantFilter } = require('./middleware/tenant');
39 * router.get('/customers', authenticateToken, setTenantContext, async (req, res) => {
40 * const filter = getTenantFilter(req);
41 * const customers = await pool.query(`SELECT * FROM customers ${filter.clause}`, filter.values);
44 * // Root MSP cross-tenant route
45 * const { requireRoot } = require('./middleware/tenant');
46 * router.get('/admin/all-tenants', authenticateToken, setTenantContext, requireRoot, handler);
48 * // Request with tenant context
51 * isMsp: false, // MSP privileges flag
52 * subdomain: 'acme' // Resolved subdomain
57 * Middleware to extract and set tenant context from subdomain or user
58 * This middleware should run AFTER authentication middleware
62 * Lazy getter for database pool (avoids circular dependency).
65 * @returns {object} PostgreSQL connection pool
68 if (!pool) pool = require('../services/db');
73 * Extracts and sets tenant context from subdomain or JWT.
75 * Resolves tenant from:
76 * 1. JWT tenantId claim (authenticated requests)
77 * 2. Subdomain in Host header (e.g., acme.ibghub.com → tenant with subdomain='acme')
78 * 3. X-Tenant-Subdomain header (development override for localhost)
80 * Validates tenant status (must be 'active'). Public auth routes bypass tenant checks.
81 * Attaches tenant context to req.tenant for downstream middleware/routes.
82 * @function setTenantContext
84 * @param {object} req - Express request object
85 * @param {object} [req.user] - User object from authenticateToken middleware
86 * @param {number} [req.user.tenantId] - Tenant ID from JWT
87 * @param {boolean} [req.user.is_msp] - MSP flag from JWT
88 * @param {object} req.headers - HTTP headers
89 * @param {string} req.headers.host - Host header with optional subdomain
90 * @param {string} [req.headers['x-tenant-subdomain']] - Dev override for tenant subdomain
91 * @param {string} req.originalUrl - Request URL path
92 * @param {object} res - Express response object
93 * @param {Function} next - Express next middleware function
94 * @returns {void} Attaches req.tenant object and calls next()
95 * @throws {403} Tenant is not active - Tenant exists but status != 'active'
97 * // Authenticated request with JWT tenantId
98 * // JWT: { tenantId: 5, is_msp: false }
99 * // => req.tenant = { id: 5, isMsp: false, subdomain: 'acme' }
101 * // Unauthenticated request with subdomain
102 * // Host: acme.ibghub.com
103 * // => req.tenant = { id: 5, isMsp: false, subdomain: 'acme' }
105 * // Public auth route (bypasses tenant check)
106 * // URL: /users/login
107 * // => req.tenant = { id: null, isMsp: false, subdomain: 'acme' }
109async function setTenantContext(req, res, next) {
111 // Compute hostname without port and derive subdomain
112 const host = req.get('host') || '';
113 const hostname = host.split(':')[0];
114 const subdomainFromHost = hostname.split('.')[0];
116 // For localhost development, allow overriding subdomain via header
117 const devSubdomain = req.get('x-tenant-subdomain');
118 const targetSubdomain = devSubdomain || subdomainFromHost;
120 // Public unauthenticated auth routes should NEVER block on tenant context
121 const publicPaths = new Set([
123 '/users/request-password-reset',
124 '/users/reset-password',
125 '/users/verify-email'
127 const pathNoQuery = (req.originalUrl || req.url || '').split('?')[0];
129 // Default tenant context (none)
133 // Short-circuit ONLY for public auth endpoints
134 if (publicPaths.has(pathNoQuery)) {
135 req.tenant = { id: null, isMsp: false, subdomain: targetSubdomain };
139 console.log(`[Tenant] Host: ${host}, Resolved Hostname: ${hostname}, Subdomain: ${targetSubdomain}`);
141 if (req.user && req.user.tenantId) {
142 // Use tenantId and is_msp from JWT for tenant context
143 tenantId = req.user.tenantId;
144 isMsp = req.user.is_msp || false;
146 // Verify tenant is active
148 const tRes = await getPool().query(
149 'SELECT status FROM tenants WHERE tenant_id = $1',
152 if (tRes.rows.length > 0) {
153 if (tRes.rows[0].status !== 'active') {
154 return res.status(403).json({ error: 'Tenant is not active', status: tRes.rows[0].status });
158 console.warn('[Tenant] Error looking up tenant from JWT:', e.message);
160 // Allow MSP/admin to impersonate tenant via header or query param
161 let impersonateTenantId = req.get('x-impersonate-tenant-id') || req.query.tenant_id || req.query.tenantId;
162 if ((req.user.role === 'msp' || req.user.role === 'admin') && impersonateTenantId) {
163 tenantId = impersonateTenantId;
165 const tRes = await getPool().query(
166 'SELECT is_msp, subdomain, status FROM tenants WHERE tenant_id = $1',
169 if (tRes.rows.length > 0) {
170 // When impersonating, isMsp should be false (act as tenant)
172 if (tRes.rows[0].status !== 'active') {
173 return res.status(403).json({ error: 'Tenant is not active', status: tRes.rows[0].status });
177 console.warn('[Tenant] Error looking up impersonated tenant:', e.message);
179 // Always override context for impersonation
183 subdomain: targetSubdomain
185 console.log(`[Tenant] MSP/Admin impersonating tenant_id=${tenantId} (source: ${impersonateTenantId === req.get('x-impersonate-tenant-id') ? 'header' : 'query'})`);
188 console.log(`[Tenant] Context from JWT: tenant_id=${tenantId}, isMsp=${isMsp}`);
190 // Unauthenticated: try resolving tenant by subdomain if not localhost
192 if (targetSubdomain && targetSubdomain !== 'localhost') {
193 const tenantQuery = await getPool().query(
194 'SELECT tenant_id, is_msp, status FROM tenants WHERE subdomain = $1',
198 if (tenantQuery.rows.length > 0) {
199 const tenantData = tenantQuery.rows[0];
200 tenantId = tenantData.tenant_id;
201 isMsp = tenantData.is_msp;
203 if (tenantData.status !== 'active') {
204 return res.status(403).json({
205 error: 'Tenant is not active',
206 status: tenantData.status
210 console.log(`[Tenant] Resolved tenant ${tenantId} from subdomain ${targetSubdomain}`);
214 console.warn('[Tenant] Skipping tenant lookup for unauthenticated request due to error:', e.message);
218 // Attach tenant info to request
222 subdomain: targetSubdomain
225 console.log(`[Tenant] Final context for request: tenant_id=${tenantId}, isMsp=${isMsp}`);
229 console.error('[Tenant] Error setting tenant context (unexpected):', err);
230 req.tenant = { id: null, isMsp: false };
236 * Enforces tenant context exists on request.
238 * Validates req.tenant.id is set by setTenantContext middleware. Returns 400 error
239 * if tenant context missing. Use on routes requiring strict tenant isolation.
240 * @function requireTenant
241 * @param {object} req - Express request object
242 * @param {object} [req.tenant] - Tenant context from setTenantContext
243 * @param {number} [req.tenant.id] - Tenant ID
244 * @param {object} res - Express response object
245 * @param {Function} next - Express next middleware function
246 * @returns {void} Calls next() if tenant exists, sends 400 otherwise
247 * @throws {400} Tenant context required - Missing or null tenant ID
249 * // Route requiring tenant context
250 * router.get('/customers', authenticateToken, setTenantContext, requireTenant, handler);
254 * Middleware to enforce tenant context is set.
255 * Use this on routes that require tenant isolation.
256 * @function requireTenant
257 * @param {object} req - Express request object
258 * @param {object} res - Express response object
259 * @param {Function} next - Express next middleware function
260 * @returns {void} Calls next() or sends 400 error if no tenant context
262 * router.get('/customers', authenticateToken, setTenantContext, requireTenant, handler);
264function requireTenant(req, res, next) {
265 if (!req.tenant || !req.tenant.id) {
266 return res.status(400).json({
267 error: 'Tenant context required. Please ensure subdomain is set.'
274 * Enforces Root MSP privileges (cross-tenant access).
276 * Validates req.tenant.isMsp is true (set by setTenantContext from JWT). Returns 403
277 * error if not MSP tenant. Use on routes requiring cross-tenant visibility (admin
278 * operations, global reports, tenant management).
279 * @function requireRoot
280 * @param {object} req - Express request object
281 * @param {object} [req.tenant] - Tenant context from setTenantContext
282 * @param {boolean} [req.tenant.isMsp] - MSP privileges flag
283 * @param {object} res - Express response object
284 * @param {Function} next - Express next middleware function
285 * @returns {void} Calls next() if MSP, sends 403 otherwise
286 * @throws {403} Root tenant privileges required - User is not Root MSP
288 * // Root MSP-only route
289 * const { setTenantContext, requireRoot } = require('./middleware/tenant');
290 * router.get('/admin/all-customers', authenticateToken, setTenantContext, requireRoot, handler);
294 * Middleware to require root tenant privileges.
295 * Use this on routes that need cross-tenant access.
296 * @function requireRoot
297 * @param {object} req - Express request object
298 * @param {object} res - Express response object
299 * @param {Function} next - Express next middleware function
300 * @returns {void} Calls next() or sends 403 error if not root MSP
302function requireRoot(req, res, next) {
303 if (!req.tenant || !req.tenant.isMsp) {
304 return res.status(403).json({
305 error: 'Root tenant privileges required for this operation'
312 * Helper function to get tenant WHERE clause for queries
313 * Root tenant users get no filter (can see all tenants),
314 * regular users get scoped to their tenant,
315 * and routes can disable tenant filtering entirely.
316 * @param {object} req - Express request with req.tenant attached
317 * @param {string} [tableAlias] - Optional table alias (e.g., 't' for tickets)
318 * @param {boolean} [enforce] - If false, disables tenant filter
319 * @returns {object} - { clause: string, params: array, nextParamIndex: number }
321function getTenantFilter(req, tableAlias = '', enforce = true) {
323 return { clause: '', params: [], nextParamIndex: 1 };
326 const prefix = tableAlias ? `${tableAlias}.` : '';
328 // MSP/root users: use impersonated tenant if provided
329 if (req.tenant && req.tenant.isMsp) {
330 // Check for impersonated tenant in query/body (support both camelCase and snake_case)
331 const impersonatedTenantId = req.query?.tenantId || req.query?.tenant_id || req.body?.tenantId || req.body?.tenant_id;
332 if (impersonatedTenantId) {
334 clause: `${prefix}tenant_id = $1`,
335 params: [impersonatedTenantId],
339 // If no impersonation, show all tenants (no filter)
340 return { clause: '', params: [], nextParamIndex: 1 };
343 // Regular users only see their own tenant's data
344 if (req.tenant && req.tenant.id) {
346 clause: `${prefix}tenant_id = $1`,
347 params: [req.tenant.id],
352 // No tenant context — return restrictive fallback
354 clause: `${prefix}tenant_id IS NULL`,
361 * Helper function to execute a query with tenant context set via RLS.
362 * This wraps the query in a transaction and sets session variables.
363 * @function withTenantContext
364 * @param {object} client - pg client or pool
365 * @param {string} tenantId - UUID of the tenant
366 * @param {boolean} isMsp - Whether this is an MSP user with cross-tenant access
367 * @param {Function} callback - Async function that performs queries
368 * @returns {Promise<*>} Result from the callback function
370async function withTenantContext(client, tenantId, isMsp, callback) {
371 const shouldRelease = !client.query; // If passed a pool, we need to get a client
372 const dbClient = shouldRelease ? await pool.connect() : client;
375 await dbClient.query('BEGIN');
377 // Set RLS session variables
378 await dbClient.query('SELECT set_config($1, $2, true)', ['app.tenant_id', tenantId]);
379 await dbClient.query('SELECT set_config($1, $2, true)', [
384 console.log(`[RLS] Set tenant context: ${tenantId}, MSP: ${isMsp}`);
386 // Execute the callback with the client
387 const result = await callback(dbClient);
389 await dbClient.query('COMMIT');
392 await dbClient.query('ROLLBACK');
393 console.error('[RLS] Transaction error:', err);
403 * Express middleware wrapper for withTenantContext.
404 * Sets up RLS context for the request and attaches db client.
405 * Use AFTER setTenantContext and authentication.
407 * @param {object} req - Express request object
408 * @param {object} res - Express response object
409 * @param {Function} next - Express next middleware function
410 * @returns {Promise<void>} Calls next() or sends error response
412async function applyRLS(req, res, next) {
413 if (!req.tenant || !req.tenant.id) {
414 return res.status(400).json({
415 error: 'Tenant context required before applying RLS'
420 const client = await getPool().connect();
423 await client.query('SELECT set_config($1, $2, true)', ['app.tenant_id', req.tenant.id]);
424 await client.query('SELECT set_config($1, $2, true)', [
426 req.tenant.isMsp ? 'on' : 'off'
429 // Attach client to request so routes can use it
430 req.dbClient = client;
432 // Clean up client after response
433 res.on('finish', () => {
435 req.dbClient.release();
441 console.error('[RLS] Error applying RLS:', err);
442 res.status(500).json({ error: 'Failed to apply tenant security' });