Some checks failed
CD / Build and push images (push) Successful in 3m5s
CI / Lint, typecheck, test (push) Successful in 2m31s
CI / Auth e2e pack (push) Failing after 2m0s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 12s
Pond Admins configure the vision's fine-grained cases through a plain-language surface, on top of the base roles from #54. - shared: `AccessRuleView` (a grant enriched with subject/scope display names) and pure conflict helpers `scopeSpecificity`/`sameGrantSubject`/ `isRuleShadowed` (unit-tested) for the client-side shadowed-rule hint. New `access` i18n namespace (de+en) with sentence templates (ADR 0012). - api: `GET /ponds/:id/grants/access-rules` (Pond-Admin) returns the pond's grants enriched with each user's display name and each label/page scope's name, resolved in one batched query per kind. - web `access/`: `AccessRulesManager` in Pond Settings — the pond's rules grouped by subject and rendered as readable de/en sentences ("Anna may not edit pages labeled “Confidential”"), an add form (subject = member or the `signed-in`/`public` pseudo-subjects; scope = label from the tree or a specific page; role; allow/deny) that warns when a rule would be shadowed by a more specific existing one (shared algorithm) and requires an explicit confirmation before granting anything to `public`. Semantics are the shared resolver's — the UI only reflects permissions.md. - tests: shared `conflicts.test.ts`; an api db case for the enriched endpoint; a browser `access-rules` pack that configures BOTH vision patterns through the UI and verifies their effect end to end — "deny label X" (an editor loses a labelled page) and "only label Y" (a signed-in non-member, new `fixture-viewer`, reads only the labelled pages) — plus the shadow hint and the public confirmation, with its own CI step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { isRuleShadowed, sameGrantSubject, scopeSpecificity } from './conflicts';
|
|
import type { Grant } from './types';
|
|
|
|
const grant = (over: Partial<Grant>): Grant => ({
|
|
subjectType: 'user',
|
|
subjectId: 'u1',
|
|
role: 'editor',
|
|
scopeType: 'pond',
|
|
scopeId: null,
|
|
effect: 'allow',
|
|
...over,
|
|
});
|
|
|
|
describe('scopeSpecificity', () => {
|
|
it('orders page > label > pond', () => {
|
|
expect(scopeSpecificity('page')).toBeGreaterThan(scopeSpecificity('label'));
|
|
expect(scopeSpecificity('label')).toBeGreaterThan(scopeSpecificity('pond'));
|
|
});
|
|
});
|
|
|
|
describe('sameGrantSubject', () => {
|
|
it('matches the same user and the same pseudo-subject', () => {
|
|
expect(sameGrantSubject(grant({}), grant({ role: 'reader' }))).toBe(true);
|
|
expect(
|
|
sameGrantSubject(
|
|
grant({ subjectType: 'public', subjectId: null }),
|
|
grant({ subjectType: 'public', subjectId: null }),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
it('distinguishes different users and subject types', () => {
|
|
expect(sameGrantSubject(grant({}), grant({ subjectId: 'u2' }))).toBe(false);
|
|
expect(
|
|
sameGrantSubject(grant({}), grant({ subjectType: 'authenticated', subjectId: null })),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isRuleShadowed', () => {
|
|
it('flags a pond rule shadowed by an opposite, more specific label rule for the same subject', () => {
|
|
const existing = [grant({ scopeType: 'label', scopeId: 'l1', effect: 'deny' })];
|
|
expect(isRuleShadowed(grant({ scopeType: 'pond', effect: 'allow' }), existing)).toBe(true);
|
|
});
|
|
it('does not flag when the more specific rule agrees in effect', () => {
|
|
const existing = [grant({ scopeType: 'label', scopeId: 'l1', effect: 'allow' })];
|
|
expect(isRuleShadowed(grant({ scopeType: 'pond', effect: 'allow' }), existing)).toBe(false);
|
|
});
|
|
it('does not flag a more specific candidate against a less specific existing rule', () => {
|
|
const existing = [grant({ scopeType: 'pond', effect: 'deny' })];
|
|
expect(
|
|
isRuleShadowed(grant({ scopeType: 'page', scopeId: 'p1', effect: 'allow' }), existing),
|
|
).toBe(false);
|
|
});
|
|
it('ignores rules for other subjects', () => {
|
|
const existing = [grant({ subjectId: 'u2', scopeType: 'page', scopeId: 'p1', effect: 'deny' })];
|
|
expect(isRuleShadowed(grant({ scopeType: 'pond', effect: 'allow' }), existing)).toBe(false);
|
|
});
|
|
});
|