Gardens are owned by their creator and shareable with other users as viewer (read) or editor (edit objects/plantings). The owner alone shares, deletes, and edits garden metadata. requireGardenRole (#7) was built as the single choke point for this — this issue extends it to consult garden_shares and audits every service method against the intended matrix.
Scope
internal/store/shares.go + internal/service/shares.go: ListShares (owner only), AddShare (owner only; target user by exact email of an existing account — no invitation emails in v1; sharing with yourself or double-sharing → clean errors), UpdateShareRole, RemoveShare (owner; or a recipient removing themselves).
API: GET,POST /api/v1/gardens/:id/shares, PATCH,DELETE /api/v1/gardens/:id/shares/:userId. Responses include user email/display name for the UI.
Extend requireGardenRole to consult shares: effective role = owner if gardens.owner_id, else share row's role, else no access (404 per #7's convention).
Audit every existing service method (gardens #7, objects #10, plants #12 n/a — user-scoped, plantings #14) against the matrix: viewer → read + /full; editor → viewer + object/planting mutations; owner → editor + garden PATCH/DELETE + shares. Fix any handler-level assumptions (e.g. list-gardens must now include shared gardens, each row carrying my_role).
Table-driven ACL test: {owner, editor, viewer, stranger} × {read full, mutate object, mutate planting, edit garden meta, delete garden, manage shares} — the matrix in one test file.
Blocked by #7 (also touches #10/#14 surfaces — rebase over whatever exists when picked up).
Acceptance criteria
Two-user curl flow: A shares with B as viewer → B lists the garden (my_role: "viewer"), reads /full, gets 403 on mutations; A upgrades B to editor → B moves an object but still can't rename the garden or see shares; A removes the share → B gets 404.
The ACL matrix test is green and covers every row above.
Sharing with an unknown email → 404 with a clear message (no account enumeration beyond what sharing inherently needs).
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md)
## Context
Gardens are owned by their creator and shareable with other users as **viewer** (read) or **editor** (edit objects/plantings). The owner alone shares, deletes, and edits garden metadata. `requireGardenRole` (#7) was built as the single choke point for this — this issue extends it to consult `garden_shares` and audits every service method against the intended matrix.
## Scope
- [ ] `internal/store/shares.go` + `internal/service/shares.go`: ListShares (owner only), AddShare (owner only; target user by **exact email of an existing account** — no invitation emails in v1; sharing with yourself or double-sharing → clean errors), UpdateShareRole, RemoveShare (owner; or a recipient removing themselves).
- [ ] API: `GET,POST /api/v1/gardens/:id/shares`, `PATCH,DELETE /api/v1/gardens/:id/shares/:userId`. Responses include user email/display name for the UI.
- [ ] Extend `requireGardenRole` to consult shares: effective role = owner if `gardens.owner_id`, else share row's role, else no access (404 per #7's convention).
- [ ] **Audit every existing service method** (gardens #7, objects #10, plants #12 n/a — user-scoped, plantings #14) against the matrix: viewer → read + `/full`; editor → viewer + object/planting mutations; owner → editor + garden PATCH/DELETE + shares. Fix any handler-level assumptions (e.g. list-gardens must now include shared gardens, each row carrying `my_role`).
- [ ] Table-driven ACL test: {owner, editor, viewer, stranger} × {read full, mutate object, mutate planting, edit garden meta, delete garden, manage shares} — the matrix in one test file.
## Out of scope
- Frontend (#17). Email notifications/invite links for non-users (post-v1). Per-object permissions (never — garden-level only).
## Key files & patterns
- Roles table: `DESIGN.md` § Domain model (`garden_shares`); choke-point rationale § Backend layout.
## Dependencies
Blocked by #7 (also touches #10/#14 surfaces — rebase over whatever exists when picked up).
## Acceptance criteria
- Two-user curl flow: A shares with B as viewer → B lists the garden (`my_role: "viewer"`), reads `/full`, gets 403 on mutations; A upgrades B to editor → B moves an object but still can't rename the garden or see shares; A removes the share → B gets 404.
- The ACL matrix test is green and covers every row above.
- Sharing with an unknown email → 404 with a clear message (no account enumeration beyond what sharing inherently needs).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of epic #20 · Design: DESIGN.md
Context
Gardens are owned by their creator and shareable with other users as viewer (read) or editor (edit objects/plantings). The owner alone shares, deletes, and edits garden metadata.
requireGardenRole(#7) was built as the single choke point for this — this issue extends it to consultgarden_sharesand audits every service method against the intended matrix.Scope
internal/store/shares.go+internal/service/shares.go: ListShares (owner only), AddShare (owner only; target user by exact email of an existing account — no invitation emails in v1; sharing with yourself or double-sharing → clean errors), UpdateShareRole, RemoveShare (owner; or a recipient removing themselves).GET,POST /api/v1/gardens/:id/shares,PATCH,DELETE /api/v1/gardens/:id/shares/:userId. Responses include user email/display name for the UI.requireGardenRoleto consult shares: effective role = owner ifgardens.owner_id, else share row's role, else no access (404 per #7's convention)./full; editor → viewer + object/planting mutations; owner → editor + garden PATCH/DELETE + shares. Fix any handler-level assumptions (e.g. list-gardens must now include shared gardens, each row carryingmy_role).Out of scope
Key files & patterns
DESIGN.md§ Domain model (garden_shares); choke-point rationale § Backend layout.Dependencies
Blocked by #7 (also touches #10/#14 surfaces — rebase over whatever exists when picked up).
Acceptance criteria
my_role: "viewer"), reads/full, gets 403 on mutations; A upgrades B to editor → B moves an object but still can't rename the garden or see shares; A removes the share → B gets 404.