trading-platform-database-v2/ddl/schemas/rbac/tables/004_user_roles.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

289 lines
9.8 KiB
PL/PgSQL

-- ============================================================================
-- RBAC Schema: User Roles Table
-- Assigns roles to users within a tenant
-- ============================================================================
-- ============================================================================
-- USER_ROLES TABLE
-- Maps users to their assigned roles
-- ============================================================================
CREATE TABLE IF NOT EXISTS rbac.user_roles (
-- Primary key
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- User reference
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
-- Role reference
role_id UUID NOT NULL REFERENCES rbac.roles(id) ON DELETE CASCADE,
-- Tenant (denormalized for RLS efficiency)
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Assignment metadata
is_primary BOOLEAN NOT NULL DEFAULT false,
assigned_reason TEXT,
-- Validity period (optional for temporary roles)
valid_from TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
valid_until TIMESTAMPTZ DEFAULT NULL,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Audit fields
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
assigned_by UUID REFERENCES users.users(id),
revoked_at TIMESTAMPTZ DEFAULT NULL,
revoked_by UUID REFERENCES users.users(id),
-- Constraints
CONSTRAINT uq_user_role UNIQUE (user_id, role_id),
CONSTRAINT chk_valid_period CHECK (valid_until IS NULL OR valid_until > valid_from)
);
-- ============================================================================
-- INDEXES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON rbac.user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON rbac.user_roles(role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_tenant_id ON rbac.user_roles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_is_active ON rbac.user_roles(is_active);
CREATE INDEX IF NOT EXISTS idx_user_roles_is_primary ON rbac.user_roles(user_id, is_primary) WHERE is_primary = true;
CREATE INDEX IF NOT EXISTS idx_user_roles_validity ON rbac.user_roles(valid_from, valid_until) WHERE is_active = true;
-- ============================================================================
-- ROW LEVEL SECURITY
-- ============================================================================
ALTER TABLE rbac.user_roles ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see role assignments in their tenant
CREATE POLICY user_roles_tenant_isolation ON rbac.user_roles
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- ============================================================================
-- TRIGGERS
-- ============================================================================
-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION rbac.update_user_roles_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_user_roles_updated_at
BEFORE UPDATE ON rbac.user_roles
FOR EACH ROW
EXECUTE FUNCTION rbac.update_user_roles_timestamp();
-- Ensure only one primary role per user
CREATE OR REPLACE FUNCTION rbac.ensure_single_primary_role()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_primary = true THEN
UPDATE rbac.user_roles
SET is_primary = false
WHERE user_id = NEW.user_id
AND id != NEW.id
AND is_primary = true;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_single_primary_role
BEFORE INSERT OR UPDATE ON rbac.user_roles
FOR EACH ROW
WHEN (NEW.is_primary = true)
EXECUTE FUNCTION rbac.ensure_single_primary_role();
-- ============================================================================
-- VIEW: User with roles and permissions
-- ============================================================================
CREATE OR REPLACE VIEW rbac.v_user_permissions AS
SELECT DISTINCT
ur.user_id,
ur.tenant_id,
u.email,
u.display_name,
r.id AS role_id,
r.name AS role_name,
r.slug AS role_slug,
r.hierarchy_level,
ur.is_primary,
p.code AS permission_code,
p.module,
p.action,
p.resource,
rp.grant_type
FROM rbac.user_roles ur
JOIN users.users u ON ur.user_id = u.id
JOIN rbac.roles r ON ur.role_id = r.id
JOIN rbac.role_permissions rp ON r.id = rp.role_id
JOIN rbac.permissions p ON rp.permission_id = p.id
WHERE ur.is_active = true
AND r.is_active = true
AND p.is_active = true
AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP)
AND ur.valid_from <= CURRENT_TIMESTAMP;
-- ============================================================================
-- FUNCTION: Check if user has permission
-- ============================================================================
CREATE OR REPLACE FUNCTION rbac.user_has_permission(
p_user_id UUID,
p_tenant_id UUID,
p_permission_code VARCHAR(100)
)
RETURNS BOOLEAN AS $$
DECLARE
v_has_allow BOOLEAN;
v_has_deny BOOLEAN;
BEGIN
-- Check for explicit deny first
SELECT EXISTS (
SELECT 1
FROM rbac.v_user_permissions
WHERE user_id = p_user_id
AND tenant_id = p_tenant_id
AND permission_code = p_permission_code
AND grant_type = 'deny'
) INTO v_has_deny;
IF v_has_deny THEN
RETURN false;
END IF;
-- Check for allow
SELECT EXISTS (
SELECT 1
FROM rbac.v_user_permissions
WHERE user_id = p_user_id
AND tenant_id = p_tenant_id
AND permission_code = p_permission_code
AND grant_type = 'allow'
) INTO v_has_allow;
RETURN v_has_allow;
END;
$$ LANGUAGE plpgsql STABLE;
-- ============================================================================
-- FUNCTION: Get user's effective permissions
-- ============================================================================
CREATE OR REPLACE FUNCTION rbac.get_user_permissions(
p_user_id UUID,
p_tenant_id UUID
)
RETURNS TABLE (
permission_code VARCHAR(100),
permission_name VARCHAR(200),
module VARCHAR(50),
action VARCHAR(20),
resource VARCHAR(100)
) AS $$
BEGIN
RETURN QUERY
SELECT DISTINCT
p.code,
p.name,
p.module,
p.action,
p.resource
FROM rbac.user_roles ur
JOIN rbac.roles r ON ur.role_id = r.id
JOIN rbac.role_permissions rp ON r.id = rp.role_id
JOIN rbac.permissions p ON rp.permission_id = p.id
WHERE ur.user_id = p_user_id
AND ur.tenant_id = p_tenant_id
AND ur.is_active = true
AND r.is_active = true
AND p.is_active = true
AND rp.grant_type = 'allow'
AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP)
AND ur.valid_from <= CURRENT_TIMESTAMP
AND NOT EXISTS (
-- Exclude denied permissions
SELECT 1
FROM rbac.user_roles ur2
JOIN rbac.role_permissions rp2 ON ur2.role_id = rp2.role_id
WHERE ur2.user_id = p_user_id
AND ur2.tenant_id = p_tenant_id
AND rp2.permission_id = p.id
AND rp2.grant_type = 'deny'
AND ur2.is_active = true
)
ORDER BY p.module, p.code;
END;
$$ LANGUAGE plpgsql STABLE;
-- ============================================================================
-- FUNCTION: Assign role to user
-- ============================================================================
CREATE OR REPLACE FUNCTION rbac.assign_role_to_user(
p_user_id UUID,
p_role_id UUID,
p_tenant_id UUID,
p_assigned_by UUID,
p_is_primary BOOLEAN DEFAULT false,
p_valid_until TIMESTAMPTZ DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
v_assignment_id UUID;
BEGIN
INSERT INTO rbac.user_roles (
user_id, role_id, tenant_id, is_primary,
valid_until, assigned_by
) VALUES (
p_user_id, p_role_id, p_tenant_id, p_is_primary,
p_valid_until, p_assigned_by
)
ON CONFLICT (user_id, role_id) DO UPDATE SET
is_active = true,
is_primary = EXCLUDED.is_primary,
valid_until = EXCLUDED.valid_until,
assigned_by = EXCLUDED.assigned_by,
revoked_at = NULL,
revoked_by = NULL,
updated_at = CURRENT_TIMESTAMP
RETURNING id INTO v_assignment_id;
RETURN v_assignment_id;
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- GRANTS
-- ============================================================================
GRANT SELECT, INSERT, UPDATE, DELETE ON rbac.user_roles TO trading_user;
GRANT SELECT ON rbac.v_user_permissions TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.user_has_permission TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.get_user_permissions TO trading_user;
GRANT EXECUTE ON FUNCTION rbac.assign_role_to_user TO trading_user;
-- ============================================================================
-- COMMENTS
-- ============================================================================
COMMENT ON TABLE rbac.user_roles IS 'Maps users to their assigned roles within a tenant';
COMMENT ON COLUMN rbac.user_roles.is_primary IS 'Primary role shown in UI, user can have multiple roles';
COMMENT ON COLUMN rbac.user_roles.valid_from IS 'When the role assignment becomes active';
COMMENT ON COLUMN rbac.user_roles.valid_until IS 'When the role assignment expires (NULL = never)';
COMMENT ON VIEW rbac.v_user_permissions IS 'Flattened view of users with their effective permissions';
COMMENT ON FUNCTION rbac.user_has_permission IS 'Check if a user has a specific permission';
COMMENT ON FUNCTION rbac.get_user_permissions IS 'Get all effective permissions for a user';