Skip to content

feat: group-based mailbox ACLs via CF Access JWT groups (#295)#306

Open
schmug wants to merge 1 commit into
mainfrom
claude/wizardly-einstein-cgUZc
Open

feat: group-based mailbox ACLs via CF Access JWT groups (#295)#306
schmug wants to merge 1 commit into
mainfrom
claude/wizardly-einstein-cgUZc

Conversation

@schmug
Copy link
Copy Markdown
Owner

@schmug schmug commented May 21, 2026

Summary

  • Extends MailboxAcl with an optional groups?: string[] field — CF Access group names whose members gain access to the mailbox. Existing email-only ACL blobs are valid without any migration (absent field = no group grants).
  • callerGroupsFromJwt() decodes the groups claim from the already-verified CF Access JWT using jose's decodeJwt (no re-verification; the global Access middleware has already verified the token — group claims are never sourced from a spoofable header).
  • callerInAcl() gains a third callerGroups: string[] = [] param; grants access when the caller is in members or belongs to a granted CF Access group name. All existing callers are backwards-compat (default value).
  • requireMailbox middleware and GET /api/v1/mailboxes both extract groups from the JWT and pass them to callerInAcl.
  • New owner-only endpoints: POST /api/v1/mailboxes/:id/acl/groups (add group, idempotent) and DELETE /api/v1/mailboxes/:id/acl/groups/:name (remove group). GET /api/v1/mailboxes/:id/acl now returns groups[] alongside owner and members.

Closes #295

Authz model / security note

Group membership is sourced exclusively from the groups claim in the cf-access-jwt-assertion header, which is signed by Cloudflare Access and already verified by the global middleware before any route handler runs. decodeJwt (no re-verification) is safe at this layer — the signature was already checked. No spoofable header is ever trusted for group claims.

Group names stored in the ACL must match the names shown in the Cloudflare Access dashboard (e.g. soc-analysts). The CF Access JWT exposes group names (strings) in the groups array claim.

Test plan

  • npm test — 1093 passing, 0 failing (all pre-existing tests continue to pass)
  • npm run typecheck — exit 0
  • callerInAcl group-grant: allows caller in granted group, denies non-member, email-only ACL unchanged, no-ACL backwards-compat intact
  • callerGroupsFromJwt: null/undefined/empty → [], malformed token → [], valid fake JWT → correct groups, non-string entries filtered
  • requireMailbox integration: group member allowed (via fake JWT), non-group member denied, no-JWT non-member denied

Files changed (5)

File Change
workers/lib/mailbox-acl.ts Add groups? to MailboxAcl; export callerGroupsFromJwt(); extend callerInAcl with group check
workers/lib/mailbox.ts Extract groups from JWT in requireMailbox; pass to callerInAcl
workers/routes/acl-members.ts GET /acl returns groups[]; add POST /groups and DELETE /groups/:name
workers/index.ts Extract groups from JWT in GET /api/v1/mailboxes; pass to callerInAcl
tests/lib/mailbox-acl.test.ts 21 new tests across 3 new describe blocks

Deferred (follow-up: #306)

  • Group management UI in AclMembersPanel — the GET /acl response now includes groups[], and the add/remove endpoints are live; the panel just needs new UI controls to surface them.
  • Route-level tests for the new group endpoints in tests/routes/acl-members.test.ts.

Spec follow-up needed

No — this feature adds a new ACL dimension and does not change or contradict any rule in SECURITY_SPEC.md.

https://claude.ai/code/session_019pmwmDXFom1HzioGUAgAbF


Generated by Claude Code

Extends the per-mailbox ACL to support Cloudflare Access group grants
alongside the existing email-member list, so orgs can manage mailbox
access by team/role without maintaining per-mailbox email lists.

Key changes:
- `MailboxAcl.groups?: string[]` — optional field; absent = no group
  grants; existing email-only ACL blobs remain valid without migration
- `callerGroupsFromJwt()` — decodes the `groups` claim from the
  already-verified CF Access JWT (no re-verification, sourced only from
  the signed token, never a spoofable header)
- `callerInAcl()` gains a third `callerGroups` param; access is granted
  when the caller is in `members` OR belongs to a granted group name
- `requireMailbox` and `GET /api/v1/mailboxes` both extract groups from
  the JWT and pass them to `callerInAcl`
- `POST /api/v1/mailboxes/:id/acl/groups` and
  `DELETE /api/v1/mailboxes/:id/acl/groups/:name` — owner-only group
  add/remove; `GET /acl` now returns `groups[]` alongside owner/members

Tests: callerInAcl group-grant, deny non-member, email-only unchanged,
no-ACL backwards-compat, callerGroupsFromJwt, requireMailbox integration
(1093 passing, 0 failing)

Deferred (follow-up): group management in AclMembersPanel UI

https://claude.ai/code/session_019pmwmDXFom1HzioGUAgAbF
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ais-hub 86d47e7 Commit Preview URL

Branch Preview URL
May 21 2026, 12:21 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add group-based mailbox ACLs (Cloudflare Access groups)

2 participants