Skip to content

Commit 63fe328

Browse files
authored
Merge pull request #1086 from constructive-io/feat/phase2-server-rls-settings
feat: read RLS config from typed rls_settings table with api_modules fallback
2 parents 15860ff + cb5e3a0 commit 63fe328

3 files changed

Lines changed: 157 additions & 6 deletions

File tree

graphql/server/src/middleware/__tests__/upload.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ describe('createUploadAuthenticateMiddleware', () => {
184184
const res = makeRes();
185185
const next = makeNext();
186186

187+
// typed rls_settings query returns no rows (table may not exist yet)
188+
rootPool.query.mockResolvedValueOnce({ rows: [] });
189+
// legacy api_modules fallback
187190
rootPool.query.mockResolvedValueOnce({
188191
rows: [
189192
{
@@ -282,6 +285,9 @@ describe('createUploadAuthenticateMiddleware', () => {
282285
const res = makeRes();
283286
const next = makeNext();
284287

288+
// typed rls_settings query returns no rows (table may not exist yet)
289+
rootPool.query.mockResolvedValueOnce({ rows: [] });
290+
// legacy api_modules fallback
285291
rootPool.query.mockResolvedValueOnce({
286292
rows: [
287293
{
@@ -330,7 +336,13 @@ describe('createUploadAuthenticateMiddleware', () => {
330336
const res = makeRes();
331337
const next = makeNext();
332338

339+
// typed rls_settings query returns no rows
340+
rootPool.query.mockResolvedValueOnce({ rows: [] });
341+
// legacy api_modules by database_id returns no rows
342+
rootPool.query.mockResolvedValueOnce({ rows: [] });
343+
// typed rls_settings by dbname returns no rows
333344
rootPool.query.mockResolvedValueOnce({ rows: [] });
345+
// legacy api_modules by dbname returns no rows
334346
rootPool.query.mockResolvedValueOnce({ rows: [] });
335347

336348
await middleware(req, res, next);

graphql/server/src/middleware/api.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,29 @@ const RLS_MODULE_SQL = `
8686
LIMIT 1
8787
`;
8888

89+
const RLS_SETTINGS_SQL = `
90+
SELECT
91+
auth_schema.schema_name AS authenticate_schema,
92+
role_schema.schema_name AS role_schema,
93+
auth_fn.name AS authenticate,
94+
auth_strict_fn.name AS authenticate_strict,
95+
role_fn.name AS current_role,
96+
role_id_fn.name AS current_role_id,
97+
ua_fn.name AS current_user_agent,
98+
ip_fn.name AS current_ip_address
99+
FROM services_public.rls_settings rs
100+
LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
101+
LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
102+
LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
103+
LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
104+
LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
105+
LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
106+
LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
107+
LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
108+
WHERE rs.database_id = $1
109+
LIMIT 1
110+
`;
111+
89112
/**
90113
* Discover auth settings table location via public metaschema tables.
91114
* Joins sessions_module with metaschema_public.schema to resolve
@@ -249,6 +272,24 @@ const toRlsModule = (row: RlsModuleRow | null): RlsModule | undefined => {
249272
};
250273
};
251274

275+
const toRlsModuleFromSettings = (row: RlsModuleData | null): RlsModule | undefined => {
276+
if (!row) return undefined;
277+
return {
278+
authenticate: row.authenticate,
279+
authenticateStrict: row.authenticate_strict,
280+
privateSchema: {
281+
schemaName: row.authenticate_schema,
282+
},
283+
publicSchema: {
284+
schemaName: row.role_schema,
285+
},
286+
currentRole: row.current_role,
287+
currentRoleId: row.current_role_id,
288+
currentIpAddress: row.current_ip_address,
289+
currentUserAgent: row.current_user_agent,
290+
};
291+
};
292+
252293
const toAuthSettings = (row: AuthSettingsRow | null): AuthSettings | undefined => {
253294
if (!row) return undefined;
254295
return {
@@ -263,14 +304,14 @@ const toAuthSettings = (row: AuthSettingsRow | null): AuthSettings | undefined =
263304
};
264305
};
265306

266-
const toApiStructure = (row: ApiRow, opts: ApiOptions, rlsModuleRow?: RlsModuleRow | null, authSettingsRow?: AuthSettingsRow | null): ApiStructure => ({
307+
const toApiStructure = (row: ApiRow, opts: ApiOptions, rlsModule?: RlsModule, authSettingsRow?: AuthSettingsRow | null): ApiStructure => ({
267308
apiId: row.api_id,
268309
dbname: row.dbname || opts.pg?.database || '',
269310
anonRole: row.anon_role || 'anon',
270311
roleName: row.role_name || 'authenticated',
271312
schema: row.schemas || [],
272313
apiModules: [],
273-
rlsModule: toRlsModule(rlsModuleRow ?? null),
314+
rlsModule,
274315
domains: [],
275316
databaseId: row.database_id,
276317
isPublic: row.is_public,
@@ -329,9 +370,24 @@ const queryApiList = async (pool: Pool, isPublic: boolean): Promise<ApiListRow[]
329370
return result.rows;
330371
};
331372

332-
const queryRlsModule = async (pool: Pool, apiId: string): Promise<RlsModuleRow | null> => {
373+
const queryRlsSettings = async (pool: Pool, databaseId: string): Promise<RlsModule | undefined> => {
374+
try {
375+
const result = await pool.query<RlsModuleData>(RLS_SETTINGS_SQL, [databaseId]);
376+
return toRlsModuleFromSettings(result.rows[0] ?? null);
377+
} catch {
378+
return undefined;
379+
}
380+
};
381+
382+
const queryRlsModuleLegacy = async (pool: Pool, apiId: string): Promise<RlsModule | undefined> => {
333383
const result = await pool.query<RlsModuleRow>(RLS_MODULE_SQL, [apiId]);
334-
return result.rows[0] ?? null;
384+
return toRlsModule(result.rows[0] ?? null);
385+
};
386+
387+
const queryRlsModule = async (pool: Pool, databaseId: string, apiId: string): Promise<RlsModule | undefined> => {
388+
const fromSettings = await queryRlsSettings(pool, databaseId);
389+
if (fromSettings) return fromSettings;
390+
return queryRlsModuleLegacy(pool, apiId);
335391
};
336392

337393
/**
@@ -423,7 +479,7 @@ const resolveApiNameHeader = async (ctx: ResolveContext): Promise<ApiStructure |
423479
return null;
424480
}
425481

426-
const rlsModule = await queryRlsModule(pool, row.api_id);
482+
const rlsModule = await queryRlsModule(pool, row.database_id, row.api_id);
427483
const authSettings = await queryAuthSettings(opts, row.dbname);
428484
log.debug(`[api-name-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettings ? 'found' : 'none'}`);
429485
return toApiStructure(row, opts, rlsModule, authSettings);
@@ -449,7 +505,7 @@ const resolveDomainLookup = async (ctx: ResolveContext): Promise<ApiStructure |
449505
return null;
450506
}
451507

452-
const rlsModule = await queryRlsModule(pool, row.api_id);
508+
const rlsModule = await queryRlsModule(pool, row.database_id, row.api_id);
453509
const authSettings = await queryAuthSettings(opts, row.dbname);
454510
log.debug(`[domain-lookup] resolved schemas: [${row.schemas?.join(', ')}], rlsModule: ${rlsModule ? 'found' : 'none'}, authSettings: ${authSettings ? 'found' : 'none'}`);
455511
return toApiStructure(row, opts, rlsModule, authSettings);

graphql/server/src/middleware/upload.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,53 @@ const RLS_MODULE_BY_DBNAME_SQL = `
8181
LIMIT 1
8282
`;
8383

84+
const RLS_SETTINGS_BY_DATABASE_ID_SQL = `
85+
SELECT
86+
auth_schema.schema_name AS authenticate_schema,
87+
role_schema.schema_name AS role_schema,
88+
auth_fn.name AS authenticate,
89+
auth_strict_fn.name AS authenticate_strict,
90+
role_fn.name AS current_role,
91+
role_id_fn.name AS current_role_id,
92+
ua_fn.name AS current_user_agent,
93+
ip_fn.name AS current_ip_address
94+
FROM services_public.rls_settings rs
95+
LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
96+
LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
97+
LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
98+
LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
99+
LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
100+
LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
101+
LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
102+
LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
103+
WHERE rs.database_id = $1
104+
LIMIT 1
105+
`;
106+
107+
const RLS_SETTINGS_BY_DBNAME_SQL = `
108+
SELECT
109+
auth_schema.schema_name AS authenticate_schema,
110+
role_schema.schema_name AS role_schema,
111+
auth_fn.name AS authenticate,
112+
auth_strict_fn.name AS authenticate_strict,
113+
role_fn.name AS current_role,
114+
role_id_fn.name AS current_role_id,
115+
ua_fn.name AS current_user_agent,
116+
ip_fn.name AS current_ip_address
117+
FROM services_public.rls_settings rs
118+
JOIN services_public.apis a ON rs.database_id = a.database_id
119+
LEFT JOIN metaschema_public.schema auth_schema ON rs.authenticate_schema_id = auth_schema.id
120+
LEFT JOIN metaschema_public.schema role_schema ON rs.role_schema_id = role_schema.id
121+
LEFT JOIN metaschema_public.function auth_fn ON rs.authenticate_function_id = auth_fn.id
122+
LEFT JOIN metaschema_public.function auth_strict_fn ON rs.authenticate_strict_function_id = auth_strict_fn.id
123+
LEFT JOIN metaschema_public.function role_fn ON rs.current_role_function_id = role_fn.id
124+
LEFT JOIN metaschema_public.function role_id_fn ON rs.current_role_id_function_id = role_id_fn.id
125+
LEFT JOIN metaschema_public.function ua_fn ON rs.current_user_agent_function_id = ua_fn.id
126+
LEFT JOIN metaschema_public.function ip_fn ON rs.current_ip_address_function_id = ip_fn.id
127+
WHERE a.dbname = $1
128+
LIMIT 1
129+
`;
130+
84131
interface RlsModuleData {
85132
authenticate: string;
86133
authenticate_strict: string;
@@ -111,6 +158,20 @@ const toRlsModule = (row: RlsModuleRow | null): RlsModule | undefined => {
111158
};
112159
};
113160

161+
const toRlsModuleFromSettings = (row: RlsModuleData | null): RlsModule | undefined => {
162+
if (!row) return undefined;
163+
return {
164+
authenticate: row.authenticate,
165+
authenticateStrict: row.authenticate_strict,
166+
privateSchema: { schemaName: row.authenticate_schema },
167+
publicSchema: { schemaName: row.role_schema },
168+
currentRole: row.current_role,
169+
currentRoleId: row.current_role_id,
170+
currentIpAddress: row.current_ip_address,
171+
currentUserAgent: row.current_user_agent,
172+
};
173+
};
174+
114175
const getBearerToken = (authorization?: string): string | null => {
115176
if (!authorization) return null;
116177
const [authType, authToken] = authorization.split(' ');
@@ -120,7 +181,27 @@ const getBearerToken = (authorization?: string): string | null => {
120181
return authToken;
121182
};
122183

184+
const queryRlsSettingsByDatabaseId = async (pool: Pool, databaseId: string): Promise<RlsModule | undefined> => {
185+
try {
186+
const result = await pool.query<RlsModuleData>(RLS_SETTINGS_BY_DATABASE_ID_SQL, [databaseId]);
187+
return toRlsModuleFromSettings(result.rows[0] ?? null);
188+
} catch {
189+
return undefined;
190+
}
191+
};
192+
193+
const queryRlsSettingsByDbname = async (pool: Pool, dbname: string): Promise<RlsModule | undefined> => {
194+
try {
195+
const result = await pool.query<RlsModuleData>(RLS_SETTINGS_BY_DBNAME_SQL, [dbname]);
196+
return toRlsModuleFromSettings(result.rows[0] ?? null);
197+
} catch {
198+
return undefined;
199+
}
200+
};
201+
123202
const queryRlsModuleByDatabaseId = async (pool: Pool, databaseId: string): Promise<RlsModule | undefined> => {
203+
const fromSettings = await queryRlsSettingsByDatabaseId(pool, databaseId);
204+
if (fromSettings) return fromSettings;
124205
const result = await pool.query<RlsModuleRow>(RLS_MODULE_BY_DATABASE_ID_SQL, [databaseId]);
125206
return toRlsModule(result.rows[0] ?? null);
126207
};
@@ -131,6 +212,8 @@ const queryRlsModuleByApiId = async (pool: Pool, apiId: string): Promise<RlsModu
131212
};
132213

133214
const queryRlsModuleByDbname = async (pool: Pool, dbname: string): Promise<RlsModule | undefined> => {
215+
const fromSettings = await queryRlsSettingsByDbname(pool, dbname);
216+
if (fromSettings) return fromSettings;
134217
const result = await pool.query<RlsModuleRow>(RLS_MODULE_BY_DBNAME_SQL, [dbname]);
135218
return toRlsModule(result.rows[0] ?? null);
136219
};

0 commit comments

Comments
 (0)