Mirrors the mbrian_admin pattern: the binary connects as a role bounded to the projax schema, so even a compromised projax process cannot read mai.workers, otto.*, vault.*, etc. - 0001: switch grants block from postgres → projax_admin (conditional on the role existing — bootstrap still works as superuser before the role is created). Wrap `create schema` in a guard so the migration is idempotent when re-run as a non-superuser app role that lacks database-level CREATE. - 0005_reown_to_projax_admin.sql: enumerate every projax-namespaced object via pg_namespace + pg_class / pg_proc and ALTER OWNER to projax_admin. Explicitly scoped — no global REASSIGN OWNED that would yank ownership from other projects sharing the postgres role. Strips residual postgres grants. No-ops with a NOTICE when the role is missing. - README: new "Manual prerequisite" deploy section. Documents the CREATE ROLE statement, the cross-schema USAGE + SELECT grants, AND the RLS policy `projax_read ON mai.projects` that's required because mai.projects has row-level security with policies scoped to `mai` and `anon` only. Without the policy, items_unified silently returns zero mai-source rows. - deploy/dokploy.yaml: DSN comment now reflects projax_admin and points at the README prereq. Verified locally against msupabase with a throwaway projax_admin role: - 13/13 tests green - mai.workers SELECT → permission denied - mai.sessions SELECT → permission denied - mai.projects SELECT → 59 rows (RLS policy in effect) - projax.items_unified SELECT → 66 rows (7 projax + 59 mai)
92 lines
4.0 KiB
PL/PgSQL
92 lines
4.0 KiB
PL/PgSQL
-- 0001_init.sql
|
|
-- projax schema: items + item_links
|
|
-- See docs/design.md §3
|
|
|
|
-- Bootstrap-only: a non-superuser app role (projax_admin) lacks CREATE on the
|
|
-- database itself, so `create schema if not exists` would trip a privilege
|
|
-- check even when the schema already exists. The guard here makes this
|
|
-- migration idempotent when re-run as the app role.
|
|
do $bootstrap$ begin
|
|
if not exists (select 1 from pg_namespace where nspname = 'projax') then
|
|
execute 'create schema projax';
|
|
end if;
|
|
end $bootstrap$;
|
|
|
|
create table if not exists projax.items (
|
|
id uuid primary key default gen_random_uuid(),
|
|
kind text[] not null default '{}',
|
|
title text not null,
|
|
slug text not null,
|
|
path text not null default '',
|
|
parent_id uuid references projax.items(id) on delete restrict,
|
|
content_md text not null default '',
|
|
aliases text[] not null default '{}',
|
|
metadata jsonb not null default '{}'::jsonb,
|
|
status text not null default 'active',
|
|
pinned boolean not null default false,
|
|
archived boolean not null default false,
|
|
start_time timestamptz,
|
|
end_time timestamptz,
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
deleted_at timestamptz,
|
|
constraint items_slug_no_dots check (slug !~ '\.'),
|
|
constraint items_status_valid check (status in ('active', 'done', 'archived')),
|
|
unique (parent_id, slug)
|
|
);
|
|
|
|
create index if not exists items_path_idx on projax.items (path);
|
|
create index if not exists items_kind_idx on projax.items using gin (kind);
|
|
create index if not exists items_parent_idx on projax.items (parent_id);
|
|
create index if not exists items_status_idx on projax.items (status) where deleted_at is null;
|
|
create index if not exists items_aliases_idx on projax.items using gin (aliases);
|
|
|
|
-- Partial uniqueness for root-level slugs (parent_id is null) — PG treats null != null in unique
|
|
create unique index if not exists items_root_slug_uniq
|
|
on projax.items (slug) where parent_id is null;
|
|
|
|
create table if not exists projax.item_links (
|
|
id uuid primary key default gen_random_uuid(),
|
|
item_id uuid not null references projax.items(id) on delete cascade,
|
|
ref_type text not null,
|
|
ref_id text not null,
|
|
rel text not null default 'contains',
|
|
note text,
|
|
metadata jsonb not null default '{}'::jsonb,
|
|
created_at timestamptz not null default now(),
|
|
unique (item_id, ref_type, ref_id, rel)
|
|
);
|
|
|
|
create index if not exists item_links_item_idx on projax.item_links (item_id);
|
|
create index if not exists item_links_ref_idx on projax.item_links (ref_type, ref_id);
|
|
|
|
-- updated_at auto-touch
|
|
create or replace function projax.set_updated_at() returns trigger
|
|
language plpgsql as $$
|
|
begin
|
|
new.updated_at := now();
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
drop trigger if exists items_set_updated_at on projax.items;
|
|
create trigger items_set_updated_at
|
|
before update on projax.items
|
|
for each row execute function projax.set_updated_at();
|
|
|
|
-- Grants: the projax service connects as the dedicated `projax_admin` role
|
|
-- (see migration 0005 + README §0). If the role does not exist yet on this
|
|
-- database, this block no-ops; first apply happens before the role exists
|
|
-- and a privileged operator runs the migrations once to bootstrap.
|
|
do $$ begin
|
|
if exists (select 1 from pg_roles where rolname = 'projax_admin') then
|
|
execute 'grant usage on schema projax to projax_admin';
|
|
execute 'grant all on all tables in schema projax to projax_admin';
|
|
execute 'grant all on all sequences in schema projax to projax_admin';
|
|
execute 'grant all on all functions in schema projax to projax_admin';
|
|
execute 'alter default privileges in schema projax grant all on tables to projax_admin';
|
|
execute 'alter default privileges in schema projax grant all on sequences to projax_admin';
|
|
execute 'alter default privileges in schema projax grant all on functions to projax_admin';
|
|
end if;
|
|
end $$;
|