Skip to content

Commit b3c3b18

Browse files
YujohnNattrassYJMastra Code (anthropic/claude-opus-4-7)
authored
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 --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@mastra/server': minor
3+
'@mastra/core': patch
4+
---
5+
6+
Gate stored-workspace handlers by author. Previously any authenticated caller within a tenant could list, read, update, or delete another user's workspace.
7+
8+
**Behavior changes**
9+
10+
- `POST /stored/workspaces` — server stamps `authorId` from the authenticated caller; any body-provided `authorId` is ignored.
11+
- `GET /stored/workspaces/:id`, `PATCH /stored/workspaces/:id`, `DELETE /stored/workspaces/:id` — return `404 Not found` unless the caller is the owner, an admin (`*`), or holds `stored-workspaces:<action>[:<id>]`.
12+
- `GET /stored/workspaces` — filters to the caller's own rows plus legacy unowned records; admins still see every row.
13+
- Legacy workspaces created before this change (no `authorId`) remain accessible to any authenticated caller for backwards compatibility.
14+
15+
**Example**
16+
17+
```ts
18+
// Client POST body — authorId is ignored if sent
19+
await fetch('/stored/workspaces', {
20+
method: 'POST',
21+
body: JSON.stringify({ name: 'My workspace', authorId: 'someone-else' }),
22+
});
23+
24+
// Stored row — authorId is stamped from the authenticated caller
25+
// {
26+
// id: 'my-workspace',
27+
// name: 'My workspace',
28+
// authorId: 'user_abc123', // from requestContext, NOT from body
29+
// ...
30+
// }
31+
```
32+
33+
**Migration**
34+
35+
- Existing rows with `authorId === null/undefined` remain readable/writable by any authenticated caller — no action required for backwards compatibility.
36+
- To lock down legacy rows, backfill `authorId` directly in the `workspaces` table with the original creator's id.
37+
- For service accounts or tooling that need cross-user access, grant `stored-workspaces:*` (or per-id `stored-workspaces:<action>:<id>`) instead of relying on the legacy unowned bypass.
38+
- Admins (callers with `*`) continue to see and mutate every row regardless of `authorId`.
39+
40+
The `@mastra/core` patch regenerates `permissions.generated.ts` to include the `auth` and `infrastructure` resources that already had routes on `main`.

client-sdks/client-js/src/route-types.generated.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72392,7 +72392,6 @@ export type PatchStoredWorkspacesStoredWorkspaceId_PathParams = {
7239272392
};
7239372393

7239472394
export type PatchStoredWorkspacesStoredWorkspaceId_Body = {
72395-
authorId?: (string | undefined) | undefined;
7239672395
metadata?:
7239772396
| (
7239872397
| {

packages/cli/src/commands/api/route-metadata.generated.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4104,7 +4104,6 @@ export const API_ROUTE_METADATA = {
41044104
],
41054105
"queryParams": [],
41064106
"bodyParams": [
4107-
"authorId",
41084107
"autoSync",
41094108
"description",
41104109
"filesystem",

0 commit comments

Comments
 (0)