feat(src): ensure extension works and tests pass
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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;
|
||||||
$$;
|
$$;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user