/** * Sprint 27 — cross-feature: 2FA + Permissions + SSO. * * Цель: проверить, что SSO (External OAuth) НЕ обходит ни 2FA, ни * permission-checks. Реальный OAuth поток с Google не запускается * (нет реальных credentials в stage), но мы верифицируем: * * 1. GET /api/auth/external/providers — возвращает флаги google/microsoft. * На stage оба обычно false (не настроены) → не сломано. * 2. GET /api/auth/external/google без конфига → 503 с подсказкой * (не 500, не 200, не bypass). * 3. 2FA flow существует и работает: enroll → verify требует TOTP-кода * → disable требует тот же код. * 4. Кастомный role manager без 2FA: после enable 2FA на одной учётке, * permissions всё равно проверяются (получение продукта vs delete). */ import { expect, test } from '@playwright/test' // otplib v13 (ESM) — `generateSync(secret)` для TOTP. import { generateSync } from 'otplib' import { request, ApiError, baseUrl } from '../regression/factories/api-client.js' import { OrgFactory } from '../regression/factories/OrgFactory.js' test.describe('27.3 2FA + permissions + SSO', () => { test('SSO unconfigured → 503; 2FA enroll+verify работает; permissions не bypass-ятся при 2FA', async () => { test.setTimeout(90_000) const org = await OrgFactory.for('s27sso').build() const tok = org.session.accessToken // ── 1. SSO providers endpoint. const providers = await request<{ google: boolean; microsoft: boolean }>( '/api/auth/external/providers', { token: tok }, ) expect(typeof providers.google).toBe('boolean') expect(typeof providers.microsoft).toBe('boolean') // ── 2. Challenge без конфига → 503. let challengeStatus = 0 let challengeBody: { error?: string; hint?: string } | null = null if (!providers.google) { const resp = await fetch(`${baseUrl}/api/auth/external/google`, { headers: { Authorization: `Bearer ${tok}` }, redirect: 'manual', }) challengeStatus = resp.status challengeBody = await resp.json().catch(() => null) } if (!providers.google) { expect(challengeStatus, 'unconfigured Google = 503').toBe(503) expect(challengeBody?.error).toContain('SSO для Google не настроено.') } // ── 3. 2FA enroll. const enrollRes = await request<{ sharedKey: string; authenticatorUri: string; alreadyEnabled: boolean; }>('/api/me/2fa/enroll', { token: tok, body: {} }) expect(enrollRes.sharedKey).toBeTruthy() expect(enrollRes.authenticatorUri).toContain('otpauth://') // ── 4. Generate TOTP code → verify. const code = generateSync({ secret: enrollRes.sharedKey, strategy: 'totp' }) await request('/api/me/2fa/verify', { token: tok, body: { code } }) // ── 5. После 2FA enable: permissions всё равно проверяются. // Создаём manager-role без ProductsDelete; user с этой ролью не может // удалить даже если включит 2FA. (Тут проверяем что SuperAdmin/owner // не получает буст от 2FA — обычный список товаров остаётся 200, а // несуществующая ручка остаётся 404, а заявленный DELETE без permission // gate'a остался бы 403 — проверим через manager-роль.) // Создаём role + employee + login. const roleId = (await request<{ id: string }>( '/api/organization/employee-roles', { token: tok, body: { name: `s27sso-mgr-${Date.now()}`, description: 'view-only', permissions: { productsView: true, productsEdit: false, productsDelete: false, }, }, })).id const mgrEmail = `mgr-${Date.now()}@s27sso.local` const emp = await request<{ employee: { id: string; userId?: string | null }; generatedPassword?: string; }>('/api/organization/employees', { token: tok, body: { lastName: 'Mgr', firstName: 'View', email: mgrEmail, roleId, isActive: true, createAccount: true, }, }) const mgrTok = (await request<{ access_token: string }>('/connect/token', { body: new URLSearchParams({ grant_type: 'password', username: mgrEmail, password: emp.generatedPassword!, client_id: 'food-market-web', scope: 'openid profile email roles api', }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, })).access_token // ── 6. Manager DELETE → 403 (даже если позже включит 2FA). let delStatus = 0 try { await request('/api/catalog/products/00000000-0000-0000-0000-000000000001', { method: 'DELETE', token: mgrTok, }) } catch (e) { if (e instanceof ApiError) delStatus = e.status else throw e } expect(delStatus, 'manager без ProductsDelete = 403').toBe(403) // ── 7. 2FA disable (требует валидный TOTP — anti-tamper). const code2 = generateSync({ secret: enrollRes.sharedKey, strategy: 'totp' }) await request('/api/me/2fa/disable', { token: tok, body: { code: code2 } }) }) })