@opengeni/db 0.12.1 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VUKRIBO5.js → chunk-CYA6BHV6.js} +150 -7
- package/dist/chunk-CYA6BHV6.js.map +1 -0
- package/dist/chunk-XRVNI2GB.js +896 -0
- package/dist/chunk-XRVNI2GB.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1166 -265
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +324 -38
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-BejThLcd.d.ts → schema-CeXWlYcG.d.ts} +431 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/drizzle/0122_codex_capacity_same_turn.sql +59 -0
- package/drizzle/0123_session_tool_policy_version.sql +14 -0
- package/drizzle/0124_session_event_duplicate_lookup.sql +4 -0
- package/drizzle/0125_document_drops_visibility.sql +29 -0
- package/drizzle/0126_document_access_constraints.sql +138 -0
- package/drizzle/0127_document_default_base_index.sql +5 -0
- package/drizzle/0128_github_installation_authority.sql +69 -0
- package/drizzle/0129_retained_process_reconciliation.sql +356 -0
- package/package.json +4 -3
- package/src/event-payload-sanitizer.ts +167 -55
- package/src/index.ts +1494 -258
- package/src/new-session-drafts.ts +127 -6
- package/src/provision-roles.ts +184 -2
- package/src/runtime-posture-cli.ts +57 -0
- package/src/runtime-posture.ts +760 -0
- package/src/schema.ts +156 -3
- package/src/session-control.ts +1 -0
- package/dist/chunk-BMFDXFPA.js +0 -155
- package/dist/chunk-BMFDXFPA.js.map +0 -1
- package/dist/chunk-VUKRIBO5.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
NewSessionDraftOptions,
|
|
3
3
|
ReasoningEffort,
|
|
4
|
+
RepositoryResourceRef,
|
|
4
5
|
ResourceRef,
|
|
5
6
|
ToolRef,
|
|
6
7
|
} from "@opengeni/contracts";
|
|
@@ -26,6 +27,34 @@ export class NewSessionDraftAccessError extends Error {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
type StoredNewSessionDraftOptions = NewSessionDraftOptions & {
|
|
31
|
+
/** JSONB-only compatibility marker; deliberately not part of public options. */
|
|
32
|
+
toolsProvided?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function storedOptions(
|
|
36
|
+
options: NewSessionDraftOptions,
|
|
37
|
+
toolsProvided: boolean,
|
|
38
|
+
): StoredNewSessionDraftOptions {
|
|
39
|
+
return { ...options, toolsProvided };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function newSessionDraftToolsProvided(row: NewSessionDraftRow): boolean {
|
|
43
|
+
const options = row.sessionOptions as StoredNewSessionDraftOptions;
|
|
44
|
+
// Rows written before the explicitness marker was introduced always carried
|
|
45
|
+
// a `tools` array. Treat a markerless row as explicit rather than widening a
|
|
46
|
+
// narrowed (including empty) legacy selection to today's workspace defaults.
|
|
47
|
+
// New rows always write the marker, so an explicit false remains omitted /
|
|
48
|
+
// workspace-default policy.
|
|
49
|
+
return options.toolsProvided === true || !Object.hasOwn(options, "toolsProvided");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function publicNewSessionDraftOptions(row: NewSessionDraftRow): NewSessionDraftOptions {
|
|
53
|
+
const options = { ...(row.sessionOptions as StoredNewSessionDraftOptions) };
|
|
54
|
+
delete options.toolsProvided;
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
export async function getNewSessionDraftInTransaction(
|
|
30
59
|
db: Database,
|
|
31
60
|
input: { workspaceId: string; subjectId: string; lock?: boolean },
|
|
@@ -54,6 +83,7 @@ export async function saveNewSessionDraftInTransaction(
|
|
|
54
83
|
text: string;
|
|
55
84
|
resources: ResourceRef[];
|
|
56
85
|
tools: ToolRef[];
|
|
86
|
+
toolsProvided: boolean;
|
|
57
87
|
model: string;
|
|
58
88
|
reasoningEffort: ReasoningEffort;
|
|
59
89
|
options: NewSessionDraftOptions;
|
|
@@ -96,7 +126,9 @@ export async function saveNewSessionDraftInTransaction(
|
|
|
96
126
|
tools: input.tools,
|
|
97
127
|
model: input.model,
|
|
98
128
|
reasoningEffort: input.reasoningEffort,
|
|
99
|
-
|
|
129
|
+
// Keep the explicit/omitted policy in the existing JSONB extension point;
|
|
130
|
+
// adding a column here would turn a client preference into a migration.
|
|
131
|
+
sessionOptions: storedOptions(input.options, input.toolsProvided),
|
|
100
132
|
updatedAt: new Date(),
|
|
101
133
|
};
|
|
102
134
|
if (current) {
|
|
@@ -124,21 +156,110 @@ export async function saveNewSessionDraftInTransaction(
|
|
|
124
156
|
throw new NewSessionDraftConflictError(raced?.revision ?? 0);
|
|
125
157
|
}
|
|
126
158
|
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
function safeRepositoryResource(resource: RepositoryResourceRef): RepositoryResourceRef {
|
|
160
|
+
// A repository's URI/ref/mount and GitHub identity are ordinary selection
|
|
161
|
+
// state. Credential bindings, connection refs, access intent, and generic
|
|
162
|
+
// provider ids are per-session authorization/runtime state and must not seed
|
|
163
|
+
// the next create.
|
|
164
|
+
return {
|
|
165
|
+
kind: "repository",
|
|
166
|
+
uri: resource.uri,
|
|
167
|
+
ref: resource.ref,
|
|
168
|
+
...(resource.mountPath ? { mountPath: resource.mountPath } : {}),
|
|
169
|
+
...(resource.subpath ? { subpath: resource.subpath } : {}),
|
|
170
|
+
...(resource.githubInstallationId
|
|
171
|
+
? { githubInstallationId: resource.githubInstallationId }
|
|
172
|
+
: {}),
|
|
173
|
+
...(resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function safeWorkingDir(value: unknown, targetSandboxId: string | undefined): string | undefined {
|
|
178
|
+
if (!targetSandboxId || typeof value !== "string") return undefined;
|
|
179
|
+
const trimmed = value.trim();
|
|
180
|
+
// Only a workspace-root-relative path is safe to remember. Absolute host
|
|
181
|
+
// paths and traversal would leak or unexpectedly target a different machine
|
|
182
|
+
// location after the user changes machines.
|
|
183
|
+
if (
|
|
184
|
+
!trimmed ||
|
|
185
|
+
trimmed === "." ||
|
|
186
|
+
trimmed === ".." ||
|
|
187
|
+
trimmed.includes("\u0000") ||
|
|
188
|
+
trimmed.startsWith("/") ||
|
|
189
|
+
trimmed.startsWith("\\") ||
|
|
190
|
+
/^[A-Za-z]:[\\/]/.test(trimmed) ||
|
|
191
|
+
trimmed.split(/[\\/]+/).some((part) => part === "..")
|
|
192
|
+
) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return trimmed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Replace one exact accepted draft with the next-create safe seed. The row is
|
|
200
|
+
* locked before the revision check so a concurrent save either commits first
|
|
201
|
+
* and wins (this returns false), or observes the incremented seed revision and
|
|
202
|
+
* reports a typed OCC conflict. A missing row is an idempotent no-op.
|
|
203
|
+
*/
|
|
204
|
+
export async function seedNewSessionDraftInTransaction(
|
|
129
205
|
db: Database,
|
|
130
206
|
input: { workspaceId: string; subjectId: string; expectedRevision: number },
|
|
131
207
|
): Promise<boolean> {
|
|
132
208
|
if (input.expectedRevision === 0) return false;
|
|
133
|
-
const
|
|
134
|
-
.
|
|
209
|
+
const [current] = await db
|
|
210
|
+
.select()
|
|
211
|
+
.from(schema.newSessionDrafts)
|
|
135
212
|
.where(
|
|
136
213
|
and(
|
|
137
214
|
eq(schema.newSessionDrafts.workspaceId, input.workspaceId),
|
|
138
215
|
eq(schema.newSessionDrafts.subjectId, input.subjectId),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
.for("update")
|
|
219
|
+
.limit(1);
|
|
220
|
+
if (!current || current.revision !== input.expectedRevision) return false;
|
|
221
|
+
|
|
222
|
+
const options = current.sessionOptions as StoredNewSessionDraftOptions;
|
|
223
|
+
const targetSandboxId =
|
|
224
|
+
typeof options.targetSandboxId === "string" ? options.targetSandboxId : undefined;
|
|
225
|
+
const safeOptions: NewSessionDraftOptions = {
|
|
226
|
+
...(options.sandboxBackend ? { sandboxBackend: options.sandboxBackend } : {}),
|
|
227
|
+
...(targetSandboxId ? { targetSandboxId } : {}),
|
|
228
|
+
...(safeWorkingDir(options.workingDir, targetSandboxId)
|
|
229
|
+
? { workingDir: safeWorkingDir(options.workingDir, targetSandboxId) }
|
|
230
|
+
: {}),
|
|
231
|
+
...(options.variableSetId ? { variableSetId: options.variableSetId } : {}),
|
|
232
|
+
...(options.rigId ? { rigId: options.rigId } : {}),
|
|
233
|
+
};
|
|
234
|
+
const resources = (Array.isArray(current.resources) ? current.resources : []).flatMap((raw) => {
|
|
235
|
+
if (!raw || typeof raw !== "object" || (raw as { kind?: unknown }).kind !== "repository") {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
return [safeRepositoryResource(raw as RepositoryResourceRef)];
|
|
239
|
+
});
|
|
240
|
+
const [seeded] = await db
|
|
241
|
+
.update(schema.newSessionDrafts)
|
|
242
|
+
.set({
|
|
243
|
+
revision: current.revision + 1,
|
|
244
|
+
text: "",
|
|
245
|
+
resources,
|
|
246
|
+
// The explicit array is retained only when the caller explicitly pinned
|
|
247
|
+
// tools. Omitted workspace-default policy is represented by [] + false.
|
|
248
|
+
tools: newSessionDraftToolsProvided(current) ? current.tools : [],
|
|
249
|
+
model: current.model,
|
|
250
|
+
reasoningEffort: current.reasoningEffort,
|
|
251
|
+
sessionOptions: storedOptions(safeOptions, newSessionDraftToolsProvided(current)),
|
|
252
|
+
updatedAt: new Date(),
|
|
253
|
+
})
|
|
254
|
+
.where(
|
|
255
|
+
and(
|
|
256
|
+
eq(schema.newSessionDrafts.id, current.id),
|
|
139
257
|
eq(schema.newSessionDrafts.revision, input.expectedRevision),
|
|
140
258
|
),
|
|
141
259
|
)
|
|
142
260
|
.returning({ id: schema.newSessionDrafts.id });
|
|
143
|
-
return
|
|
261
|
+
return Boolean(seeded);
|
|
144
262
|
}
|
|
263
|
+
|
|
264
|
+
/** @deprecated Kept as a compatibility name for low-level callers. */
|
|
265
|
+
export const consumeNewSessionDraftInTransaction = seedNewSessionDraftInTransaction;
|
package/src/provision-roles.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
2
|
import type { RlsStrategy } from "./index";
|
|
3
|
+
import {
|
|
4
|
+
RUNTIME_FULL_DML_TABLES,
|
|
5
|
+
RUNTIME_READ_INSERT_TABLES,
|
|
6
|
+
RUNTIME_READ_ONLY_TABLES,
|
|
7
|
+
} from "./runtime-posture";
|
|
3
8
|
|
|
4
9
|
export type ProvisionResult = {
|
|
5
10
|
appRole: string | null;
|
|
@@ -101,7 +106,12 @@ export async function provisionRoles(
|
|
|
101
106
|
"OPENGENI_APP_DATABASE_PASSWORD (or appPassword) is required for rlsStrategy 'force'",
|
|
102
107
|
);
|
|
103
108
|
}
|
|
104
|
-
|
|
109
|
+
// Ownership and role-graph edges cannot be safely guessed away. Refuse to
|
|
110
|
+
// mutate an existing role until an operator has explicitly transferred
|
|
111
|
+
// objects/removed memberships; role attributes and direct grants, however,
|
|
112
|
+
// are deterministic and are converged below on every run.
|
|
113
|
+
await assertAppRoleSafeToNormalize(sql, appRole);
|
|
114
|
+
await ensureRestrictedAppLoginRole(sql, appRole, appPassword);
|
|
105
115
|
provisionedAppRole = appRole;
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -166,6 +176,108 @@ END $$;
|
|
|
166
176
|
`);
|
|
167
177
|
}
|
|
168
178
|
|
|
179
|
+
async function ensureRestrictedAppLoginRole(
|
|
180
|
+
sql: postgres.Sql,
|
|
181
|
+
role: string,
|
|
182
|
+
password: string,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
await sql.unsafe(`
|
|
185
|
+
DO $$
|
|
186
|
+
BEGIN
|
|
187
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = ${literal(role)}) THEN
|
|
188
|
+
EXECUTE format(
|
|
189
|
+
'CREATE ROLE %I WITH LOGIN NOSUPERUSER NOBYPASSRLS NOCREATEROLE NOCREATEDB NOREPLICATION NOINHERIT PASSWORD %L',
|
|
190
|
+
${literal(role)},
|
|
191
|
+
${literal(password)}
|
|
192
|
+
);
|
|
193
|
+
ELSE
|
|
194
|
+
EXECUTE format(
|
|
195
|
+
'ALTER ROLE %I WITH LOGIN NOSUPERUSER NOBYPASSRLS NOCREATEROLE NOCREATEDB NOREPLICATION NOINHERIT PASSWORD %L',
|
|
196
|
+
${literal(role)},
|
|
197
|
+
${literal(password)}
|
|
198
|
+
);
|
|
199
|
+
END IF;
|
|
200
|
+
END $$;
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fail rather than silently revoking role relationships or transferring owned
|
|
206
|
+
* objects. Those operations have effects outside OpenGeni's runtime grant
|
|
207
|
+
* contract and require an explicit, audited operator decision.
|
|
208
|
+
*/
|
|
209
|
+
async function assertAppRoleSafeToNormalize(sql: postgres.Sql, role: string): Promise<void> {
|
|
210
|
+
const exists = await sql<{ exists: boolean }[]>`
|
|
211
|
+
select exists(select 1 from pg_roles where rolname = ${role}) as exists
|
|
212
|
+
`;
|
|
213
|
+
if (!exists[0]?.exists) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const memberships = await sql<{ relationship: string }[]>`
|
|
218
|
+
select ('inherits:' || parent.rolname)::text as relationship
|
|
219
|
+
from pg_auth_members membership
|
|
220
|
+
join pg_roles member on member.oid = membership.member
|
|
221
|
+
join pg_roles parent on parent.oid = membership.roleid
|
|
222
|
+
where member.rolname = ${role}
|
|
223
|
+
union all
|
|
224
|
+
select ('member:' || member.rolname)::text as relationship
|
|
225
|
+
from pg_auth_members membership
|
|
226
|
+
join pg_roles member on member.oid = membership.member
|
|
227
|
+
join pg_roles parent on parent.oid = membership.roleid
|
|
228
|
+
where parent.rolname = ${role}
|
|
229
|
+
order by relationship
|
|
230
|
+
`;
|
|
231
|
+
if (memberships.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Refusing to normalize app role ${role}: remove role relationships first (${memberships
|
|
234
|
+
.map((row) => row.relationship)
|
|
235
|
+
.join(", ")})`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ownedObjects = await sql<{ object_name: string }[]>`
|
|
240
|
+
select ('database:' || d.datname)::text as object_name
|
|
241
|
+
from pg_database d
|
|
242
|
+
join pg_roles owner on owner.oid = d.datdba
|
|
243
|
+
where owner.rolname = ${role}
|
|
244
|
+
union all
|
|
245
|
+
select ('schema:' || n.nspname)::text as object_name
|
|
246
|
+
from pg_namespace n
|
|
247
|
+
join pg_roles owner on owner.oid = n.nspowner
|
|
248
|
+
where owner.rolname = ${role}
|
|
249
|
+
and n.nspname <> 'information_schema'
|
|
250
|
+
and n.nspname !~ '^pg_'
|
|
251
|
+
union all
|
|
252
|
+
select ('relation:' || n.nspname || '.' || c.relname)::text as object_name
|
|
253
|
+
from pg_class c
|
|
254
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
255
|
+
join pg_roles owner on owner.oid = c.relowner
|
|
256
|
+
where owner.rolname = ${role}
|
|
257
|
+
and n.nspname <> 'information_schema'
|
|
258
|
+
and n.nspname !~ '^pg_'
|
|
259
|
+
union all
|
|
260
|
+
select (
|
|
261
|
+
'routine:' || n.nspname || '.' || p.proname || '(' ||
|
|
262
|
+
pg_get_function_identity_arguments(p.oid) || ')'
|
|
263
|
+
)::text as object_name
|
|
264
|
+
from pg_proc p
|
|
265
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
266
|
+
join pg_roles owner on owner.oid = p.proowner
|
|
267
|
+
where owner.rolname = ${role}
|
|
268
|
+
and n.nspname <> 'information_schema'
|
|
269
|
+
and n.nspname !~ '^pg_'
|
|
270
|
+
order by object_name
|
|
271
|
+
`;
|
|
272
|
+
if (ownedObjects.length > 0) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Refusing to normalize app role ${role}: transfer owned objects first (${ownedObjects
|
|
275
|
+
.map((row) => row.object_name)
|
|
276
|
+
.join(", ")})`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
169
281
|
async function ensureDatabase(sql: postgres.Sql, database: string, owner: string): Promise<void> {
|
|
170
282
|
const existing = await sql<{ exists: boolean }[]>`
|
|
171
283
|
select exists(select 1 from pg_database where datname = ${database}) as exists
|
|
@@ -203,16 +315,86 @@ async function grantAppRoleIfSchemaExists(
|
|
|
203
315
|
role: string,
|
|
204
316
|
schema: string,
|
|
205
317
|
): Promise<void> {
|
|
318
|
+
const runtimeFullDmlTables = `ARRAY[${RUNTIME_FULL_DML_TABLES.map(literal).join(", ")}]`;
|
|
319
|
+
const runtimeReadOnlyTables = `ARRAY[${RUNTIME_READ_ONLY_TABLES.map(literal).join(", ")}]`;
|
|
320
|
+
const runtimeReadInsertTables = `ARRAY[${RUNTIME_READ_INSERT_TABLES.map(literal).join(", ")}]`;
|
|
206
321
|
await sql.unsafe(`
|
|
207
322
|
DO $$
|
|
323
|
+
DECLARE
|
|
324
|
+
owner_role text := current_user;
|
|
325
|
+
runtime_table text;
|
|
208
326
|
BEGIN
|
|
327
|
+
EXECUTE format('REVOKE CREATE ON DATABASE %I FROM %I', current_database(), ${literal(role)});
|
|
209
328
|
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ${literal(schema)}) THEN
|
|
210
329
|
EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});
|
|
211
|
-
EXECUTE format('
|
|
330
|
+
EXECUTE format('REVOKE CREATE ON SCHEMA %I FROM %I', ${literal(schema)}, ${literal(role)});
|
|
331
|
+
EXECUTE format('REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA %I FROM %I', ${literal(schema)}, ${literal(role)});
|
|
332
|
+
FOREACH runtime_table IN ARRAY ${runtimeFullDmlTables} LOOP
|
|
333
|
+
IF to_regclass(format('%I.%I', ${literal(schema)}, runtime_table)) IS NOT NULL THEN
|
|
334
|
+
EXECUTE format(
|
|
335
|
+
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.%I TO %I',
|
|
336
|
+
${literal(schema)},
|
|
337
|
+
runtime_table,
|
|
338
|
+
${literal(role)}
|
|
339
|
+
);
|
|
340
|
+
END IF;
|
|
341
|
+
END LOOP;
|
|
342
|
+
FOREACH runtime_table IN ARRAY ${runtimeReadOnlyTables} LOOP
|
|
343
|
+
IF to_regclass(format('%I.%I', ${literal(schema)}, runtime_table)) IS NOT NULL THEN
|
|
344
|
+
EXECUTE format(
|
|
345
|
+
'GRANT SELECT ON TABLE %I.%I TO %I',
|
|
346
|
+
${literal(schema)},
|
|
347
|
+
runtime_table,
|
|
348
|
+
${literal(role)}
|
|
349
|
+
);
|
|
350
|
+
END IF;
|
|
351
|
+
END LOOP;
|
|
352
|
+
FOREACH runtime_table IN ARRAY ${runtimeReadInsertTables} LOOP
|
|
353
|
+
IF to_regclass(format('%I.%I', ${literal(schema)}, runtime_table)) IS NOT NULL THEN
|
|
354
|
+
EXECUTE format(
|
|
355
|
+
'GRANT SELECT, INSERT ON TABLE %I.%I TO %I',
|
|
356
|
+
${literal(schema)},
|
|
357
|
+
runtime_table,
|
|
358
|
+
${literal(role)}
|
|
359
|
+
);
|
|
360
|
+
END IF;
|
|
361
|
+
END LOOP;
|
|
362
|
+
-- Migration 0110 creates this target-schema-local SECURITY DEFINER
|
|
363
|
+
-- capability before opengeni_app may exist. Re-converge its exact EXECUTE
|
|
364
|
+
-- grant here so the supported migrate-then-provision order is equivalent to
|
|
365
|
+
-- provisioning the role before that migration.
|
|
366
|
+
IF to_regprocedure(
|
|
367
|
+
format('%I.lock_nested_agent_depth_configuration()', ${literal(schema)})
|
|
368
|
+
) IS NOT NULL THEN
|
|
369
|
+
EXECUTE format(
|
|
370
|
+
'GRANT EXECUTE ON FUNCTION %I.lock_nested_agent_depth_configuration() TO %I',
|
|
371
|
+
${literal(schema)},
|
|
372
|
+
${literal(role)}
|
|
373
|
+
);
|
|
374
|
+
END IF;
|
|
375
|
+
EXECUTE format('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA %I TO %I', ${literal(schema)}, ${literal(role)});
|
|
376
|
+
EXECUTE format(
|
|
377
|
+
'ALTER DEFAULT PRIVILEGES FOR ROLE %I IN SCHEMA %I REVOKE ALL PRIVILEGES ON TABLES FROM %I',
|
|
378
|
+
owner_role,
|
|
379
|
+
${literal(schema)},
|
|
380
|
+
${literal(role)}
|
|
381
|
+
);
|
|
382
|
+
EXECUTE format(
|
|
383
|
+
'ALTER DEFAULT PRIVILEGES FOR ROLE %I IN SCHEMA %I GRANT USAGE, SELECT ON SEQUENCES TO %I',
|
|
384
|
+
owner_role,
|
|
385
|
+
${literal(schema)},
|
|
386
|
+
${literal(role)}
|
|
387
|
+
);
|
|
212
388
|
END IF;
|
|
213
389
|
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'opengeni_private') THEN
|
|
214
390
|
EXECUTE format('GRANT USAGE ON SCHEMA opengeni_private TO %I', ${literal(role)});
|
|
391
|
+
EXECUTE format('REVOKE CREATE ON SCHEMA opengeni_private FROM %I', ${literal(role)});
|
|
215
392
|
EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA opengeni_private TO %I', ${literal(role)});
|
|
393
|
+
EXECUTE format(
|
|
394
|
+
'ALTER DEFAULT PRIVILEGES FOR ROLE %I IN SCHEMA opengeni_private GRANT EXECUTE ON FUNCTIONS TO %I',
|
|
395
|
+
owner_role,
|
|
396
|
+
${literal(role)}
|
|
397
|
+
);
|
|
216
398
|
END IF;
|
|
217
399
|
END $$;
|
|
218
400
|
`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { dbSearchPath, getSettings } from "@opengeni/config";
|
|
2
|
+
import {
|
|
3
|
+
assertRuntimeDatabasePosture,
|
|
4
|
+
createDb,
|
|
5
|
+
FORCE_RLS_TABLES,
|
|
6
|
+
PROTECTED_NO_DIRECT_DML_TABLES,
|
|
7
|
+
RUNTIME_FULL_DML_TABLES,
|
|
8
|
+
RUNTIME_READ_INSERT_TABLES,
|
|
9
|
+
RUNTIME_READ_ONLY_TABLES,
|
|
10
|
+
} from "./index";
|
|
11
|
+
|
|
12
|
+
const settings = getSettings();
|
|
13
|
+
const searchPath = dbSearchPath(settings);
|
|
14
|
+
const client = createDb(settings.databaseUrl, {
|
|
15
|
+
...(searchPath ? { searchPath } : {}),
|
|
16
|
+
rlsStrategy: settings.rlsStrategy,
|
|
17
|
+
max: 1,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const posture = await assertRuntimeDatabasePosture(client.db, {
|
|
22
|
+
rlsStrategy: settings.rlsStrategy,
|
|
23
|
+
expectedRole: settings.runtimeDatabaseRole,
|
|
24
|
+
targetSchema: settings.dbSchema.trim() || "public",
|
|
25
|
+
});
|
|
26
|
+
// Structural evidence only: never print a connection string, secret, GUC, or
|
|
27
|
+
// tenant row. The command is intended for release Jobs and audit artifacts.
|
|
28
|
+
console.log(
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
ok: true,
|
|
31
|
+
rlsStrategy: settings.rlsStrategy,
|
|
32
|
+
currentUser: posture.identity.currentUser,
|
|
33
|
+
sessionUser: posture.identity.sessionUser,
|
|
34
|
+
memberships: posture.memberships.length,
|
|
35
|
+
ownedSchemas: posture.ownedSchemas.length,
|
|
36
|
+
ownedRelations: posture.ownedRelations.length,
|
|
37
|
+
declaredProtectedTables: FORCE_RLS_TABLES.length,
|
|
38
|
+
activeProtectedTables: posture.tables.filter((table) => table.rlsActive).length,
|
|
39
|
+
declaredFullDmlTables: RUNTIME_FULL_DML_TABLES.length,
|
|
40
|
+
privilegedFullDmlTables: posture.tables.filter(
|
|
41
|
+
(table) =>
|
|
42
|
+
table.select &&
|
|
43
|
+
table.insert &&
|
|
44
|
+
table.update &&
|
|
45
|
+
table.delete &&
|
|
46
|
+
!table.truncate &&
|
|
47
|
+
!table.references &&
|
|
48
|
+
!table.trigger,
|
|
49
|
+
).length,
|
|
50
|
+
declaredReadOnlyTables: RUNTIME_READ_ONLY_TABLES.length,
|
|
51
|
+
declaredReadInsertTables: RUNTIME_READ_INSERT_TABLES.length,
|
|
52
|
+
declaredProtectedNoDirectDmlTables: PROTECTED_NO_DIRECT_DML_TABLES.length,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
} finally {
|
|
56
|
+
await client.close();
|
|
57
|
+
}
|