Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
This commit is contained in:
237
api/src/middleware/tenant-auth.ts
Normal file
237
api/src/middleware/tenant-auth.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Tenant-Aware Authentication Middleware
|
||||
* Enforces tenant isolation in all queries
|
||||
* Superior to Azure with more flexible permission model
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify'
|
||||
import { identityService, TokenValidationResult } from '../services/identity.js'
|
||||
import { getDb } from '../db/index.js'
|
||||
import { logger } from '../lib/logger.js'
|
||||
|
||||
export interface TenantContext {
|
||||
tenantId?: string
|
||||
userId: string
|
||||
email: string
|
||||
role: string
|
||||
tenantRole?: string
|
||||
permissions: Record<string, any>
|
||||
isSystemAdmin: boolean
|
||||
}
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
tenantContext?: TenantContext
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tenant context from request
|
||||
*/
|
||||
export async function extractTenantContext(
|
||||
request: FastifyRequest
|
||||
): Promise<TenantContext | null> {
|
||||
// Get token from Authorization header
|
||||
const authHeader = request.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7)
|
||||
|
||||
// Validate token
|
||||
const validation = await identityService.validateToken(token)
|
||||
if (!validation.valid || !validation.userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const db = getDb()
|
||||
const userResult = await db.query('SELECT id, email, role FROM users WHERE id = $1', [
|
||||
validation.userId,
|
||||
])
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = userResult.rows[0]
|
||||
const isSystemAdmin = user.role === 'ADMIN'
|
||||
|
||||
// Get tenant information if tenant ID is present
|
||||
let tenantRole: string | undefined
|
||||
let tenantPermissions: Record<string, any> = {}
|
||||
|
||||
if (validation.tenantId) {
|
||||
const tenantUserResult = await db.query(
|
||||
`SELECT role, permissions FROM tenant_users
|
||||
WHERE tenant_id = $1 AND user_id = $2`,
|
||||
[validation.tenantId, validation.userId]
|
||||
)
|
||||
|
||||
if (tenantUserResult.rows.length > 0) {
|
||||
tenantRole = tenantUserResult.rows[0].role
|
||||
tenantPermissions = tenantUserResult.rows[0].permissions || {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tenantId: validation.tenantId,
|
||||
userId: validation.userId,
|
||||
email: validation.email || user.email,
|
||||
role: user.role,
|
||||
tenantRole,
|
||||
permissions: { ...tenantPermissions, ...(validation.permissions || {}) },
|
||||
isSystemAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant-aware authentication middleware
|
||||
*/
|
||||
export async function tenantAuthMiddleware(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
// Skip auth for health check and GraphQL introspection
|
||||
if (request.url === '/health' || request.method === 'OPTIONS') {
|
||||
return
|
||||
}
|
||||
|
||||
const context = await extractTenantContext(request)
|
||||
|
||||
if (!context) {
|
||||
// Allow unauthenticated requests - GraphQL will handle auth per query/mutation
|
||||
return
|
||||
}
|
||||
|
||||
// Attach tenant context to request
|
||||
request.tenantContext = context
|
||||
|
||||
// Set tenant context in database session for RLS policies
|
||||
const db = getDb()
|
||||
if (context.userId) {
|
||||
await db.query(`SET LOCAL app.current_user_id = $1`, [context.userId])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication middleware
|
||||
*/
|
||||
export function requireAuth(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): TenantContext {
|
||||
const context = request.tenantContext
|
||||
|
||||
if (!context) {
|
||||
reply.code(401).send({
|
||||
error: 'Authentication required',
|
||||
code: 'UNAUTHENTICATED',
|
||||
})
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Require tenant membership middleware
|
||||
*/
|
||||
export function requireTenant(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): TenantContext {
|
||||
const context = requireAuth(request, reply)
|
||||
|
||||
if (!context.tenantId && !context.isSystemAdmin) {
|
||||
reply.code(403).send({
|
||||
error: 'Tenant membership required',
|
||||
code: 'TENANT_REQUIRED',
|
||||
})
|
||||
throw new Error('Tenant membership required')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Require specific tenant role
|
||||
*/
|
||||
export function requireTenantRole(
|
||||
allowedRoles: string[]
|
||||
) {
|
||||
return (request: FastifyRequest, reply: FastifyReply): TenantContext => {
|
||||
const context = requireTenant(request, reply)
|
||||
|
||||
if (context.isSystemAdmin) {
|
||||
return context
|
||||
}
|
||||
|
||||
if (!context.tenantRole || !allowedRoles.includes(context.tenantRole)) {
|
||||
reply.code(403).send({
|
||||
error: 'Insufficient permissions',
|
||||
code: 'FORBIDDEN',
|
||||
required: allowedRoles,
|
||||
current: context.tenantRole,
|
||||
})
|
||||
throw new Error('Insufficient tenant permissions')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require system admin middleware
|
||||
*/
|
||||
export function requireSystemAdmin(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
): TenantContext {
|
||||
const context = requireAuth(request, reply)
|
||||
|
||||
if (!context.isSystemAdmin) {
|
||||
reply.code(403).send({
|
||||
error: 'System administrator access required',
|
||||
code: 'FORBIDDEN',
|
||||
})
|
||||
throw new Error('System administrator access required')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter resources by tenant automatically
|
||||
*/
|
||||
export function filterByTenant(
|
||||
query: string,
|
||||
params: any[],
|
||||
context: TenantContext
|
||||
): { query: string; params: any[] } {
|
||||
// If system admin, don't filter
|
||||
if (context.isSystemAdmin) {
|
||||
return { query, params }
|
||||
}
|
||||
|
||||
// If no tenant context, filter to show only system resources (tenant_id IS NULL)
|
||||
if (!context.tenantId) {
|
||||
const whereClause = query.includes('WHERE') ? 'AND tenant_id IS NULL' : 'WHERE tenant_id IS NULL'
|
||||
return {
|
||||
query: `${query} ${whereClause}`,
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by tenant_id
|
||||
const whereClause = query.includes('WHERE')
|
||||
? `AND tenant_id = $${params.length + 1}`
|
||||
: `WHERE tenant_id = $${params.length + 1}`
|
||||
|
||||
return {
|
||||
query: `${query} ${whereClause}`,
|
||||
params: [...params, context.tenantId],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user