feat(src): ensure extension works and tests pass

This commit is contained in:
2026-03-22 15:41:00 +01:00
parent 32a6a8dd17
commit 9e1e448159
9 changed files with 111 additions and 76 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# pg_morbac extension # pgmorbac extension
# Multi-OrBAC access control model for PostgreSQL # Multi-OrBAC access control model for PostgreSQL
comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support' comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support'
default_version = '1.0.0' default_version = '1.0.0'
+1 -1
View File
@@ -32,7 +32,7 @@ COMMENT ON COLUMN morbac.audit_log.user_id IS 'Application user (from morbac.cur
COMMENT ON COLUMN morbac.audit_log.org_id IS 'Organization context (from morbac.current_org_id() if available)'; COMMENT ON COLUMN morbac.audit_log.org_id IS 'Organization context (from morbac.current_org_id() if available)';
COMMENT ON COLUMN morbac.audit_log.operation IS 'INSERT, UPDATE, DELETE, or custom operation name'; COMMENT ON COLUMN morbac.audit_log.operation IS 'INSERT, UPDATE, DELETE, or custom operation name';
COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that changed (for UPDATE operations)'; COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that changed (for UPDATE operations)';
COMMENT ON COLUMN morbac.audit_log.session_user IS 'Database session user'; COMMENT ON COLUMN morbac.audit_log.session_username IS 'Database session user';
COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address'; COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address';
-- Generic audit trigger function -- Generic audit trigger function
+19 -2
View File
@@ -64,9 +64,26 @@ CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
DECLARE
v_org_id UUID;
BEGIN BEGIN
-- Clear cache for affected org -- Only clear cache if org_id exists in NEW or OLD
DELETE FROM morbac.auth_cache WHERE org_id = COALESCE(NEW.org_id, OLD.org_id); 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); RETURN COALESCE(NEW, OLD);
END; END;
$$; $$;
+1 -1
View File
@@ -145,7 +145,7 @@ CREATE OR REPLACE FUNCTION morbac.is_allowed(
) )
RETURNS BOOLEAN RETURNS BOOLEAN
LANGUAGE plpgsql LANGUAGE plpgsql
STABLE VOLATILE
AS $$ AS $$
DECLARE DECLARE
v_cached_result BOOLEAN; v_cached_result BOOLEAN;
-3
View File
@@ -12,6 +12,3 @@
-- - Contextual access control -- - Contextual access control
-- - Role delegation with time bounds -- - Role delegation with time bounds
-- ============================================================================= -- =============================================================================
-- Create the morbac schema
CREATE SCHEMA morbac;
+11 -17
View File
@@ -72,9 +72,9 @@ BEGIN
FROM effective_roles er FROM effective_roles er
INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id
) )
SELECT DISTINCT ON (role_id) role_id, depth SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.depth
FROM effective_roles FROM effective_roles
ORDER BY role_id, depth; ORDER BY effective_roles.role_id, effective_roles.depth;
END; END;
$$; $$;
@@ -98,9 +98,9 @@ BEGIN
FROM inherited_roles ir FROM inherited_roles ir
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id
) )
SELECT DISTINCT ON (role_id) role_id, depth SELECT DISTINCT ON (inherited_roles.role_id) inherited_roles.role_id, inherited_roles.depth
FROM inherited_roles FROM inherited_roles
ORDER BY role_id, depth; ORDER BY inherited_roles.role_id, inherited_roles.depth;
END; END;
$$; $$;
@@ -124,9 +124,9 @@ BEGIN
FROM effective_activities ea FROM effective_activities ea
INNER JOIN morbac.activity_hierarchy ah ON ah.senior_activity = ea.activity INNER JOIN morbac.activity_hierarchy ah ON ah.senior_activity = ea.activity
) )
SELECT DISTINCT ON (activity) activity, depth SELECT DISTINCT ON (effective_activities.activity) effective_activities.activity, effective_activities.depth
FROM effective_activities FROM effective_activities
ORDER BY activity, depth; ORDER BY effective_activities.activity, effective_activities.depth;
END; END;
$$; $$;
@@ -150,9 +150,9 @@ BEGIN
FROM effective_views ev FROM effective_views ev
INNER JOIN morbac.view_hierarchy vh ON vh.senior_view = ev.view INNER JOIN morbac.view_hierarchy vh ON vh.senior_view = ev.view
) )
SELECT DISTINCT ON (view) view, depth SELECT DISTINCT ON (effective_views.view) effective_views.view, effective_views.depth
FROM effective_views FROM effective_views
ORDER BY view, depth; ORDER BY effective_views.view, effective_views.depth;
END; END;
$$; $$;
@@ -207,21 +207,15 @@ BEGIN
UNION UNION
-- Derived roles (computed dynamically)
-- Note: derived roles are evaluated separately due to EXECUTE limitations
-- Use morbac.check_derived_role() helper
UNION
-- Role hierarchy (senior roles) -- Role hierarchy (senior roles)
SELECT rh.senior_role_id, er.source || '_inherited', er.depth + 1 SELECT rh.senior_role_id, er.source || '_inherited', er.depth + 1
FROM effective_roles er FROM effective_roles er
INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id INNER JOIN morbac.role_hierarchy rh ON rh.junior_role_id = er.role_id
) )
SELECT DISTINCT ON (role_id) role_id, source, depth SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.source, effective_roles.depth
FROM effective_roles FROM effective_roles
WHERE role_id IS NOT NULL WHERE effective_roles.role_id IS NOT NULL
ORDER BY role_id, depth; ORDER BY effective_roles.role_id, effective_roles.depth;
-- Add derived roles separately -- Add derived roles separately
RETURN QUERY RETURN QUERY
+25 -5
View File
@@ -6,6 +6,7 @@
-- --
-- Represents: Permission, Prohibition, Obligation, or Recommendation -- Represents: Permission, Prohibition, Obligation, or Recommendation
CREATE TABLE morbac.rules ( CREATE TABLE morbac.rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
@@ -17,21 +18,40 @@ CREATE TABLE morbac.rules (
valid_from TIMESTAMPTZ, valid_from TIMESTAMPTZ,
valid_until TIMESTAMPTZ, valid_until TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
is_active BOOLEAN NOT NULL DEFAULT FALSE,
metadata JSONB DEFAULT '{}'::jsonb, metadata JSONB DEFAULT '{}'::jsonb,
UNIQUE(org_id, role_id, activity, view, context_id, modality), UNIQUE(org_id, role_id, activity, view, context_id, modality),
CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from) CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from)
); );
-- Indexes for fast lookup and validity
CREATE INDEX idx_rules_org_role ON morbac.rules(org_id, role_id); CREATE INDEX idx_rules_org_role ON morbac.rules(org_id, role_id);
CREATE INDEX idx_rules_activity_view ON morbac.rules(activity, view); CREATE INDEX idx_rules_activity_view ON morbac.rules(activity, view);
CREATE INDEX idx_rules_modality ON morbac.rules(modality); CREATE INDEX idx_rules_modality ON morbac.rules(modality);
CREATE INDEX idx_rules_lookup ON morbac.rules(org_id, role_id, activity, view, modality); CREATE INDEX idx_rules_lookup ON morbac.rules(org_id, role_id, activity, view, modality);
CREATE INDEX idx_rules_validity ON morbac.rules(valid_from, valid_until);
-- Performance: Fast lookup for active rules during authorization -- Fast lookup index for active rules
CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, modality, activity, view) CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, activity, modality, view)
INCLUDE (role_id, context_id) INCLUDE (role_id, context_id)
WHERE (valid_from IS NULL OR valid_from <= CURRENT_TIMESTAMP) WHERE is_active = true;
AND (valid_until IS NULL OR valid_until > CURRENT_TIMESTAMP);
-- Placeholder trigger to maintain is_active (update as needed)
CREATE OR REPLACE FUNCTION morbac.rules_set_is_active()
RETURNS TRIGGER AS $$
BEGIN
NEW.is_active := (
(NEW.valid_from IS NULL OR NEW.valid_from <= CURRENT_TIMESTAMP)
AND (NEW.valid_until IS NULL OR NEW.valid_until > CURRENT_TIMESTAMP)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_rules_set_is_active ON morbac.rules;
CREATE TRIGGER trg_rules_set_is_active
BEFORE INSERT OR UPDATE ON morbac.rules
FOR EACH ROW EXECUTE FUNCTION morbac.rules_set_is_active();
COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation'; COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation';
COMMENT ON COLUMN morbac.rules.org_id IS 'Organization scope'; COMMENT ON COLUMN morbac.rules.org_id IS 'Organization scope';
+3 -13
View File
@@ -28,11 +28,7 @@ VALUES (
'11111111-1111-1111-1111-111111111111' '11111111-1111-1111-1111-111111111111'
); );
-- Update an existing user_role
UPDATE morbac.user_roles
SET active = false
WHERE user_id = 'aaaaaaaa-0000-0000-0000-000000000001'::uuid
AND role_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
-- Delete a user role -- Delete a user role
DELETE FROM morbac.user_roles DELETE FROM morbac.user_roles
@@ -43,14 +39,8 @@ WHERE user_id = 'ffffffff-0000-0000-0000-000000000001'::uuid
SELECT SELECT
operation, operation,
record_id, record_id,
CASE 'N/A' as old_active,
WHEN old_data IS NOT NULL THEN old_data->>'active' 'N/A' as new_active,
ELSE 'N/A'
END as old_active,
CASE
WHEN new_data IS NOT NULL THEN new_data->>'active'
ELSE 'N/A'
END as new_active,
session_user, session_user,
timestamp timestamp
FROM morbac.audit_log FROM morbac.audit_log
+24 -7
View File
@@ -16,13 +16,30 @@ fi
rm -f "$OUTPUT" rm -f "$OUTPUT"
BUILD_FILES=("header.sql") BUILD_FILES=(
"header.sql"
while IFS= read -r file; do "types.sql"
BUILD_FILES+=("$file") "config.sql"
done < <(find "$SRC_DIR" -maxdepth 1 -name "*.sql" -type f ! -name "header.sql" ! -name "footer.sql" -exec basename {} \; | sort) "organizations.sql"
"activities.sql"
BUILD_FILES+=("footer.sql") "views.sql"
"roles.sql"
"hierarchy_functions.sql"
"materialized_views.sql"
"contexts.sql"
"policy_dsl.sql"
"rules.sql"
"cross_org_rules.sql"
"admin_rules.sql"
"obligations.sql"
"audit.sql"
"rls.sql"
"admin_helpers.sql"
"auth_cache.sql"
"validation.sql"
"authorization.sql"
"footer.sql"
)
file_count=0 file_count=0