import { describe, expect, test } from 'vitest'; import { NextRequest } from 'next/server'; import { resolveRequestPrincipal, type PrincipalKind, type PrincipalResolution, } from '@/lib/server/authz/principal'; import { decideResourceAuthorization, isResourceAuthorizationAllowed, type ResourceAuthorizationDecision, } from '@/lib/server/authz/resource-ownership'; function authenticated( kind: PrincipalKind, principalId: string, capabilities: readonly string[] = [], ): PrincipalResolution { return { state: 'authenticated', principal: { principalId, kind, capabilities, authMethod: kind === 'device' ? 'device-proof' : kind === 'ops' ? 'ops-cookie' : kind === 'service' ? 'service-bearer' : 'session', }, }; } describe('provider-neutral request principal', () => { test('is explicitly unavailable and ignores spoofable identity headers', async () => { const request = new NextRequest('https://server.example/api/generate-classroom/job-1', { headers: { authorization: 'Bearer attacker-controlled', 'x-learner-key': 'user:victim', 'x-openmaic-client': 'victim', }, }); await expect(resolveRequestPrincipal(request)).resolves.toEqual({ state: 'unavailable', reason: 'resolver_not_configured', }); }); }); describe('resource ownership policy', () => { test('allows an authenticated owner', () => { expect( decideResourceAuthorization({ principal: authenticated('user', 'principal-1'), ownership: { state: 'owned', ownerPrincipalId: 'principal-1' }, action: 'delete', }), ).toEqual({ effect: 'allow', reason: 'owner_match' }); }); test('allows a bound device to act for its account and an unbound device for its guest data', () => { const boundDevice: PrincipalResolution = { state: 'authenticated', principal: { principalId: 'device-1', kind: 'device', userId: 'account-1', capabilities: [], authMethod: 'device-proof', }, }; expect( decideResourceAuthorization({ principal: boundDevice, ownership: { state: 'owned', ownerPrincipalId: 'account-1' }, action: 'read', }), ).toEqual({ effect: 'allow', reason: 'owner_match' }); expect( decideResourceAuthorization({ principal: boundDevice, ownership: { state: 'guest-owned', guestPrincipalId: 'device-1' }, action: 'read', }), ).toEqual({ effect: 'allow', reason: 'guest_match' }); expect( decideResourceAuthorization({ principal: authenticated('user', 'device-1'), ownership: { state: 'guest-owned', guestPrincipalId: 'device-1' }, action: 'read', }), ).toEqual({ effect: 'deny', reason: 'owner_mismatch' }); }); test('keeps missing and unknown legacy ownership indeterminate', () => { const decisions = [ decideResourceAuthorization({ principal: { state: 'unavailable', reason: 'resolver_not_configured' }, ownership: { state: 'legacy-unowned' }, action: 'read', }), decideResourceAuthorization({ principal: authenticated('user', 'principal-1'), ownership: { state: 'unknown' }, action: 'read', }), decideResourceAuthorization({ principal: authenticated('user', 'principal-1'), ownership: { state: 'owned', ownerPrincipalId: ' ' }, action: 'read', }), ]; expect(decisions).toEqual([ { effect: 'indeterminate', reason: 'owner_missing' }, { effect: 'indeterminate', reason: 'owner_unknown' }, { effect: 'indeterminate', reason: 'owner_unknown' }, ]); expect(decisions.every((decision) => !isResourceAuthorizationAllowed(decision))).toBe(true); }); test('does not elevate ops or service principals by kind alone', () => { for (const kind of ['ops', 'service'] as const) { expect( decideResourceAuthorization({ principal: authenticated(kind, `${kind}-principal`), ownership: { state: 'owned', ownerPrincipalId: 'different-owner' }, action: 'delete', overrideCapability: 'classroom-job:manage:any', }), ).toEqual({ effect: 'deny', reason: 'owner_mismatch' }); } }); test('allows only an exact explicit override capability', () => { expect( decideResourceAuthorization({ principal: authenticated('ops', 'ops-principal', ['classroom-job:manage:any']), ownership: { state: 'owned', ownerPrincipalId: 'different-owner' }, action: 'delete', overrideCapability: 'classroom-job:manage:any', }), ).toEqual({ effect: 'allow', reason: 'explicit_capability' }); }); test('keeps public visibility read-only', () => { expect( decideResourceAuthorization({ principal: { state: 'anonymous' }, ownership: { state: 'public-read' }, action: 'read', }), ).toEqual({ effect: 'allow', reason: 'public_read' }); expect( decideResourceAuthorization({ principal: { state: 'anonymous' }, ownership: { state: 'public-read' }, action: 'delete', }), ).toEqual({ effect: 'deny', reason: 'action_not_allowed' }); }); test('future enforcement treats anything except explicit allow as denied', () => { const decisions: ResourceAuthorizationDecision[] = [ { effect: 'allow', reason: 'owner_match' }, { effect: 'deny', reason: 'owner_mismatch' }, { effect: 'indeterminate', reason: 'resolver_unavailable' }, ]; expect(decisions.map(isResourceAuthorizationAllowed)).toEqual([true, false, false]); }); });