Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ CREATE TABLE metaschema_public.database (
-- Write-once: first row to set it wins; immutable once true.
platform boolean NOT NULL DEFAULT false,

-- Operational access standing. NULL = in good standing; set = every serving
-- lane (pg-wire proxy, gateways, GraphQL, workers) refuses work for this
-- database. System-controlled: tenants read it, only the system role or a
-- platform admin writes it (guard trigger in the metaschema module). The
-- reason names who set it — 'billing' clears automatically when allowance
-- returns, 'admin' only when an admin lifts it. Not an audit log: billing
-- state is the record of WHY; this is only the current on/off derived from it.
suspended_at timestamptz,
suspended_reason text,
Comment on lines +28 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

No upgrade path for existing databases

The new suspended_at/suspended_reason columns, database_suspension_chk, and database_suspended_at_idx are added only inside the write-once CREATE TABLE (table.sql:28-50) and the regenerated base install script, with no ALTER TABLE migration, no version bump, and no extension upgrade script in sql/. Installations where metaschema_public.database already exists never receive the new columns, so the suspension feature silently does nothing there while fresh installs get it.

📋 Prompt for AI Agents

In packages/metaschema-schema, add a migration path for existing installs: bump the extension version (package.json, metaschema-schema.control, and the sql/ artifact name) and add an upgrade script (e.g. metaschema-schema--0.44.0--0.45.0.sql) that runs ALTER TABLE metaschema_public.database ADD COLUMN suspended_at timestamptz, ADD COLUMN suspended_reason text;, adds the database_suspension_chk CHECK constraint, and creates database_suspended_at_idx, so databases where the table already exists receive the same columns/constraint/index as fresh installs.


created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),

Expand All @@ -27,11 +37,20 @@ CREATE TABLE metaschema_public.database (
ALTER TABLE metaschema_public.database
ADD CONSTRAINT db_namechk CHECK (char_length(name) > 2);

ALTER TABLE metaschema_public.database
ADD CONSTRAINT database_suspension_chk CHECK (
(suspended_at IS NULL) = (suspended_reason IS NULL)
AND (suspended_reason IS NULL OR suspended_reason IN ('billing', 'admin'))
);

CREATE UNIQUE INDEX databases_database_platform_singleton_idx
ON metaschema_public.database (platform)
WHERE platform;
CREATE INDEX database_owner_id_idx ON metaschema_public.database ( owner_id );
CREATE INDEX database_suspended_at_idx ON metaschema_public.database ( suspended_at );

COMMENT ON COLUMN metaschema_public.database.schema_hash IS '@behavior -*';
COMMENT ON COLUMN metaschema_public.database.suspended_at IS '@behavior -insert -update';
COMMENT ON COLUMN metaschema_public.database.suspended_reason IS '@behavior -insert -update';
Comment on lines +53 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 security · medium

Suspended columns lack promised guard trigger

The comment at table.sql:24 promises a guard trigger restricting writes to the system role/platform admin, but no trigger or RLS policy on metaschema_public.database exists anywhere in metaschema-schema or metaschema-modules. Only the PostGraphile @behavior -insert -update comments (table.sql:53-54) restrict writes, and those are API-layer hints that do not constrain direct SQL, so any role with UPDATE on the table can set or clear suspension.

📋 Prompt for AI Agents

In packages/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql, the new suspended_at and suspended_reason columns (lines 28-29) are documented as system-controlled with a guard trigger, but no such trigger exists. Add a BEFORE INSERT OR UPDATE trigger function in metaschema_private that raises an exception unless the current role is the system role or a designated platform-admin role, and attach it to metaschema_public.database guarding the suspended_at/suspended_reason columns, so the invariant is enforced at the database level rather than only by the PostGraphile @behavior -insert -update comment.


COMMIT;
Binary file not shown.
16 changes: 16 additions & 0 deletions packages/metaschema-schema/sql/metaschema-schema--0.44.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ CREATE TABLE metaschema_public.database (
label text,
hash uuid,
platform boolean NOT NULL DEFAULT false,
suspended_at timestamptz,
suspended_reason text,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now(),
UNIQUE (schema_hash)
Expand All @@ -346,12 +348,26 @@ ALTER TABLE metaschema_public.database
ADD CONSTRAINT db_namechk
CHECK (char_length(name) > 2);

ALTER TABLE metaschema_public.database
ADD CONSTRAINT database_suspension_chk
CHECK (
(suspended_at IS NULL) = (suspended_reason IS NULL)
AND (suspended_reason IS NULL
OR suspended_reason IN ('billing', 'admin'))
);

CREATE UNIQUE INDEX databases_database_platform_singleton_idx ON metaschema_public.database (platform) WHERE platform;

CREATE INDEX database_owner_id_idx ON metaschema_public.database (owner_id);

CREATE INDEX database_suspended_at_idx ON metaschema_public.database (suspended_at);

COMMENT ON COLUMN metaschema_public.database.schema_hash IS '@behavior -*';

COMMENT ON COLUMN metaschema_public.database.suspended_at IS '@behavior -insert -update';

COMMENT ON COLUMN metaschema_public.database.suspended_reason IS '@behavior -insert -update';

CREATE TABLE metaschema_public.schema (
id uuid PRIMARY KEY DEFAULT uuidv7(),
database_id uuid NOT NULL,
Expand Down
Loading