Files
openmaic/OpenMAIC/tests/server/resource-ownership-authorization.test.ts
2026-08-16 14:58:47 +08:00

178 lines
5.7 KiB
TypeScript

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]);
});
});