Files
pgmorbac/src/authorization.sql
T

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