Skip to content

Commit 8a10d98

Browse files
dantelexcursoragent
andcommitted
chore: bump version to 0.5.2
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8211611 commit 8a10d98

16 files changed

Lines changed: 554 additions & 35 deletions

File tree

apps/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"private": true,
55
"bin": {
66
"connect": "./dist/cli.js"

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api",
3-
"version": "0.5.0",
3+
"version": "0.5.2",
44
"private": true,
55
"scripts": {
66
"dev": "nest start --watch",
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
-- Row Level Security (RLS) Migration
2+
-- Adds database-level tenant isolation as defense-in-depth
3+
-- The application already scopes queries by workspaceId; RLS adds a second layer
4+
5+
-- ============================================================================
6+
-- ENABLE RLS ON WORKSPACE-SCOPED TABLES
7+
-- ============================================================================
8+
9+
-- Agent table
10+
ALTER TABLE "Agent" ENABLE ROW LEVEL SECURITY;
11+
ALTER TABLE "Agent" FORCE ROW LEVEL SECURITY;
12+
13+
-- Service table
14+
ALTER TABLE "Service" ENABLE ROW LEVEL SECURITY;
15+
ALTER TABLE "Service" FORCE ROW LEVEL SECURITY;
16+
17+
-- ApiKey table
18+
ALTER TABLE "ApiKey" ENABLE ROW LEVEL SECURITY;
19+
ALTER TABLE "ApiKey" FORCE ROW LEVEL SECURITY;
20+
21+
-- DebugSession table
22+
ALTER TABLE "DebugSession" ENABLE ROW LEVEL SECURITY;
23+
ALTER TABLE "DebugSession" FORCE ROW LEVEL SECURITY;
24+
25+
-- EnvironmentShare table
26+
ALTER TABLE "EnvironmentShare" ENABLE ROW LEVEL SECURITY;
27+
ALTER TABLE "EnvironmentShare" FORCE ROW LEVEL SECURITY;
28+
29+
-- AgentMessage table
30+
ALTER TABLE "AgentMessage" ENABLE ROW LEVEL SECURITY;
31+
ALTER TABLE "AgentMessage" FORCE ROW LEVEL SECURITY;
32+
33+
-- Webhook table
34+
ALTER TABLE "Webhook" ENABLE ROW LEVEL SECURITY;
35+
ALTER TABLE "Webhook" FORCE ROW LEVEL SECURITY;
36+
37+
-- ============================================================================
38+
-- CREATE RLS POLICIES
39+
-- ============================================================================
40+
-- Policy logic:
41+
-- - If app.current_workspace_id = '__rls_bypass__' → allow (for admin/migrations)
42+
-- - If app.current_workspace_id matches workspaceId → allow
43+
-- - Otherwise → deny (including when NULL/unset for security)
44+
--
45+
-- This is a deny-by-default policy. The application MUST set the context before queries.
46+
47+
-- Agent policies
48+
CREATE POLICY workspace_isolation_select ON "Agent"
49+
FOR SELECT
50+
USING (
51+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
52+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
53+
);
54+
55+
CREATE POLICY workspace_isolation_insert ON "Agent"
56+
FOR INSERT
57+
WITH CHECK (
58+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
59+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
60+
);
61+
62+
CREATE POLICY workspace_isolation_update ON "Agent"
63+
FOR UPDATE
64+
USING (
65+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
66+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
67+
);
68+
69+
CREATE POLICY workspace_isolation_delete ON "Agent"
70+
FOR DELETE
71+
USING (
72+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
73+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
74+
);
75+
76+
-- Service policies
77+
CREATE POLICY workspace_isolation_select ON "Service"
78+
FOR SELECT
79+
USING (
80+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
81+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
82+
);
83+
84+
CREATE POLICY workspace_isolation_insert ON "Service"
85+
FOR INSERT
86+
WITH CHECK (
87+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
88+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
89+
);
90+
91+
CREATE POLICY workspace_isolation_update ON "Service"
92+
FOR UPDATE
93+
USING (
94+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
95+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
96+
);
97+
98+
CREATE POLICY workspace_isolation_delete ON "Service"
99+
FOR DELETE
100+
USING (
101+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
102+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
103+
);
104+
105+
-- ApiKey policies
106+
CREATE POLICY workspace_isolation_select ON "ApiKey"
107+
FOR SELECT
108+
USING (
109+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
110+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
111+
);
112+
113+
CREATE POLICY workspace_isolation_insert ON "ApiKey"
114+
FOR INSERT
115+
WITH CHECK (
116+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
117+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
118+
);
119+
120+
CREATE POLICY workspace_isolation_update ON "ApiKey"
121+
FOR UPDATE
122+
USING (
123+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
124+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
125+
);
126+
127+
CREATE POLICY workspace_isolation_delete ON "ApiKey"
128+
FOR DELETE
129+
USING (
130+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
131+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
132+
);
133+
134+
-- DebugSession policies
135+
CREATE POLICY workspace_isolation_select ON "DebugSession"
136+
FOR SELECT
137+
USING (
138+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
139+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
140+
);
141+
142+
CREATE POLICY workspace_isolation_insert ON "DebugSession"
143+
FOR INSERT
144+
WITH CHECK (
145+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
146+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
147+
);
148+
149+
CREATE POLICY workspace_isolation_update ON "DebugSession"
150+
FOR UPDATE
151+
USING (
152+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
153+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
154+
);
155+
156+
CREATE POLICY workspace_isolation_delete ON "DebugSession"
157+
FOR DELETE
158+
USING (
159+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
160+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
161+
);
162+
163+
-- EnvironmentShare policies
164+
CREATE POLICY workspace_isolation_select ON "EnvironmentShare"
165+
FOR SELECT
166+
USING (
167+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
168+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
169+
);
170+
171+
CREATE POLICY workspace_isolation_insert ON "EnvironmentShare"
172+
FOR INSERT
173+
WITH CHECK (
174+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
175+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
176+
);
177+
178+
CREATE POLICY workspace_isolation_update ON "EnvironmentShare"
179+
FOR UPDATE
180+
USING (
181+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
182+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
183+
);
184+
185+
CREATE POLICY workspace_isolation_delete ON "EnvironmentShare"
186+
FOR DELETE
187+
USING (
188+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
189+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
190+
);
191+
192+
-- AgentMessage policies
193+
CREATE POLICY workspace_isolation_select ON "AgentMessage"
194+
FOR SELECT
195+
USING (
196+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
197+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
198+
);
199+
200+
CREATE POLICY workspace_isolation_insert ON "AgentMessage"
201+
FOR INSERT
202+
WITH CHECK (
203+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
204+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
205+
);
206+
207+
CREATE POLICY workspace_isolation_update ON "AgentMessage"
208+
FOR UPDATE
209+
USING (
210+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
211+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
212+
);
213+
214+
CREATE POLICY workspace_isolation_delete ON "AgentMessage"
215+
FOR DELETE
216+
USING (
217+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
218+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
219+
);
220+
221+
-- Webhook policies
222+
CREATE POLICY workspace_isolation_select ON "Webhook"
223+
FOR SELECT
224+
USING (
225+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
226+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
227+
);
228+
229+
CREATE POLICY workspace_isolation_insert ON "Webhook"
230+
FOR INSERT
231+
WITH CHECK (
232+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
233+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
234+
);
235+
236+
CREATE POLICY workspace_isolation_update ON "Webhook"
237+
FOR UPDATE
238+
USING (
239+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
240+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
241+
);
242+
243+
CREATE POLICY workspace_isolation_delete ON "Webhook"
244+
FOR DELETE
245+
USING (
246+
current_setting('app.current_workspace_id', true) = '__rls_bypass__'
247+
OR "workspaceId" = current_setting('app.current_workspace_id', true)
248+
);
249+
250+
-- ============================================================================
251+
-- NOTES
252+
-- ============================================================================
253+
-- 1. Policies are DENY by default - if app.current_workspace_id is not set, access is denied
254+
-- 2. Use '__rls_bypass__' for admin/migration operations that need cross-workspace access
255+
-- 3. FORCE ROW LEVEL SECURITY ensures even table owners are subject to RLS
256+
-- 4. The application must SET app.current_workspace_id before queries
257+
-- 5. Tables without direct workspaceId (like DiagnosticResult) inherit isolation
258+
-- through their foreign key relationships
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- Setup script for RLS-enforced application role
2+
-- Run this ONCE on your production database as superuser before deploying
3+
4+
-- Create application role with RLS enforcement
5+
-- Replace 'YOUR_SECURE_PASSWORD' with a strong password for production
6+
DROP ROLE IF EXISTS privateconnect_app;
7+
CREATE ROLE privateconnect_app LOGIN PASSWORD 'YOUR_SECURE_PASSWORD' NOBYPASSRLS;
8+
9+
-- Grant schema access
10+
GRANT USAGE ON SCHEMA public TO privateconnect_app;
11+
12+
-- Grant table permissions
13+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO privateconnect_app;
14+
15+
-- Grant sequence permissions (needed for auto-increment IDs)
16+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO privateconnect_app;
17+
18+
-- Set default privileges for future tables created by the migration user
19+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO privateconnect_app;
20+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO privateconnect_app;
21+
22+
-- Verify the role was created correctly
23+
SELECT rolname, rolsuper, rolbypassrls
24+
FROM pg_roles
25+
WHERE rolname = 'privateconnect_app';
26+
27+
-- Expected output:
28+
-- rolname | rolsuper | rolbypassrls
29+
-- --------------------+----------+--------------
30+
-- privateconnect_app | f | f

apps/api/src/app.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Module } from '@nestjs/common';
22
import { ThrottlerModule } from '@nestjs/throttler';
3+
import { ContextModule } from './context/context.module';
34
import { PrismaModule } from './prisma/prisma.module';
45
import { AuthModule } from './auth/auth.module';
56
import { AdminModule } from './admin/admin.module';
@@ -23,6 +24,8 @@ import { AskModule } from './ask/ask.module';
2324

2425
@Module({
2526
imports: [
27+
// Context module must be first for RLS to work
28+
ContextModule,
2629
// Rate limiting configuration
2730
ThrottlerModule.forRoot([
2831
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
Injectable,
3+
NestInterceptor,
4+
ExecutionContext,
5+
CallHandler,
6+
} from '@nestjs/common';
7+
import { Observable } from 'rxjs';
8+
import { ContextService } from './context.service';
9+
10+
/**
11+
* Interceptor that captures workspaceId from the request and stores it
12+
* in AsyncLocalStorage for use by PrismaService (RLS context)
13+
*/
14+
@Injectable()
15+
export class ContextInterceptor implements NestInterceptor {
16+
constructor(private readonly contextService: ContextService) {}
17+
18+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
19+
const request = context.switchToHttp().getRequest();
20+
21+
// Extract workspace info from request (set by AuthGuard or ApiKeyGuard)
22+
const workspaceId = request.workspaceId || request.workspace?.id;
23+
const isAdmin = request.user?.isAdmin ?? false;
24+
25+
// Run the handler within the context
26+
return new Observable((subscriber) => {
27+
this.contextService.run({ workspaceId, isAdmin }, () => {
28+
next.handle().subscribe({
29+
next: (value) => subscriber.next(value),
30+
error: (err) => subscriber.error(err),
31+
complete: () => subscriber.complete(),
32+
});
33+
});
34+
});
35+
}
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Global, Module } from '@nestjs/common';
2+
import { APP_INTERCEPTOR } from '@nestjs/core';
3+
import { ContextService } from './context.service';
4+
import { ContextInterceptor } from './context.interceptor';
5+
6+
@Global()
7+
@Module({
8+
providers: [
9+
ContextService,
10+
{
11+
provide: APP_INTERCEPTOR,
12+
useClass: ContextInterceptor,
13+
},
14+
],
15+
exports: [ContextService],
16+
})
17+
export class ContextModule {}

0 commit comments

Comments
 (0)