Files
UmbraAudio/infra/postgres/001_control_plane.sql
B.D. Murphy 0c9bea72e0 Add enterprise/infra tooling and repoint Qwen3-TTS submodule to internal fork
Two groups of changes, committed together as the follow-up to the initial push:

1. .gitmodules — submodule Qwen3-TTS now points at the internal Gitea fork
   (ssh://git@mini-eq:2222/umbragit/Qwen3-TTS.git) instead of upstream GitHub,
   so a clone actually gets the UmbraAudio branding/integration commit
   012e452 rather than vanilla upstream.

2. New infra/ tooling for the cross-machine work in spec 21: agent_bridge,
   remote_credential_broker (Keychain-backed, secrets read at runtime and
   generated via read_secret/random_secret — no hardcoded credentials), the
   enterprise bootstrap scripts, and the oldbox SSH/auth prompts. Plus
   backup/ restore scripts, two specs (18 UmbraTrans, 19 enterprise control
   plane), umbraaudio/voices/lmstudio_gate.py, and in-flight edits to the
   discovery/attribution/profiling modules, config, chapter_builder, emotion,
   pronunciation, qwen_tts and routes.

Also adds *.db to .gitignore: SQLite project databases are runtime state and no
.db file was tracked before.

Verified before staging: .env's only change is UMBRA_TTS__QWEN_TTS_HELPER_URL and
the committed copy has zero secret-looking values; the broker ships only
config.example.json.
2026-10-06 06:57:41 -07:00

585 lines
22 KiB
PL/PgSQL

BEGIN;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE SCHEMA IF NOT EXISTS app;
CREATE TABLE IF NOT EXISTS schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS organizations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
slug citext NOT NULL UNIQUE,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended', 'archived')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
display_name text,
auth_provider text NOT NULL DEFAULT 'bootstrap',
auth_subject text,
status text NOT NULL DEFAULT 'invited'
CHECK (status IN ('invited', 'active', 'suspended', 'deactivated', 'locked')),
mfa_required boolean NOT NULL DEFAULT false,
last_login_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS users_auth_subject_idx
ON users (auth_provider, auth_subject)
WHERE auth_subject IS NOT NULL;
CREATE TABLE IF NOT EXISTS projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id),
name text NOT NULL,
slug citext NOT NULL,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'on_hold', 'released', 'archived')),
created_by uuid REFERENCES users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (organization_id, slug),
UNIQUE (id, organization_id)
);
CREATE TABLE IF NOT EXISTS roles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
scope text NOT NULL CHECK (scope IN ('platform', 'organization', 'project')),
description text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (scope, name)
);
CREATE TABLE IF NOT EXISTS permissions (
key text PRIMARY KEY,
description text NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS role_permissions (
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_key text NOT NULL REFERENCES permissions(key) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_key)
);
CREATE TABLE IF NOT EXISTS user_roles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
scope text NOT NULL CHECK (scope IN ('platform', 'organization', 'project')),
organization_id uuid REFERENCES organizations(id) ON DELETE CASCADE,
project_id uuid,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended', 'revoked')),
created_at timestamptz NOT NULL DEFAULT now(),
created_by uuid REFERENCES users(id),
CHECK (
(scope = 'platform' AND organization_id IS NULL AND project_id IS NULL)
OR (scope = 'organization' AND organization_id IS NOT NULL AND project_id IS NULL)
OR (scope = 'project' AND organization_id IS NOT NULL AND project_id IS NOT NULL)
),
FOREIGN KEY (project_id, organization_id)
REFERENCES projects(id, organization_id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS user_roles_platform_unique
ON user_roles (user_id, role_id)
WHERE scope = 'platform';
CREATE UNIQUE INDEX IF NOT EXISTS user_roles_organization_unique
ON user_roles (user_id, role_id, organization_id)
WHERE scope = 'organization';
CREATE UNIQUE INDEX IF NOT EXISTS user_roles_project_unique
ON user_roles (user_id, role_id, project_id)
WHERE scope = 'project';
CREATE TABLE IF NOT EXISTS invitations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id uuid,
email citext NOT NULL,
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
token_hash text NOT NULL UNIQUE,
invited_by uuid NOT NULL REFERENCES users(id),
expires_at timestamptz NOT NULL,
accepted_at timestamptz,
revoked_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (project_id, organization_id)
REFERENCES projects(id, organization_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
revoked_at timestamptz,
ip_address inet,
user_agent text
);
CREATE TABLE IF NOT EXISTS mfa_methods (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
method text NOT NULL CHECK (method IN ('totp', 'webauthn', 'recovery_code')),
secret_reference text,
verified_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
last_used_at timestamptz,
revoked_at timestamptz
);
CREATE TABLE IF NOT EXISTS bootstrap_tokens (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash text NOT NULL UNIQUE,
expires_at timestamptz NOT NULL,
consumed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS audit_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
actor_user_id uuid REFERENCES users(id),
organization_id uuid REFERENCES organizations(id),
project_id uuid,
action text NOT NULL,
target_type text,
target_id text,
request_id text,
ip_address inet,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS projects_organization_idx ON projects (organization_id);
CREATE INDEX IF NOT EXISTS user_roles_user_idx ON user_roles (user_id, status);
CREATE INDEX IF NOT EXISTS user_roles_organization_idx ON user_roles (organization_id, status);
CREATE INDEX IF NOT EXISTS user_roles_project_idx ON user_roles (project_id, status);
CREATE INDEX IF NOT EXISTS invitations_email_idx ON invitations (email);
CREATE INDEX IF NOT EXISTS audit_events_org_created_idx ON audit_events (organization_id, created_at DESC);
CREATE INDEX IF NOT EXISTS audit_events_project_created_idx ON audit_events (project_id, created_at DESC);
CREATE OR REPLACE FUNCTION app.validate_user_role_scope()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
role_scope text;
BEGIN
SELECT scope INTO role_scope FROM roles WHERE id = NEW.role_id;
IF role_scope IS NULL OR role_scope <> NEW.scope THEN
RAISE EXCEPTION 'role scope does not match assignment scope';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS user_roles_validate_scope ON user_roles;
CREATE TRIGGER user_roles_validate_scope
BEFORE INSERT OR UPDATE OF role_id, scope ON user_roles
FOR EACH ROW EXECUTE FUNCTION app.validate_user_role_scope();
CREATE OR REPLACE FUNCTION app.set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS organizations_set_updated_at ON organizations;
CREATE TRIGGER organizations_set_updated_at
BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION app.set_updated_at();
DROP TRIGGER IF EXISTS users_set_updated_at ON users;
CREATE TRIGGER users_set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION app.set_updated_at();
DROP TRIGGER IF EXISTS projects_set_updated_at ON projects;
CREATE TRIGGER projects_set_updated_at
BEFORE UPDATE ON projects
FOR EACH ROW EXECUTE FUNCTION app.set_updated_at();
CREATE OR REPLACE FUNCTION app.current_user_id()
RETURNS uuid
LANGUAGE sql
STABLE
AS $$
SELECT NULLIF(current_setting('app.user_id', true), '')::uuid
$$;
CREATE OR REPLACE FUNCTION app.is_platform_admin()
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT EXISTS (
SELECT 1
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = app.current_user_id()
AND ur.scope = 'platform'
AND ur.status = 'active'
AND r.name = 'platform_admin'
)
$$;
CREATE OR REPLACE FUNCTION app.is_organization_member(target_organization_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT EXISTS (
SELECT 1
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = app.current_user_id()
AND ur.organization_id = target_organization_id
AND ur.scope = 'organization'
AND ur.status = 'active'
AND r.name IN ('organization_admin', 'member')
)
$$;
CREATE OR REPLACE FUNCTION app.is_organization_admin(target_organization_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT EXISTS (
SELECT 1
FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = app.current_user_id()
AND ur.organization_id = target_organization_id
AND ur.scope = 'organization'
AND ur.status = 'active'
AND r.name = 'organization_admin'
)
$$;
CREATE OR REPLACE FUNCTION app.is_organization_admin_for_project(target_project_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT app.is_platform_admin()
OR EXISTS (
SELECT 1
FROM projects p
WHERE p.id = target_project_id
AND app.is_organization_admin(p.organization_id)
)
$$;
CREATE OR REPLACE FUNCTION app.can_access_project(target_project_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT app.is_platform_admin()
OR EXISTS (
SELECT 1
FROM projects p
WHERE p.id = target_project_id
AND app.is_organization_admin(p.organization_id)
)
OR EXISTS (
SELECT 1
FROM user_roles ur
WHERE ur.user_id = app.current_user_id()
AND ur.project_id = target_project_id
AND ur.scope = 'project'
AND ur.status = 'active'
)
$$;
CREATE OR REPLACE FUNCTION app.can_view_user(target_user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT target_user_id = app.current_user_id()
OR app.is_platform_admin()
OR EXISTS (
SELECT 1
FROM user_roles mine
JOIN user_roles theirs
ON theirs.user_id = target_user_id
AND theirs.organization_id = mine.organization_id
AND theirs.scope = 'organization'
AND theirs.status = 'active'
JOIN roles r ON r.id = mine.role_id
WHERE mine.user_id = app.current_user_id()
AND mine.scope = 'organization'
AND mine.status = 'active'
AND r.name = 'organization_admin'
)
$$;
CREATE OR REPLACE FUNCTION app.prevent_audit_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'audit_events is append-only';
END;
$$;
DROP TRIGGER IF EXISTS audit_events_append_only ON audit_events;
CREATE TRIGGER audit_events_append_only
BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION app.prevent_audit_mutation();
INSERT INTO schema_migrations(version) VALUES ('001_control_plane')
ON CONFLICT (version) DO NOTHING;
INSERT INTO roles (name, scope, description) VALUES
('platform_admin', 'platform', 'Global control-plane administration'),
('organization_admin', 'organization', 'Administration within one organization'),
('member', 'organization', 'Read-only organization membership'),
('project_owner', 'project', 'Project administration'),
('project_manager', 'project', 'Project production management'),
('translator', 'project', 'Translation work'),
('proofreader', 'project', 'Translation review'),
('audio_director', 'project', 'Voice and audio approval'),
('voice_manager', 'organization', 'Voice library and licensing management'),
('release_manager', 'project', 'Release approval'),
('viewer', 'project', 'Read-only project access')
ON CONFLICT (scope, name) DO UPDATE SET description = EXCLUDED.description;
INSERT INTO permissions (key, description) VALUES
('platform.manage', 'Manage platform-level resources'),
('organization.manage', 'Manage organization settings'),
('organization.users.manage', 'Invite and manage organization users'),
('organization.projects.manage', 'Create and manage projects'),
('project.access.manage', 'Assign project access'),
('project.source.edit', 'Edit approved source material'),
('project.translation.edit', 'Edit target translations'),
('project.translation.review', 'Review target translations'),
('project.audio.approve', 'Approve audio output'),
('project.release.approve', 'Approve releases'),
('voice.catalog.manage', 'Manage voice catalog and licensing'),
('artifact.download', 'Download authorized artifacts')
ON CONFLICT (key) DO UPDATE SET description = EXCLUDED.description;
INSERT INTO role_permissions (role_id, permission_key)
SELECT r.id, p.key
FROM roles r
JOIN (
VALUES
('platform_admin', 'platform.manage'),
('organization_admin', 'organization.manage'),
('organization_admin', 'organization.users.manage'),
('organization_admin', 'organization.projects.manage'),
('organization_admin', 'project.access.manage'),
('member', 'artifact.download'),
('project_owner', 'project.access.manage'),
('project_owner', 'project.release.approve'),
('project_manager', 'project.translation.edit'),
('project_manager', 'project.audio.approve'),
('translator', 'project.translation.edit'),
('proofreader', 'project.translation.review'),
('audio_director', 'project.audio.approve'),
('voice_manager', 'voice.catalog.manage'),
('release_manager', 'project.release.approve'),
('viewer', 'artifact.download')
) AS grants(role_name, permission_key)
ON grants.role_name = r.name
JOIN permissions p ON p.key = grants.permission_key
ON CONFLICT DO NOTHING;
ALTER TABLE roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE permissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE role_permissions ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS roles_read ON roles;
CREATE POLICY roles_read ON roles
FOR SELECT USING (app.current_user_id() IS NOT NULL OR app.is_platform_admin());
DROP POLICY IF EXISTS roles_manage ON roles;
CREATE POLICY roles_manage ON roles
FOR ALL USING (app.is_platform_admin())
WITH CHECK (app.is_platform_admin());
DROP POLICY IF EXISTS permissions_read ON permissions;
CREATE POLICY permissions_read ON permissions
FOR SELECT USING (app.current_user_id() IS NOT NULL OR app.is_platform_admin());
DROP POLICY IF EXISTS permissions_manage ON permissions;
CREATE POLICY permissions_manage ON permissions
FOR ALL USING (app.is_platform_admin())
WITH CHECK (app.is_platform_admin());
DROP POLICY IF EXISTS role_permissions_read ON role_permissions;
CREATE POLICY role_permissions_read ON role_permissions
FOR SELECT USING (app.current_user_id() IS NOT NULL OR app.is_platform_admin());
DROP POLICY IF EXISTS role_permissions_manage ON role_permissions;
CREATE POLICY role_permissions_manage ON role_permissions
FOR ALL USING (app.is_platform_admin())
WITH CHECK (app.is_platform_admin());
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE mfa_methods ENABLE ROW LEVEL SECURITY;
ALTER TABLE bootstrap_tokens ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_events ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS organizations_select ON organizations;
CREATE POLICY organizations_select ON organizations
FOR SELECT USING (app.is_platform_admin() OR app.is_organization_member(id));
DROP POLICY IF EXISTS organizations_insert ON organizations;
CREATE POLICY organizations_insert ON organizations
FOR INSERT WITH CHECK (app.is_platform_admin());
DROP POLICY IF EXISTS organizations_update ON organizations;
CREATE POLICY organizations_update ON organizations
FOR UPDATE USING (app.is_platform_admin() OR app.is_organization_admin(id))
WITH CHECK (app.is_platform_admin() OR app.is_organization_admin(id));
DROP POLICY IF EXISTS organizations_delete ON organizations;
CREATE POLICY organizations_delete ON organizations
FOR DELETE USING (app.is_platform_admin());
DROP POLICY IF EXISTS projects_select ON projects;
CREATE POLICY projects_select ON projects
FOR SELECT USING (app.can_access_project(id));
DROP POLICY IF EXISTS projects_insert ON projects;
CREATE POLICY projects_insert ON projects
FOR INSERT WITH CHECK (app.is_platform_admin() OR app.is_organization_admin(organization_id));
DROP POLICY IF EXISTS projects_update ON projects;
CREATE POLICY projects_update ON projects
FOR UPDATE USING (app.is_platform_admin() OR app.is_organization_admin(organization_id))
WITH CHECK (app.is_platform_admin() OR app.is_organization_admin(organization_id));
DROP POLICY IF EXISTS projects_delete ON projects;
CREATE POLICY projects_delete ON projects
FOR DELETE USING (app.is_platform_admin() OR app.is_organization_admin(organization_id));
DROP POLICY IF EXISTS users_select ON users;
CREATE POLICY users_select ON users
FOR SELECT USING (app.can_view_user(id));
DROP POLICY IF EXISTS users_manage ON users;
CREATE POLICY users_manage ON users
FOR ALL USING (app.is_platform_admin())
WITH CHECK (app.is_platform_admin());
DROP POLICY IF EXISTS user_roles_select ON user_roles;
CREATE POLICY user_roles_select ON user_roles
FOR SELECT USING (
app.is_platform_admin()
OR user_id = app.current_user_id()
OR (scope = 'organization' AND app.is_organization_admin(organization_id))
OR (scope = 'project' AND app.is_organization_admin_for_project(project_id))
);
DROP POLICY IF EXISTS user_roles_manage ON user_roles;
CREATE POLICY user_roles_manage ON user_roles
FOR ALL USING (
app.is_platform_admin()
OR (scope = 'organization' AND app.is_organization_admin(organization_id))
OR (scope = 'project' AND app.is_organization_admin_for_project(project_id))
)
WITH CHECK (
app.is_platform_admin()
OR (scope = 'organization' AND app.is_organization_admin(organization_id))
OR (scope = 'project' AND app.is_organization_admin_for_project(project_id))
);
DROP POLICY IF EXISTS invitations_select ON invitations;
CREATE POLICY invitations_select ON invitations
FOR SELECT USING (
app.is_platform_admin()
OR app.is_organization_admin(organization_id)
OR email = (SELECT email FROM users WHERE id = app.current_user_id())
);
DROP POLICY IF EXISTS invitations_manage ON invitations;
CREATE POLICY invitations_manage ON invitations
FOR ALL USING (app.is_platform_admin() OR app.is_organization_admin(organization_id))
WITH CHECK (app.is_platform_admin() OR app.is_organization_admin(organization_id));
DROP POLICY IF EXISTS sessions_self ON sessions;
CREATE POLICY sessions_self ON sessions
FOR ALL USING (user_id = app.current_user_id())
WITH CHECK (user_id = app.current_user_id());
DROP POLICY IF EXISTS mfa_methods_self ON mfa_methods;
CREATE POLICY mfa_methods_self ON mfa_methods
FOR ALL USING (user_id = app.current_user_id())
WITH CHECK (user_id = app.current_user_id());
DROP POLICY IF EXISTS bootstrap_tokens_self ON bootstrap_tokens;
CREATE POLICY bootstrap_tokens_self ON bootstrap_tokens
FOR ALL USING (user_id = app.current_user_id())
WITH CHECK (user_id = app.current_user_id());
DROP POLICY IF EXISTS audit_events_select ON audit_events;
CREATE POLICY audit_events_select ON audit_events
FOR SELECT USING (
app.is_platform_admin()
OR app.is_organization_admin(organization_id)
);
DROP POLICY IF EXISTS audit_events_insert ON audit_events;
CREATE POLICY audit_events_insert ON audit_events
FOR INSERT WITH CHECK (
actor_user_id = app.current_user_id()
OR app.is_platform_admin()
);
REVOKE ALL ON SCHEMA app FROM PUBLIC;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public, app TO
umbracontrol_api, umbracontrol_worker, umbracontrol_readonly;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO
umbracontrol_api, umbracontrol_worker, umbracontrol_readonly;
GRANT SELECT, INSERT, UPDATE, DELETE ON
organizations, projects, users, roles, permissions, role_permissions,
user_roles, invitations, sessions, mfa_methods, bootstrap_tokens, audit_events
TO umbracontrol_api;
GRANT SELECT ON
organizations, projects, users, roles, permissions, role_permissions,
user_roles, audit_events
TO umbracontrol_worker, umbracontrol_readonly;
GRANT INSERT ON audit_events TO umbracontrol_worker;
GRANT USAGE, CREATE ON SCHEMA public TO umbracontrol_migrator;
GRANT USAGE ON SCHEMA public TO umbracontrol_backup;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO umbracontrol_backup;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO umbracontrol_migrator;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO umbracontrol_migrator;
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA app FROM PUBLIC;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA app TO
umbracontrol_api, umbracontrol_worker, umbracontrol_readonly;
COMMIT;