Implements FED-010: Agent Spawn via Federation feature that enables spawning and managing Claude agents on remote federated Mosaic Stack instances via COMMAND message type. Features: - Federation agent command types (spawn, status, kill) - FederationAgentService for handling agent operations - Integration with orchestrator's agent spawner/lifecycle services - API endpoints for spawning, querying status, and killing agents - Full command routing through federation COMMAND infrastructure - Comprehensive test coverage (12/12 tests passing) Architecture: - Hub → Spoke: Spawn agents on remote instances - Command flow: FederationController → FederationAgentService → CommandService → Remote Orchestrator - Response handling: Remote orchestrator returns agent status/results - Security: Connection validation, signature verification Files created: - apps/api/src/federation/types/federation-agent.types.ts - apps/api/src/federation/federation-agent.service.ts - apps/api/src/federation/federation-agent.service.spec.ts Files modified: - apps/api/src/federation/command.service.ts (agent command routing) - apps/api/src/federation/federation.controller.ts (agent endpoints) - apps/api/src/federation/federation.module.ts (service registration) - apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint) - apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration) Testing: - 12/12 tests passing for FederationAgentService - All command service tests passing - TypeScript compilation successful - Linting passed Refs #93 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
352 lines
8.7 KiB
Markdown
352 lines
8.7 KiB
Markdown
# Multi-Tenant Row-Level Security (RLS)
|
|
|
|
## Overview
|
|
|
|
Mosaic Stack implements multi-tenancy using PostgreSQL Row-Level Security (RLS) to ensure complete data isolation between workspaces at the database level. This provides defense-in-depth security, preventing data leakage even if application-level checks fail.
|
|
|
|
## Architecture
|
|
|
|
### Core Concepts
|
|
|
|
1. **Workspaces**: Top-level tenant containers
|
|
2. **Teams**: Sub-groups within workspaces for collaboration
|
|
3. **Workspace Members**: Users associated with workspaces (OWNER, ADMIN, MEMBER, GUEST roles)
|
|
4. **Team Members**: Users associated with teams (OWNER, ADMIN, MEMBER roles)
|
|
|
|
### Database Schema
|
|
|
|
```
|
|
User
|
|
├── WorkspaceMember (role: OWNER, ADMIN, MEMBER, GUEST)
|
|
│ └── Workspace
|
|
│ ├── Team
|
|
│ │ └── TeamMember (role: OWNER, ADMIN, MEMBER)
|
|
│ ├── Task, Event, Project, etc.
|
|
│ └── All tenant-scoped data
|
|
```
|
|
|
|
## RLS Implementation
|
|
|
|
### Tables with RLS Enabled
|
|
|
|
All tenant-scoped tables have RLS enabled:
|
|
|
|
- `workspaces`
|
|
- `workspace_members`
|
|
- `teams`
|
|
- `team_members`
|
|
- `tasks`
|
|
- `events`
|
|
- `projects`
|
|
- `activity_logs`
|
|
- `memory_embeddings`
|
|
- `domains`
|
|
- `ideas`
|
|
- `relationships`
|
|
- `agents`
|
|
- `agent_sessions`
|
|
- `user_layouts`
|
|
- `knowledge_entries`
|
|
- `knowledge_tags`
|
|
- `knowledge_entry_tags`
|
|
- `knowledge_links`
|
|
- `knowledge_embeddings`
|
|
- `knowledge_entry_versions`
|
|
|
|
### Helper Functions
|
|
|
|
The RLS implementation uses several helper functions:
|
|
|
|
#### `current_user_id()`
|
|
|
|
Returns the current user's UUID from the session variable `app.current_user_id`.
|
|
|
|
```sql
|
|
SELECT current_user_id(); -- Returns UUID or NULL
|
|
```
|
|
|
|
#### `is_workspace_member(workspace_uuid, user_uuid)`
|
|
|
|
Checks if a user is a member of a workspace.
|
|
|
|
```sql
|
|
SELECT is_workspace_member('workspace-uuid', 'user-uuid'); -- Returns BOOLEAN
|
|
```
|
|
|
|
#### `is_workspace_admin(workspace_uuid, user_uuid)`
|
|
|
|
Checks if a user is an owner or admin of a workspace.
|
|
|
|
```sql
|
|
SELECT is_workspace_admin('workspace-uuid', 'user-uuid'); -- Returns BOOLEAN
|
|
```
|
|
|
|
### Policy Pattern
|
|
|
|
All RLS policies follow a consistent pattern:
|
|
|
|
```sql
|
|
CREATE POLICY <table>_workspace_access ON <table>
|
|
FOR ALL
|
|
USING (
|
|
is_workspace_member(workspace_id, current_user_id())
|
|
);
|
|
```
|
|
|
|
For tables without direct `workspace_id`, policies join through parent tables:
|
|
|
|
```sql
|
|
CREATE POLICY knowledge_links_access ON knowledge_links
|
|
FOR ALL
|
|
USING (
|
|
source_id IN (
|
|
SELECT id FROM knowledge_entries
|
|
WHERE is_workspace_member(workspace_id, current_user_id())
|
|
)
|
|
);
|
|
```
|
|
|
|
## API Integration
|
|
|
|
### Setting the Current User
|
|
|
|
Before executing any queries, the API **must** set the current user ID:
|
|
|
|
```typescript
|
|
import { prisma } from "@mosaic/database";
|
|
|
|
async function withUserContext<T>(userId: string, fn: () => Promise<T>): Promise<T> {
|
|
await prisma.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
|
return fn();
|
|
}
|
|
```
|
|
|
|
### Example Usage in API Routes
|
|
|
|
```typescript
|
|
import { withUserContext } from "@/lib/db-context";
|
|
|
|
// In a tRPC procedure or API route
|
|
export async function getTasks(userId: string, workspaceId: string) {
|
|
return withUserContext(userId, async () => {
|
|
// RLS automatically filters to workspaces the user can access
|
|
const tasks = await prisma.task.findMany({
|
|
where: {
|
|
workspaceId,
|
|
},
|
|
});
|
|
return tasks;
|
|
});
|
|
}
|
|
```
|
|
|
|
### Middleware Pattern
|
|
|
|
For tRPC or Next.js API routes, use middleware to automatically set the user context:
|
|
|
|
```typescript
|
|
// middleware/auth.ts
|
|
export async function withAuth(userId: string, handler: () => Promise<any>) {
|
|
await prisma.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
|
return handler();
|
|
}
|
|
|
|
// In tRPC procedure
|
|
.query(async ({ ctx }) => {
|
|
return withAuth(ctx.user.id, async () => {
|
|
// All queries here are automatically scoped to the user's workspaces
|
|
return prisma.workspace.findMany();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Transaction Pattern
|
|
|
|
For transactions, set the user context within the transaction:
|
|
|
|
```typescript
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
|
|
|
// All queries in this transaction are scoped to the user
|
|
const workspace = await tx.workspace.create({
|
|
data: { name: "New Workspace", ownerId: userId },
|
|
});
|
|
|
|
await tx.workspaceMember.create({
|
|
data: {
|
|
workspaceId: workspace.id,
|
|
userId,
|
|
role: "OWNER",
|
|
},
|
|
});
|
|
|
|
return workspace;
|
|
});
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
### Defense in Depth
|
|
|
|
RLS provides **database-level** security, but should not be the only security layer:
|
|
|
|
1. **Application-level validation**: Always validate workspace access in your API
|
|
2. **RLS policies**: Prevent data leakage at the database level
|
|
3. **API authentication**: Verify user identity before setting `app.current_user_id`
|
|
|
|
### Important Notes
|
|
|
|
- **RLS does not replace application logic**: Application code should still check permissions
|
|
- **Performance**: RLS policies use indexes on `workspace_id` for efficiency
|
|
- **Bypass for admin operations**: System-level operations may need to bypass RLS using a privileged connection
|
|
- **Testing**: Always test RLS policies with different user roles
|
|
|
|
### Admin/System Operations
|
|
|
|
For system-level operations (migrations, admin tasks), use a separate connection or temporarily disable RLS:
|
|
|
|
```sql
|
|
-- Disable RLS for superuser (use with caution)
|
|
SET SESSION AUTHORIZATION postgres;
|
|
-- Or use a connection with a superuser role
|
|
```
|
|
|
|
## Testing RLS
|
|
|
|
### Manual Testing
|
|
|
|
```sql
|
|
-- Set user context
|
|
SET app.current_user_id = 'user-uuid-here';
|
|
|
|
-- Try to query another workspace (should return empty)
|
|
SELECT * FROM tasks WHERE workspace_id = 'other-workspace-uuid';
|
|
|
|
-- Query your own workspace (should return data)
|
|
SELECT * FROM tasks WHERE workspace_id = 'my-workspace-uuid';
|
|
```
|
|
|
|
### Automated Tests
|
|
|
|
```typescript
|
|
import { prisma } from "@mosaic/database";
|
|
|
|
describe("RLS Policies", () => {
|
|
it("should prevent cross-workspace access", async () => {
|
|
const user1Id = "user-1-uuid";
|
|
const user2Id = "user-2-uuid";
|
|
const workspace1Id = "workspace-1-uuid";
|
|
const workspace2Id = "workspace-2-uuid";
|
|
|
|
// Set context as user 1
|
|
await prisma.$executeRaw`SET LOCAL app.current_user_id = ${user1Id}`;
|
|
|
|
// Should only see workspace 1's tasks
|
|
const tasks = await prisma.task.findMany();
|
|
expect(tasks.every((t) => t.workspaceId === workspace1Id)).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Migration Strategy
|
|
|
|
### Existing Data
|
|
|
|
If migrating from a non-RLS setup:
|
|
|
|
1. Enable RLS on tables (already done in migration `20260129221004_add_rls_policies`)
|
|
2. Create policies (already done)
|
|
3. Update application code to set `app.current_user_id`
|
|
4. Test thoroughly with different user roles
|
|
|
|
### Rolling Back RLS
|
|
|
|
If needed, RLS can be disabled per table:
|
|
|
|
```sql
|
|
ALTER TABLE <table_name> DISABLE ROW LEVEL SECURITY;
|
|
```
|
|
|
|
Or policies can be dropped:
|
|
|
|
```sql
|
|
DROP POLICY <policy_name> ON <table_name>;
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Indexes
|
|
|
|
All tenant-scoped tables have indexes on `workspace_id`:
|
|
|
|
```sql
|
|
CREATE INDEX tasks_workspace_id_idx ON tasks(workspace_id);
|
|
```
|
|
|
|
### Function Optimization
|
|
|
|
Helper functions are marked as `STABLE` and `SECURITY DEFINER` for optimal performance:
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION is_workspace_member(workspace_uuid UUID, user_uuid UUID)
|
|
RETURNS BOOLEAN AS $$
|
|
...
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
```
|
|
|
|
### Query Planning
|
|
|
|
Check query plans to ensure RLS policies are efficient:
|
|
|
|
```sql
|
|
EXPLAIN ANALYZE
|
|
SELECT * FROM tasks WHERE workspace_id = 'workspace-uuid';
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
### Team-Level Permissions
|
|
|
|
Currently, RLS ensures workspace-level isolation. Future enhancements could include:
|
|
|
|
- Team-specific data visibility
|
|
- Project-level permissions
|
|
- Fine-grained role-based access control (RBAC)
|
|
|
|
### Audit Logging
|
|
|
|
RLS policies could be extended to automatically log all data access:
|
|
|
|
```sql
|
|
CREATE POLICY tasks_audit ON tasks
|
|
FOR ALL
|
|
USING (
|
|
is_workspace_member(workspace_id, current_user_id())
|
|
AND log_access('tasks', id, current_user_id()) IS NOT NULL
|
|
);
|
|
```
|
|
|
|
## References
|
|
|
|
- [PostgreSQL Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
|
- [Prisma Raw Database Access](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access)
|
|
- [Multi-Tenancy Patterns](https://docs.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models)
|
|
|
|
## Migration Files
|
|
|
|
- `20260129220941_add_team_model` - Adds Team and TeamMember models
|
|
- `20260129221004_add_rls_policies` - Enables RLS and creates policies
|
|
|
|
## Summary
|
|
|
|
Row-Level Security in Mosaic Stack provides:
|
|
|
|
✅ **Database-level tenant isolation**
|
|
✅ **Defense in depth security**
|
|
✅ **Automatic filtering of all queries**
|
|
✅ **Performance-optimized with indexes**
|
|
✅ **Extensible for future RBAC features**
|
|
|
|
Always remember: **Set `app.current_user_id` before executing queries!**
|