Commit b3c3b18
feat(server): gate stored-workspace handlers by author (#16974)
## Summary
Closes the cross-author leak on stored-workspace handlers. Previously
any authenticated caller within a tenant could list, read, update, or
delete another user's workspace — only tenant scope was enforced.
Stored-workspace handlers now mirror the authorship pattern already used
for stored-agents and stored-skills:
- `GET /stored/workspaces` filters results to the caller's own rows plus
legacy unowned records. Admins (`*`) still see everything. A non-admin
querying `?authorId=user-b` gets an empty list.
- `GET /stored/workspaces/:id` throws `404 Not found` for callers who
aren't the owner, an admin, or a `stored-workspaces:read[:<id>]` holder.
- `POST /stored/workspaces` stamps `authorId` from the authenticated
caller. Any body-supplied `authorId` is ignored.
- `PATCH /stored/workspaces/:id` throws `404 Not found` unless the
caller is the owner, an admin, or has `stored-workspaces:edit[:<id>]`.
- `DELETE /stored/workspaces/:id` throws `404 Not found` unless the
caller is the owner, an admin, or has `stored-workspaces:delete[:<id>]`.
Legacy workspaces without an `authorId` remain accessible to any
authenticated caller so existing single-user setups continue to work.
## Scope notes
This is a deliberately scoped-down version of COR-907. It uses the
existing `authorId` column and the existing `OwnedRecord` helpers, which
already treat `visibility === undefined` as non-public. Adding a
`visibility` column for workspaces (and the `public`/`private` toggle in
storage schemas + adapters) is deferred to a follow-up.
## Verification
- `pnpm --filter ./packages/server build:lib` — passes
- `pnpm --filter ./packages/server test` — 1607 passed, 4 skipped, 1
todo (including 22 new tests in `stored-workspaces.test.ts`)
- `pnpm --filter ./packages/server check:permissions` — clean (after
`generate:permissions` re-baked `auth:*` / `infrastructure:*` patterns
that were already stale on `main`)
- `pnpm --filter ./packages/server check:core-imports` — clean against
the `@mastra/core >=1.34.0-0` floor
## Files
- `packages/server/src/server/handlers/stored-workspaces.ts` — apply
`resolveAuthorFilter` / `matchesAuthorFilter` to LIST,
`assertReadAccess` to GET, `assertWriteAccess` to PATCH/DELETE, force
`authorId` from caller on POST.
- `packages/server/src/server/handlers/stored-workspaces.test.ts` —
net-new, 22 tests covering owner / admin / scoped-grant / cross-author /
legacy-unowned paths for all five routes.
- `packages/core/src/auth/ee/interfaces/permissions.generated.ts` —
regenerated (adds the `auth` and `infrastructure` patterns that were
already stale on `main`).
- `.changeset/server-stored-workspaces-authorship.md` — `@mastra/server`
minor, `@mastra/core` patch (regen only).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## ELI5
This PR makes saved workspaces private to their creators: created
workspaces are stamped with the creator as owner and only that owner (or
admins / explicitly-permitted principals) can view or modify them. Old
workspaces without an owner remain broadly accessible for compatibility.
## Overview
Closes a cross-author leak in stored-workspace handlers by enforcing
authorship-based access control (matching stored-agents/stored-skills).
Uses the existing authorId column and OwnedRecord/authorship helpers
(resolveAuthorFilter, matchesAuthorFilter, getCallerAuthorId,
assertReadAccess, assertWriteAccess). Adding a visibility column or
public/private semantics is deferred to a follow-up.
## Key Changes
- LIST (GET /stored/workspaces)
- Non-admin callers see only their own workspaces plus legacy unowned
records.
- Admins (*) see all rows.
- Non-admin queries for another author (e.g., ?authorId=other) return an
empty list.
- Storage fetch uses an equality authorId filter when possible and then
post-filters results with matchesAuthorFilter; returned totals are the
storage totals while runtimeRegistered indicates post-filtered counts.
- GET (GET /stored/workspaces/:id)
- Returns 404 unless caller is the owner, an admin, or holds
stored-workspaces:read[:<id>] — enforced via assertReadAccess.
- POST (POST /stored/workspaces)
- authorId is stamped from the authenticated caller (getCallerAuthorId);
any authorId provided in the request body is ignored.
- Preserves undefined authorId when no auth is configured for backward
compatibility.
- PATCH (PATCH /stored/workspaces/:id)
- Returns 404 unless caller is owner, admin, or holds
stored-workspaces:edit[:<id>] — enforced via assertWriteAccess.
- PATCH cannot change authorId (author attribution is removed from
update payload).
- DELETE (DELETE /stored/workspaces/:id)
- Returns 404 unless caller is owner, admin, or holds
stored-workspaces:delete[:<id>] — enforced via assertWriteAccess.
- Legacy behavior
- Workspaces lacking authorId remain accessible to any authenticated
caller to preserve existing single-user setups; locking them down
requires backfilling authorId (migration guidance included in
changeset).
## Implementation Notes
- Handlers updated:
packages/server/src/server/handlers/stored-workspaces.ts
- Uses resolveAuthorFilter + matchesAuthorFilter for LIST post-filtering
and runtimeRegistered annotation.
- Replaces direct scope assertions with assertReadAccess /
assertWriteAccess and stamps authorId on create.
- Schema: packages/server/src/server/schemas/stored-workspaces.ts
- updateStoredWorkspaceBodySchema excludes authorId so PATCH cannot
transfer ownership.
- Tests: packages/server/src/server/handlers/stored-workspaces.test.ts
- Adds 22 tests covering LIST/GET/CREATE/UPDATE/DELETE authorization,
admin vs owner vs scoped-permission grants, legacy-unowned behavior, and
error cases.
- Permissions: packages/core/.../permissions.generated.ts
- Regenerated to include auth and infrastructure resources/patterns.
- Changeset: .changeset/server-stored-workspaces-authorship.md
- Release notes and migration guidance to backfill authorId for legacy
rows.
## Verification
- Build: pnpm --filter ./packages/server build:lib — passes.
- Tests: pnpm --filter ./packages/server test — 1607 passed, 4 skipped,
1 todo (includes the 22 new tests).
- check:permissions and check:core-imports clean after regenerating
permission patterns.
## Scope
Deliberately constrained to author-based access using existing
columns/helpers; public/private visibility and a visibility column are
deferred to a follow-up.
<!-- review_stack_entry_start -->
[](https://app.coderabbit.ai/change-stack/mastra-ai/mastra/pull/16974?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: YJ <yj@example.com>
Co-authored-by: Mastra Code (anthropic/claude-opus-4-7) <noreply@mastra.ai>1 parent b7286f4 commit b3c3b18
6 files changed
Lines changed: 576 additions & 8 deletions
File tree
- .changeset
- client-sdks/client-js/src
- packages
- cli/src/commands/api
- server/src/server
- handlers
- schemas
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72392 | 72392 | | |
72393 | 72393 | | |
72394 | 72394 | | |
72395 | | - | |
72396 | 72395 | | |
72397 | 72396 | | |
72398 | 72397 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4104 | 4104 | | |
4105 | 4105 | | |
4106 | 4106 | | |
4107 | | - | |
4108 | 4107 | | |
4109 | 4108 | | |
4110 | 4109 | | |
| |||
0 commit comments