@open-mercato/core 0.10.1-develop.7752.1.fd7c06569b → 0.10.1-develop.7756.1.8f5dec1b96
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/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/dbFixtures.js +1 -1
- package/dist/helpers/integration/dbFixtures.js.map +1 -1
- package/dist/modules/auth/api/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +1 -1
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +10 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/commands/acl.js +2 -2
- package/dist/modules/auth/commands/acl.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +1 -1
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/commands/users.js +7 -3
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/executionPrincipal.js +2 -2
- package/dist/modules/auth/lib/executionPrincipal.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +38 -11
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +2 -2
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260828120000_auth.js +45 -0
- package/dist/modules/auth/migrations/Migration20260828120000_auth.js.map +7 -0
- package/dist/modules/auth/services/rbacService.js +138 -59
- package/dist/modules/auth/services/rbacService.js.map +3 -3
- package/dist/modules/entities/cli.js +12 -1
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/lib/helpers.js +13 -0
- package/dist/modules/entities/lib/helpers.js.map +2 -2
- package/dist/modules/entities/lib/rotate-custom-entity-documents.js +157 -0
- package/dist/modules/entities/lib/rotate-custom-entity-documents.js.map +7 -0
- package/dist/modules/notifications/lib/notificationRecipients.js +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +179 -11
- package/dist/modules/query_index/lib/engine.js.map +3 -3
- package/package.json +7 -7
- package/src/helpers/integration/dbFixtures.ts +1 -1
- package/src/modules/auth/api/roles/acl/route.ts +2 -2
- package/src/modules/auth/api/roles/route.ts +1 -1
- package/src/modules/auth/api/users/acl/route.ts +10 -2
- package/src/modules/auth/commands/acl.ts +2 -2
- package/src/modules/auth/commands/roles.ts +1 -1
- package/src/modules/auth/commands/users.ts +7 -3
- package/src/modules/auth/data/entities.ts +12 -0
- package/src/modules/auth/lib/executionPrincipal.ts +2 -2
- package/src/modules/auth/lib/grantChecks.ts +115 -11
- package/src/modules/auth/lib/setup-app.ts +2 -2
- package/src/modules/auth/migrations/Migration20260828120000_auth.ts +70 -0
- package/src/modules/auth/services/rbacService.ts +161 -76
- package/src/modules/entities/cli.ts +12 -1
- package/src/modules/entities/lib/helpers.ts +17 -0
- package/src/modules/entities/lib/rotate-custom-entity-documents.ts +200 -0
- package/src/modules/notifications/lib/notificationRecipients.ts +6 -0
- package/src/modules/query_index/lib/engine.ts +187 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/lib/setup-app.ts"],
|
|
4
|
-
"sourcesContent": ["import { hash } from 'bcryptjs'\nimport { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'\nimport { normalizeTenantId } from './tenantAccess'\nimport { computeEmailHash, emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { getDefaultEncryptionMaps, type Module } from '@open-mercato/shared/modules/registry'\nimport type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'\nimport { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { upsertCanonicalEncryptionMap } from '@open-mercato/core/modules/entities/lib/encryption-maps'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n lockRoleAclWriterAuthorizationState,\n lockUserRoleWriterAuthorizationState,\n} from '@open-mercato/core/modules/auth/lib/authorizationStateLocks'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'setup' })\n\nconst DEFAULT_ROLE_NAMES = ['employee', 'admin', 'superadmin'] as const\nconst DEMO_SUPERADMIN_EMAIL = 'superadmin@acme.com'\nconst DEFAULT_DERIVED_EMAIL_DOMAIN = DEMO_SUPERADMIN_EMAIL.split('@')[1] ?? 'acme.com'\n\nexport type EnsureRolesOptions = {\n roleNames?: string[]\n tenantId?: string\n}\n\nasync function ensureRolesInContext(\n em: EntityManager,\n roleNames: string[],\n tenantId: string,\n) {\n for (const name of roleNames) {\n const existing = await findOneWithDecryption(em, Role, { name, tenantId }, {}, { tenantId, organizationId: null })\n if (existing) continue\n em.persist(em.create(Role, { name, tenantId, minActiveHolders: name === 'admin' ? 1 : 0, createdAt: new Date() }))\n }\n}\n\nexport async function ensureRoles(em: EntityManager, options: EnsureRolesOptions = {}) {\n const roleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const tenantId = normalizeTenantId(options.tenantId ?? null)\n if (!tenantId) {\n throw new Error('ensureRoles requires a tenantId \u2014 global roles are not supported')\n }\n await em.transactional(async (tem) => {\n await ensureRolesInContext(tem, roleNames, tenantId)\n await tem.flush()\n })\n}\n\nasync function findRoleByName(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role | null> {\n const normalizedTenant = normalizeTenantId(tenantId ?? null) ?? null\n return findOneWithDecryption(em, Role, { name, tenantId: normalizedTenant }, {}, { tenantId: normalizedTenant, organizationId: null })\n}\n\nasync function findRoleByNameOrFail(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role> {\n const role = await findRoleByName(em, name, tenantId)\n if (!role) throw new Error(`ROLE_NOT_FOUND:${name}`)\n return role\n}\n\ntype PrimaryUserInput = {\n email: string\n password?: string\n hashedPassword?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n confirm?: boolean\n}\n\nconst DERIVED_EMAIL_ENV = {\n admin: 'OM_INIT_ADMIN_EMAIL',\n employee: 'OM_INIT_EMPLOYEE_EMAIL',\n} as const\n\nexport type DemoUserRole = 'superadmin' | 'admin' | 'employee'\nexport type DemoUserEmail = { role: DemoUserRole; email: string }\n\n/**\n * Returns the canonical list of demo user emails the setup path may have\n * seeded. Honors OM_INIT_ADMIN_EMAIL / OM_INIT_EMPLOYEE_EMAIL exactly the\n * same way the derived-user seeding branch does, so the deactivation loop\n * never drifts from the seeding loop.\n *\n * Pure function \u2014 no side effects, safe to unit-test in isolation.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport function resolveDemoUserEmails(): DemoUserEmail[] {\n const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n return [\n { role: 'superadmin', email: DEMO_SUPERADMIN_EMAIL },\n { role: 'admin', email: adminEmail },\n { role: 'employee', email: employeeEmail },\n ]\n}\n\nexport type SetupInitialTenantOptions = {\n orgName: string\n primaryUser: PrimaryUserInput\n roleNames?: string[]\n includeDerivedUsers?: boolean\n failIfUserExists?: boolean\n primaryUserRoles?: string[]\n includeSuperadminRole?: boolean\n /** Optional list of enabled modules. When provided, module setup hooks are called. */\n modules?: Module[]\n /**\n * Optional global slug to persist on the new Organization. When set, a\n * pre-flight uniqueness check across all tenants throws OrgSlugExistsError\n * on collision so scriptable callers can fail loudly instead of silently\n * reusing or clobbering an existing organization.\n */\n orgSlug?: string\n /**\n * Opt-in flag that allows seeding the derived admin/employee accounts when\n * the OM_INIT_ADMIN_PASSWORD / OM_INIT_EMPLOYEE_PASSWORD env vars are unset.\n *\n * - In non-production (`NODE_ENV !== 'production'`): the demo accounts are\n * seeded with the well-known `'secret'` password so local dev workflows\n * (e.g. `mercato init`) stay predictable for developers.\n * - In production: the fallback uses a randomly generated 96-bit password\n * (printed once by the CLI) so an operator who deliberately opts in still\n * avoids the historical hardcoded credential. When this flag is false/unset\n * in production and the env vars are missing, `DerivedUserPasswordRequiredError`\n * is thrown instead of silently seeding any account.\n */\n allowDemoDerivedPasswords?: boolean\n}\n\nexport class OrgSlugExistsError extends Error {\n constructor(public readonly slug: string) {\n super(`ORG_SLUG_EXISTS: an organization with slug \"${slug}\" already exists`)\n this.name = 'OrgSlugExistsError'\n }\n}\n\nexport class DerivedUserPasswordRequiredError extends Error {\n constructor(public readonly missing: string[]) {\n super(\n `DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(', ')} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`,\n )\n this.name = 'DerivedUserPasswordRequiredError'\n }\n}\n\nexport type SetupInitialTenantResult = {\n tenantId: string\n organizationId: string\n users: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }>\n reusedExistingUser: boolean\n}\n\nexport async function upsertSetupEncryptionMaps(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n specs: readonly ModuleEncryptionMap[],\n): Promise<string[]> {\n const materializedEntityIds: string[] = []\n for (const spec of specs) {\n if (spec.keyScope === 'system') continue\n await upsertCanonicalEncryptionMap(em, {\n entityId: spec.entityId,\n tenantId,\n organizationId,\n fields: spec.fields,\n isActive: true,\n })\n materializedEntityIds.push(spec.entityId)\n }\n return materializedEntityIds\n}\n\nexport async function invalidateSetupEncryptionMaps(\n encryptionService: Pick<TenantDataEncryptionService, 'invalidateMap'>,\n entityIds: readonly string[],\n tenantId: string,\n organizationId: string,\n): Promise<void> {\n for (const entityId of entityIds) {\n await encryptionService.invalidateMap(entityId, tenantId, organizationId)\n }\n await encryptionService.invalidateMap('auth:user', tenantId, null)\n}\n\nexport async function setupInitialTenant(\n em: EntityManager,\n options: SetupInitialTenantOptions,\n): Promise<SetupInitialTenantResult> {\n const {\n primaryUser,\n includeDerivedUsers = true,\n failIfUserExists = false,\n primaryUserRoles,\n includeSuperadminRole = true,\n } = options\n const primaryRolesInput = primaryUserRoles && primaryUserRoles.length ? primaryUserRoles : ['superadmin']\n const primaryRoles = includeSuperadminRole\n ? primaryRolesInput\n : primaryRolesInput.filter((role) => role !== 'superadmin')\n if (primaryRoles.length === 0) {\n throw new Error('PRIMARY_ROLES_REQUIRED')\n }\n const defaultRoleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const resolvedRoleNames = includeSuperadminRole\n ? defaultRoleNames\n : defaultRoleNames.filter((role) => role !== 'superadmin')\n const roleNames = Array.from(new Set([...resolvedRoleNames, ...primaryRoles]))\n const resolvedModules = options.modules ?? tryGetModules()\n const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules)\n\n const mainEmail = primaryUser.email\n const existingUserByHash = await findOneWithDecryption(\n em,\n User,\n { emailHash: { $in: emailHashLookupValues(mainEmail) }, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n const existingUser = existingUserByHash ?? await findOneWithDecryption(\n em,\n User,\n { email: mainEmail, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n if (existingUser && failIfUserExists) {\n throw new Error('USER_EXISTS')\n }\n\n if (options.orgSlug) {\n const slugConflict = await findOneWithDecryption(\n em,\n Organization,\n { slug: options.orgSlug },\n {},\n { tenantId: null, organizationId: null },\n )\n if (slugConflict) {\n throw new OrgSlugExistsError(options.orgSlug)\n }\n }\n\n let tenantId: string | undefined\n let organizationId: string | undefined\n let reusedExistingUser = false\n let materializedEncryptionEntityIds: string[] = []\n const userSnapshots: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }> = []\n\n await em.transactional(async (tem) => {\n if (!existingUser) return\n reusedExistingUser = true\n tenantId = existingUser.tenantId ? String(existingUser.tenantId) : undefined\n organizationId = existingUser.organizationId ? String(existingUser.organizationId) : undefined\n const roleTenantId = normalizeTenantId(existingUser.tenantId ?? null) ?? null\n if (!roleTenantId) {\n throw new Error('Cannot reuse a user without a tenantId \u2014 global roles are not supported')\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n const requiredRoleSet = new Set([...roleNames, ...primaryRoles])\n const requiredRoles = await Promise.all(\n Array.from(requiredRoleSet).map((roleName) => findRoleByNameOrFail(tem, roleName, roleTenantId)),\n )\n await lockUserRoleWriterAuthorizationState(tem, {\n userIds: [String(existingUser.id)],\n roleIds: requiredRoles.map((role) => String(role.id)),\n })\n const links = await findWithDecryption(\n tem,\n UserRole,\n { user: existingUser },\n { populate: ['role'] },\n { tenantId: roleTenantId, organizationId: null },\n )\n const currentRoles = new Set(links.map((link) => link.role.name))\n for (const role of requiredRoles) {\n const roleName = role.name\n if (!currentRoles.has(roleName)) {\n tem.persist(tem.create(UserRole, { user: existingUser, role, createdAt: new Date() }))\n }\n }\n await tem.flush()\n const roles = Array.from(new Set([...currentRoles, ...roleNames]))\n userSnapshots.push({ user: existingUser, roles, created: false })\n })\n\n if (!existingUser) {\n const baseUsers: Array<{\n email: string\n roles: string[]\n name?: string | null\n passwordHash?: string | null\n generatedPassword?: string | null\n }> = [\n { email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },\n ]\n if (includeDerivedUsers) {\n const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)\n const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)\n const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const envAdminPwd = readEnvValue('OM_INIT_ADMIN_PASSWORD')\n const envEmployeePwd = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')\n const isProduction = process.env.NODE_ENV === 'production'\n const allowDemo = options.allowDemoDerivedPasswords === true\n if (isProduction && !allowDemo) {\n const missing: string[] = []\n if (!envAdminPwd) missing.push('OM_INIT_ADMIN_PASSWORD')\n if (!envEmployeePwd) missing.push('OM_INIT_EMPLOYEE_PASSWORD')\n if (missing.length) {\n throw new DerivedUserPasswordRequiredError(missing)\n }\n }\n // In non-production, fall back to the well-known DEMO_DERIVED_PASSWORD so\n // local dev workflows stay predictable (admin@/employee@ login with the\n // documented demo password). In production we either have env-supplied\n // values, or \u2014 for opt-in --include-demo-users flows \u2014 we generate a\n // random one-time password and surface it via `generatedPassword` so the\n // operator can capture it. Production callers without env vars and\n // without the opt-in already threw above.\n const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd\n const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd\n const adminPasswordHash = await hash(adminPasswordPlain, 10)\n const employeePasswordHash = await hash(employeePasswordPlain, 10)\n addUniqueBaseUser(baseUsers, {\n email: adminEmail,\n roles: ['admin'],\n passwordHash: adminPasswordHash,\n generatedPassword: envAdminPwd ? null : adminPasswordPlain,\n })\n addUniqueBaseUser(baseUsers, {\n email: employeeEmail,\n roles: ['employee'],\n passwordHash: employeePasswordHash,\n generatedPassword: envEmployeePwd ? null : employeePasswordPlain,\n })\n }\n const passwordHash = await resolvePasswordHash(primaryUser)\n\n await em.transactional(async (tem) => {\n const tenant = tem.create(Tenant, {\n name: options.orgName,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(tenant)\n await tem.flush()\n\n const organization = tem.create(Organization, {\n name: options.orgName,\n slug: options.orgSlug ?? null,\n tenant,\n isActive: true,\n depth: 0,\n ancestorIds: [],\n childIds: [],\n descendantIds: [],\n logoPreserveAspectRatio: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(organization)\n await tem.flush()\n\n tenantId = String(tenant.id)\n organizationId = String(organization.id)\n const roleTenantId = tenantId\n\n if (isTenantDataEncryptionEnabled()) {\n try {\n const kms = createKmsService()\n if (kms.isHealthy()) {\n if (isEncryptionDebugEnabled()) {\n logger.info('Provisioning tenant DEK', { tenantId: String(tenant.id) })\n }\n await kms.createTenantDek(String(tenant.id))\n if (isEncryptionDebugEnabled()) {\n logger.info('Created tenant DEK during setup', { tenantId: String(tenant.id) })\n }\n } else {\n if (isEncryptionDebugEnabled()) {\n logger.warn('KMS not healthy, skipping tenant DEK creation', { tenantId: String(tenant.id) })\n }\n }\n } catch (err) {\n if (isEncryptionDebugEnabled()) {\n logger.warn('Failed to create tenant DEK', { err })\n }\n }\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n if (isTenantDataEncryptionEnabled()) {\n // System-scoped maps are resolved from module code, never from a tenant row.\n // Persisting one here would make the tenant-scoped encryption CLIs believe they\n // own that entity and re-wrap its `system:<entityId>` ciphertext under the tenant\n // DEK, which runtime decryption can no longer read.\n materializedEncryptionEntityIds = await upsertSetupEncryptionMaps(\n tem,\n String(tenant.id),\n String(organization.id),\n defaultEncryptionMaps,\n )\n }\n })\n\n await em.transactional(async (tem) => {\n if (!tenantId || !organizationId) return\n const roleTenantId = tenantId\n const encryptionService = isTenantDataEncryptionEnabled()\n ? new TenantDataEncryptionService(tem as any, { kms: createKmsService() })\n : null\n if (encryptionService) {\n await invalidateSetupEncryptionMaps(\n encryptionService,\n materializedEncryptionEntityIds,\n String(tenantId),\n String(organizationId),\n )\n }\n\n for (const base of baseUsers) {\n const resolvedPasswordHash = base.passwordHash ?? passwordHash\n let user = await findOneWithDecryption(tem, User, { email: base.email }, {}, { tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const confirm = primaryUser.confirm ?? true\n const encryptedPayload = encryptionService\n ? await encryptionService.encryptEntityPayload('auth:user', { email: base.email }, tenantId, organizationId)\n : { email: base.email, emailHash: computeEmailHash(base.email) }\n if (user) {\n user.passwordHash = resolvedPasswordHash\n user.organizationId = organizationId\n user.tenantId = tenantId\n if (isTenantDataEncryptionEnabled()) {\n user.email = encryptedPayload.email as any\n user.emailHash = (encryptedPayload as any).emailHash ?? computeEmailHash(base.email)\n }\n if (base.name) user.name = base.name\n if (confirm) user.isConfirmed = true\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null })\n } else {\n user = tem.create(User, {\n email: (encryptedPayload as any).email ?? base.email,\n emailHash: isTenantDataEncryptionEnabled() ? (encryptedPayload as any).emailHash ?? computeEmailHash(base.email) : undefined,\n passwordHash: resolvedPasswordHash,\n organizationId,\n tenantId,\n name: base.name ?? undefined,\n isConfirmed: confirm,\n createdAt: new Date(),\n })\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null })\n }\n await tem.flush()\n const assignedRoles = await Promise.all(\n base.roles.map((roleName) => findRoleByNameOrFail(tem, roleName, roleTenantId)),\n )\n await lockUserRoleWriterAuthorizationState(tem, {\n userIds: [String(user.id)],\n roleIds: assignedRoles.map((role) => String(role.id)),\n })\n for (const role of assignedRoles) {\n const existingLink = await findOneWithDecryption(tem, UserRole, { user, role }, {}, { tenantId: tenantId ?? null, organizationId: null })\n if (!existingLink) tem.persist(tem.create(UserRole, { user, role, createdAt: new Date() }))\n }\n await tem.flush()\n }\n })\n }\n\n if (!tenantId || !organizationId) {\n throw new Error('SETUP_FAILED')\n }\n\n if (!reusedExistingUser) {\n await rebuildHierarchyForTenant(em, tenantId)\n }\n\n await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole })\n await deactivateDemoUsersIfSelfOnboardingEnabled(em)\n\n // Call module onTenantCreated hooks\n for (const mod of resolvedModules) {\n if (mod.setup?.onTenantCreated) {\n await mod.setup.onTenantCreated({ em, tenantId, organizationId })\n }\n }\n\n // Custom roles are often created inside onTenantCreated hooks, so they did not\n // exist yet when ensureDefaultRoleAcls ran above and were skipped there. Sync\n // again now that the hooks have had a chance to create them \u2014 idempotent, same\n // pattern as the `mercato auth sync-role-acls` CLI command.\n await ensureCustomRoleAcls(em, tenantId, resolvedModules)\n\n return {\n tenantId,\n organizationId,\n users: userSnapshots,\n reusedExistingUser,\n }\n}\n\nfunction resolvePrimaryName(input: PrimaryUserInput): string | null {\n if (input.displayName && input.displayName.trim()) return input.displayName.trim()\n const parts = [input.firstName, input.lastName].map((value) => value?.trim()).filter(Boolean)\n if (parts.length) return parts.join(' ')\n return null\n}\n\nfunction readEnvValue(key: string): string | undefined {\n const value = process.env[key]\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction addUniqueBaseUser(\n baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null }>,\n entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null },\n) {\n if (!entry.email) return\n const normalized = entry.email.toLowerCase()\n if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return\n baseUsers.push(entry)\n}\n\n/**\n * Well-known demo password seeded for derived admin@/employee@ accounts in\n * non-production (`NODE_ENV !== 'production'`) when no env override is set.\n * Keeps the documented `yarn dev` / `mercato init` DX predictable. Never used\n * in production: the production branch either consumes env-supplied values or\n * falls back to `generateDerivedPassword()` so credentials remain non-guessable.\n */\nconst DEMO_DERIVED_PASSWORD = 'secret'\n\n/**\n * Generate a 16-character base64url password (96 bits of entropy) for a derived\n * demo user in production when no env override is provided AND the caller\n * explicitly opted in via `allowDemoDerivedPasswords`. Surfaced via the\n * `users[].generatedPassword` snapshot so CLI callers can print it to the\n * operator \u2014 there is no other recovery path for these credentials.\n */\nfunction generateDerivedPassword(): string {\n return randomBytes(12).toString('base64url')\n}\n\nfunction isDemoModeEnabled(): boolean {\n const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')\n return parsed === false ? false : true\n}\n\nfunction shouldKeepDemoSuperadminDuringInit(): boolean {\n if (process.env.OM_INIT_FLOW !== 'true') return false\n if (!readEnvValue('OM_INIT_SUPERADMIN_EMAIL')) return false\n return isDemoModeEnabled()\n}\n\nasync function resolvePasswordHash(input: PrimaryUserInput): Promise<string | null> {\n if (typeof input.hashedPassword === 'string') return input.hashedPassword\n if (input.password) return hash(input.password, 10)\n return null\n}\n\nexport async function ensureDefaultRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n options: { includeSuperadminRole?: boolean } = {},\n) {\n const includeSuperadminRole = options.includeSuperadminRole ?? true\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const superadminRole = includeSuperadminRole ? await findRoleByName(em, 'superadmin', roleTenantId) : null\n const adminRole = await findRoleByName(em, 'admin', roleTenantId)\n const employeeRole = await findRoleByName(em, 'employee', roleTenantId)\n\n // Merge features from all enabled modules' setup configs\n const builtInRoles = ['superadmin', 'admin', 'employee'] as const\n const superadminFeatures: string[] = []\n const adminFeatures: string[] = []\n const employeeFeatures: string[] = []\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of modules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n if (roleFeatures.superadmin) superadminFeatures.push(...roleFeatures.superadmin)\n if (roleFeatures.admin) adminFeatures.push(...roleFeatures.admin)\n if (roleFeatures.employee) employeeFeatures.push(...roleFeatures.employee)\n\n // Collect features for custom roles (any key not in builtInRoles)\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if ((builtInRoles as readonly string[]).includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n logger.info('Seeded default role features', {\n superadmin: superadminFeatures,\n admin: adminFeatures,\n employee: employeeFeatures,\n ...(customRoleFeatures.size > 0\n ? Object.fromEntries(customRoleFeatures)\n : {}),\n })\n\n if (includeSuperadminRole && superadminRole) {\n await ensureRoleAclFor(em, superadminRole, tenantId, superadminFeatures, { isSuperAdmin: true })\n }\n if (adminRole) {\n await ensureRoleAclFor(em, adminRole, tenantId, adminFeatures)\n }\n if (employeeRole) {\n await ensureRoleAclFor(em, employeeRole, tenantId, employeeFeatures)\n }\n\n // Seed ACLs for custom roles defined by app modules.\n // NOTE: Custom roles may not exist yet if they are created in seedDefaults\n // (which runs after this function). In that case, use ensureCustomRoleAcls()\n // after seedDefaults to pick them up.\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n }\n }\n}\n\n/**\n * Seed ACLs for custom roles defined in module defaultRoleFeatures.\n * Call this AFTER seedDefaults to pick up roles created by app modules.\n * Safe to call multiple times \u2014 ensureRoleAclFor merges features idempotently.\n */\nexport async function ensureCustomRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules?: Module[],\n): Promise<void> {\n const resolvedModules = modules ?? tryGetModules()\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const builtInRoles = ['superadmin', 'admin', 'employee']\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of resolvedModules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if (builtInRoles.includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n if (customRoleFeatures.size === 0) return\n\n let seeded = 0\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n seeded++\n }\n }\n if (seeded > 0) {\n logger.info('Seeded custom role ACLs', { seeded })\n }\n}\n\nasync function ensureRoleAclFor(\n em: EntityManager,\n role: Role,\n tenantId: string,\n features: string[],\n options: { isSuperAdmin?: boolean } = {},\n) {\n // Deduplicated on the create path too, not only on merge. `ensureDefaultRoleAcls` concatenates\n // every enabled module's declared features, and two modules legitimately declare the same\n // grant (the example module asks for `payment_gateways.view`, and so does that module itself),\n // so a first init used to store the string twice while the second run silently collapsed it.\n // Same list, two shapes, depending only on how many times setup had run.\n const uniqueFeatures = Array.from(new Set(features))\n await withAtomicFlush(em, [async () => {\n await lockRoleAclWriterAuthorizationState(em, [String(role.id)])\n const existing = await findOneWithDecryption(em, RoleAcl, { role, tenantId }, {}, { tenantId, organizationId: null })\n if (!existing) {\n const acl = em.create(RoleAcl, {\n role,\n tenantId,\n featuresJson: uniqueFeatures,\n isSuperAdmin: !!options.isSuperAdmin,\n createdAt: new Date(),\n })\n await em.persist(acl).flush()\n return\n }\n const currentFeatures = Array.isArray(existing.featuresJson) ? existing.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...uniqueFeatures]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) existing.featuresJson = merged\n if (options.isSuperAdmin && !existing.isSuperAdmin) {\n existing.isSuperAdmin = true\n }\n if (changed || options.isSuperAdmin) {\n await em.persist(existing).flush()\n }\n }], { transaction: true, label: 'auth.ensure-role-acl' })\n}\n\n/**\n * Neutralizes every demo account the setup path may have seeded\n * (superadmin, admin, employee \u2014 honoring OM_INIT_*_EMAIL overrides)\n * when SELF_SERVICE_ONBOARDING_ENABLED is on and the operator did not\n * opt in to keeping demo credentials via shouldKeepDemoSuperadminDuringInit.\n *\n * Each user is processed in its own try/catch so a single failure (e.g.\n * decryption error on a legacy row) does not skip the remaining accounts.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport async function deactivateDemoUsersIfSelfOnboardingEnabled(em: EntityManager) {\n if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return\n if (shouldKeepDemoSuperadminDuringInit()) return\n for (const { role, email } of resolveDemoUserEmails()) {\n try {\n const user = await findOneWithDecryption(\n em,\n User,\n { email },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!user) continue\n let dirty = false\n if (user.passwordHash) {\n user.passwordHash = null\n dirty = true\n }\n if (user.isConfirmed !== false) {\n user.isConfirmed = false\n dirty = true\n }\n if (dirty) {\n await em.persist(user).flush()\n }\n } catch (error) {\n logger.error('Failed to deactivate demo user', { role, email, err: error })\n }\n }\n}\n\n/**\n * @deprecated Renamed to {@link deactivateDemoUsersIfSelfOnboardingEnabled}\n * because the helper now neutralizes admin/employee demo accounts in addition\n * to superadmin (security tracker finding #5). Kept as an internal alias to\n * avoid breaking any out-of-tree caller that imported the old name; will be\n * removed in a future major.\n */\nconst deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled\n\n/** Try to get modules from runtime registry; returns empty array if not yet registered. */\nfunction tryGetModules(): Module[] {\n try {\n const { getModules } = require('@open-mercato/shared/lib/modules/registry')\n return getModules()\n } catch {\n return []\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { hash } from 'bcryptjs'\nimport { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'\nimport { normalizeTenantId } from './tenantAccess'\nimport { computeEmailHash, emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { getDefaultEncryptionMaps, type Module } from '@open-mercato/shared/modules/registry'\nimport type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'\nimport { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { upsertCanonicalEncryptionMap } from '@open-mercato/core/modules/entities/lib/encryption-maps'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n lockRoleAclWriterAuthorizationState,\n lockUserRoleWriterAuthorizationState,\n} from '@open-mercato/core/modules/auth/lib/authorizationStateLocks'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'setup' })\n\nconst DEFAULT_ROLE_NAMES = ['employee', 'admin', 'superadmin'] as const\nconst DEMO_SUPERADMIN_EMAIL = 'superadmin@acme.com'\nconst DEFAULT_DERIVED_EMAIL_DOMAIN = DEMO_SUPERADMIN_EMAIL.split('@')[1] ?? 'acme.com'\n\nexport type EnsureRolesOptions = {\n roleNames?: string[]\n tenantId?: string\n}\n\nasync function ensureRolesInContext(\n em: EntityManager,\n roleNames: string[],\n tenantId: string,\n) {\n for (const name of roleNames) {\n const existing = await findOneWithDecryption(em, Role, { name, tenantId }, {}, { tenantId, organizationId: null })\n if (existing) continue\n em.persist(em.create(Role, { name, tenantId, minActiveHolders: name === 'admin' ? 1 : 0, createdAt: new Date() }))\n }\n}\n\nexport async function ensureRoles(em: EntityManager, options: EnsureRolesOptions = {}) {\n const roleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const tenantId = normalizeTenantId(options.tenantId ?? null)\n if (!tenantId) {\n throw new Error('ensureRoles requires a tenantId \u2014 global roles are not supported')\n }\n await em.transactional(async (tem) => {\n await ensureRolesInContext(tem, roleNames, tenantId)\n await tem.flush()\n })\n}\n\nasync function findRoleByName(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role | null> {\n const normalizedTenant = normalizeTenantId(tenantId ?? null) ?? null\n return findOneWithDecryption(em, Role, { name, tenantId: normalizedTenant }, {}, { tenantId: normalizedTenant, organizationId: null })\n}\n\nasync function findRoleByNameOrFail(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role> {\n const role = await findRoleByName(em, name, tenantId)\n if (!role) throw new Error(`ROLE_NOT_FOUND:${name}`)\n return role\n}\n\ntype PrimaryUserInput = {\n email: string\n password?: string\n hashedPassword?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n confirm?: boolean\n}\n\nconst DERIVED_EMAIL_ENV = {\n admin: 'OM_INIT_ADMIN_EMAIL',\n employee: 'OM_INIT_EMPLOYEE_EMAIL',\n} as const\n\nexport type DemoUserRole = 'superadmin' | 'admin' | 'employee'\nexport type DemoUserEmail = { role: DemoUserRole; email: string }\n\n/**\n * Returns the canonical list of demo user emails the setup path may have\n * seeded. Honors OM_INIT_ADMIN_EMAIL / OM_INIT_EMPLOYEE_EMAIL exactly the\n * same way the derived-user seeding branch does, so the deactivation loop\n * never drifts from the seeding loop.\n *\n * Pure function \u2014 no side effects, safe to unit-test in isolation.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport function resolveDemoUserEmails(): DemoUserEmail[] {\n const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n return [\n { role: 'superadmin', email: DEMO_SUPERADMIN_EMAIL },\n { role: 'admin', email: adminEmail },\n { role: 'employee', email: employeeEmail },\n ]\n}\n\nexport type SetupInitialTenantOptions = {\n orgName: string\n primaryUser: PrimaryUserInput\n roleNames?: string[]\n includeDerivedUsers?: boolean\n failIfUserExists?: boolean\n primaryUserRoles?: string[]\n includeSuperadminRole?: boolean\n /** Optional list of enabled modules. When provided, module setup hooks are called. */\n modules?: Module[]\n /**\n * Optional global slug to persist on the new Organization. When set, a\n * pre-flight uniqueness check across all tenants throws OrgSlugExistsError\n * on collision so scriptable callers can fail loudly instead of silently\n * reusing or clobbering an existing organization.\n */\n orgSlug?: string\n /**\n * Opt-in flag that allows seeding the derived admin/employee accounts when\n * the OM_INIT_ADMIN_PASSWORD / OM_INIT_EMPLOYEE_PASSWORD env vars are unset.\n *\n * - In non-production (`NODE_ENV !== 'production'`): the demo accounts are\n * seeded with the well-known `'secret'` password so local dev workflows\n * (e.g. `mercato init`) stay predictable for developers.\n * - In production: the fallback uses a randomly generated 96-bit password\n * (printed once by the CLI) so an operator who deliberately opts in still\n * avoids the historical hardcoded credential. When this flag is false/unset\n * in production and the env vars are missing, `DerivedUserPasswordRequiredError`\n * is thrown instead of silently seeding any account.\n */\n allowDemoDerivedPasswords?: boolean\n}\n\nexport class OrgSlugExistsError extends Error {\n constructor(public readonly slug: string) {\n super(`ORG_SLUG_EXISTS: an organization with slug \"${slug}\" already exists`)\n this.name = 'OrgSlugExistsError'\n }\n}\n\nexport class DerivedUserPasswordRequiredError extends Error {\n constructor(public readonly missing: string[]) {\n super(\n `DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(', ')} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`,\n )\n this.name = 'DerivedUserPasswordRequiredError'\n }\n}\n\nexport type SetupInitialTenantResult = {\n tenantId: string\n organizationId: string\n users: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }>\n reusedExistingUser: boolean\n}\n\nexport async function upsertSetupEncryptionMaps(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n specs: readonly ModuleEncryptionMap[],\n): Promise<string[]> {\n const materializedEntityIds: string[] = []\n for (const spec of specs) {\n if (spec.keyScope === 'system') continue\n await upsertCanonicalEncryptionMap(em, {\n entityId: spec.entityId,\n tenantId,\n organizationId,\n fields: spec.fields,\n isActive: true,\n })\n materializedEntityIds.push(spec.entityId)\n }\n return materializedEntityIds\n}\n\nexport async function invalidateSetupEncryptionMaps(\n encryptionService: Pick<TenantDataEncryptionService, 'invalidateMap'>,\n entityIds: readonly string[],\n tenantId: string,\n organizationId: string,\n): Promise<void> {\n for (const entityId of entityIds) {\n await encryptionService.invalidateMap(entityId, tenantId, organizationId)\n }\n await encryptionService.invalidateMap('auth:user', tenantId, null)\n}\n\nexport async function setupInitialTenant(\n em: EntityManager,\n options: SetupInitialTenantOptions,\n): Promise<SetupInitialTenantResult> {\n const {\n primaryUser,\n includeDerivedUsers = true,\n failIfUserExists = false,\n primaryUserRoles,\n includeSuperadminRole = true,\n } = options\n const primaryRolesInput = primaryUserRoles && primaryUserRoles.length ? primaryUserRoles : ['superadmin']\n const primaryRoles = includeSuperadminRole\n ? primaryRolesInput\n : primaryRolesInput.filter((role) => role !== 'superadmin')\n if (primaryRoles.length === 0) {\n throw new Error('PRIMARY_ROLES_REQUIRED')\n }\n const defaultRoleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const resolvedRoleNames = includeSuperadminRole\n ? defaultRoleNames\n : defaultRoleNames.filter((role) => role !== 'superadmin')\n const roleNames = Array.from(new Set([...resolvedRoleNames, ...primaryRoles]))\n const resolvedModules = options.modules ?? tryGetModules()\n const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules)\n\n const mainEmail = primaryUser.email\n const existingUserByHash = await findOneWithDecryption(\n em,\n User,\n { emailHash: { $in: emailHashLookupValues(mainEmail) }, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n const existingUser = existingUserByHash ?? await findOneWithDecryption(\n em,\n User,\n { email: mainEmail, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n if (existingUser && failIfUserExists) {\n throw new Error('USER_EXISTS')\n }\n\n if (options.orgSlug) {\n const slugConflict = await findOneWithDecryption(\n em,\n Organization,\n { slug: options.orgSlug },\n {},\n { tenantId: null, organizationId: null },\n )\n if (slugConflict) {\n throw new OrgSlugExistsError(options.orgSlug)\n }\n }\n\n let tenantId: string | undefined\n let organizationId: string | undefined\n let reusedExistingUser = false\n let materializedEncryptionEntityIds: string[] = []\n const userSnapshots: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }> = []\n\n await em.transactional(async (tem) => {\n if (!existingUser) return\n reusedExistingUser = true\n tenantId = existingUser.tenantId ? String(existingUser.tenantId) : undefined\n organizationId = existingUser.organizationId ? String(existingUser.organizationId) : undefined\n const roleTenantId = normalizeTenantId(existingUser.tenantId ?? null) ?? null\n if (!roleTenantId) {\n throw new Error('Cannot reuse a user without a tenantId \u2014 global roles are not supported')\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n const requiredRoleSet = new Set([...roleNames, ...primaryRoles])\n const requiredRoles = await Promise.all(\n Array.from(requiredRoleSet).map((roleName) => findRoleByNameOrFail(tem, roleName, roleTenantId)),\n )\n await lockUserRoleWriterAuthorizationState(tem, {\n userIds: [String(existingUser.id)],\n roleIds: requiredRoles.map((role) => String(role.id)),\n })\n const links = await findWithDecryption(\n tem,\n UserRole,\n { user: existingUser },\n { populate: ['role'] },\n { tenantId: roleTenantId, organizationId: null },\n )\n const currentRoles = new Set(links.map((link) => link.role.name))\n for (const role of requiredRoles) {\n const roleName = role.name\n if (!currentRoles.has(roleName)) {\n tem.persist(tem.create(UserRole, { user: existingUser, role, createdAt: new Date() }))\n }\n }\n await tem.flush()\n const roles = Array.from(new Set([...currentRoles, ...roleNames]))\n userSnapshots.push({ user: existingUser, roles, created: false })\n })\n\n if (!existingUser) {\n const baseUsers: Array<{\n email: string\n roles: string[]\n name?: string | null\n passwordHash?: string | null\n generatedPassword?: string | null\n }> = [\n { email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },\n ]\n if (includeDerivedUsers) {\n const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)\n const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)\n const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const envAdminPwd = readEnvValue('OM_INIT_ADMIN_PASSWORD')\n const envEmployeePwd = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')\n const isProduction = process.env.NODE_ENV === 'production'\n const allowDemo = options.allowDemoDerivedPasswords === true\n if (isProduction && !allowDemo) {\n const missing: string[] = []\n if (!envAdminPwd) missing.push('OM_INIT_ADMIN_PASSWORD')\n if (!envEmployeePwd) missing.push('OM_INIT_EMPLOYEE_PASSWORD')\n if (missing.length) {\n throw new DerivedUserPasswordRequiredError(missing)\n }\n }\n // In non-production, fall back to the well-known DEMO_DERIVED_PASSWORD so\n // local dev workflows stay predictable (admin@/employee@ login with the\n // documented demo password). In production we either have env-supplied\n // values, or \u2014 for opt-in --include-demo-users flows \u2014 we generate a\n // random one-time password and surface it via `generatedPassword` so the\n // operator can capture it. Production callers without env vars and\n // without the opt-in already threw above.\n const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd\n const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd\n const adminPasswordHash = await hash(adminPasswordPlain, 10)\n const employeePasswordHash = await hash(employeePasswordPlain, 10)\n addUniqueBaseUser(baseUsers, {\n email: adminEmail,\n roles: ['admin'],\n passwordHash: adminPasswordHash,\n generatedPassword: envAdminPwd ? null : adminPasswordPlain,\n })\n addUniqueBaseUser(baseUsers, {\n email: employeeEmail,\n roles: ['employee'],\n passwordHash: employeePasswordHash,\n generatedPassword: envEmployeePwd ? null : employeePasswordPlain,\n })\n }\n const passwordHash = await resolvePasswordHash(primaryUser)\n\n await em.transactional(async (tem) => {\n const tenant = tem.create(Tenant, {\n name: options.orgName,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(tenant)\n await tem.flush()\n\n const organization = tem.create(Organization, {\n name: options.orgName,\n slug: options.orgSlug ?? null,\n tenant,\n isActive: true,\n depth: 0,\n ancestorIds: [],\n childIds: [],\n descendantIds: [],\n logoPreserveAspectRatio: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(organization)\n await tem.flush()\n\n tenantId = String(tenant.id)\n organizationId = String(organization.id)\n const roleTenantId = tenantId\n\n if (isTenantDataEncryptionEnabled()) {\n try {\n const kms = createKmsService()\n if (kms.isHealthy()) {\n if (isEncryptionDebugEnabled()) {\n logger.info('Provisioning tenant DEK', { tenantId: String(tenant.id) })\n }\n await kms.createTenantDek(String(tenant.id))\n if (isEncryptionDebugEnabled()) {\n logger.info('Created tenant DEK during setup', { tenantId: String(tenant.id) })\n }\n } else {\n if (isEncryptionDebugEnabled()) {\n logger.warn('KMS not healthy, skipping tenant DEK creation', { tenantId: String(tenant.id) })\n }\n }\n } catch (err) {\n if (isEncryptionDebugEnabled()) {\n logger.warn('Failed to create tenant DEK', { err })\n }\n }\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n if (isTenantDataEncryptionEnabled()) {\n // System-scoped maps are resolved from module code, never from a tenant row.\n // Persisting one here would make the tenant-scoped encryption CLIs believe they\n // own that entity and re-wrap its `system:<entityId>` ciphertext under the tenant\n // DEK, which runtime decryption can no longer read.\n materializedEncryptionEntityIds = await upsertSetupEncryptionMaps(\n tem,\n String(tenant.id),\n String(organization.id),\n defaultEncryptionMaps,\n )\n }\n })\n\n await em.transactional(async (tem) => {\n if (!tenantId || !organizationId) return\n const roleTenantId = tenantId\n const encryptionService = isTenantDataEncryptionEnabled()\n ? new TenantDataEncryptionService(tem as any, { kms: createKmsService() })\n : null\n if (encryptionService) {\n await invalidateSetupEncryptionMaps(\n encryptionService,\n materializedEncryptionEntityIds,\n String(tenantId),\n String(organizationId),\n )\n }\n\n for (const base of baseUsers) {\n const resolvedPasswordHash = base.passwordHash ?? passwordHash\n let user = await findOneWithDecryption(tem, User, { email: base.email }, {}, { tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const confirm = primaryUser.confirm ?? true\n const encryptedPayload = encryptionService\n ? await encryptionService.encryptEntityPayload('auth:user', { email: base.email }, tenantId, organizationId)\n : { email: base.email, emailHash: computeEmailHash(base.email) }\n if (user) {\n user.passwordHash = resolvedPasswordHash\n user.organizationId = organizationId\n user.tenantId = tenantId\n if (isTenantDataEncryptionEnabled()) {\n user.email = encryptedPayload.email as any\n user.emailHash = (encryptedPayload as any).emailHash ?? computeEmailHash(base.email)\n }\n if (base.name) user.name = base.name\n if (confirm) user.isConfirmed = true\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null })\n } else {\n user = tem.create(User, {\n email: (encryptedPayload as any).email ?? base.email,\n emailHash: isTenantDataEncryptionEnabled() ? (encryptedPayload as any).emailHash ?? computeEmailHash(base.email) : undefined,\n passwordHash: resolvedPasswordHash,\n organizationId,\n tenantId,\n name: base.name ?? undefined,\n isConfirmed: confirm,\n createdAt: new Date(),\n })\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null })\n }\n await tem.flush()\n const assignedRoles = await Promise.all(\n base.roles.map((roleName) => findRoleByNameOrFail(tem, roleName, roleTenantId)),\n )\n await lockUserRoleWriterAuthorizationState(tem, {\n userIds: [String(user.id)],\n roleIds: assignedRoles.map((role) => String(role.id)),\n })\n for (const role of assignedRoles) {\n const existingLink = await findOneWithDecryption(tem, UserRole, { user, role, deletedAt: null }, {}, { tenantId: tenantId ?? null, organizationId: null })\n if (!existingLink) tem.persist(tem.create(UserRole, { user, role, createdAt: new Date() }))\n }\n await tem.flush()\n }\n })\n }\n\n if (!tenantId || !organizationId) {\n throw new Error('SETUP_FAILED')\n }\n\n if (!reusedExistingUser) {\n await rebuildHierarchyForTenant(em, tenantId)\n }\n\n await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole })\n await deactivateDemoUsersIfSelfOnboardingEnabled(em)\n\n // Call module onTenantCreated hooks\n for (const mod of resolvedModules) {\n if (mod.setup?.onTenantCreated) {\n await mod.setup.onTenantCreated({ em, tenantId, organizationId })\n }\n }\n\n // Custom roles are often created inside onTenantCreated hooks, so they did not\n // exist yet when ensureDefaultRoleAcls ran above and were skipped there. Sync\n // again now that the hooks have had a chance to create them \u2014 idempotent, same\n // pattern as the `mercato auth sync-role-acls` CLI command.\n await ensureCustomRoleAcls(em, tenantId, resolvedModules)\n\n return {\n tenantId,\n organizationId,\n users: userSnapshots,\n reusedExistingUser,\n }\n}\n\nfunction resolvePrimaryName(input: PrimaryUserInput): string | null {\n if (input.displayName && input.displayName.trim()) return input.displayName.trim()\n const parts = [input.firstName, input.lastName].map((value) => value?.trim()).filter(Boolean)\n if (parts.length) return parts.join(' ')\n return null\n}\n\nfunction readEnvValue(key: string): string | undefined {\n const value = process.env[key]\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction addUniqueBaseUser(\n baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null }>,\n entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null },\n) {\n if (!entry.email) return\n const normalized = entry.email.toLowerCase()\n if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return\n baseUsers.push(entry)\n}\n\n/**\n * Well-known demo password seeded for derived admin@/employee@ accounts in\n * non-production (`NODE_ENV !== 'production'`) when no env override is set.\n * Keeps the documented `yarn dev` / `mercato init` DX predictable. Never used\n * in production: the production branch either consumes env-supplied values or\n * falls back to `generateDerivedPassword()` so credentials remain non-guessable.\n */\nconst DEMO_DERIVED_PASSWORD = 'secret'\n\n/**\n * Generate a 16-character base64url password (96 bits of entropy) for a derived\n * demo user in production when no env override is provided AND the caller\n * explicitly opted in via `allowDemoDerivedPasswords`. Surfaced via the\n * `users[].generatedPassword` snapshot so CLI callers can print it to the\n * operator \u2014 there is no other recovery path for these credentials.\n */\nfunction generateDerivedPassword(): string {\n return randomBytes(12).toString('base64url')\n}\n\nfunction isDemoModeEnabled(): boolean {\n const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')\n return parsed === false ? false : true\n}\n\nfunction shouldKeepDemoSuperadminDuringInit(): boolean {\n if (process.env.OM_INIT_FLOW !== 'true') return false\n if (!readEnvValue('OM_INIT_SUPERADMIN_EMAIL')) return false\n return isDemoModeEnabled()\n}\n\nasync function resolvePasswordHash(input: PrimaryUserInput): Promise<string | null> {\n if (typeof input.hashedPassword === 'string') return input.hashedPassword\n if (input.password) return hash(input.password, 10)\n return null\n}\n\nexport async function ensureDefaultRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n options: { includeSuperadminRole?: boolean } = {},\n) {\n const includeSuperadminRole = options.includeSuperadminRole ?? true\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const superadminRole = includeSuperadminRole ? await findRoleByName(em, 'superadmin', roleTenantId) : null\n const adminRole = await findRoleByName(em, 'admin', roleTenantId)\n const employeeRole = await findRoleByName(em, 'employee', roleTenantId)\n\n // Merge features from all enabled modules' setup configs\n const builtInRoles = ['superadmin', 'admin', 'employee'] as const\n const superadminFeatures: string[] = []\n const adminFeatures: string[] = []\n const employeeFeatures: string[] = []\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of modules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n if (roleFeatures.superadmin) superadminFeatures.push(...roleFeatures.superadmin)\n if (roleFeatures.admin) adminFeatures.push(...roleFeatures.admin)\n if (roleFeatures.employee) employeeFeatures.push(...roleFeatures.employee)\n\n // Collect features for custom roles (any key not in builtInRoles)\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if ((builtInRoles as readonly string[]).includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n logger.info('Seeded default role features', {\n superadmin: superadminFeatures,\n admin: adminFeatures,\n employee: employeeFeatures,\n ...(customRoleFeatures.size > 0\n ? Object.fromEntries(customRoleFeatures)\n : {}),\n })\n\n if (includeSuperadminRole && superadminRole) {\n await ensureRoleAclFor(em, superadminRole, tenantId, superadminFeatures, { isSuperAdmin: true })\n }\n if (adminRole) {\n await ensureRoleAclFor(em, adminRole, tenantId, adminFeatures)\n }\n if (employeeRole) {\n await ensureRoleAclFor(em, employeeRole, tenantId, employeeFeatures)\n }\n\n // Seed ACLs for custom roles defined by app modules.\n // NOTE: Custom roles may not exist yet if they are created in seedDefaults\n // (which runs after this function). In that case, use ensureCustomRoleAcls()\n // after seedDefaults to pick them up.\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n }\n }\n}\n\n/**\n * Seed ACLs for custom roles defined in module defaultRoleFeatures.\n * Call this AFTER seedDefaults to pick up roles created by app modules.\n * Safe to call multiple times \u2014 ensureRoleAclFor merges features idempotently.\n */\nexport async function ensureCustomRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules?: Module[],\n): Promise<void> {\n const resolvedModules = modules ?? tryGetModules()\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const builtInRoles = ['superadmin', 'admin', 'employee']\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of resolvedModules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if (builtInRoles.includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n if (customRoleFeatures.size === 0) return\n\n let seeded = 0\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n seeded++\n }\n }\n if (seeded > 0) {\n logger.info('Seeded custom role ACLs', { seeded })\n }\n}\n\nasync function ensureRoleAclFor(\n em: EntityManager,\n role: Role,\n tenantId: string,\n features: string[],\n options: { isSuperAdmin?: boolean } = {},\n) {\n // Deduplicated on the create path too, not only on merge. `ensureDefaultRoleAcls` concatenates\n // every enabled module's declared features, and two modules legitimately declare the same\n // grant (the example module asks for `payment_gateways.view`, and so does that module itself),\n // so a first init used to store the string twice while the second run silently collapsed it.\n // Same list, two shapes, depending only on how many times setup had run.\n const uniqueFeatures = Array.from(new Set(features))\n await withAtomicFlush(em, [async () => {\n await lockRoleAclWriterAuthorizationState(em, [String(role.id)])\n const existing = await findOneWithDecryption(em, RoleAcl, { role, tenantId, deletedAt: null }, {}, { tenantId, organizationId: null })\n if (!existing) {\n const acl = em.create(RoleAcl, {\n role,\n tenantId,\n featuresJson: uniqueFeatures,\n isSuperAdmin: !!options.isSuperAdmin,\n createdAt: new Date(),\n })\n await em.persist(acl).flush()\n return\n }\n const currentFeatures = Array.isArray(existing.featuresJson) ? existing.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...uniqueFeatures]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) existing.featuresJson = merged\n if (options.isSuperAdmin && !existing.isSuperAdmin) {\n existing.isSuperAdmin = true\n }\n if (changed || options.isSuperAdmin) {\n await em.persist(existing).flush()\n }\n }], { transaction: true, label: 'auth.ensure-role-acl' })\n}\n\n/**\n * Neutralizes every demo account the setup path may have seeded\n * (superadmin, admin, employee \u2014 honoring OM_INIT_*_EMAIL overrides)\n * when SELF_SERVICE_ONBOARDING_ENABLED is on and the operator did not\n * opt in to keeping demo credentials via shouldKeepDemoSuperadminDuringInit.\n *\n * Each user is processed in its own try/catch so a single failure (e.g.\n * decryption error on a legacy row) does not skip the remaining accounts.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport async function deactivateDemoUsersIfSelfOnboardingEnabled(em: EntityManager) {\n if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return\n if (shouldKeepDemoSuperadminDuringInit()) return\n for (const { role, email } of resolveDemoUserEmails()) {\n try {\n const user = await findOneWithDecryption(\n em,\n User,\n { email },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!user) continue\n let dirty = false\n if (user.passwordHash) {\n user.passwordHash = null\n dirty = true\n }\n if (user.isConfirmed !== false) {\n user.isConfirmed = false\n dirty = true\n }\n if (dirty) {\n await em.persist(user).flush()\n }\n } catch (error) {\n logger.error('Failed to deactivate demo user', { role, email, err: error })\n }\n }\n}\n\n/**\n * @deprecated Renamed to {@link deactivateDemoUsersIfSelfOnboardingEnabled}\n * because the helper now neutralizes admin/employee demo accounts in addition\n * to superadmin (security tracker finding #5). Kept as an internal alias to\n * avoid breaking any out-of-tree caller that imported the old name; will be\n * removed in a future major.\n */\nconst deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled\n\n/** Try to get modules from runtime registry; returns empty array if not yet registered. */\nfunction tryGetModules(): Module[] {\n try {\n const { getModules } = require('@open-mercato/shared/lib/modules/registry')\n return getModules()\n } catch {\n return []\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260828120000_auth extends Migration {
|
|
3
|
+
up() {
|
|
4
|
+
this.addSql(`
|
|
5
|
+
with ranked as (
|
|
6
|
+
select id,
|
|
7
|
+
row_number() over (
|
|
8
|
+
partition by user_id, tenant_id
|
|
9
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
10
|
+
) as rn
|
|
11
|
+
from user_acls
|
|
12
|
+
where deleted_at is null
|
|
13
|
+
)
|
|
14
|
+
update user_acls
|
|
15
|
+
set deleted_at = now()
|
|
16
|
+
from ranked
|
|
17
|
+
where user_acls.id = ranked.id and ranked.rn > 1;
|
|
18
|
+
`);
|
|
19
|
+
this.addSql(`create unique index if not exists "user_acls_active_unique_idx" on "user_acls" ("user_id", "tenant_id") where "deleted_at" is null;`);
|
|
20
|
+
this.addSql(`
|
|
21
|
+
with ranked as (
|
|
22
|
+
select id,
|
|
23
|
+
row_number() over (
|
|
24
|
+
partition by role_id, tenant_id
|
|
25
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
26
|
+
) as rn
|
|
27
|
+
from role_acls
|
|
28
|
+
where deleted_at is null
|
|
29
|
+
)
|
|
30
|
+
update role_acls
|
|
31
|
+
set deleted_at = now()
|
|
32
|
+
from ranked
|
|
33
|
+
where role_acls.id = ranked.id and ranked.rn > 1;
|
|
34
|
+
`);
|
|
35
|
+
this.addSql(`create unique index if not exists "role_acls_active_unique_idx" on "role_acls" ("role_id", "tenant_id") where "deleted_at" is null;`);
|
|
36
|
+
}
|
|
37
|
+
down() {
|
|
38
|
+
this.addSql(`drop index if exists "user_acls_active_unique_idx";`);
|
|
39
|
+
this.addSql(`drop index if exists "role_acls_active_unique_idx";`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
Migration20260828120000_auth
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=Migration20260828120000_auth.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/auth/migrations/Migration20260828120000_auth.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\n// `user_acls` and `role_acls` decide what a principal may do, and neither had\n// any uniqueness beyond its primary key. `RbacService.loadAcl` resolves the\n// per-user row with a bare `findOne` and RETURNS EARLY on a hit \u2014 the per-user\n// ACL replaces the role ACLs rather than merging with them \u2014 so two rows for\n// the same `(user_id, tenant_id)` were schema-legal and resolved with no\n// `ORDER BY`: whichever row Postgres happened to return decided the user's\n// entire feature set, and could change between requests.\n//\n// The write path makes that reachable rather than theoretical:\n// `auth.user_acl.update` reads with `findOne` and creates when it misses, so\n// two concurrent saves for the same user both miss and both insert.\n//\n// Partial unique indexes scoped to live rows (`where deleted_at is null`),\n// matching the shape already used for the sidebar tables in\n// Migration20260427143311. A `@Unique` decorator cannot express a partial\n// index, so the entities carry a comment pointing here instead.\n//\n// Historical duplicates are collapsed first \u2014 most recently updated row wins,\n// the rest are soft-deleted, never dropped \u2014 so the indexes can be created on\n// a database that already carries some.\nexport class Migration20260828120000_auth extends Migration {\n\n override up(): void | Promise<void> {\n this.addSql(`\n with ranked as (\n select id,\n row_number() over (\n partition by user_id, tenant_id\n order by coalesce(updated_at, created_at) desc, created_at desc, id desc\n ) as rn\n from user_acls\n where deleted_at is null\n )\n update user_acls\n set deleted_at = now()\n from ranked\n where user_acls.id = ranked.id and ranked.rn > 1;\n `);\n this.addSql(`create unique index if not exists \"user_acls_active_unique_idx\" on \"user_acls\" (\"user_id\", \"tenant_id\") where \"deleted_at\" is null;`);\n\n this.addSql(`\n with ranked as (\n select id,\n row_number() over (\n partition by role_id, tenant_id\n order by coalesce(updated_at, created_at) desc, created_at desc, id desc\n ) as rn\n from role_acls\n where deleted_at is null\n )\n update role_acls\n set deleted_at = now()\n from ranked\n where role_acls.id = ranked.id and ranked.rn > 1;\n `);\n this.addSql(`create unique index if not exists \"role_acls_active_unique_idx\" on \"role_acls\" (\"role_id\", \"tenant_id\") where \"deleted_at\" is null;`);\n }\n\n override down(): void | Promise<void> {\n // Only the indexes are reversible. The collapse soft-deleted rows rather\n // than dropping them, so the data is still there to un-delete by hand if a\n // deployment ever needs it; un-deleting automatically would recreate the\n // ambiguity this migration exists to remove.\n this.addSql(`drop index if exists \"user_acls_active_unique_idx\";`);\n this.addSql(`drop index if exists \"role_acls_active_unique_idx\";`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAsBnB,MAAM,qCAAqC,UAAU;AAAA,EAEjD,KAA2B;AAClC,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAcX;AACD,SAAK,OAAO,qIAAqI;AAEjJ,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAcX;AACD,SAAK,OAAO,qIAAqI;AAAA,EACnJ;AAAA,EAES,OAA6B;AAKpC,SAAK,OAAO,qDAAqD;AACjE,SAAK,OAAO,qDAAqD;AAAA,EACnE;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from "@open-mercato/shared/security/featurePolicy";
|
|
12
12
|
import { filterGrantsByEnabledModules } from "@open-mercato/shared/security/enabledModulesRegistry";
|
|
13
13
|
import { resolveRoleOrganizationScope, roleAclAllowsOrganization } from "./roleOrganizationScope.js";
|
|
14
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
|
+
const logger = createLogger("auth.rbac");
|
|
14
16
|
async function resolveRoleOrganizationScopeWithEntityManager(em, tenantId, organizationId) {
|
|
15
17
|
if (!organizationId) return null;
|
|
16
18
|
if (!tenantId) return /* @__PURE__ */ new Set();
|
|
@@ -258,24 +260,64 @@ class RbacService {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
// A super-admin grant is deliberately GLOBAL: it survives every scope the
|
|
264
|
+
// caller asks about, which is what lets a platform super-admin work inside a
|
|
265
|
+
// customer tenant. What it must not do is survive the question of whose grant
|
|
266
|
+
// it is. Both lookups below are therefore evaluated against the user's OWN
|
|
267
|
+
// tenant (`users.tenant_id`), never against no tenant at all.
|
|
268
|
+
//
|
|
269
|
+
// Without that binding a single `user_acls` row — or a role link — stamped
|
|
270
|
+
// with ANY tenant confers platform super-admin everywhere, so a row written
|
|
271
|
+
// under the wrong tenant becomes a full privilege escalation with nothing in
|
|
272
|
+
// the schema or the UI to show for it. `user_acls` has no organization column
|
|
273
|
+
// and no uniqueness beyond its primary key, so such a row is cheap to create
|
|
274
|
+
// and invisible to the tenant/organization cross-wire checks.
|
|
275
|
+
//
|
|
276
|
+
// This is the tenant binding `resolveCanonicalStaffAuthContext` already
|
|
277
|
+
// applies when it computes `auth.isSuperAdmin` (see lib/sessionIntegrity.ts:
|
|
278
|
+
// `userAclGrantsSuperAdmin` / `roleAclGrantsSuperAdmin`, both bound to the
|
|
279
|
+
// user's own tenant). Before this change the two authorities disagreed:
|
|
280
|
+
// session integrity would say "not a super-admin" while `loadAcl` handed the
|
|
281
|
+
// same request `['*']`.
|
|
282
|
+
//
|
|
283
|
+
// The memo stays keyed by `userId` alone because the answer now depends only
|
|
284
|
+
// on the user's own tenant, which is a property of the user — not on the scope
|
|
285
|
+
// being asked about. `invalidateUserCache` therefore still clears it correctly.
|
|
286
|
+
async isGlobalSuperAdmin(em, userId, userTenantId, useCache = true) {
|
|
287
|
+
if (useCache && this.globalSuperAdminCache.has(userId)) return this.globalSuperAdminCache.get(userId);
|
|
288
|
+
if (!userTenantId) {
|
|
289
|
+
logger.warn("Tenant-less principal cannot receive super-admin grants; assign an owning tenant before upgrading", { userId });
|
|
290
|
+
if (useCache) this.globalSuperAdminCache.set(userId, false);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const userSuper = await em.findOne(UserAcl, {
|
|
294
|
+
user: userId,
|
|
295
|
+
tenantId: userTenantId,
|
|
296
|
+
isSuperAdmin: true,
|
|
297
|
+
deletedAt: null
|
|
298
|
+
});
|
|
299
|
+
if (userSuper?.isSuperAdmin) {
|
|
300
|
+
if (useCache) this.globalSuperAdminCache.set(userId, true);
|
|
267
301
|
return true;
|
|
268
302
|
}
|
|
269
303
|
const links = await findWithDecryption(
|
|
270
304
|
em,
|
|
271
305
|
UserRole,
|
|
272
|
-
|
|
306
|
+
// Roles that belong to the user's own tenant, as
|
|
307
|
+
// `resolveCanonicalStaffAuthContext` already requires. The encryption
|
|
308
|
+
// scope stays `{ null, null }` as before — this change is about which rows
|
|
309
|
+
// count, not about which key decrypts them.
|
|
310
|
+
{
|
|
311
|
+
user: userId,
|
|
312
|
+
deletedAt: null,
|
|
313
|
+
role: { tenantId: userTenantId, deletedAt: null }
|
|
314
|
+
},
|
|
273
315
|
{ populate: ["role"] },
|
|
274
316
|
{ tenantId: null, organizationId: null }
|
|
275
317
|
);
|
|
276
318
|
const linkList = Array.isArray(links) ? links : [];
|
|
277
319
|
if (!linkList.length) {
|
|
278
|
-
this.globalSuperAdminCache.set(userId, false);
|
|
320
|
+
if (useCache) this.globalSuperAdminCache.set(userId, false);
|
|
279
321
|
return false;
|
|
280
322
|
}
|
|
281
323
|
const roleIds = Array.from(new Set(linkList.map((link) => {
|
|
@@ -283,12 +325,17 @@ class RbacService {
|
|
|
283
325
|
return role?.id ? String(role.id) : null;
|
|
284
326
|
}).filter((id) => typeof id === "string" && id.length > 0)));
|
|
285
327
|
if (!roleIds.length) {
|
|
286
|
-
this.globalSuperAdminCache.set(userId, false);
|
|
328
|
+
if (useCache) this.globalSuperAdminCache.set(userId, false);
|
|
287
329
|
return false;
|
|
288
330
|
}
|
|
289
|
-
const roleSupers = await em.find(RoleAcl, {
|
|
331
|
+
const roleSupers = await em.find(RoleAcl, {
|
|
332
|
+
isSuperAdmin: true,
|
|
333
|
+
tenantId: userTenantId,
|
|
334
|
+
deletedAt: null,
|
|
335
|
+
role: { $in: roleIds }
|
|
336
|
+
});
|
|
290
337
|
const result = roleSupers.some((roleAcl) => !!roleAcl.isSuperAdmin && !isRestrictedRoleAcl(roleAcl));
|
|
291
|
-
this.globalSuperAdminCache.set(userId, result);
|
|
338
|
+
if (useCache) this.globalSuperAdminCache.set(userId, result);
|
|
292
339
|
return result;
|
|
293
340
|
}
|
|
294
341
|
/**
|
|
@@ -316,13 +363,6 @@ class RbacService {
|
|
|
316
363
|
const cacheKey = this.getCacheKey(userId, scope);
|
|
317
364
|
const cached = userId.startsWith("api_key:") ? null : await this.getFromCache(cacheKey);
|
|
318
365
|
if (cached) return cached;
|
|
319
|
-
if (!userId.startsWith("api_key:")) {
|
|
320
|
-
if (await this.isGlobalSuperAdmin(userId)) {
|
|
321
|
-
const result2 = { isSuperAdmin: true, features: ["*"], organizations: null };
|
|
322
|
-
await this.setCache(cacheKey, result2, userId, scope);
|
|
323
|
-
return result2;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
366
|
if (userId.startsWith("api_key:")) {
|
|
327
367
|
const apiKeyId = userId.slice("api_key:".length);
|
|
328
368
|
const em2 = this.em.fork();
|
|
@@ -351,7 +391,11 @@ class RbacService {
|
|
|
351
391
|
tenantId2,
|
|
352
392
|
evaluatedOrganizationId
|
|
353
393
|
);
|
|
354
|
-
const racls = await em2.find(RoleAcl, {
|
|
394
|
+
const racls = await em2.find(RoleAcl, {
|
|
395
|
+
tenantId: tenantId2,
|
|
396
|
+
deletedAt: null,
|
|
397
|
+
role: { $in: roleIds2, deletedAt: null }
|
|
398
|
+
});
|
|
355
399
|
for (const acl of racls) {
|
|
356
400
|
if (roleAclAllowsOrganization(acl, roleOrganizationScope)) {
|
|
357
401
|
isSuper2 = isSuper2 || !!acl.isSuperAdmin;
|
|
@@ -398,6 +442,11 @@ class RbacService {
|
|
|
398
442
|
await this.setCache(cacheKey, result2, userId, scope);
|
|
399
443
|
return result2;
|
|
400
444
|
}
|
|
445
|
+
if (await this.isGlobalSuperAdmin(em, userId, user.tenantId ?? null)) {
|
|
446
|
+
const result2 = { isSuperAdmin: true, features: ["*"], organizations: null };
|
|
447
|
+
await this.setCache(cacheKey, result2, userId, scope);
|
|
448
|
+
return result2;
|
|
449
|
+
}
|
|
401
450
|
const tenantId = scope.tenantId || user.tenantId || null;
|
|
402
451
|
const orgId = scope.organizationId || user.organizationId || null;
|
|
403
452
|
if (!tenantId) {
|
|
@@ -405,7 +454,17 @@ class RbacService {
|
|
|
405
454
|
await this.setCache(cacheKey, result2, userId, scope);
|
|
406
455
|
return result2;
|
|
407
456
|
}
|
|
408
|
-
const uacl = await em.findOne(
|
|
457
|
+
const uacl = await em.findOne(
|
|
458
|
+
UserAcl,
|
|
459
|
+
{ user: userId, tenantId, deletedAt: null },
|
|
460
|
+
// `createdAt`/`id`, not `updatedAt`: `updated_at` is nullable and Postgres
|
|
461
|
+
// sorts NULLs FIRST under DESC, so ordering by it would prefer a row that
|
|
462
|
+
// was never updated. The migration's one-time collapse uses
|
|
463
|
+
// `coalesce(updated_at, created_at)` because there it is picking the row
|
|
464
|
+
// an operator would call current; here the only requirement is that the
|
|
465
|
+
// answer not vary between requests.
|
|
466
|
+
{ orderBy: { createdAt: "desc", id: "desc" } }
|
|
467
|
+
);
|
|
409
468
|
if (uacl) {
|
|
410
469
|
const result2 = {
|
|
411
470
|
isSuperAdmin: !!uacl.isSuperAdmin,
|
|
@@ -418,12 +477,16 @@ class RbacService {
|
|
|
418
477
|
const links = await findWithDecryption(
|
|
419
478
|
em,
|
|
420
479
|
UserRole,
|
|
421
|
-
{
|
|
480
|
+
{
|
|
481
|
+
user: userId,
|
|
482
|
+
deletedAt: null,
|
|
483
|
+
role: { tenantId, deletedAt: null }
|
|
484
|
+
},
|
|
422
485
|
{ populate: ["role"] },
|
|
423
486
|
{ tenantId, organizationId: orgId }
|
|
424
487
|
);
|
|
425
488
|
const linkList = Array.isArray(links) ? links : [];
|
|
426
|
-
const roleIds = linkList.map((
|
|
489
|
+
const roleIds = linkList.map((link) => link.role?.id ? String(link.role.id) : null).filter((roleId) => roleId !== null);
|
|
427
490
|
let isSuper = false;
|
|
428
491
|
const features = [];
|
|
429
492
|
let organizations = [];
|
|
@@ -434,7 +497,11 @@ class RbacService {
|
|
|
434
497
|
tenantId,
|
|
435
498
|
scope.organizationId
|
|
436
499
|
);
|
|
437
|
-
const racls = await em.find(RoleAcl, {
|
|
500
|
+
const racls = await em.find(RoleAcl, {
|
|
501
|
+
tenantId,
|
|
502
|
+
deletedAt: null,
|
|
503
|
+
role: { $in: roleIds }
|
|
504
|
+
}, {});
|
|
438
505
|
const roleAcls = Array.isArray(racls) ? racls : [];
|
|
439
506
|
for (const r of roleAcls) {
|
|
440
507
|
if (roleAclAllowsOrganization(r, roleOrganizationScope)) {
|
|
@@ -484,7 +551,7 @@ class RbacService {
|
|
|
484
551
|
tenantId2,
|
|
485
552
|
evaluatedOrganizationId
|
|
486
553
|
);
|
|
487
|
-
const roleAcls2 = tenantId2 && roleIds2.length ? await em.find(RoleAcl, { tenantId: tenantId2, role: { $in: roleIds2 } }) : [];
|
|
554
|
+
const roleAcls2 = tenantId2 && roleIds2.length ? await em.find(RoleAcl, { tenantId: tenantId2, deletedAt: null, role: { $in: roleIds2, deletedAt: null } }) : [];
|
|
488
555
|
let isSuperAdmin2 = false;
|
|
489
556
|
const features2 = [];
|
|
490
557
|
let organizations2 = [];
|
|
@@ -515,39 +582,25 @@ class RbacService {
|
|
|
515
582
|
if (isSuperAdmin2 && organizations2 !== null && !features2.includes("*")) features2.push("*");
|
|
516
583
|
return { isSuperAdmin: isSuperAdmin2 && organizations2 === null, features: features2, organizations: organizations2 };
|
|
517
584
|
}
|
|
518
|
-
const
|
|
519
|
-
UserAcl,
|
|
520
|
-
{ user: userId, isSuperAdmin: true }
|
|
521
|
-
);
|
|
522
|
-
if (directGlobalGrant?.isSuperAdmin) {
|
|
523
|
-
return { isSuperAdmin: true, features: ["*"], organizations: null };
|
|
524
|
-
}
|
|
525
|
-
const globalLinks = await findWithDecryption(
|
|
585
|
+
const user = await findOneWithDecryption(
|
|
526
586
|
em,
|
|
527
|
-
|
|
528
|
-
{
|
|
529
|
-
{
|
|
587
|
+
User,
|
|
588
|
+
{ id: userId },
|
|
589
|
+
{},
|
|
530
590
|
{ tenantId: null, organizationId: null }
|
|
531
591
|
);
|
|
532
|
-
const globalRoleIds = globalLinks.map((link) => typeof link.role === "string" ? link.role : link.role?.id ? String(link.role.id) : null).filter((roleId) => typeof roleId === "string" && roleId.length > 0);
|
|
533
|
-
if (globalRoleIds.length) {
|
|
534
|
-
const globalRoleGrants = await em.find(
|
|
535
|
-
RoleAcl,
|
|
536
|
-
{
|
|
537
|
-
role: { $in: globalRoleIds },
|
|
538
|
-
isSuperAdmin: true
|
|
539
|
-
}
|
|
540
|
-
);
|
|
541
|
-
if (globalRoleGrants.some((grant) => grant.isSuperAdmin && !isRestrictedRoleAcl(grant))) {
|
|
542
|
-
return { isSuperAdmin: true, features: ["*"], organizations: null };
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
const user = await em.findOne(User, { id: userId });
|
|
546
592
|
if (!user) return { isSuperAdmin: false, features: [], organizations: null };
|
|
593
|
+
if (await this.isGlobalSuperAdmin(em, userId, user.tenantId ?? null, false)) {
|
|
594
|
+
return { isSuperAdmin: true, features: ["*"], organizations: null };
|
|
595
|
+
}
|
|
547
596
|
const tenantId = scope.tenantId || user.tenantId || null;
|
|
548
597
|
const organizationId = scope.organizationId || user.organizationId || null;
|
|
549
598
|
if (!tenantId) return { isSuperAdmin: false, features: [], organizations: null };
|
|
550
|
-
const userAcl = await em.findOne(
|
|
599
|
+
const userAcl = await em.findOne(
|
|
600
|
+
UserAcl,
|
|
601
|
+
{ user: userId, tenantId, deletedAt: null },
|
|
602
|
+
{ orderBy: { createdAt: "desc", id: "desc" } }
|
|
603
|
+
);
|
|
551
604
|
if (userAcl) {
|
|
552
605
|
return {
|
|
553
606
|
isSuperAdmin: userAcl.isSuperAdmin === true,
|
|
@@ -558,12 +611,12 @@ class RbacService {
|
|
|
558
611
|
const links = await findWithDecryption(
|
|
559
612
|
em,
|
|
560
613
|
UserRole,
|
|
561
|
-
{ user: userId, role: { tenantId } },
|
|
614
|
+
{ user: userId, deletedAt: null, role: { tenantId, deletedAt: null } },
|
|
562
615
|
{ populate: ["role"] },
|
|
563
616
|
{ tenantId, organizationId }
|
|
564
617
|
);
|
|
565
618
|
const roleIds = links.map((link) => typeof link.role === "string" ? link.role : link.role?.id ? String(link.role.id) : null).filter((roleId) => typeof roleId === "string" && roleId.length > 0);
|
|
566
|
-
const roleAcls = roleIds.length ? await em.find(RoleAcl, { tenantId, role: { $in: roleIds } }) : [];
|
|
619
|
+
const roleAcls = roleIds.length ? await em.find(RoleAcl, { tenantId, deletedAt: null, role: { $in: roleIds, deletedAt: null } }) : [];
|
|
567
620
|
const roleScope = await resolveRoleOrganizationScopeWithEntityManager(
|
|
568
621
|
em,
|
|
569
622
|
tenantId,
|
|
@@ -690,8 +743,22 @@ class RbacService {
|
|
|
690
743
|
const denyAll = () => [];
|
|
691
744
|
if (!required.length) return { unrestricted: true, filterOrganizationIds: allowAll };
|
|
692
745
|
if (!input.tenantId) return { unrestricted: false, filterOrganizationIds: denyAll };
|
|
693
|
-
if (!userId.startsWith("api_key:")
|
|
694
|
-
|
|
746
|
+
if (!userId.startsWith("api_key:")) {
|
|
747
|
+
let globalSuperAdmin = this.globalSuperAdminCache.get(userId);
|
|
748
|
+
if (globalSuperAdmin === void 0) {
|
|
749
|
+
const em = this.em.fork();
|
|
750
|
+
const user = await findOneWithDecryption(
|
|
751
|
+
em,
|
|
752
|
+
User,
|
|
753
|
+
{ id: userId },
|
|
754
|
+
{},
|
|
755
|
+
{ tenantId: null, organizationId: null }
|
|
756
|
+
);
|
|
757
|
+
globalSuperAdmin = !!user && await this.isGlobalSuperAdmin(em, userId, user.tenantId ?? null);
|
|
758
|
+
}
|
|
759
|
+
if (globalSuperAdmin && authorizeFeatures(required, { grantedFeatures: ["*"], unrestricted: true })) {
|
|
760
|
+
return { unrestricted: true, filterOrganizationIds: allowAll };
|
|
761
|
+
}
|
|
695
762
|
}
|
|
696
763
|
const grants = await this.loadFeatureOrganizationGrants(userId, input.tenantId);
|
|
697
764
|
if (grants.kind === "none") return { unrestricted: false, filterOrganizationIds: denyAll };
|
|
@@ -763,7 +830,8 @@ class RbacService {
|
|
|
763
830
|
if (!roleIds2.length) return { kind: "none" };
|
|
764
831
|
const roleAcls2 = await em.find(RoleAcl, {
|
|
765
832
|
tenantId,
|
|
766
|
-
|
|
833
|
+
deletedAt: null,
|
|
834
|
+
role: { $in: roleIds2, deletedAt: null }
|
|
767
835
|
});
|
|
768
836
|
const keyOrganizationId = typeof key.organizationId === "string" && key.organizationId.trim().length > 0 ? key.organizationId.trim() : null;
|
|
769
837
|
return {
|
|
@@ -772,9 +840,19 @@ class RbacService {
|
|
|
772
840
|
roleGrants: roleAcls2.map(toFeatureOrganizationRoleGrant)
|
|
773
841
|
};
|
|
774
842
|
}
|
|
775
|
-
const user = await
|
|
843
|
+
const user = await findOneWithDecryption(
|
|
844
|
+
em,
|
|
845
|
+
User,
|
|
846
|
+
{ id: userId },
|
|
847
|
+
{},
|
|
848
|
+
{ tenantId: null, organizationId: null }
|
|
849
|
+
);
|
|
776
850
|
if (!user) return { kind: "none" };
|
|
777
|
-
const userAcl = await em.findOne(
|
|
851
|
+
const userAcl = await em.findOne(
|
|
852
|
+
UserAcl,
|
|
853
|
+
{ user: userId, tenantId, deletedAt: null },
|
|
854
|
+
{ orderBy: { createdAt: "desc", id: "desc" } }
|
|
855
|
+
);
|
|
778
856
|
if (userAcl) {
|
|
779
857
|
return {
|
|
780
858
|
kind: "user_acl",
|
|
@@ -786,7 +864,7 @@ class RbacService {
|
|
|
786
864
|
const links = await findWithDecryption(
|
|
787
865
|
em,
|
|
788
866
|
UserRole,
|
|
789
|
-
{ user: userId, role: { tenantId } },
|
|
867
|
+
{ user: userId, deletedAt: null, role: { tenantId, deletedAt: null } },
|
|
790
868
|
{ populate: ["role"] },
|
|
791
869
|
{ tenantId, organizationId: null }
|
|
792
870
|
);
|
|
@@ -794,7 +872,8 @@ class RbacService {
|
|
|
794
872
|
if (!roleIds.length) return { kind: "none" };
|
|
795
873
|
const roleAcls = await em.find(RoleAcl, {
|
|
796
874
|
tenantId,
|
|
797
|
-
|
|
875
|
+
deletedAt: null,
|
|
876
|
+
role: { $in: roleIds, deletedAt: null }
|
|
798
877
|
});
|
|
799
878
|
return { kind: "roles", roleGrants: roleAcls.map(toFeatureOrganizationRoleGrant) };
|
|
800
879
|
}
|