feat: add account roles audit and task authorization

This commit is contained in:
inman committed 2026-09-01 19:14:06 +08:00
1 parent 337aaf7c88
commit 191c1a1aad
15 files changed
+4961 -457

No files matched your search

@@ -0,0 +1,100 @@
-- Fixed-scope account roles, audit attribution, and reversible task archive.
-- Existing accounts remain administrators; no organization/tenant UI is added.
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_role_check;
ALTER TABLE users
ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'user'));
ALTER TABLE users
ADD COLUMN IF NOT EXISTS must_change_password boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS password_changed_at timestamptz NOT NULL DEFAULT now();
-- User-supplied idempotency keys are account-scoped. System/AgentBus keys
-- remain deployment-scoped through the NULL actor partial index.
ALTER TABLE tasks
DROP CONSTRAINT IF EXISTS tasks_organization_id_idempotency_key_key;
ALTER TABLE idempotency_keys
DROP CONSTRAINT IF EXISTS idempotency_keys_organization_id_scope_idempotency_key_key;
ALTER TABLE idempotency_keys
ADD COLUMN IF NOT EXISTS actor_user_id uuid REFERENCES users(id);
UPDATE idempotency_keys i
SET actor_user_id = t.created_by
FROM tasks t
WHERE i.task_id = t.id
AND i.actor_user_id IS NULL
AND t.source = 'manual';
CREATE UNIQUE INDEX IF NOT EXISTS idempotency_keys_actor_unique_idx
ON idempotency_keys (organization_id, scope, idempotency_key, actor_user_id)
WHERE actor_user_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idempotency_keys_system_unique_idx
ON idempotency_keys (organization_id, scope, idempotency_key)
WHERE actor_user_id IS NULL;
ALTER TABLE agent_session_messages
ADD COLUMN IF NOT EXISTS actor_user_id uuid REFERENCES users(id),
ADD COLUMN IF NOT EXISTS input_source text;
ALTER TABLE agent_session_messages
DROP CONSTRAINT IF EXISTS agent_session_messages_input_source_check;
ALTER TABLE agent_session_messages
ADD CONSTRAINT agent_session_messages_input_source_check
CHECK (input_source IS NULL OR input_source IN ('manual', 'agentbus', 'reparse', 'system'));
-- Only the initial input has a provable historical platform actor. Do not
-- invent actors for later turns that predate explicit attribution.
UPDATE agent_session_messages m
SET actor_user_id = t.created_by,
input_source = CASE WHEN t.source = 'agentbus' THEN 'agentbus' ELSE 'manual' END
FROM tasks t
WHERE m.task_id = t.id
AND m.role = 'user'
AND m.turn_no = 1
AND (m.actor_user_id IS NULL OR m.input_source IS NULL);
ALTER TABLE task_input_attachments
ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES users(id);
UPDATE task_input_attachments a
SET created_by = t.created_by
FROM tasks t
WHERE a.task_id = t.id
AND a.created_by IS NULL
AND a.source = 'manual';
ALTER TABLE tasks
ADD COLUMN IF NOT EXISTS archived_at timestamptz,
ADD COLUMN IF NOT EXISTS archived_by uuid REFERENCES users(id),
ADD COLUMN IF NOT EXISTS archive_reason text;
ALTER TABLE tasks
DROP CONSTRAINT IF EXISTS tasks_archive_reason_length_check;
ALTER TABLE tasks
ADD CONSTRAINT tasks_archive_reason_length_check
CHECK (archive_reason IS NULL OR char_length(archive_reason) <= 500);
CREATE INDEX IF NOT EXISTS users_org_role_active_idx
ON users (organization_id, role, is_active, username);
CREATE INDEX IF NOT EXISTS tasks_owner_visible_created_idx
ON tasks (organization_id, created_by, created_at DESC, id DESC)
WHERE archived_at IS NULL;
CREATE INDEX IF NOT EXISTS tasks_archive_created_idx
ON tasks (organization_id, archived_at DESC, created_at DESC, id DESC)
WHERE archived_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS agent_session_messages_actor_idx
ON agent_session_messages (organization_id, actor_user_id, created_at DESC)
WHERE actor_user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS audit_events_actor_created_idx
ON audit_events (organization_id, actor_user_id, created_at DESC);
@@ -0,0 +1,24 @@
-- Add the fixed-scope team-lead role and read-only operations-dashboard indexes.
-- This remains a single-organization deployment; no tenant selector is added.
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_role_check;
ALTER TABLE users
ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'team_lead', 'user'));
CREATE INDEX IF NOT EXISTS tasks_operations_dashboard_created_idx
ON tasks (organization_id, created_at DESC, id DESC)
WHERE source = 'manual';
CREATE INDEX IF NOT EXISTS tasks_operations_dashboard_actor_idx
ON tasks (organization_id, created_by, created_at DESC, id DESC)
WHERE source = 'manual';
CREATE INDEX IF NOT EXISTS tasks_operations_dashboard_status_idx
ON tasks (organization_id, status, created_at DESC, id DESC)
WHERE source = 'manual';
CREATE INDEX IF NOT EXISTS tasks_operations_dashboard_business_idx
ON tasks (organization_id, business_route_id, created_at DESC, id DESC)
WHERE source = 'manual';
@@ -0,0 +1,68 @@
-- Administrator-managed allowlists for the 18 registered manual business routes.
-- Administrators always retain every route; team leads and ordinary users are
-- denied by default until an administrator grants explicit route IDs.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS business_authorization_revision integer NOT NULL DEFAULT 0;
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_business_authorization_revision_check;
ALTER TABLE users
ADD CONSTRAINT users_business_authorization_revision_check
CHECK (business_authorization_revision >= 0);
CREATE UNIQUE INDEX IF NOT EXISTS users_organization_id_id_authorization_idx
ON users (organization_id, id);
CREATE TABLE IF NOT EXISTS user_business_route_authorizations (
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
route_id text NOT NULL,
granted_by uuid REFERENCES users(id),
granted_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (organization_id, user_id, route_id),
CONSTRAINT user_business_route_authorizations_route_check CHECK (route_id IN (
'team_order_create',
'team_order_batch_create',
'shared_plan_create',
'shared_child_order_create',
'passenger_list_import_independent',
'passenger_list_import_shared_child',
'arrangement_guide_create',
'arrangement_vehicle_create',
'arrangement_hotel_create',
'arrangement_transport_create',
'arrangement_other_create',
'order_update_shared_plan',
'order_update_shared_child',
'order_update_independent',
'arrangement_hotel_update',
'order_cancel',
'order_restore',
'confirmation_export'
))
);
CREATE INDEX IF NOT EXISTS user_business_route_authorizations_user_idx
ON user_business_route_authorizations (organization_id, user_id, route_id);
CREATE INDEX IF NOT EXISTS user_business_route_authorizations_route_idx
ON user_business_route_authorizations (organization_id, route_id, user_id);
ALTER TABLE user_business_route_authorizations
DROP CONSTRAINT IF EXISTS user_business_route_authorizations_user_scope_fkey;
ALTER TABLE user_business_route_authorizations
ADD CONSTRAINT user_business_route_authorizations_user_scope_fkey
FOREIGN KEY (organization_id, user_id)
REFERENCES users (organization_id, id)
ON DELETE CASCADE;
ALTER TABLE user_business_route_authorizations
DROP CONSTRAINT IF EXISTS user_business_route_authorizations_granter_scope_fkey;
ALTER TABLE user_business_route_authorizations
ADD CONSTRAINT user_business_route_authorizations_granter_scope_fkey
FOREIGN KEY (organization_id, granted_by)
REFERENCES users (organization_id, id);