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
comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support'
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.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.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';
-- Generic audit trigger function
+19 -2
View File
@@ -64,9 +64,26 @@ CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_org_id UUID;
BEGIN
-- Clear cache for affected org
DELETE FROM morbac.auth_cache WHERE org_id = COALESCE(NEW.org_id, OLD.org_id);
-- Only clear cache if org_id exists in NEW or OLD
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;
$$;
+1 -1
View File
@@ -145,7 +145,7 @@ CREATE OR REPLACE FUNCTION morbac.is_allowed(
)
RETURNS BOOLEAN
LANGUAGE plpgsql
STABLE
VOLATILE
AS $$
DECLARE
v_cached_result BOOLEAN;
-3
View File
@@ -12,6 +12,3 @@
-- - Contextual access control
-- - 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
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
ORDER BY role_id, depth;
ORDER BY effective_roles.role_id, effective_roles.depth;
END;
$$;
@@ -98,9 +98,9 @@ BEGIN
FROM inherited_roles ir
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
ORDER BY role_id, depth;
ORDER BY inherited_roles.role_id, inherited_roles.depth;
END;
$$;
@@ -124,9 +124,9 @@ BEGIN
FROM effective_activities ea
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
ORDER BY activity, depth;
ORDER BY effective_activities.activity, effective_activities.depth;
END;
$$;
@@ -150,9 +150,9 @@ BEGIN
FROM effective_views ev
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
ORDER BY view, depth;
ORDER BY effective_views.view, effective_views.depth;
END;
$$;
@@ -207,21 +207,15 @@ BEGIN
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)
SELECT rh.senior_role_id, er.source || '_inherited', er.depth + 1
FROM effective_roles er
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
WHERE role_id IS NOT NULL
ORDER BY role_id, depth;
WHERE effective_roles.role_id IS NOT NULL
ORDER BY effective_roles.role_id, effective_roles.depth;
-- Add derived roles separately
RETURN QUERY
+25 -5
View File
@@ -6,6 +6,7 @@
--
-- Represents: Permission, Prohibition, Obligation, or Recommendation
CREATE TABLE morbac.rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
@@ -17,21 +18,40 @@ CREATE TABLE morbac.rules (
valid_from TIMESTAMPTZ,
valid_until TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
is_active BOOLEAN NOT NULL DEFAULT FALSE,
metadata JSONB DEFAULT '{}'::jsonb,
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)
);
-- Indexes for fast lookup and validity
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_modality ON morbac.rules(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
CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, modality, activity, view)
-- Fast lookup index for active rules
CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, activity, modality, view)
INCLUDE (role_id, context_id)
WHERE (valid_from IS NULL OR valid_from <= CURRENT_TIMESTAMP)
AND (valid_until IS NULL OR valid_until > CURRENT_TIMESTAMP);
WHERE is_active = true;
-- 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 COLUMN morbac.rules.org_id IS 'Organization scope';
+3 -13
View File
@@ -28,11 +28,7 @@ VALUES (
'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 FROM morbac.user_roles
@@ -43,14 +39,8 @@ WHERE user_id = 'ffffffff-0000-0000-0000-000000000001'::uuid
SELECT
operation,
record_id,
CASE
WHEN old_data IS NOT NULL THEN old_data->>'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,
'N/A' as old_active,
'N/A' as new_active,
session_user,
timestamp
FROM morbac.audit_log
+24 -7
View File
@@ -16,13 +16,30 @@ fi
rm -f "$OUTPUT"
BUILD_FILES=("header.sql")
while IFS= read -r file; do
BUILD_FILES+=("$file")
done < <(find "$SRC_DIR" -maxdepth 1 -name "*.sql" -type f ! -name "header.sql" ! -name "footer.sql" -exec basename {} \; | sort)
BUILD_FILES+=("footer.sql")
BUILD_FILES=(
"header.sql"
"types.sql"
"config.sql"
"organizations.sql"
"activities.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