-- Migration 018: RBAC Permissions System -- -- Implements role-based access control for API Gateway -- Supports hot-reload via PostgreSQL NOTIFY/LISTEN -- -- Created: Wave 70 Agent 7 -- Performance: Sub-100ns cached permission checks -- ============================================================================ -- RBAC Schema -- ============================================================================ -- Roles table: Define system roles CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) UNIQUE NOT NULL, description TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); -- Permissions table: Define endpoint permissions CREATE TABLE IF NOT EXISTS permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint VARCHAR(255) UNIQUE NOT NULL, -- e.g., "trading.submit_order" description TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); -- Role-Permission mapping: Many-to-many relationship CREATE TABLE IF NOT EXISTS role_permissions ( role_id UUID REFERENCES roles(id) ON DELETE CASCADE, permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY (role_id, permission_id) ); -- User-Role mapping: Many-to-many relationship -- Note: Assumes 'users' table exists from previous migrations CREATE TABLE IF NOT EXISTS user_roles ( user_id UUID REFERENCES users(id) ON DELETE CASCADE, role_id UUID REFERENCES roles(id) ON DELETE CASCADE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, role_id) ); -- ============================================================================ -- Performance Indexes -- ============================================================================ -- Critical path: Fast user permission lookups CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles(role_id); -- Fast role permission lookups CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON role_permissions(role_id); CREATE INDEX IF NOT EXISTS idx_role_permissions_permission_id ON role_permissions(permission_id); -- Fast permission endpoint lookups CREATE INDEX IF NOT EXISTS idx_permissions_endpoint ON permissions(endpoint); -- Fast role name lookups CREATE INDEX IF NOT EXISTS idx_roles_name ON roles(name); -- ============================================================================ -- Default Roles -- ============================================================================ INSERT INTO roles (name, description) VALUES ('admin', 'Full system access - all permissions granted') ON CONFLICT (name) DO NOTHING; INSERT INTO roles (name, description) VALUES ('trader', 'Trading operations - submit/cancel orders, view positions') ON CONFLICT (name) DO NOTHING; INSERT INTO roles (name, description) VALUES ('analyst', 'Read-only access - view data and reports') ON CONFLICT (name) DO NOTHING; INSERT INTO roles (name, description) VALUES ('risk_manager', 'Risk management - view/update risk limits, circuit breakers') ON CONFLICT (name) DO NOTHING; INSERT INTO roles (name, description) VALUES ('developer', 'Development access - backtesting, ML training, configuration') ON CONFLICT (name) DO NOTHING; -- ============================================================================ -- Default Permissions -- ============================================================================ -- Trading Service Permissions INSERT INTO permissions (endpoint, description) VALUES ('trading.submit_order', 'Submit trading orders to market') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('trading.cancel_order', 'Cancel pending orders') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('trading.view_positions', 'View current positions') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('trading.view_orders', 'View order history') ON CONFLICT (endpoint) DO NOTHING; -- Configuration Permissions INSERT INTO permissions (endpoint, description) VALUES ('config.update', 'Update system configuration') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('config.view', 'View system configuration') ON CONFLICT (endpoint) DO NOTHING; -- Backtesting Permissions INSERT INTO permissions (endpoint, description) VALUES ('backtesting.run', 'Run strategy backtests') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('backtesting.view_results', 'View backtest results') ON CONFLICT (endpoint) DO NOTHING; -- ML Training Permissions INSERT INTO permissions (endpoint, description) VALUES ('ml.train_model', 'Train ML models') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('ml.deploy_model', 'Deploy trained models to production') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('ml.view_metrics', 'View ML model metrics') ON CONFLICT (endpoint) DO NOTHING; -- Risk Management Permissions INSERT INTO permissions (endpoint, description) VALUES ('risk.update_limits', 'Update risk limits and thresholds') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('risk.view_metrics', 'View risk metrics and VaR') ON CONFLICT (endpoint) DO NOTHING; INSERT INTO permissions (endpoint, description) VALUES ('risk.circuit_breaker', 'Activate circuit breaker / kill switch') ON CONFLICT (endpoint) DO NOTHING; -- ============================================================================ -- Default Role-Permission Mappings -- ============================================================================ -- Admin: All permissions INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r CROSS JOIN permissions p WHERE r.name = 'admin' ON CONFLICT (role_id, permission_id) DO NOTHING; -- Trader: Trading operations INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'trader' AND p.endpoint IN ( 'trading.submit_order', 'trading.cancel_order', 'trading.view_positions', 'trading.view_orders', 'config.view', 'backtesting.view_results' ) ON CONFLICT (role_id, permission_id) DO NOTHING; -- Analyst: Read-only access INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'analyst' AND p.endpoint IN ( 'trading.view_positions', 'trading.view_orders', 'config.view', 'backtesting.view_results', 'ml.view_metrics', 'risk.view_metrics' ) ON CONFLICT (role_id, permission_id) DO NOTHING; -- Risk Manager: Risk operations INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'risk_manager' AND p.endpoint IN ( 'risk.update_limits', 'risk.view_metrics', 'risk.circuit_breaker', 'trading.view_positions', 'trading.view_orders', 'config.view' ) ON CONFLICT (role_id, permission_id) DO NOTHING; -- Developer: Development access INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'developer' AND p.endpoint IN ( 'backtesting.run', 'backtesting.view_results', 'ml.train_model', 'ml.deploy_model', 'ml.view_metrics', 'config.update', 'config.view' ) ON CONFLICT (role_id, permission_id) DO NOTHING; -- ============================================================================ -- Hot-Reload Support: PostgreSQL NOTIFY/LISTEN -- ============================================================================ -- Function to notify on permission changes CREATE OR REPLACE FUNCTION notify_permission_change() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('permission_changes', json_build_object( 'table', TG_TABLE_NAME, 'operation', TG_OP, 'timestamp', NOW() )::text); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers for permission change notifications DROP TRIGGER IF EXISTS trigger_role_permissions_change ON role_permissions; CREATE TRIGGER trigger_role_permissions_change AFTER INSERT OR UPDATE OR DELETE ON role_permissions FOR EACH STATEMENT EXECUTE FUNCTION notify_permission_change(); DROP TRIGGER IF EXISTS trigger_user_roles_change ON user_roles; CREATE TRIGGER trigger_user_roles_change AFTER INSERT OR UPDATE OR DELETE ON user_roles FOR EACH STATEMENT EXECUTE FUNCTION notify_permission_change(); DROP TRIGGER IF EXISTS trigger_permissions_change ON permissions; CREATE TRIGGER trigger_permissions_change AFTER INSERT OR UPDATE OR DELETE ON permissions FOR EACH STATEMENT EXECUTE FUNCTION notify_permission_change(); DROP TRIGGER IF EXISTS trigger_roles_change ON roles; CREATE TRIGGER trigger_roles_change AFTER INSERT OR UPDATE OR DELETE ON roles FOR EACH STATEMENT EXECUTE FUNCTION notify_permission_change(); -- ============================================================================ -- Updated_at Triggers -- ============================================================================ CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trigger_roles_updated_at ON roles; CREATE TRIGGER trigger_roles_updated_at BEFORE UPDATE ON roles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS trigger_permissions_updated_at ON permissions; CREATE TRIGGER trigger_permissions_updated_at BEFORE UPDATE ON permissions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Utility Views -- ============================================================================ -- View: User permissions (flattened for easy querying) CREATE OR REPLACE VIEW user_permissions_view AS SELECT u.id AS user_id, u.username, r.name AS role_name, p.endpoint AS permission, p.description AS permission_description FROM users u JOIN user_roles ur ON u.id = ur.user_id JOIN roles r ON ur.role_id = r.id JOIN role_permissions rp ON r.id = rp.role_id JOIN permissions p ON rp.permission_id = p.id ORDER BY u.username, r.name, p.endpoint; -- View: Role permission counts CREATE OR REPLACE VIEW role_permission_counts AS SELECT r.name AS role_name, r.description AS role_description, COUNT(rp.permission_id) AS permission_count FROM roles r LEFT JOIN role_permissions rp ON r.id = rp.role_id GROUP BY r.id, r.name, r.description ORDER BY permission_count DESC, r.name; -- ============================================================================ -- Comments for Documentation -- ============================================================================ COMMENT ON TABLE roles IS 'System roles for RBAC'; COMMENT ON TABLE permissions IS 'Endpoint permissions for API Gateway'; COMMENT ON TABLE role_permissions IS 'Role-permission mappings (many-to-many)'; COMMENT ON TABLE user_roles IS 'User-role assignments (many-to-many)'; COMMENT ON COLUMN permissions.endpoint IS 'Endpoint identifier (e.g., trading.submit_order)'; COMMENT ON FUNCTION notify_permission_change() IS 'Triggers PostgreSQL NOTIFY on permission changes for cache invalidation'; COMMENT ON VIEW user_permissions_view IS 'Flattened view of user permissions for auditing'; COMMENT ON VIEW role_permission_counts IS 'Summary of permissions per role'; -- ============================================================================ -- Migration Complete -- ============================================================================ -- Verify migration DO $$ DECLARE role_count INTEGER; permission_count INTEGER; mapping_count INTEGER; BEGIN SELECT COUNT(*) INTO role_count FROM roles; SELECT COUNT(*) INTO permission_count FROM permissions; SELECT COUNT(*) INTO mapping_count FROM role_permissions; RAISE NOTICE 'RBAC Migration Complete:'; RAISE NOTICE ' - Roles: %', role_count; RAISE NOTICE ' - Permissions: %', permission_count; RAISE NOTICE ' - Role-Permission Mappings: %', mapping_count; END $$;