UNPKG

@mastra/core

Version:

Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.

254 lines (197 loc) 11.5 kB
# Fine-Grained Authorization (FGA) Fine-Grained Authorization (FGA) adds resource-level permission checks to your Mastra application. While RBAC answers "can this role do this action?", FGA answers **"can this user do this action on this specific resource?"** ## When to use FGA FGA is designed for multi-tenant B2B products where permissions are contextual: - A user might be an **admin** of Team A but only a **member** of Team B - Thread access should be limited to the user's own organization - Workflow execution should be scoped to a specific team or project - Tool access depends on the user's relationship to a resource ## Configuration Configure FGA in your Mastra server config alongside authentication and RBAC: ```typescript import { Mastra } from '@mastra/core/mastra'; import { MastraFGAPermissions } from '@mastra/core/auth/ee'; import { MastraAuthWorkos, MastraFGAWorkos } from '@mastra/auth-workos'; const mastra = new Mastra({ server: { auth: new MastraAuthWorkos({ /* ... */ fetchMemberships: true, mapUserToResourceId: user => user.teamId, }), fga: new MastraFGAWorkos({ resourceMapping: { agent: { fgaResourceType: 'team', deriveId: (ctx) => ctx.user.teamId }, workflow: { fgaResourceType: 'team', deriveId: (ctx) => ctx.user.teamId }, thread: { fgaResourceType: 'workspace-thread', deriveId: ({ resourceId }) => resourceId }, }, permissionMapping: { [MastraFGAPermissions.AGENTS_EXECUTE]: 'manage-workflows', [MastraFGAPermissions.WORKFLOWS_EXECUTE]: 'manage-workflows', [MastraFGAPermissions.MEMORY_READ]: 'read', [MastraFGAPermissions.MEMORY_WRITE]: 'update', }, }), storedResources: { scope: true, }, }, }); ``` When using `MastraFGAWorkos`, set `fetchMemberships: true` on `MastraAuthWorkos`. WorkOS FGA checks need the user's organization memberships to resolve the correct membership ID for authorization. Use `thread` as the resource-mapping key for memory authorization. `MastraFGAWorkos` still accepts the legacy alias `memory`, but new configs should prefer `thread`. When `server.fga` is configured, Mastra enforces FGA on protected actions. If a protected action has no authenticated user, Mastra denies it. If `server.fga` is not configured, these FGA checks are skipped and Mastra keeps the previous behavior. ### Resource mapping The `resourceMapping` tells Mastra how to resolve FGA resource types and IDs from request context. Keys are Mastra resource types, values define the FGA resource type and how to derive the ID: ```typescript resourceMapping: { // When checking "can user execute agent X?", resolve the FGA resource // as the user's team (type: 'team', id: user.teamId) agent: { fgaResourceType: 'team', deriveId: (ctx) => ctx.user.teamId, }, } ``` `deriveId()` receives: - `user` — the authenticated user - `resourceId` — the owning Mastra resource ID when available (for example, a thread's `resourceId`) - `requestContext` — the current request context for advanced tenant resolution - `metadata` — provider-specific metadata for the attempted action Return `undefined` from `deriveId()` to fall back to the original Mastra resource ID. For thread and memory checks, Mastra still passes the raw `threadId` as the resource being checked, but it also forwards the thread's owning `resourceId` into `deriveId()`. This lets you map thread permissions to composite tenant IDs such as `userId-teamId-orgId`. ### Permission mapping The `permissionMapping` translates Mastra's internal permission strings to your FGA provider's permission slugs: ```typescript import { MastraFGAPermissions } from '@mastra/core/auth/ee'; permissionMapping: { [MastraFGAPermissions.AGENTS_EXECUTE]: 'manage-workflows', // Mastra permission -> WorkOS permission slug [MastraFGAPermissions.MEMORY_READ]: 'read', } ``` If no mapping exists for a permission, the original string is passed through. Use `validatePermissions()` to validate the full set of permissions Mastra may emit at startup. Use this when a provider requires every Mastra permission to have an explicit provider permission slug. ### Stored resource scoping FGA authorizes access to a resource. It does not automatically filter stored records that live in shared storage. Enable stored resource scoping when the built-in stored resource APIs are used in a multi-tenant app. ```typescript const mastra = new Mastra({ server: { auth: new MastraAuthWorkos({ /* ... */ mapUserToResourceId: user => user.teamId, }), storedResources: { scope: true, }, }, }); ``` With `scope: true`, Mastra reads `MASTRA_RESOURCE_ID_KEY` from the request context. `mapUserToResourceId()` sets this value after authentication. Stored resource handlers persist the scope in record metadata and filter list, read, update, publish, and delete operations by that scope. Use an object when the scope needs custom request logic: ```typescript storedResources: { scope: { metadataKey: 'teamId', resolve: ({ user }) => user.teamId, requireScope: true, }, }, ``` If `requireScope` is `true` or omitted, scoped stored resource routes fail when no scope can be resolved. ### Route policy coverage Mastra includes route-level FGA metadata for built-in resource routes, including agents, workflows, tools, MCP tools, memory threads, responses, conversations, and stored resources. Stored resource route coverage includes `/stored/agents`, `/stored/mcp-clients`, `/stored/prompt-blocks`, `/stored/scorers`, `/stored/skills`, and `/stored/workspaces`. A route is checked when it has route-level `fga` metadata, when Mastra can derive built-in metadata for that route, or when the provider supplies metadata with `resolveRouteFGA()`. To deny protected routes that do not resolve FGA metadata, configure route policy coverage on the FGA provider: ```typescript const fga = new MastraFGAWorkos({ resourceMapping: { project: { fgaResourceType: 'project' }, }, permissionMapping: { 'projects:read': 'read', }, requireForProtectedRoutes: true, auditProtectedRoutes: 'warn', validatePermissions: async permissions => { // Throw if a Mastra permission is missing from permissionMapping. }, }); ``` Set `auditProtectedRoutes: 'error'` to fail startup when protected routes are missing built-in FGA metadata. If `requireForProtectedRoutes` is enabled, Mastra logs this audit as a warning by default. For custom routes, prefer route-level `fga` metadata. This keeps authorization policy next to the route: ```typescript import { createRoute } from '@mastra/server/server-adapter'; export const getProjectRoute = createRoute({ method: 'GET', path: '/projects/:projectId', responseType: 'json', requiresAuth: true, fga: { resourceType: 'project', resourceIdParam: 'projectId', permission: 'projects:read', }, handler: async () => { return { project: null }; }, }); ``` Use `resolveRouteFGA()` only when route metadata must be derived centrally from route, params, or request context. A route map scales better than string-prefix checks: ```typescript import type { FGARouteConfig, FGARouteResolver } from '@mastra/core/auth/ee'; const routeFGA = { 'GET /billing/:accountId': { resourceType: 'account', resourceIdParam: 'accountId', permission: 'billing:read', }, } satisfies Record<string, FGARouteConfig>; const resolveRouteFGA: FGARouteResolver = ({ route }) => routeFGA[`${route.method} ${route.path}`]; const fga = new MastraFGAWorkos({ /* ... */ resolveRouteFGA, }); ``` ## Enforcement points When an FGA provider is configured, Mastra automatically checks authorization at these lifecycle points: | Lifecycle point | Permission checked | Resource type | Resource ID | | ---------------------------------------------------------------- | ----------------------------------------------- | -------------------- | ------------------------------------------------------------------- | | Agent execution (`generate`, `stream`) | `agents:execute` | `agent` | `agentId` | | Built-in workflow HTTP execution routes and `Workflow.execute()` | `workflows:execute` | `workflow` | `workflowId` | | Standalone tool execution | `tools:execute` | `tool` | `toolName` | | Agent tool execution | `tools:execute` | `tool` | `${agentId}:${toolName}` | | MCP tool execution | `tools:execute` | `tool` | `JSON.stringify([serverName, toolName])` | | Thread and memory access | `memory:read`, `memory:write`, `memory:delete` | `thread` | `threadId` | | Stored resource routes | Stored resource permission for the route action | Stored resource type | Route record ID, or the stored-resource scope for collection routes | | HTTP resource routes | Configured per route | Configured per route | Configured per route | Direct SDK calls to `createRun().start()`, `resume()`, or `restart()` are not independently checked by core FGA in this release. Make those calls from a protected route or guard them in application code. Pass a `requestContext` with an authenticated user when invoking protected entry points directly. Core agent, internal workflow, tool, and memory checks also pass `requestContext` and action metadata to the FGA provider. Route checks pass `requestContext`. Thread checks pass the owning `resourceId` when available. ## Custom FGA provider Implement `IFGAProvider` to use any FGA backend: ```typescript import { FGADeniedError } from '@mastra/core/auth/ee' import type { FGACheckParams, IFGAProvider, MastraFGAPermissionInput } from '@mastra/core/auth/ee' class MyFGAProvider implements IFGAProvider { async check(user: any, params: FGACheckParams): Promise<boolean> { // Your authorization logic return true } async require(user: any, params: FGACheckParams): Promise<void> { const allowed = await this.check(user, params) if (!allowed) { throw new FGADeniedError(user, params.resource, params.permission) } } async filterAccessible<T extends { id: string }>( user: any, resources: T[], resourceType: string, permission: MastraFGAPermissionInput, ): Promise<T[]> { // Filter resources the user can access return resources } } ``` ## Related - [Authentication overview](https://mastra.ai/docs/server/auth) - [WorkOS authentication](https://mastra.ai/docs/server/auth/workos)