-- 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.';