feat(src): ensure extension works and tests pass
This commit is contained in:
+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.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
@@ -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;
|
||||
$$;
|
||||
|
||||
@@ -145,7 +145,7 @@ CREATE OR REPLACE FUNCTION morbac.is_allowed(
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
VOLATILE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cached_result BOOLEAN;
|
||||
|
||||
@@ -12,6 +12,3 @@
|
||||
-- - Contextual access control
|
||||
-- - Role delegation with time bounds
|
||||
-- =============================================================================
|
||||
|
||||
-- Create the morbac schema
|
||||
CREATE SCHEMA morbac;
|
||||
|
||||
+37
-43
@@ -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;
|
||||
$$;
|
||||
|
||||
@@ -183,45 +183,39 @@ BEGIN
|
||||
|
||||
UNION
|
||||
|
||||
-- Delegated roles (active and not revoked)
|
||||
SELECT d.role_id, 'delegation'::TEXT, 0 AS depth
|
||||
FROM morbac.delegations d
|
||||
WHERE d.delegatee_id = p_user_id
|
||||
AND d.org_id = p_org_id
|
||||
AND NOT d.revoked
|
||||
AND now() BETWEEN d.valid_from AND d.valid_until
|
||||
-- Check delegator has the role
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM morbac.user_roles ur
|
||||
WHERE ur.user_id = d.delegator_id
|
||||
AND ur.role_id = d.role_id
|
||||
AND ur.org_id = d.org_id
|
||||
)
|
||||
-- Check not negatively assigned to delegatee
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM morbac.negative_role_assignments nra
|
||||
WHERE nra.user_id = p_user_id
|
||||
AND nra.role_id = d.role_id
|
||||
AND nra.org_id = p_org_id
|
||||
)
|
||||
-- Delegated roles (active and not revoked)
|
||||
SELECT d.role_id, 'delegation'::TEXT, 0 AS depth
|
||||
FROM morbac.delegations d
|
||||
WHERE d.delegatee_id = p_user_id
|
||||
AND d.org_id = p_org_id
|
||||
AND NOT d.revoked
|
||||
AND now() BETWEEN d.valid_from AND d.valid_until
|
||||
-- Check delegator has the role
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM morbac.user_roles ur
|
||||
WHERE ur.user_id = d.delegator_id
|
||||
AND ur.role_id = d.role_id
|
||||
AND ur.org_id = d.org_id
|
||||
)
|
||||
-- Check not negatively assigned to delegatee
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM morbac.negative_role_assignments nra
|
||||
WHERE nra.user_id = p_user_id
|
||||
AND nra.role_id = d.role_id
|
||||
AND nra.org_id = p_org_id
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
-- 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
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user