145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
import { createAccessToken } from '@/lib/server/access-token';
|
|
import { requireOpsAccess } from '@/lib/server/ops-access';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('requireOpsAccess', () => {
|
|
it('rejects course management on a learner deployment', async () => {
|
|
vi.stubEnv('NODE_ENV', 'production');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'learner');
|
|
|
|
const response = await requireOpsAccess(new NextRequest('http://localhost/api/courses'));
|
|
expect(response?.status).toBe(403);
|
|
await expect(response?.json()).resolves.toMatchObject({ errorCode: 'FORBIDDEN' });
|
|
});
|
|
|
|
it('fails closed when a production ops deployment has no Works identity origin', async () => {
|
|
vi.stubEnv('NODE_ENV', 'production');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', '');
|
|
|
|
const response = await requireOpsAccess(new NextRequest('http://localhost/api/courses'));
|
|
expect(response?.status).toBe(503);
|
|
});
|
|
|
|
it('accepts a valid access-code session only for development compatibility', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
const token = createAccessToken('test-access-code');
|
|
const request = new NextRequest('http://localhost/api/courses', {
|
|
headers: { cookie: `openmaic_access=${token}` },
|
|
});
|
|
|
|
await expect(requireOpsAccess(request)).resolves.toBeNull();
|
|
});
|
|
|
|
it('accepts a same-origin mutation with a development ops session', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
const token = createAccessToken('test-access-code');
|
|
const request = new NextRequest('https://ops.example/api/courses', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
origin: 'https://ops.example',
|
|
},
|
|
});
|
|
|
|
await expect(requireOpsAccess(request)).resolves.toBeNull();
|
|
});
|
|
|
|
it('rejects missing or cross-origin mutation origins', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
const token = createAccessToken('test-access-code');
|
|
|
|
for (const origin of [undefined, 'https://attacker.example']) {
|
|
const response = await requireOpsAccess(
|
|
new NextRequest('https://ops.example/api/courses', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
...(origin ? { origin } : {}),
|
|
},
|
|
}),
|
|
);
|
|
expect(response?.status).toBe(403);
|
|
await expect(response?.json()).resolves.toMatchObject({ errorCode: 'FORBIDDEN' });
|
|
}
|
|
});
|
|
|
|
it('uses a configured public origin and never trusts forged forwarded hosts', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
vi.stubEnv('ACCESS_CODE_TRUST_PROXY_HEADERS', 'true');
|
|
vi.stubEnv('OPS_PUBLIC_ORIGIN', 'https://ops.example');
|
|
const token = createAccessToken('test-access-code');
|
|
|
|
const accepted = new NextRequest('http://internal:3000/api/courses', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
origin: 'https://ops.example',
|
|
'x-forwarded-host': 'attacker.example',
|
|
'x-forwarded-proto': 'https',
|
|
},
|
|
});
|
|
await expect(requireOpsAccess(accepted)).resolves.toBeNull();
|
|
|
|
const rejected = new NextRequest('http://internal:3000/api/courses', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
origin: 'https://attacker.example',
|
|
'x-forwarded-host': 'attacker.example',
|
|
'x-forwarded-proto': 'https',
|
|
},
|
|
});
|
|
const response = await requireOpsAccess(rejected);
|
|
expect(response?.status).toBe(403);
|
|
await expect(response?.json()).resolves.toMatchObject({ errorCode: 'FORBIDDEN' });
|
|
});
|
|
|
|
it('fails closed when the configured public origin is malformed', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'ops');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
vi.stubEnv('OPS_PUBLIC_ORIGIN', 'https://ops.example/path');
|
|
const token = createAccessToken('test-access-code');
|
|
const response = await requireOpsAccess(
|
|
new NextRequest('https://ops.example/api/courses', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
origin: 'https://ops.example',
|
|
},
|
|
}),
|
|
);
|
|
expect(response?.status).toBe(403);
|
|
});
|
|
|
|
it('keeps development role=all aligned with the ops session contract', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development');
|
|
vi.stubEnv('OPENMAIC_DEPLOYMENT_ROLE', 'all');
|
|
vi.stubEnv('ACCESS_CODE', 'test-access-code');
|
|
const token = createAccessToken('test-access-code');
|
|
const request = new NextRequest('https://ops.example/api/coursewares', {
|
|
method: 'POST',
|
|
headers: {
|
|
cookie: `openmaic_access=${token}`,
|
|
origin: 'https://ops.example',
|
|
},
|
|
});
|
|
|
|
await expect(requireOpsAccess(request)).resolves.toBeNull();
|
|
});
|
|
});
|