167 lines
5.5 KiB
PL/PgSQL
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.';
|