Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
104 lines
5.3 KiB
SQL
104 lines
5.3 KiB
SQL
-- 008: groups — sub-workspace sharing scopes for connections and assets.
|
|
-- Group membership gates which connections a non-Owner/Admin can publish through.
|
|
--
|
|
-- Child tables carry a denormalised workspace_id so that:
|
|
-- * RLS is the same direct column predicate as every other tenant table
|
|
-- (no subquery policies);
|
|
-- * composite FKs pin every row to ONE workspace — a group cannot hold a
|
|
-- connection or a member from another tenant, even via the owner pool;
|
|
-- * the epoch bump can read workspace_id from the row itself. Looking it up
|
|
-- in groups would miss cascaded deletes (DELETE FROM groups -> its
|
|
-- group_members rows), because the parent row is already gone when the
|
|
-- child's AFTER trigger runs — deleting a group would silently skip the bump.
|
|
|
|
-- Composite FK targets.
|
|
ALTER TABLE connections ADD CONSTRAINT connections_id_workspace_key UNIQUE (id, workspace_id);
|
|
|
|
CREATE TABLE groups (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
name text NOT NULL CHECK (btrim(name) <> ''),
|
|
description text NOT NULL DEFAULT '',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (workspace_id, name),
|
|
UNIQUE (id, workspace_id)
|
|
);
|
|
|
|
CREATE TABLE group_members (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
group_id uuid NOT NULL,
|
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
added_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (group_id, user_id),
|
|
FOREIGN KEY (group_id, workspace_id) REFERENCES groups(id, workspace_id) ON DELETE CASCADE,
|
|
-- Only workspace members can join a group; leaving the workspace leaves its groups.
|
|
FOREIGN KEY (workspace_id, user_id) REFERENCES memberships(workspace_id, user_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE group_connections (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
group_id uuid NOT NULL,
|
|
connection_id uuid NOT NULL,
|
|
assigned_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (group_id, connection_id),
|
|
FOREIGN KEY (group_id, workspace_id) REFERENCES groups(id, workspace_id) ON DELETE CASCADE,
|
|
FOREIGN KEY (connection_id, workspace_id) REFERENCES connections(id, workspace_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE group_assets (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id uuid NOT NULL,
|
|
group_id uuid NOT NULL,
|
|
asset_id text NOT NULL CHECK (btrim(asset_id) <> ''), -- media asset storage key or id
|
|
assigned_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (group_id, asset_id),
|
|
FOREIGN KEY (group_id, workspace_id) REFERENCES groups(id, workspace_id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX group_members_user ON group_members (workspace_id, user_id);
|
|
CREATE INDEX group_connections_connection ON group_connections (connection_id);
|
|
|
|
CREATE TRIGGER trg_group_ws_immutable BEFORE UPDATE ON groups
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_group_member_ws_immutable BEFORE UPDATE ON group_members
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_group_connection_ws_immutable BEFORE UPDATE ON group_connections
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
CREATE TRIGGER trg_group_asset_ws_immutable BEFORE UPDATE ON group_assets
|
|
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
|
|
|
|
-- Continuous authz: group membership is an authorization input (it gates
|
|
-- publishing), so changes bump the workspace epoch exactly like memberships.
|
|
-- Reuses bump_authz_epoch() (001/005): it reads the row's own workspace_id,
|
|
-- which also covers cascades from DELETE FROM groups / memberships.
|
|
CREATE TRIGGER trg_group_member_bump_epoch AFTER INSERT OR UPDATE OR DELETE ON group_members
|
|
FOR EACH ROW EXECUTE FUNCTION bump_authz_epoch();
|
|
|
|
ALTER TABLE groups ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_members ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_connections ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_assets ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE groups FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_members FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_connections FORCE ROW LEVEL SECURITY;
|
|
ALTER TABLE group_assets FORCE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation ON groups
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON group_members
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON group_connections
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
CREATE POLICY tenant_isolation ON group_assets
|
|
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
|
|
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON groups, group_members, group_connections, group_assets TO sp_app;
|