381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
|
|
/**
|
||
|
|
* Unified Permission Checking Utility
|
||
|
|
*
|
||
|
|
* This module provides a centralized system for checking permissions across
|
||
|
|
* both global user roles and tenant-specific roles.
|
||
|
|
*
|
||
|
|
* WHEN TO USE WHICH PERMISSION CHECK:
|
||
|
|
*
|
||
|
|
* 1. Use checkGlobalPermission() for:
|
||
|
|
* - Platform-wide features (user management, system settings)
|
||
|
|
* - Cross-tenant operations
|
||
|
|
* - Administrative tools
|
||
|
|
* - Features that aren't tenant-specific
|
||
|
|
*
|
||
|
|
* 2. Use checkTenantPermission() for:
|
||
|
|
* - Tenant-scoped operations (team management, tenant settings)
|
||
|
|
* - Resource access within a tenant (orders, inventory, recipes)
|
||
|
|
* - Organization-specific features
|
||
|
|
* - Most application features
|
||
|
|
*
|
||
|
|
* 3. Use checkCombinedPermission() for:
|
||
|
|
* - Features that require EITHER global OR tenant permissions
|
||
|
|
* - Mixed access scenarios (e.g., super admin OR tenant owner)
|
||
|
|
* - Fallback permission checks
|
||
|
|
*
|
||
|
|
* EXAMPLES:
|
||
|
|
*
|
||
|
|
* // Check if user can manage platform users (global only)
|
||
|
|
* checkGlobalPermission(user, { requiredRole: 'admin' })
|
||
|
|
*
|
||
|
|
* // Check if user can manage tenant team (tenant only)
|
||
|
|
* checkTenantPermission(tenantAccess, { requiredRole: 'owner' })
|
||
|
|
*
|
||
|
|
* // Check if user can access a feature (either global admin or tenant owner)
|
||
|
|
* checkCombinedPermission(user, tenantAccess, {
|
||
|
|
* globalRoles: ['admin', 'super_admin'],
|
||
|
|
* tenantRoles: ['owner']
|
||
|
|
* })
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
GLOBAL_USER_ROLES,
|
||
|
|
TENANT_ROLES,
|
||
|
|
ROLE_HIERARCHY,
|
||
|
|
hasGlobalRole,
|
||
|
|
hasTenantRole,
|
||
|
|
type GlobalUserRole,
|
||
|
|
type TenantRole,
|
||
|
|
type Role
|
||
|
|
} from '../types/roles';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* User object structure (from Auth service)
|
||
|
|
*/
|
||
|
|
export interface User {
|
||
|
|
id: string;
|
||
|
|
email: string;
|
||
|
|
role: GlobalUserRole;
|
||
|
|
full_name?: string;
|
||
|
|
is_active: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tenant access object structure (from Tenant service)
|
||
|
|
*/
|
||
|
|
export interface TenantAccess {
|
||
|
|
has_access: boolean;
|
||
|
|
role: TenantRole;
|
||
|
|
permissions: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Permission check options for global permissions
|
||
|
|
*/
|
||
|
|
export interface GlobalPermissionOptions {
|
||
|
|
requiredRole: GlobalUserRole;
|
||
|
|
allowHigherRoles?: boolean; // Default: true
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Permission check options for tenant permissions
|
||
|
|
*/
|
||
|
|
export interface TenantPermissionOptions {
|
||
|
|
requiredRole?: TenantRole;
|
||
|
|
requiredPermission?: string;
|
||
|
|
allowHigherRoles?: boolean; // Default: true
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Permission check options for combined (global + tenant) permissions
|
||
|
|
*/
|
||
|
|
export interface CombinedPermissionOptions {
|
||
|
|
globalRoles?: GlobalUserRole[];
|
||
|
|
tenantRoles?: TenantRole[];
|
||
|
|
tenantPermissions?: string[];
|
||
|
|
requireBoth?: boolean; // Default: false (OR logic), true for AND logic
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a user has a specific global permission
|
||
|
|
*
|
||
|
|
* @param user - User object from auth store
|
||
|
|
* @param options - Permission requirements
|
||
|
|
* @returns true if user has the required global permission
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* // Check if user is an admin
|
||
|
|
* checkGlobalPermission(user, { requiredRole: 'admin' })
|
||
|
|
*
|
||
|
|
* // Check if user is exactly a manager (no higher roles)
|
||
|
|
* checkGlobalPermission(user, { requiredRole: 'manager', allowHigherRoles: false })
|
||
|
|
*/
|
||
|
|
export function checkGlobalPermission(
|
||
|
|
user: User | null | undefined,
|
||
|
|
options: GlobalPermissionOptions
|
||
|
|
): boolean {
|
||
|
|
if (!user || !user.is_active) return false;
|
||
|
|
|
||
|
|
const { requiredRole, allowHigherRoles = true } = options;
|
||
|
|
|
||
|
|
if (allowHigherRoles) {
|
||
|
|
// Check if user has the required role or higher
|
||
|
|
return hasGlobalRole(user.role, requiredRole);
|
||
|
|
} else {
|
||
|
|
// Check for exact role match
|
||
|
|
return user.role === requiredRole;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a user has a specific tenant permission
|
||
|
|
*
|
||
|
|
* @param tenantAccess - Tenant access object from tenant store
|
||
|
|
* @param options - Permission requirements
|
||
|
|
* @returns true if user has the required tenant permission
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* // Check if user is a tenant owner
|
||
|
|
* checkTenantPermission(tenantAccess, { requiredRole: 'owner' })
|
||
|
|
*
|
||
|
|
* // Check if user has a specific permission
|
||
|
|
* checkTenantPermission(tenantAccess, { requiredPermission: 'manage_team' })
|
||
|
|
*
|
||
|
|
* // Check if user is exactly an admin (no higher roles)
|
||
|
|
* checkTenantPermission(tenantAccess, { requiredRole: 'admin', allowHigherRoles: false })
|
||
|
|
*/
|
||
|
|
export function checkTenantPermission(
|
||
|
|
tenantAccess: TenantAccess | null | undefined,
|
||
|
|
options: TenantPermissionOptions
|
||
|
|
): boolean {
|
||
|
|
if (!tenantAccess || !tenantAccess.has_access) return false;
|
||
|
|
|
||
|
|
const { requiredRole, requiredPermission, allowHigherRoles = true } = options;
|
||
|
|
|
||
|
|
// Check role-based permission
|
||
|
|
if (requiredRole) {
|
||
|
|
if (allowHigherRoles) {
|
||
|
|
if (!hasTenantRole(tenantAccess.role, requiredRole)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
if (tenantAccess.role !== requiredRole) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check specific permission
|
||
|
|
if (requiredPermission) {
|
||
|
|
if (!tenantAccess.permissions?.includes(requiredPermission)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check combined global and tenant permissions
|
||
|
|
*
|
||
|
|
* @param user - User object from auth store
|
||
|
|
* @param tenantAccess - Tenant access object from tenant store
|
||
|
|
* @param options - Permission requirements
|
||
|
|
* @returns true if user meets the permission criteria
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* // Check if user is either global admin OR tenant owner (OR logic)
|
||
|
|
* checkCombinedPermission(user, tenantAccess, {
|
||
|
|
* globalRoles: ['admin', 'super_admin'],
|
||
|
|
* tenantRoles: ['owner']
|
||
|
|
* })
|
||
|
|
*
|
||
|
|
* // Check if user is global admin AND tenant member (AND logic)
|
||
|
|
* checkCombinedPermission(user, tenantAccess, {
|
||
|
|
* globalRoles: ['admin'],
|
||
|
|
* tenantRoles: ['member', 'admin', 'owner'],
|
||
|
|
* requireBoth: true
|
||
|
|
* })
|
||
|
|
*/
|
||
|
|
export function checkCombinedPermission(
|
||
|
|
user: User | null | undefined,
|
||
|
|
tenantAccess: TenantAccess | null | undefined,
|
||
|
|
options: CombinedPermissionOptions
|
||
|
|
): boolean {
|
||
|
|
const {
|
||
|
|
globalRoles = [],
|
||
|
|
tenantRoles = [],
|
||
|
|
tenantPermissions = [],
|
||
|
|
requireBoth = false
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
// Check global roles
|
||
|
|
const hasGlobalAccess = globalRoles.length === 0 || (
|
||
|
|
user?.is_active &&
|
||
|
|
globalRoles.some(role => hasGlobalRole(user.role, role))
|
||
|
|
);
|
||
|
|
|
||
|
|
// Check tenant roles
|
||
|
|
const hasTenantRoleAccess = tenantRoles.length === 0 || (
|
||
|
|
tenantAccess?.has_access &&
|
||
|
|
tenantRoles.some(role => hasTenantRole(tenantAccess.role, role))
|
||
|
|
);
|
||
|
|
|
||
|
|
// Check tenant permissions
|
||
|
|
const hasTenantPermissionAccess = tenantPermissions.length === 0 || (
|
||
|
|
tenantAccess?.has_access &&
|
||
|
|
tenantPermissions.some(perm => tenantAccess.permissions?.includes(perm))
|
||
|
|
);
|
||
|
|
|
||
|
|
// Combine tenant role and permission checks (must pass at least one)
|
||
|
|
const hasTenantAccess = hasTenantRoleAccess || hasTenantPermissionAccess;
|
||
|
|
|
||
|
|
if (requireBoth) {
|
||
|
|
// AND logic: must have both global and tenant access
|
||
|
|
return hasGlobalAccess && hasTenantAccess;
|
||
|
|
} else {
|
||
|
|
// OR logic: must have either global or tenant access
|
||
|
|
return hasGlobalAccess || hasTenantAccess;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user can manage team members
|
||
|
|
*
|
||
|
|
* @param user - User object
|
||
|
|
* @param tenantAccess - Tenant access object
|
||
|
|
* @returns true if user can manage team
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* canManageTeam(user, tenantAccess)
|
||
|
|
*/
|
||
|
|
export function canManageTeam(
|
||
|
|
user: User | null | undefined,
|
||
|
|
tenantAccess: TenantAccess | null | undefined
|
||
|
|
): boolean {
|
||
|
|
return checkCombinedPermission(user, tenantAccess, {
|
||
|
|
globalRoles: [GLOBAL_USER_ROLES.ADMIN, GLOBAL_USER_ROLES.SUPER_ADMIN],
|
||
|
|
tenantRoles: [TENANT_ROLES.OWNER, TENANT_ROLES.ADMIN]
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user is tenant owner
|
||
|
|
*
|
||
|
|
* @param user - User object
|
||
|
|
* @param tenantAccess - Tenant access object
|
||
|
|
* @returns true if user is owner
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* isTenantOwner(user, tenantAccess)
|
||
|
|
*/
|
||
|
|
export function isTenantOwner(
|
||
|
|
user: User | null | undefined,
|
||
|
|
tenantAccess: TenantAccess | null | undefined
|
||
|
|
): boolean {
|
||
|
|
return checkCombinedPermission(user, tenantAccess, {
|
||
|
|
globalRoles: [GLOBAL_USER_ROLES.SUPER_ADMIN], // Super admin can act as owner
|
||
|
|
tenantRoles: [TENANT_ROLES.OWNER]
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if user can perform administrative actions
|
||
|
|
*
|
||
|
|
* @param user - User object
|
||
|
|
* @returns true if user has admin access
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* canPerformAdminActions(user)
|
||
|
|
*/
|
||
|
|
export function canPerformAdminActions(
|
||
|
|
user: User | null | undefined
|
||
|
|
): boolean {
|
||
|
|
return checkGlobalPermission(user, {
|
||
|
|
requiredRole: GLOBAL_USER_ROLES.ADMIN
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get user's effective permissions for a tenant
|
||
|
|
*
|
||
|
|
* @param user - User object
|
||
|
|
* @param tenantAccess - Tenant access object
|
||
|
|
* @returns Object with permission flags
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* const perms = getEffectivePermissions(user, tenantAccess)
|
||
|
|
* if (perms.canManageTeam) { ... }
|
||
|
|
*/
|
||
|
|
export function getEffectivePermissions(
|
||
|
|
user: User | null | undefined,
|
||
|
|
tenantAccess: TenantAccess | null | undefined
|
||
|
|
) {
|
||
|
|
return {
|
||
|
|
// Global permissions
|
||
|
|
isGlobalAdmin: checkGlobalPermission(user, { requiredRole: GLOBAL_USER_ROLES.ADMIN }),
|
||
|
|
isSuperAdmin: checkGlobalPermission(user, { requiredRole: GLOBAL_USER_ROLES.SUPER_ADMIN }),
|
||
|
|
isManager: checkGlobalPermission(user, { requiredRole: GLOBAL_USER_ROLES.MANAGER }),
|
||
|
|
|
||
|
|
// Tenant permissions
|
||
|
|
isTenantOwner: checkTenantPermission(tenantAccess, { requiredRole: TENANT_ROLES.OWNER }),
|
||
|
|
isTenantAdmin: checkTenantPermission(tenantAccess, { requiredRole: TENANT_ROLES.ADMIN }),
|
||
|
|
isTenantMember: checkTenantPermission(tenantAccess, { requiredRole: TENANT_ROLES.MEMBER }),
|
||
|
|
isTenantViewer: checkTenantPermission(tenantAccess, { requiredRole: TENANT_ROLES.VIEWER }),
|
||
|
|
|
||
|
|
// Combined permissions
|
||
|
|
canManageTeam: canManageTeam(user, tenantAccess),
|
||
|
|
canTransferOwnership: isTenantOwner(user, tenantAccess),
|
||
|
|
canPerformAdminActions: canPerformAdminActions(user),
|
||
|
|
|
||
|
|
// Access flags
|
||
|
|
hasGlobalAccess: !!user?.is_active,
|
||
|
|
hasTenantAccess: !!tenantAccess?.has_access,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Permission validation error types
|
||
|
|
*/
|
||
|
|
export class PermissionError extends Error {
|
||
|
|
constructor(message: string, public readonly requiredPermissions: string[]) {
|
||
|
|
super(message);
|
||
|
|
this.name = 'PermissionError';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assert that user has required permissions, throw error if not
|
||
|
|
*
|
||
|
|
* @param user - User object
|
||
|
|
* @param tenantAccess - Tenant access object
|
||
|
|
* @param options - Permission requirements
|
||
|
|
* @throws PermissionError if user lacks required permissions
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* assertPermission(user, tenantAccess, {
|
||
|
|
* tenantRoles: ['owner'],
|
||
|
|
* errorMessage: 'Only tenant owners can perform this action'
|
||
|
|
* })
|
||
|
|
*/
|
||
|
|
export function assertPermission(
|
||
|
|
user: User | null | undefined,
|
||
|
|
tenantAccess: TenantAccess | null | undefined,
|
||
|
|
options: CombinedPermissionOptions & { errorMessage?: string }
|
||
|
|
): void {
|
||
|
|
const hasPermission = checkCombinedPermission(user, tenantAccess, options);
|
||
|
|
|
||
|
|
if (!hasPermission) {
|
||
|
|
const requiredPerms = [
|
||
|
|
...(options.globalRoles || []),
|
||
|
|
...(options.tenantRoles || []),
|
||
|
|
...(options.tenantPermissions || [])
|
||
|
|
];
|
||
|
|
|
||
|
|
throw new PermissionError(
|
||
|
|
options.errorMessage || 'You do not have permission to perform this action',
|
||
|
|
requiredPerms
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|