---
id: w1/m9/t003
title: Scope core.Authorize + list/get to the caller's workspace (retire workspace:default)
worker: worker1
status: done
estimate: 30m
depends_on: [w1/m9/t002]
---

## Objective

Every verb authorizes against the **caller's** workspace instead of the hardcoded `workspace:default`: `core.Base.Authorize` resolves the caller's tenant (identity → `tenant_members`, client → `tenant_oauth_clients`; cached), and `List`/`Get` return only that tenant's services.

## Context

- `lego/backend/internal/core/base.go` — the `Authorize` gate every feature `service.go` calls first (`TestAuthzGuardsEveryVerb` sweeps this).
- `deploy/gitops/authz/model.fga` comment: "until the control plane grows real workspaces (w1/m2), every check targets `workspace:default`" — this task retires that placeholder.
- Apps are k8s CRs, not rows, for bare-CR clusters: tenant→service mapping comes from the store's `apps` rows; bare-CR Apps (no store) keep default-workspace behavior — preserve the store-off path unchanged.

## Steps

1. Add a workspace resolver on `core.Base` (store-backed, TTL-cached; falls back to `workspace:default` when the store is off).
2. Thread it through `Authorize` object ids (`workspace:tea-<id>`, `service:srv-<id>`).
3. Filter `List`/`Get` by the tenant's services when the store is on; update the model.fga comment.

## Files

- `lego/backend/internal/core/base.go`
- `lego/backend/internal/apps/service.go` (list/get filtering)
- `deploy/gitops/authz/model.fga` (comment only)

## Acceptance criteria

- [ ] With the store on: tenant A's key lists only A's services; `Get` on B's service is 403 (not 404-leak — match existing error convention).
- [ ] With the store off: behavior is byte-identical to today (default workspace, allow-all when FGA unset).
- [ ] `TestAuthzGuardsEveryVerb` green.

## Out of scope

- Prod enablement (t004); per-service (non-workspace) grants; role management UI.
