@open-mercato/enterprise 0.6.5-develop.4863.1.169bfbb3a3 → 0.6.5-develop.4964.1.ae0edca575
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/security/api/enforcement/[id]/route.js +35 -1
- package/dist/modules/security/api/enforcement/[id]/route.js.map +2 -2
- package/dist/modules/security/api/enforcement/_shared.js +63 -1
- package/dist/modules/security/api/enforcement/_shared.js.map +3 -3
- package/dist/modules/security/api/enforcement/compliance/route.js +12 -3
- package/dist/modules/security/api/enforcement/compliance/route.js.map +2 -2
- package/dist/modules/security/api/enforcement/route.js +25 -2
- package/dist/modules/security/api/enforcement/route.js.map +2 -2
- package/dist/modules/security/api/users/[id]/mfa/reset/route.js +6 -1
- package/dist/modules/security/api/users/[id]/mfa/reset/route.js.map +2 -2
- package/dist/modules/security/api/users/[id]/mfa/status/route.js +13 -2
- package/dist/modules/security/api/users/[id]/mfa/status/route.js.map +2 -2
- package/dist/modules/security/api/users/_shared.js +56 -1
- package/dist/modules/security/api/users/_shared.js.map +2 -2
- package/dist/modules/security/api/users/mfa/compliance/route.js +17 -7
- package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
- package/dist/modules/security/commands/createEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/resetUserMfa.js +6 -1
- package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
- package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/services/MfaAdminService.js +22 -5
- package/dist/modules/security/services/MfaAdminService.js.map +2 -2
- package/dist/modules/security/services/MfaEnforcementService.js +28 -6
- package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/security/api/enforcement/[id]/route.ts +50 -1
- package/src/modules/security/api/enforcement/_shared.ts +83 -2
- package/src/modules/security/api/enforcement/compliance/route.ts +10 -1
- package/src/modules/security/api/enforcement/route.ts +30 -2
- package/src/modules/security/api/users/[id]/mfa/reset/route.ts +6 -1
- package/src/modules/security/api/users/[id]/mfa/status/route.ts +13 -2
- package/src/modules/security/api/users/_shared.ts +69 -1
- package/src/modules/security/api/users/mfa/compliance/route.ts +16 -7
- package/src/modules/security/commands/createEnforcementPolicy.ts +6 -1
- package/src/modules/security/commands/deleteEnforcementPolicy.ts +6 -1
- package/src/modules/security/commands/resetUserMfa.ts +6 -1
- package/src/modules/security/commands/updateEnforcementPolicy.ts +6 -1
- package/src/modules/security/services/MfaAdminService.ts +29 -6
- package/src/modules/security/services/MfaEnforcementService.ts +42 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/MfaEnforcementService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async listPolicies(filters?: EnforcementPolicyListFilters): Promise<MfaEnforcementPolicy[]> {\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string, actor?: EnforcementActorContext): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.4964.1.ae0edca575",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@open-mercato/core": "0.6.5-develop.
|
|
68
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
67
|
+
"@open-mercato/core": "0.6.5-develop.4964.1.ae0edca575",
|
|
68
|
+
"@open-mercato/ui": "0.6.5-develop.4964.1.ae0edca575",
|
|
69
69
|
"@simplewebauthn/browser": "^13.3.0",
|
|
70
70
|
"@simplewebauthn/server": "^13.3.1",
|
|
71
71
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -75,12 +75,12 @@
|
|
|
75
75
|
"qrcode": "^1.5.4"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
78
|
+
"@open-mercato/shared": "0.6.5-develop.4964.1.ae0edca575",
|
|
79
79
|
"react": "^19.0.0",
|
|
80
80
|
"react-dom": "^19.0.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
83
|
+
"@open-mercato/shared": "0.6.5-develop.4964.1.ae0edca575",
|
|
84
84
|
"@types/jest": "^30.0.0",
|
|
85
85
|
"@types/react": "^19.2.16",
|
|
86
86
|
"@types/react-dom": "^19.2.3",
|
|
@@ -2,14 +2,48 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { updateEnforcementPolicySchema } from '../../../data/validators'
|
|
5
|
+
import { EnforcementScope } from '../../../data/entities'
|
|
6
|
+
import type { MfaEnforcementPolicy } from '../../../data/entities'
|
|
7
|
+
import type { UpdateEnforcementPolicyInput } from '../../../data/validators'
|
|
5
8
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'
|
|
6
9
|
import { securityApiError } from '../../i18n'
|
|
7
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
assertActorOwnsEnforcementScope,
|
|
12
|
+
mapEnforcementError,
|
|
13
|
+
resolveEnforcementContext,
|
|
14
|
+
type EnforcementRequestContext,
|
|
15
|
+
} from '../_shared'
|
|
8
16
|
|
|
9
17
|
const paramsSchema = z.object({
|
|
10
18
|
id: z.string().uuid(),
|
|
11
19
|
})
|
|
12
20
|
|
|
21
|
+
function scopeIdFromPolicy(policy: {
|
|
22
|
+
scope: EnforcementScope
|
|
23
|
+
tenantId?: string | null
|
|
24
|
+
organizationId?: string | null
|
|
25
|
+
}): string | null {
|
|
26
|
+
if (policy.scope === EnforcementScope.TENANT) {
|
|
27
|
+
return policy.tenantId ?? null
|
|
28
|
+
}
|
|
29
|
+
if (policy.scope === EnforcementScope.ORGANISATION) {
|
|
30
|
+
if (!policy.tenantId || !policy.organizationId) return null
|
|
31
|
+
return `${policy.tenantId}:${policy.organizationId}`
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function assertActorOwnsRequestedPolicyScope(
|
|
37
|
+
context: EnforcementRequestContext,
|
|
38
|
+
current: MfaEnforcementPolicy,
|
|
39
|
+
data: UpdateEnforcementPolicyInput,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const scope = data.scope ?? current.scope
|
|
42
|
+
const tenantId = data.tenantId ?? current.tenantId ?? null
|
|
43
|
+
const organizationId = data.organizationId ?? current.organizationId ?? null
|
|
44
|
+
await assertActorOwnsEnforcementScope(context, scope, scopeIdFromPolicy({ scope, tenantId, organizationId }))
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
const okResponseSchema = z.object({
|
|
14
48
|
ok: z.literal(true),
|
|
15
49
|
})
|
|
@@ -41,6 +75,13 @@ export async function PUT(req: Request, context: { params: Promise<{ id: string
|
|
|
41
75
|
}
|
|
42
76
|
|
|
43
77
|
try {
|
|
78
|
+
const policy = await requestContext.enforcementService.getPolicyById(params.data.id)
|
|
79
|
+
if (!policy) {
|
|
80
|
+
return securityApiError(404, 'Enforcement policy not found')
|
|
81
|
+
}
|
|
82
|
+
await assertActorOwnsEnforcementScope(requestContext, policy.scope, scopeIdFromPolicy(policy))
|
|
83
|
+
await assertActorOwnsRequestedPolicyScope(requestContext, policy, parsedBody.data)
|
|
84
|
+
|
|
44
85
|
const commandBus = requestContext.container.resolve<CommandBus>('commandBus')
|
|
45
86
|
const { result } = await commandBus.execute('security.enforcement.update', {
|
|
46
87
|
input: {
|
|
@@ -65,6 +106,12 @@ export async function DELETE(req: Request, context: { params: Promise<{ id: stri
|
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
try {
|
|
109
|
+
const policy = await requestContext.enforcementService.getPolicyById(params.data.id)
|
|
110
|
+
if (!policy) {
|
|
111
|
+
return securityApiError(404, 'Enforcement policy not found')
|
|
112
|
+
}
|
|
113
|
+
await assertActorOwnsEnforcementScope(requestContext, policy.scope, scopeIdFromPolicy(policy))
|
|
114
|
+
|
|
68
115
|
const commandBus = requestContext.container.resolve<CommandBus>('commandBus')
|
|
69
116
|
const { result } = await commandBus.execute('security.enforcement.delete', {
|
|
70
117
|
input: { id: params.data.id },
|
|
@@ -92,6 +139,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
92
139
|
errors: [
|
|
93
140
|
{ status: 400, description: 'Invalid input', schema: securityErrorSchema },
|
|
94
141
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
142
|
+
{ status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },
|
|
95
143
|
{ status: 404, description: 'Policy not found', schema: securityErrorSchema },
|
|
96
144
|
{ status: 409, description: 'Conflict', schema: securityErrorSchema },
|
|
97
145
|
],
|
|
@@ -105,6 +153,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
105
153
|
errors: [
|
|
106
154
|
{ status: 400, description: 'Invalid policy id', schema: securityErrorSchema },
|
|
107
155
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
156
|
+
{ status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },
|
|
108
157
|
{ status: 404, description: 'Policy not found', schema: securityErrorSchema },
|
|
109
158
|
],
|
|
110
159
|
},
|
|
@@ -2,16 +2,23 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { Organization, Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
5
|
-
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
6
6
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
7
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
8
|
-
import
|
|
8
|
+
import { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
9
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
10
|
+
import { EnforcementScope, type MfaEnforcementPolicy } from '../../data/entities'
|
|
9
11
|
import type { MfaEnforcementServiceError, MfaEnforcementService } from '../../services/MfaEnforcementService'
|
|
10
12
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
11
13
|
|
|
12
14
|
type RequestContainer = Awaited<ReturnType<typeof createRequestContainer>>
|
|
13
15
|
type Auth = NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>
|
|
14
16
|
|
|
17
|
+
export type EnforcementActorContext = {
|
|
18
|
+
tenantId: string | null
|
|
19
|
+
isSuperAdmin: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export type EnforcementRequestContext = {
|
|
16
23
|
auth: Auth
|
|
17
24
|
container: RequestContainer
|
|
@@ -41,6 +48,80 @@ export async function resolveEnforcementContext(req: Request): Promise<Enforceme
|
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
function normalizeNullableString(value: unknown): string | null {
|
|
52
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeOrganizationList(values: unknown): string[] | null {
|
|
56
|
+
if (values === null || values === undefined) return null
|
|
57
|
+
if (!Array.isArray(values)) return null
|
|
58
|
+
const result: string[] = []
|
|
59
|
+
for (const value of values) {
|
|
60
|
+
if (typeof value !== 'string') continue
|
|
61
|
+
const trimmed = value.trim()
|
|
62
|
+
if (trimmed) result.push(trimmed)
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveActorContext(ctx: EnforcementRequestContext): Promise<EnforcementActorContext> {
|
|
68
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })
|
|
69
|
+
return {
|
|
70
|
+
tenantId: normalizeNullableString(ctx.auth.tenantId),
|
|
71
|
+
isSuperAdmin,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function assertActorOwnsOrganization(
|
|
76
|
+
ctx: EnforcementRequestContext,
|
|
77
|
+
organizationId: string,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const rbacService = ctx.container.resolve<RbacService>('rbacService')
|
|
80
|
+
const acl = await rbacService.loadAcl(ctx.auth.sub, {
|
|
81
|
+
tenantId: normalizeNullableString(ctx.auth.tenantId),
|
|
82
|
+
organizationId: normalizeNullableString(ctx.auth.orgId),
|
|
83
|
+
})
|
|
84
|
+
const organizations = normalizeOrganizationList(acl?.organizations)
|
|
85
|
+
if (organizations === null || organizations.includes('__all__')) return
|
|
86
|
+
if (!organizations.includes(organizationId)) {
|
|
87
|
+
throw forbidden('Not authorized to target this organization.')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function assertActorOwnsEnforcementScope(
|
|
92
|
+
ctx: EnforcementRequestContext,
|
|
93
|
+
scope: EnforcementScope,
|
|
94
|
+
scopeId: string | null | undefined,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (scope === EnforcementScope.PLATFORM) {
|
|
97
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })
|
|
98
|
+
if (!isSuperAdmin) {
|
|
99
|
+
throw forbidden('Platform scope requires platform administrator privileges.')
|
|
100
|
+
}
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (scope === EnforcementScope.TENANT) {
|
|
105
|
+
await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, scopeId)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalizedScopeId = normalizeNullableString(scopeId)
|
|
110
|
+
if (!normalizedScopeId) {
|
|
111
|
+
throw new CrudHttpError(400, { error: "organisation scopeId must use '<tenantId>:<organizationId>' format" })
|
|
112
|
+
}
|
|
113
|
+
const [tenantId, organizationId] = normalizedScopeId.split(':')
|
|
114
|
+
if (!tenantId || !organizationId) {
|
|
115
|
+
throw new CrudHttpError(400, { error: "organisation scopeId must use '<tenantId>:<organizationId>' format" })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, tenantId)
|
|
119
|
+
|
|
120
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })
|
|
121
|
+
if (isSuperAdmin) return
|
|
122
|
+
await assertActorOwnsOrganization(ctx, organizationId)
|
|
123
|
+
}
|
|
124
|
+
|
|
44
125
|
export async function mapEnforcementError(error: unknown): Promise<NextResponse> {
|
|
45
126
|
if (error instanceof CrudHttpError) {
|
|
46
127
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
@@ -3,7 +3,12 @@ import { z } from 'zod'
|
|
|
3
3
|
import { EnforcementScope } from '../../../data/entities'
|
|
4
4
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'
|
|
5
5
|
import { securityApiError } from '../../i18n'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertActorOwnsEnforcementScope,
|
|
8
|
+
mapEnforcementError,
|
|
9
|
+
resolveActorContext,
|
|
10
|
+
resolveEnforcementContext,
|
|
11
|
+
} from '../_shared'
|
|
7
12
|
|
|
8
13
|
const complianceQuerySchema = z.object({
|
|
9
14
|
scope: z.nativeEnum(EnforcementScope).default(EnforcementScope.PLATFORM),
|
|
@@ -37,9 +42,12 @@ export async function GET(req: Request) {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
try {
|
|
45
|
+
await assertActorOwnsEnforcementScope(context, parsedQuery.data.scope, parsedQuery.data.scopeId)
|
|
46
|
+
const actor = await resolveActorContext(context)
|
|
40
47
|
const report = await context.enforcementService.getComplianceReport(
|
|
41
48
|
parsedQuery.data.scope,
|
|
42
49
|
parsedQuery.data.scopeId,
|
|
50
|
+
actor,
|
|
43
51
|
)
|
|
44
52
|
return NextResponse.json({
|
|
45
53
|
scope: parsedQuery.data.scope,
|
|
@@ -63,6 +71,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
63
71
|
errors: [
|
|
64
72
|
{ status: 400, description: 'Invalid query parameters', schema: securityErrorSchema },
|
|
65
73
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
74
|
+
{ status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },
|
|
66
75
|
],
|
|
67
76
|
},
|
|
68
77
|
},
|
|
@@ -5,7 +5,13 @@ import { enforcementPolicySchema } from '../../data/validators'
|
|
|
5
5
|
import { EnforcementScope } from '../../data/entities'
|
|
6
6
|
import { buildSecurityOpenApi, securityErrorSchema } from '../openapi'
|
|
7
7
|
import { securityApiError } from '../i18n'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertActorOwnsEnforcementScope,
|
|
10
|
+
attachPolicyScopeNames,
|
|
11
|
+
mapEnforcementError,
|
|
12
|
+
resolveActorContext,
|
|
13
|
+
resolveEnforcementContext,
|
|
14
|
+
} from './_shared'
|
|
9
15
|
|
|
10
16
|
const enforcementPolicyResponseSchema = z.object({
|
|
11
17
|
id: z.string().uuid(),
|
|
@@ -52,7 +58,8 @@ export async function GET(req: Request) {
|
|
|
52
58
|
return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
const
|
|
61
|
+
const actor = await resolveActorContext(context)
|
|
62
|
+
const policies = await context.enforcementService.listPolicies(parsedQuery.data, actor)
|
|
56
63
|
return NextResponse.json({
|
|
57
64
|
items: await attachPolicyScopeNames(context.container, policies),
|
|
58
65
|
})
|
|
@@ -78,6 +85,11 @@ export async function POST(req: Request) {
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
try {
|
|
88
|
+
await assertActorOwnsEnforcementScope(
|
|
89
|
+
context,
|
|
90
|
+
parsedBody.data.scope,
|
|
91
|
+
scopeIdFromPolicyInput(parsedBody.data),
|
|
92
|
+
)
|
|
81
93
|
const commandBus = context.container.resolve<CommandBus>('commandBus')
|
|
82
94
|
const { result } = await commandBus.execute('security.enforcement.create', {
|
|
83
95
|
input: parsedBody.data,
|
|
@@ -89,6 +101,21 @@ export async function POST(req: Request) {
|
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
function scopeIdFromPolicyInput(data: {
|
|
105
|
+
scope: EnforcementScope
|
|
106
|
+
tenantId?: string | null
|
|
107
|
+
organizationId?: string | null
|
|
108
|
+
}): string | null {
|
|
109
|
+
if (data.scope === EnforcementScope.TENANT) {
|
|
110
|
+
return data.tenantId ?? null
|
|
111
|
+
}
|
|
112
|
+
if (data.scope === EnforcementScope.ORGANISATION) {
|
|
113
|
+
if (!data.tenantId || !data.organizationId) return null
|
|
114
|
+
return `${data.tenantId}:${data.organizationId}`
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
export const openApi = buildSecurityOpenApi({
|
|
93
120
|
summary: 'Enforcement policy routes',
|
|
94
121
|
methods: {
|
|
@@ -115,6 +142,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
115
142
|
errors: [
|
|
116
143
|
{ status: 400, description: 'Invalid payload', schema: securityErrorSchema },
|
|
117
144
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
145
|
+
{ status: 403, description: 'Not authorized for the requested scope', schema: securityErrorSchema },
|
|
118
146
|
{ status: 409, description: 'Conflict', schema: securityErrorSchema },
|
|
119
147
|
],
|
|
120
148
|
},
|
|
@@ -3,7 +3,11 @@ import { z } from 'zod'
|
|
|
3
3
|
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'
|
|
5
5
|
import { securityApiError } from '../../../../i18n'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertActorCanAccessSecurityUserTarget,
|
|
8
|
+
mapSecurityUsersError,
|
|
9
|
+
resolveSecurityUsersContext,
|
|
10
|
+
} from '../../../_shared'
|
|
7
11
|
import { requireSudo } from '../../../../../lib/sudo-middleware'
|
|
8
12
|
|
|
9
13
|
const paramsSchema = z.object({
|
|
@@ -44,6 +48,7 @@ export async function POST(req: Request, routeContext: { params: Promise<{ id: s
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
try {
|
|
51
|
+
await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id)
|
|
47
52
|
await requireSudo(req, 'security.admin.mfa.reset')
|
|
48
53
|
const commandBus = context.container.resolve<CommandBus>('commandBus')
|
|
49
54
|
const { result } = await commandBus.execute('security.admin.mfa.reset', {
|
|
@@ -2,7 +2,12 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../../../openapi'
|
|
4
4
|
import { securityApiError } from '../../../../i18n'
|
|
5
|
-
import {
|
|
5
|
+
import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
6
|
+
import {
|
|
7
|
+
assertActorCanAccessSecurityUserTarget,
|
|
8
|
+
mapSecurityUsersError,
|
|
9
|
+
resolveSecurityUsersContext,
|
|
10
|
+
} from '../../../_shared'
|
|
6
11
|
|
|
7
12
|
const paramsSchema = z.object({
|
|
8
13
|
id: z.string().uuid(),
|
|
@@ -35,7 +40,12 @@ export async function GET(req: Request, routeContext: { params: Promise<{ id: st
|
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
try {
|
|
38
|
-
|
|
43
|
+
await assertActorCanAccessSecurityUserTarget(context, parsedParams.data.id)
|
|
44
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container })
|
|
45
|
+
const status = await context.mfaAdminService.getUserMfaStatus(parsedParams.data.id, {
|
|
46
|
+
tenantId: context.auth.tenantId ?? null,
|
|
47
|
+
isSuperAdmin,
|
|
48
|
+
})
|
|
39
49
|
return NextResponse.json({
|
|
40
50
|
...status,
|
|
41
51
|
methods: status.methods.map((method) => ({
|
|
@@ -60,6 +70,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
60
70
|
errors: [
|
|
61
71
|
{ status: 400, description: 'Invalid user id', schema: securityErrorSchema },
|
|
62
72
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
73
|
+
{ status: 403, description: 'Not authorized to access this user', schema: securityErrorSchema },
|
|
63
74
|
{ status: 404, description: 'User not found', schema: securityErrorSchema },
|
|
64
75
|
],
|
|
65
76
|
},
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
3
|
+
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
3
4
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
6
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
|
+
import { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
9
|
+
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
10
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
6
11
|
import { isSudoRequiredError } from '../../lib/sudo-middleware'
|
|
7
12
|
import type { MfaAdminService, MfaAdminServiceError } from '../../services/MfaAdminService'
|
|
8
13
|
import { localizeSecurityApiBody, securityApiError } from '../i18n'
|
|
@@ -41,6 +46,69 @@ export async function resolveSecurityUsersContext(
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function normalizeNullableString(value: unknown): string | null {
|
|
50
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeOrganizationList(values: unknown): string[] | null {
|
|
54
|
+
if (values === null || values === undefined) return null
|
|
55
|
+
if (!Array.isArray(values)) return null
|
|
56
|
+
const result: string[] = []
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
if (typeof value !== 'string') continue
|
|
59
|
+
const trimmed = value.trim()
|
|
60
|
+
if (trimmed) result.push(trimmed)
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function assertActorCanAccessSecurityUserTarget(
|
|
66
|
+
ctx: SecurityUsersRequestContext,
|
|
67
|
+
targetUserId: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })
|
|
70
|
+
if (isSuperAdmin) return
|
|
71
|
+
|
|
72
|
+
const em = ctx.container.resolve<EntityManager>('em')
|
|
73
|
+
const target = await findOneWithDecryption(
|
|
74
|
+
em,
|
|
75
|
+
User,
|
|
76
|
+
{ id: targetUserId, deletedAt: null } as FilterQuery<User>,
|
|
77
|
+
{},
|
|
78
|
+
{ tenantId: null, organizationId: null },
|
|
79
|
+
)
|
|
80
|
+
if (!target) {
|
|
81
|
+
throw new CrudHttpError(404, { error: 'User not found' })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const actorTenantId = normalizeNullableString(ctx.auth.tenantId)
|
|
85
|
+
const targetTenantId = normalizeNullableString((target as { tenantId?: string | null }).tenantId)
|
|
86
|
+
if (!targetTenantId || targetTenantId !== actorTenantId) {
|
|
87
|
+
throw new CrudHttpError(404, { error: 'User not found' })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rbacService = ctx.container.resolve<RbacService>('rbacService')
|
|
91
|
+
const acl = await rbacService.loadAcl(ctx.auth.sub, {
|
|
92
|
+
tenantId: actorTenantId,
|
|
93
|
+
organizationId: normalizeNullableString(ctx.auth.orgId),
|
|
94
|
+
})
|
|
95
|
+
const organizations = normalizeOrganizationList(acl?.organizations)
|
|
96
|
+
if (organizations !== null && !organizations.includes('__all__')) {
|
|
97
|
+
const targetOrganizationId = normalizeNullableString((target as { organizationId?: string | null }).organizationId)
|
|
98
|
+
if (!targetOrganizationId || !organizations.includes(targetOrganizationId)) {
|
|
99
|
+
throw forbidden('Not authorized to access this user.')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function assertActorOwnsTenantScope(
|
|
105
|
+
ctx: SecurityUsersRequestContext,
|
|
106
|
+
requestedTenantId: string | null | undefined,
|
|
107
|
+
): Promise<string | null> {
|
|
108
|
+
const resolved = await enforceTenantSelection({ auth: ctx.auth, container: ctx.container }, requestedTenantId)
|
|
109
|
+
return resolved ?? ctx.auth.tenantId ?? null
|
|
110
|
+
}
|
|
111
|
+
|
|
44
112
|
export async function mapSecurityUsersError(error: unknown): Promise<NextResponse> {
|
|
45
113
|
if (error instanceof CrudHttpError) {
|
|
46
114
|
return NextResponse.json(await localizeSecurityApiBody(error.body), { status: error.status })
|
|
@@ -2,7 +2,12 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { buildSecurityOpenApi, securityErrorSchema } from '../../../openapi'
|
|
4
4
|
import { securityApiError } from '../../../i18n'
|
|
5
|
-
import {
|
|
5
|
+
import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
6
|
+
import {
|
|
7
|
+
assertActorOwnsTenantScope,
|
|
8
|
+
mapSecurityUsersError,
|
|
9
|
+
resolveSecurityUsersContext,
|
|
10
|
+
} from '../../_shared'
|
|
6
11
|
|
|
7
12
|
const querySchema = z.object({
|
|
8
13
|
tenantId: z.string().uuid().optional(),
|
|
@@ -37,13 +42,16 @@ export async function GET(req: Request) {
|
|
|
37
42
|
return securityApiError(400, 'Invalid query parameters', { issues: parsedQuery.error.issues })
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
const tenantId = parsedQuery.data.tenantId ?? context.auth.tenantId ?? null
|
|
41
|
-
if (!tenantId) {
|
|
42
|
-
return securityApiError(400, 'Tenant context is required.')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
45
|
try {
|
|
46
|
-
const
|
|
46
|
+
const tenantId = await assertActorOwnsTenantScope(context, parsedQuery.data.tenantId)
|
|
47
|
+
if (!tenantId) {
|
|
48
|
+
return securityApiError(400, 'Tenant context is required.')
|
|
49
|
+
}
|
|
50
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: context.auth, container: context.container })
|
|
51
|
+
const items = await context.mfaAdminService.bulkComplianceCheck(tenantId, {
|
|
52
|
+
tenantId: context.auth.tenantId ?? null,
|
|
53
|
+
isSuperAdmin,
|
|
54
|
+
})
|
|
47
55
|
return NextResponse.json({ items })
|
|
48
56
|
} catch (error) {
|
|
49
57
|
return await mapSecurityUsersError(error)
|
|
@@ -62,6 +70,7 @@ export const openApi = buildSecurityOpenApi({
|
|
|
62
70
|
errors: [
|
|
63
71
|
{ status: 400, description: 'Invalid query or missing tenant context', schema: securityErrorSchema },
|
|
64
72
|
{ status: 401, description: 'Unauthorized', schema: securityErrorSchema },
|
|
73
|
+
{ status: 403, description: 'Not authorized for the requested tenant scope', schema: securityErrorSchema },
|
|
65
74
|
],
|
|
66
75
|
},
|
|
67
76
|
},
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod'
|
|
|
2
2
|
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
3
3
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
4
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
5
6
|
import { enforcementPolicySchema } from '../data/validators'
|
|
6
7
|
import type { MfaEnforcementService } from '../services/MfaEnforcementService'
|
|
7
8
|
|
|
@@ -34,8 +35,12 @@ registerCommand({
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const enforcementService = ctx.container.resolve<MfaEnforcementService>('mfaEnforcementService')
|
|
38
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth: ctx.auth, container: ctx.container })
|
|
37
39
|
try {
|
|
38
|
-
const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub
|
|
40
|
+
const policy = await enforcementService.createPolicy(parsed.data, ctx.auth.sub, {
|
|
41
|
+
tenantId: ctx.auth.tenantId ?? null,
|
|
42
|
+
isSuperAdmin,
|
|
43
|
+
})
|
|
39
44
|
return { id: policy.id }
|
|
40
45
|
} catch (error) {
|
|
41
46
|
if (isEnforcementServiceError(error)) {
|