-- Authorization decision cache to avoid repeated expensive computations. -- TTL is configurable via morbac.config (cache_ttl_seconds). CREATE TABLE morbac.auth_cache ( user_id UUID NOT NULL, org_id UUID NOT NULL, activity TEXT NOT NULL, view TEXT NOT NULL, allowed BOOLEAN NOT NULL, computed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '5 minutes'), PRIMARY KEY (user_id, org_id, activity, view) ); CREATE INDEX idx_auth_cache_expires ON morbac.auth_cache(expires_at); CREATE INDEX idx_auth_cache_user_org ON morbac.auth_cache(user_id, org_id); COMMENT ON TABLE morbac.auth_cache IS 'Authorization decision cache - expires after 5 minutes or when policies change'; CREATE OR REPLACE FUNCTION morbac.invalidate_cache(p_user_id UUID DEFAULT NULL, p_org_id UUID DEFAULT NULL) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN IF p_user_id IS NOT NULL AND p_org_id IS NOT NULL THEN DELETE FROM morbac.auth_cache WHERE user_id = p_user_id AND org_id = p_org_id; ELSIF p_org_id IS NOT NULL THEN DELETE FROM morbac.auth_cache WHERE org_id = p_org_id; ELSIF p_user_id IS NOT NULL THEN DELETE FROM morbac.auth_cache WHERE user_id = p_user_id; ELSE DELETE FROM morbac.auth_cache; END IF; END; $$; COMMENT ON FUNCTION morbac.invalidate_cache(UUID, UUID) IS 'Invalidate auth cache for specific user/org or all entries'; CREATE OR REPLACE FUNCTION morbac.cleanup_auth_cache() RETURNS INTEGER LANGUAGE plpgsql AS $$ DECLARE v_deleted INTEGER; BEGIN DELETE FROM morbac.auth_cache WHERE expires_at < CURRENT_TIMESTAMP; GET DIAGNOSTICS v_deleted = ROW_COUNT; RETURN v_deleted; END; $$; COMMENT ON FUNCTION morbac.cleanup_auth_cache() IS 'Remove expired cache entries - call periodically via cron'; -- Invalidate cache on rule or role changes CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_org_id UUID; BEGIN IF TG_OP = 'DELETE' THEN BEGIN v_org_id := OLD.org_id; EXCEPTION WHEN undefined_column THEN v_org_id := NULL; END; ELSE BEGIN v_org_id := NEW.org_id; EXCEPTION WHEN undefined_column THEN v_org_id := NULL; END; END IF; IF v_org_id IS NOT NULL THEN DELETE FROM morbac.auth_cache WHERE org_id = v_org_id; END IF; RETURN COALESCE(NEW, OLD); END; $$; CREATE TRIGGER trg_invalidate_cache_rules AFTER INSERT OR UPDATE OR DELETE ON morbac.rules FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); CREATE TRIGGER trg_invalidate_cache_user_roles AFTER INSERT OR UPDATE OR DELETE ON morbac.user_roles FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); CREATE TRIGGER trg_invalidate_cache_delegations AFTER INSERT OR UPDATE OR DELETE ON morbac.delegations FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); CREATE TRIGGER trg_invalidate_cache_cross_org AFTER INSERT OR UPDATE OR DELETE ON morbac.cross_org_rules FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); -- Refresh materialized hierarchy views when hierarchies change CREATE OR REPLACE FUNCTION morbac.refresh_on_hierarchy_change() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN PERFORM morbac.refresh_hierarchy_cache(); DELETE FROM morbac.auth_cache; RETURN COALESCE(NEW, OLD); END; $$; CREATE TRIGGER trg_refresh_role_hierarchy AFTER INSERT OR UPDATE OR DELETE ON morbac.role_hierarchy FOR EACH STATEMENT EXECUTE FUNCTION morbac.refresh_on_hierarchy_change(); CREATE TRIGGER trg_refresh_activity_hierarchy AFTER INSERT OR UPDATE OR DELETE ON morbac.activity_hierarchy FOR EACH STATEMENT EXECUTE FUNCTION morbac.refresh_on_hierarchy_change(); CREATE TRIGGER trg_refresh_view_hierarchy AFTER INSERT OR UPDATE OR DELETE ON morbac.view_hierarchy FOR EACH STATEMENT EXECUTE FUNCTION morbac.refresh_on_hierarchy_change(); -- Invalidate entire cache when org hierarchy changes. -- Scoped rules (scope != 'self') depend on the org tree, so any org change -- may affect which orgs a rule covers. CREATE OR REPLACE FUNCTION morbac.invalidate_all_cache() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN DELETE FROM morbac.auth_cache; RETURN NULL; END; $$; CREATE TRIGGER trg_invalidate_cache_orgs AFTER INSERT OR UPDATE OR DELETE ON morbac.orgs FOR EACH STATEMENT EXECUTE FUNCTION morbac.invalidate_all_cache();