Files
pgmorbac/src/validation.sql
T

167 lines
5.5 KiB
PL/PgSQL

-- Validation functions for SoD, cardinality, and rule conflict detection.
--
-- Rule conflict detection (from Multi-OrBAC paper):
-- Conflicts exist when two rules share the same (org, role, activity, view, context)
-- but their modalities create a situation where one permanently overrides the other:
-- - prohibition + permission -> permission is dead (always overridden)
-- - prohibition + obligation -> obligation is always voided
-- - prohibition + recommendation -> recommendation is always voided
-- - obligation + recommendation -> recommendation is always voided
--
-- Note: hierarchy-based overlaps are intentional design, not flagged here.
CREATE OR REPLACE FUNCTION morbac.check_sod_violation(
p_user_id UUID,
p_role_id UUID,
p_org_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_conflict_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1
FROM morbac.user_roles ur
INNER JOIN morbac.sod_conflicts sod ON (
(sod.role_a_id = ur.role_id AND sod.role_b_id = p_role_id)
OR (sod.role_b_id = ur.role_id AND sod.role_a_id = p_role_id)
)
WHERE ur.user_id = p_user_id
AND ur.org_id = p_org_id
AND sod.org_id = p_org_id
) INTO v_conflict_exists;
RETURN v_conflict_exists;
END;
$$;
COMMENT ON FUNCTION morbac.check_sod_violation(UUID, UUID, UUID) IS
'Returns true if assigning role would violate Separation of Duty constraints';
CREATE OR REPLACE FUNCTION morbac.check_cardinality_violation(
p_role_id UUID,
p_adding BOOLEAN DEFAULT TRUE
)
RETURNS TEXT
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
v_current_count INTEGER;
v_min_users INTEGER;
v_max_users INTEGER;
BEGIN
SELECT
COUNT(DISTINCT ur.user_id),
rc.min_users,
rc.max_users
INTO v_current_count, v_min_users, v_max_users
FROM morbac.user_roles ur
LEFT JOIN morbac.role_cardinality rc ON rc.role_id = ur.role_id
WHERE ur.role_id = p_role_id
GROUP BY rc.min_users, rc.max_users;
IF p_adding AND v_max_users IS NOT NULL THEN
IF v_current_count >= v_max_users THEN
RETURN format('Maximum users (%s) reached for role', v_max_users);
END IF;
END IF;
IF NOT p_adding AND v_min_users IS NOT NULL THEN
IF v_current_count <= v_min_users THEN
RETURN format('Minimum users (%s) required for role', v_min_users);
END IF;
END IF;
RETURN NULL;
END;
$$;
COMMENT ON FUNCTION morbac.check_cardinality_violation(UUID, BOOLEAN) IS
'Returns error message if cardinality constraint would be violated, NULL otherwise';
CREATE OR REPLACE FUNCTION morbac.detect_rule_conflicts(
p_org_id UUID,
p_role_id UUID,
p_activity TEXT,
p_view TEXT,
p_context_id UUID,
p_modality morbac.modality,
p_exclude_id UUID DEFAULT NULL
)
RETURNS TABLE(
conflicting_rule_id UUID,
conflicting_modality morbac.modality,
description TEXT
)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
RETURN QUERY
SELECT
r.id,
r.modality,
CASE
WHEN p_modality = 'prohibition'
THEN 'prohibition voids existing ' || r.modality::text
WHEN r.modality = 'prohibition'
THEN p_modality::text || ' will always be overridden by existing prohibition'
WHEN p_modality = 'obligation' AND r.modality = 'recommendation'
THEN 'obligation voids existing recommendation'
ELSE
'recommendation will always be overridden by existing obligation'
END
FROM morbac.rules r
WHERE r.org_id = p_org_id
AND r.role_id = p_role_id
AND r.activity = p_activity
AND r.view = p_view
AND r.context_id = p_context_id
AND r.modality != p_modality
AND (p_exclude_id IS NULL OR r.id != p_exclude_id)
AND (
(p_modality = 'prohibition' AND r.modality IN ('permission', 'obligation', 'recommendation'))
OR (r.modality = 'prohibition' AND p_modality IN ('permission', 'obligation', 'recommendation'))
OR (p_modality = 'obligation' AND r.modality = 'recommendation')
OR (p_modality = 'recommendation' AND r.modality = 'obligation')
);
END;
$$;
COMMENT ON FUNCTION morbac.detect_rule_conflicts(UUID, UUID, TEXT, TEXT, UUID, morbac.modality, UUID) IS
'Returns rules that directly conflict with the given tuple due to modality precedence (prohibition > obligation > recommendation > permission).';
-- Trigger: warn (non-blocking) when a new/updated rule conflicts with an existing one
CREATE OR REPLACE FUNCTION morbac.trg_warn_rule_conflicts()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_conflict RECORD;
BEGIN
FOR v_conflict IN
SELECT * FROM morbac.detect_rule_conflicts(
NEW.org_id, NEW.role_id, NEW.activity, NEW.view, NEW.context_id, NEW.modality,
CASE WHEN TG_OP = 'UPDATE' THEN NEW.id ELSE NULL END
)
LOOP
RAISE WARNING 'Rule conflict: % (conflicts with rule %)',
v_conflict.description, v_conflict.conflicting_rule_id;
END LOOP;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_rules_conflict_check ON morbac.rules;
CREATE TRIGGER trg_rules_conflict_check
BEFORE INSERT OR UPDATE ON morbac.rules
FOR EACH ROW EXECUTE FUNCTION morbac.trg_warn_rule_conflicts();
COMMENT ON FUNCTION morbac.trg_warn_rule_conflicts() IS
'Trigger function: emits a WARNING (non-blocking) when a rule conflicts with an existing rule due to modality precedence.';