feat(threads): governed group settings — rename, list members, remove participant
- MessageService.renameThread / removeParticipant / listParticipants, each fail-closed via OPA (kernel stays generic — no dm/group branching) - dev OPA: iios.thread.update + iios.thread.participant.remove rules (group requires ADMIN; ungoverned threads unchanged) - REST: PATCH /v1/threads/:id, GET + DELETE /v1/threads/:id/participants - tests: admin renames/removes, member is denied, member list carries roles (message.spec + dev-opa.port.spec) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,22 @@ describe('DevOpaPort (dev policy plane — membership rules)', () => {
|
||||
expect(asAdmin.allow).toBe(true);
|
||||
});
|
||||
|
||||
it('group rename (thread.update) requires ADMIN; ungoverned threads allow it', async () => {
|
||||
const asMember = await opa.decide({ action: 'iios.thread.update', membership: 'group', callerRole: 'MEMBER' });
|
||||
expect(asMember.allow).toBe(false);
|
||||
expect(asMember.obligations[0]?.reason).toMatch(/admin/);
|
||||
expect((await opa.decide({ action: 'iios.thread.update', membership: 'group', callerRole: 'ADMIN' })).allow).toBe(true);
|
||||
expect((await opa.decide({ action: 'iios.thread.update' })).allow).toBe(true); // no membership attr → allow
|
||||
});
|
||||
|
||||
it('group remove requires ADMIN; a member on an ungoverned thread may remove', async () => {
|
||||
const asMember = await opa.decide({ action: 'iios.thread.participant.remove', membership: 'group', callerRole: 'MEMBER' });
|
||||
expect(asMember.allow).toBe(false);
|
||||
expect(asMember.obligations[0]?.reason).toMatch(/admin/);
|
||||
expect((await opa.decide({ action: 'iios.thread.participant.remove', membership: 'group', callerRole: 'ADMIN' })).allow).toBe(true);
|
||||
expect((await opa.decide({ action: 'iios.thread.participant.remove', callerRole: 'MEMBER' })).allow).toBe(true);
|
||||
});
|
||||
|
||||
it('self-join is governed only on membership threads', async () => {
|
||||
// generic / support thread (no membership attr) → open join, unchanged
|
||||
expect((await opa.decide({ action: 'iios.thread.join', alreadyMember: false })).allow).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user