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.
585 lines
22 KiB
PL/PgSQL
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;
|