Depends on PAR-412 and D1. Commandment 7, as amended.
RequestContext.guest(tenantId, requestId), but only for workspaces with public access on.Forbidden, which the UI turns into a sign-in prompt (PAR-397).@GuestReadable annotation on facade methods, plus an ArchUnit or unit test that fails if a write method carries it.Plan (PAR-423): (1) TenantBearerFilter gains an optional anonymous-guest gate: a request with no Authorization and no X-Actor-Token may pass on GET /api/public/w/{slug}/... only, rate-limited per IP (Caffeine token bucket, 429 + Retry-After), and a HandlerInterceptor lets it reach only handlers that take @InWorkspace; non-public workspace -> 404 from the @InWorkspace resolver. (2) @GuestReadable on facade methods + a Spring AOP advisor that refuses guests on any facade method without it; ArchUnit-style tests for write-ish names and for guest-reachable GETs. (3) Postgres tests through the real filter chain. (4) Web: signed-out server client sends no service key; workspace summary endpoint GET /api/public/w/{slug} replaces Kysely access decisions on public pages. Rule 7 doc update.
PR open: https://github.com/ParadauxIO/taskl.ie/pull/49 (against rebuild). Note for PAR-417: after both land, signed-out GET /api/public/workspaces/mine needs admitting as a guest endpoint, and teams/statuses/people facade reads need @GuestReadable (details in the PR).
Review of #49: I've approved the design, but there is one fix to make first.
Approved:
@GuestReadable is checked by ArchUnit (not frozen), and an AOP guard returns 403 before any transaction opens.Retry-After.Sent back: ListTeams (from PAR-417) decides who is a member by the key name (!"service".equals(k.name())). That fails open, and teams is now guest-readable. It must use ctx.privateTeams(), and any other decision made by name must change the same way, with tests.
Not done (accepted): the public pages still read teams, statuses, users, projects, sprints and views from Kysely. These move as each domain lands; D5 covers projects and sprints.
Deploy follow-up: gitops needs GUEST_READS_CLIENT_IP_HEADER=X-Forwarded-For on the API. Without it, all server-rendered guests share one rate-limit bucket. I'll raise this with Rían and won't edit gitops myself.
Merged into rebuild as #49 (11ce737) after the ListTeams fix. ListTeams now uses ctx.privateTeams(), there are tests covering guests, tenantless keys, bound keys named "service" and members, and no other access decision depends on a key name.
From here on, every domain marks public reads @GuestReadable (readOnly) and member-only GETs @MembersOnly; the guest section of docs/architecture.md has the rules. I've told the PAR-415, PAR-418 and PAR-422 agents to rebase and annotate.
Deploy follow-up, waiting on Rían: set GUEST_READS_CLIENT_IP_HEADER=X-Forwarded-For on the API in gitops before rebuild ships.
The gitops change for the rate-limit setting is raised as ParadauxIO/gitops#83. It sets GUEST_READS_CLIENT_IP_HEADER=X-Forwarded-For and GUEST_READS_TRUSTED_HOPS=1 in the tasklie base.
It's safe to merge before cutover, because current images ignore it. Merging restarts the backend, which takes about a minute because the deployment uses the Recreate strategy.
Note from PAR-413 (merged): the minimal public-read rule already exists in
security/WorkspaceAccessService.resolve. When a workspace has a public team, guests, non-members and the bare service key get read-onlyissue.read. This ticket generalises it:@GuestReadableallow-list on facade methods, plus a test that no write method carries itReuse
AccessStore.hasPublicTeam; don't add a second definition of "public".