192 lines
6.6 KiB
PL/PgSQL
192 lines
6.6 KiB
PL/PgSQL
-- =============================================================================
|
|
-- AUTHORIZATION DECISION FUNCTION (CRITICAL)
|
|
-- =============================================================================
|
|
-- Implements the canonical OrBAC authorization decision
|
|
--
|
|
-- Semantics (from Multi-OrBAC paper):
|
|
-- - Access is allowed if and only if:
|
|
-- 1. At least one applicable permission exists
|
|
-- 2. AND no applicable prohibition exists
|
|
-- - Prohibitions have precedence over permissions
|
|
-- - Contexts must be evaluated
|
|
-- - Default deny (no permission = deny)
|
|
-- - Obligations and recommendations do NOT affect authorization
|
|
|
|
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
|
p_user_id UUID,
|
|
p_org_id UUID,
|
|
p_activity TEXT,
|
|
p_view TEXT
|
|
)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
AS $$
|
|
DECLARE
|
|
v_rule RECORD;
|
|
BEGIN
|
|
-- STEP 1: Check for prohibitions first (prohibition precedence)
|
|
-- If any applicable prohibition exists, deny immediately
|
|
-- Checks:
|
|
-- - Direct rules for (activity, view) combinations
|
|
-- - Activity hierarchy (senior activities imply junior)
|
|
-- - View hierarchy (senior views imply junior)
|
|
-- - Comprehensive roles (direct, delegated, derived, inherited)
|
|
-- - Temporal validity (if rule has time constraints)
|
|
|
|
FOR v_rule IN
|
|
SELECT r.context_id
|
|
FROM morbac.rules r
|
|
WHERE r.org_id = p_org_id
|
|
AND r.modality = 'prohibition'
|
|
-- Match activity or any senior activity in hierarchy
|
|
AND r.activity IN (
|
|
SELECT activity FROM morbac.get_effective_activities(p_activity)
|
|
)
|
|
-- Match view or any senior view in hierarchy
|
|
AND r.view IN (
|
|
SELECT view FROM morbac.get_effective_views(p_view)
|
|
)
|
|
-- Match comprehensive roles (direct, delegated, derived, inherited)
|
|
AND r.role_id IN (
|
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
|
)
|
|
-- Check temporal validity
|
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
|
LOOP
|
|
-- Evaluate context
|
|
IF morbac.eval_context(v_rule.context_id) THEN
|
|
-- Prohibition found - immediate deny (prohibition precedence)
|
|
RETURN FALSE;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
-- STEP 2: Check cross-organizational prohibitions
|
|
FOR v_rule IN
|
|
SELECT cr.context_id
|
|
FROM morbac.cross_org_rules cr
|
|
WHERE cr.target_org_id = p_org_id
|
|
AND cr.modality = 'prohibition'
|
|
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
|
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
|
-- User has role in source org
|
|
AND cr.role_id IN (
|
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id)
|
|
)
|
|
-- Check temporal validity
|
|
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
|
LOOP
|
|
IF morbac.eval_context(v_rule.context_id) THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
-- STEP 3: No prohibitions found, check for permissions
|
|
FOR v_rule IN
|
|
SELECT r.context_id
|
|
FROM morbac.rules r
|
|
WHERE r.org_id = p_org_id
|
|
AND r.modality = 'permission'
|
|
-- Match activity or any senior activity in hierarchy
|
|
AND r.activity IN (
|
|
SELECT activity FROM morbac.get_effective_activities(p_activity)
|
|
)
|
|
-- Match view or any senior view in hierarchy
|
|
AND r.view IN (
|
|
SELECT view FROM morbac.get_effective_views(p_view)
|
|
)
|
|
-- Match comprehensive roles
|
|
AND r.role_id IN (
|
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
|
)
|
|
-- Check temporal validity
|
|
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
|
LOOP
|
|
-- Evaluate context
|
|
IF morbac.eval_context(v_rule.context_id) THEN
|
|
-- Permission found and no prohibition - allow
|
|
RETURN TRUE;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
-- STEP 4: Check cross-organizational permissions
|
|
FOR v_rule IN
|
|
SELECT cr.context_id
|
|
FROM morbac.cross_org_rules cr
|
|
WHERE cr.target_org_id = p_org_id
|
|
AND cr.modality = 'permission'
|
|
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
|
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
|
AND cr.role_id IN (
|
|
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id)
|
|
)
|
|
-- Check temporal validity
|
|
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
|
LOOP
|
|
IF morbac.eval_context(v_rule.context_id) THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
-- No permission found - deny (default deny)
|
|
RETURN FALSE;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION morbac.is_allowed_nocache(UUID, UUID, TEXT, TEXT) IS
|
|
'Authorization without cache - use for debugging or when cache must be bypassed';
|
|
|
|
-- Cached authorization check (default, recommended for production use)
|
|
CREATE OR REPLACE FUNCTION morbac.is_allowed(
|
|
p_user_id UUID,
|
|
p_org_id UUID,
|
|
p_activity TEXT,
|
|
p_view TEXT
|
|
)
|
|
RETURNS BOOLEAN
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
AS $$
|
|
DECLARE
|
|
v_cached_result BOOLEAN;
|
|
v_computed_result BOOLEAN;
|
|
v_expires_at TIMESTAMPTZ;
|
|
BEGIN
|
|
-- Try cache first
|
|
SELECT allowed, expires_at INTO v_cached_result, v_expires_at
|
|
FROM morbac.auth_cache
|
|
WHERE user_id = p_user_id
|
|
AND org_id = p_org_id
|
|
AND activity = p_activity
|
|
AND view = p_view
|
|
AND expires_at > CURRENT_TIMESTAMP;
|
|
|
|
IF FOUND THEN
|
|
RETURN v_cached_result;
|
|
END IF;
|
|
|
|
-- Cache miss - compute authorization
|
|
v_computed_result := morbac.is_allowed_nocache(p_user_id, p_org_id, p_activity, p_view);
|
|
|
|
-- Store in cache (TTL from config)
|
|
INSERT INTO morbac.auth_cache (user_id, org_id, activity, view, allowed, expires_at)
|
|
VALUES (
|
|
p_user_id,
|
|
p_org_id,
|
|
p_activity,
|
|
p_view,
|
|
v_computed_result,
|
|
CURRENT_TIMESTAMP + make_interval(secs => morbac.get_config('cache_ttl_seconds')::integer)
|
|
)
|
|
ON CONFLICT (user_id, org_id, activity, view) DO UPDATE
|
|
SET allowed = v_computed_result,
|
|
computed_at = CURRENT_TIMESTAMP,
|
|
expires_at = CURRENT_TIMESTAMP + make_interval(secs => morbac.get_config('cache_ttl_seconds')::integer);
|
|
|
|
RETURN v_computed_result;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION morbac.is_allowed(UUID, UUID, TEXT, TEXT) IS
|
|
'Complete OrBAC authorization with caching (default) - use is_allowed_nocache() for debugging';
|