---
id: w6/m4/t002
title: Add workspace identity + ownerId scoping to KeyValue
worker: worker6
status: done
estimate: 45m
depends_on: []
---

## Objective

Bring managed KeyValue up to the same workspace-identity contract Apps and (post-t001) Postgres already have: stamp both workspace labels on create, and expose `ownerId` on `KeyValueView`/`ListKeyValues`.

## Context

- `lego/backend/internal/keyvalue/service.go` — `CreateKeyValue` (~lines 182-201) stamps no labels at all today; `KeyValueView` has no `OwnerID` field; `ListKeyValues` (~lines 153-165) has no `ownerID` parameter/filter.
- Reference implementation to mirror: `lego/backend/internal/apps/service.go` (`OwnerID` field at line 129, stamp at line 348, `List(ctx, ownerID)` filter at lines 187-203) and `lego/backend/internal/postgres/service.go` post-t001 (`PostgresView.OwnerID`, `ListPostgres(ctx, ownerID)`).
- `docs/ADR022-tenant-isolation.md:104` — documents that an unlabeled `KeyValue` CR is a "correct safe default" for isolation (fail-closed) but also means a tenant's own App can't reach its own Valkey instance; stamping `core.LabelWorkspace` here closes that functional gap too, as a side effect.

## Steps

1. Add `RootDir`-equivalent: add `OwnerID string` field to `KeyValueView` (JSON `ownerId,omitempty`), read from `kv.Labels[core.LabelTenant]`, mirroring `postgres/service.go`'s `pgView`.
2. In `CreateKeyValue`, stamp both `core.LabelTenant` and `core.LabelWorkspace` on the `KeyValue` CR's labels (same tenant id), mirroring `postgres/service.go`'s post-t001 `CreatePostgres`.
3. Add an `ownerID` parameter to `ListKeyValues`, authorizing on `core.WorkspaceObject(ownerID)` and filtering via `client.MatchingLabels{core.LabelTenant: ownerID}` when non-empty — mirror `postgres/service.go`'s `ListPostgres` signature/behavior exactly (including its `AuthorizeOn` call).
4. Update REST/GraphQL/MCP callers of `ListKeyValues` to pass through an `ownerId` argument if the surface already threads one for Apps/Postgres (check `keyvalue/rest.go`, `graphql.go`, `mcp.go` for the existing pattern to extend).

## Files

- `lego/backend/internal/keyvalue/service.go`
- `lego/backend/internal/keyvalue/rest.go`, `graphql.go`, `mcp.go` (if `ownerId` needs threading through the adapters)

## Acceptance criteria

- [ ] A `KeyValue` CR created via `CreateKeyValue` carries both `bex.co/tenant` and `app.bex.co/workspace` labels
- [ ] `KeyValueView.OwnerID` is populated for a workspace-created store
- [ ] `ListKeyValues(ownerID)` scopes correctly, same contract as `ListPostgres`
- [ ] A KeyValue store's own App can now reach it over the network (same-workspace NetworkPolicy match succeeds) — spot-check against `docs/ADR022-tenant-isolation.md`'s matrix

## Out of scope

- Dashboard UI changes for `ownerId` display (not currently surfaced for Postgres either — out of scope here too).
