Files
MikrotikManager/backend/src/services/bandwidth-control-role.ts
T

120 lines
9.4 KiB
TypeScript

/** Catalog observation only. This is neither a control generation issuer nor a
* readiness/native capability. The caller owns its bounded SQL facade and must
* run this assertion at the relevant live authority boundary. No default I/O. */
export interface BandwidthControlRoleSql {
query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>
}
export interface BandwidthControlRoleExpected { databaseName: string; runtimeRole: string; ownerRole: string }
export class BandwidthControlRuntimeRoleRefused extends Error {
readonly code = "BANDWIDTH_CONTROL_RUNTIME_ROLE_REFUSED"
constructor() { super("Bandwidth control runtime role refused") }
}
const TABLES = ["bandwidth_attempts", "bandwidth_node_occupancy", "bandwidth_attempt_events", "bandwidth_source_identity"] as const
/** Names are parameters; catalog paths, namespace, relations and privilege
* vocabulary are fixed. ACL-provided privilege names include newer PostgreSQL
* privileges without sending unsupported names to an older server. */
export const BANDWIDTH_CONTROL_ROLE_SQL = `WITH
runtime AS (SELECT r.* FROM pg_catalog.pg_roles r WHERE r.rolname=$2 AND pg_catalog.current_database()::text=$1::text),
expected_owner AS (SELECT r.oid FROM pg_catalog.pg_roles r WHERE r.rolname=$3),
control_schema AS (SELECT n.* FROM pg_catalog.pg_namespace n WHERE n.nspname='mmapp_control'),
required(name,allowed) AS (VALUES
('bandwidth_attempts',ARRAY['SELECT','INSERT','UPDATE']::text[]),
('bandwidth_node_occupancy',ARRAY['SELECT','INSERT','DELETE']::text[]),
('bandwidth_attempt_events',ARRAY['SELECT','INSERT']::text[]),
('bandwidth_source_identity',ARRAY['SELECT']::text[])),
relations AS (
SELECT e.name,e.allowed,c.oid,c.relowner,c.relacl,c.relkind
FROM required e LEFT JOIN control_schema n ON true
LEFT JOIN pg_catalog.pg_class c ON c.relnamespace=n.oid AND c.relname=e.name),
member_roles AS (
SELECT r.* FROM pg_catalog.pg_roles r CROSS JOIN runtime rt
WHERE r.oid=rt.oid OR pg_catalog.pg_has_role(rt.oid,r.oid,'MEMBER')),
permissions AS (
SELECT r.name,m.oid AS role_oid,
COALESCE(r.oid IS NOT NULL AND r.relkind IN ('r','p') AND NOT EXISTS (
SELECT 1 FROM (
SELECT pg_catalog.unnest(ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']::text[]) AS privilege
UNION SELECT a.privilege_type FROM pg_catalog.aclexplode(COALESCE(r.relacl,pg_catalog.acldefault('r',r.relowner))) a
) p WHERE pg_catalog.has_table_privilege(m.oid,r.oid,p.privilege) IS DISTINCT FROM (p.privilege=ANY(r.allowed))
),false) AS exact_table,
COALESCE(EXISTS (
SELECT 1 FROM (
SELECT pg_catalog.unnest(ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']::text[]) AS privilege
UNION SELECT a.privilege_type FROM pg_catalog.aclexplode(COALESCE(r.relacl,pg_catalog.acldefault('r',r.relowner))) a
) p WHERE (pg_catalog.has_table_privilege(m.oid,r.oid,p.privilege) AND NOT p.privilege=ANY(r.allowed))
OR pg_catalog.has_table_privilege(m.oid,r.oid,p.privilege || ' WITH GRANT OPTION')
) OR EXISTS (
SELECT 1 FROM pg_catalog.unnest(ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::text[]) p(privilege)
WHERE (pg_catalog.has_any_column_privilege(m.oid,r.oid,p.privilege) AND NOT p.privilege=ANY(r.allowed))
OR pg_catalog.has_any_column_privilege(m.oid,r.oid,p.privilege || ' WITH GRANT OPTION')
),true) AS forbidden
FROM relations r CROSS JOIN member_roles m)
SELECT pg_catalog.jsonb_build_object(
'databaseName',pg_catalog.current_database(),
'sessionRole',SESSION_USER::text,'currentRole',CURRENT_USER::text,
'runtimeRoleExists',EXISTS(SELECT 1 FROM runtime),
'ownerRoleExists',EXISTS(SELECT 1 FROM expected_owner),
'runtimeFlagsSafe',COALESCE((SELECT NOT (rolsuper OR rolcreaterole OR rolcreatedb OR rolreplication OR rolbypassrls) FROM runtime),false),
'ownerMembership',COALESCE((SELECT pg_catalog.pg_has_role(rt.oid,o.oid,'MEMBER') FROM runtime rt CROSS JOIN expected_owner o),true),
'dangerousMembership',EXISTS (
SELECT 1 FROM member_roles m WHERE m.rolsuper OR m.rolcreaterole OR m.rolcreatedb OR m.rolreplication OR m.rolbypassrls
OR m.rolname IN ('pg_read_server_files','pg_write_server_files','pg_execute_server_program','pg_read_all_data','pg_write_all_data','pg_maintain','pg_signal_backend','pg_checkpoint','pg_create_subscription','pg_database_owner','pg_use_reserved_connections')
OR EXISTS(SELECT 1 FROM control_schema n WHERE n.nspowner=m.oid OR pg_catalog.has_schema_privilege(m.oid,n.oid,'CREATE'))
OR EXISTS(SELECT 1 FROM relations r WHERE r.relowner=m.oid)
OR EXISTS(SELECT 1 FROM permissions p WHERE p.role_oid=m.oid AND p.forbidden)),
'schemaExists',EXISTS(SELECT 1 FROM control_schema),
'schemaOwner',(SELECT o.rolname::text FROM control_schema n JOIN pg_catalog.pg_roles o ON o.oid=n.nspowner),
'schemaUsage',COALESCE((SELECT pg_catalog.has_schema_privilege(rt.oid,n.oid,'USAGE') FROM runtime rt CROSS JOIN control_schema n),false),
'schemaCreate',COALESCE((SELECT pg_catalog.has_schema_privilege(rt.oid,n.oid,'CREATE') FROM runtime rt CROSS JOIN control_schema n),true),
'publicSchemaUsage',EXISTS(SELECT 1 FROM control_schema n CROSS JOIN LATERAL pg_catalog.aclexplode(COALESCE(n.nspacl,pg_catalog.acldefault('n',n.nspowner))) a WHERE a.grantee=0 AND a.privilege_type='USAGE'),
'publicSchemaCreate',EXISTS(SELECT 1 FROM control_schema n CROSS JOIN LATERAL pg_catalog.aclexplode(COALESCE(n.nspacl,pg_catalog.acldefault('n',n.nspowner))) a WHERE a.grantee=0 AND a.privilege_type='CREATE'),
'tables',(SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
'name',r.name,'exists',r.oid IS NOT NULL AND r.relkind IN ('r','p'),
'ownerRole',o.rolname::text,'ownedByRuntime',COALESCE(r.relowner=rt.oid,true),
'privilegesExact',COALESCE(p.exact_table AND NOT p.forbidden,false)) ORDER BY r.name COLLATE "C")
FROM relations r CROSS JOIN runtime rt LEFT JOIN pg_catalog.pg_roles o ON o.oid=r.relowner
LEFT JOIN permissions p ON p.name=r.name AND p.role_oid=rt.oid)
) AS observation`
function refuse(): never { throw new BandwidthControlRuntimeRoleRefused() }
const plain = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype
const label = (value: unknown): value is string => typeof value === "string" && value.trim() === value && value.length > 0 && Buffer.byteLength(value) <= 63 && !/[\x00-\x1f\x7f]/.test(value)
const exactKeys = (value: Record<string, unknown>, fields: string[]) => Object.keys(value).sort().join(",") === [...fields].sort().join(",")
export function captureBandwidthControlRoleExpected(expected: BandwidthControlRoleExpected): BandwidthControlRoleExpected {
if (!plain(expected) || !exactKeys(expected, ["databaseName", "runtimeRole", "ownerRole"])) refuse()
const captured = { databaseName: expected.databaseName, runtimeRole: expected.runtimeRole, ownerRole: expected.ownerRole }
if (!Object.values(captured).every(label) || captured.runtimeRole === captured.ownerRole) refuse()
return Object.freeze(captured)
}
export async function assertBandwidthControlRuntimeRole(sql: BandwidthControlRoleSql, expected: BandwidthControlRoleExpected, live: () => void): Promise<void> {
const captured = captureBandwidthControlRoleExpected(expected)
live()
let result: { rows: Record<string, unknown>[] }
try { result = await sql.query(BANDWIDTH_CONTROL_ROLE_SQL, [captured.databaseName, captured.runtimeRole, captured.ownerRole]) }
catch { live(); refuse() }
live()
validateBandwidthControlRuntimeRoleRows(result?.rows, captured)
}
/** Shared pure decoder for standalone and same-statement identity snapshots. */
export function validateBandwidthControlRuntimeRoleRows(rows: unknown, expected: BandwidthControlRoleExpected): void {
const captured = captureBandwidthControlRoleExpected(expected)
if (!Array.isArray(rows) || rows.length !== 1 || !plain(rows[0]) || !exactKeys(rows[0], ["observation"])) refuse()
const value = rows[0].observation
if (!plain(value) || !exactKeys(value, ["databaseName","sessionRole","currentRole","runtimeRoleExists","ownerRoleExists","runtimeFlagsSafe","ownerMembership","dangerousMembership","schemaExists","schemaOwner","schemaUsage","schemaCreate","publicSchemaUsage","publicSchemaCreate","tables"])) refuse()
if (value.databaseName !== captured.databaseName || value.sessionRole !== captured.runtimeRole || value.currentRole !== captured.runtimeRole
|| value.runtimeRoleExists !== true || value.ownerRoleExists !== true || value.runtimeFlagsSafe !== true
|| value.ownerMembership !== false || value.dangerousMembership !== false || value.schemaExists !== true || value.schemaOwner !== captured.ownerRole
|| value.schemaUsage !== true || value.schemaCreate !== false || value.publicSchemaUsage !== false || value.publicSchemaCreate !== false
|| !Array.isArray(value.tables) || value.tables.length !== TABLES.length) refuse()
const seen = new Set<string>()
for (const row of value.tables as unknown[]) {
if (!plain(row) || !exactKeys(row, ["name","exists","ownerRole","ownedByRuntime","privilegesExact"])
|| typeof row.name !== "string" || !TABLES.includes(row.name as typeof TABLES[number]) || seen.has(row.name)
|| row.exists !== true || row.ownerRole !== captured.ownerRole || row.ownedByRuntime !== false || row.privilegesExact !== true) refuse()
seen.add(row.name as string)
}
}