289 lines
9.8 KiB
PL/PgSQL
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';
|