feat(all): add scope handling in rules
This commit is contained in:
@@ -23,3 +23,10 @@ CREATE INDEX idx_activity_hierarchy_junior ON morbac.activity_hierarchy(junior_a
|
||||
COMMENT ON TABLE morbac.activity_hierarchy IS 'Activity hierarchy - senior activities imply junior activities';
|
||||
COMMENT ON COLUMN morbac.activity_hierarchy.senior_activity IS 'Senior activity (implies junior)';
|
||||
COMMENT ON COLUMN morbac.activity_hierarchy.junior_activity IS 'Junior activity (implied by senior)';
|
||||
|
||||
INSERT INTO morbac.activities (name, description) VALUES
|
||||
('create', 'Create new entities'),
|
||||
('read', 'Read or list entities'),
|
||||
('update', 'Modify existing entities'),
|
||||
('delete', 'Remove entities')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
CREATE OR REPLACE FUNCTION morbac.can_manage_user_role(
|
||||
p_admin_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_target_role_id UUID
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_role_name TEXT;
|
||||
BEGIN
|
||||
SELECT name INTO v_role_name
|
||||
FROM morbac.roles
|
||||
WHERE id = p_target_role_id AND org_id = p_org_id;
|
||||
|
||||
IF v_role_name IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
RETURN morbac.is_admin_allowed(
|
||||
p_admin_user_id,
|
||||
p_org_id,
|
||||
'assign_role',
|
||||
v_role_name
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.can_manage_user_role(UUID, UUID, UUID) IS
|
||||
'Check if user can assign/revoke a specific role in organization';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.can_manage_roles(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN morbac.is_admin_allowed(
|
||||
p_user_id,
|
||||
p_org_id,
|
||||
'manage',
|
||||
'roles'
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.can_manage_roles(UUID, UUID) IS
|
||||
'Check if user can create/modify/delete roles in organization';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.can_manage_policies(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN morbac.is_admin_allowed(
|
||||
p_user_id,
|
||||
p_org_id,
|
||||
'manage',
|
||||
'policies'
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.can_manage_policies(UUID, UUID) IS
|
||||
'Check if user can manage policies in organization';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.admin_assign_role(
|
||||
p_admin_user_id UUID,
|
||||
p_target_user_id UUID,
|
||||
p_role_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN
|
||||
RAISE EXCEPTION 'User % does not have permission to assign role % in org %',
|
||||
p_admin_user_id, p_role_id, p_org_id;
|
||||
END IF;
|
||||
|
||||
IF morbac.check_sod_violation(p_target_user_id, p_role_id, p_org_id) THEN
|
||||
RAISE EXCEPTION 'Role assignment would violate Separation of Duty constraints';
|
||||
END IF;
|
||||
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
VALUES (p_target_user_id, p_role_id, p_org_id)
|
||||
ON CONFLICT (user_id, role_id, org_id) DO NOTHING;
|
||||
|
||||
DECLARE
|
||||
v_cardinality_error TEXT;
|
||||
BEGIN
|
||||
v_cardinality_error := morbac.check_cardinality_violation(p_role_id);
|
||||
IF v_cardinality_error IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Role assignment violates cardinality constraint: %', v_cardinality_error;
|
||||
END IF;
|
||||
END;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.admin_assign_role(UUID, UUID, UUID, UUID) IS
|
||||
'Assign role to user with admin permission check and constraint validation';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.admin_revoke_role(
|
||||
p_admin_user_id UUID,
|
||||
p_target_user_id UUID,
|
||||
p_role_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN
|
||||
RAISE EXCEPTION 'User % does not have permission to revoke role % in org %',
|
||||
p_admin_user_id, p_role_id, p_org_id;
|
||||
END IF;
|
||||
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = p_target_user_id
|
||||
AND role_id = p_role_id
|
||||
AND org_id = p_org_id;
|
||||
|
||||
DECLARE
|
||||
v_cardinality_error TEXT;
|
||||
BEGIN
|
||||
v_cardinality_error := morbac.check_cardinality_violation(p_role_id, FALSE);
|
||||
IF v_cardinality_error IS NOT NULL THEN
|
||||
RAISE WARNING 'Role revocation may violate cardinality constraint: %', v_cardinality_error;
|
||||
END IF;
|
||||
END;
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.admin_revoke_role(UUID, UUID, UUID, UUID) IS
|
||||
'Revoke role from user with admin permission check';
|
||||
@@ -1,20 +0,0 @@
|
||||
-- Meta-policies defining who can create/modify policies (AdministrationPermission)
|
||||
|
||||
CREATE TABLE morbac.admin_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||
admin_activity TEXT NOT NULL,
|
||||
admin_target TEXT NOT NULL,
|
||||
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
||||
modality morbac.modality NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
UNIQUE(org_id, role_id, admin_activity, admin_target, context_id, modality)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_admin_rules_org_role ON morbac.admin_rules(org_id, role_id);
|
||||
|
||||
COMMENT ON TABLE morbac.admin_rules IS 'Administration rules - meta-policies for policy management';
|
||||
COMMENT ON COLUMN morbac.admin_rules.admin_activity IS 'Admin action: create_rule, modify_rule, delete_rule, assign_role, etc.';
|
||||
COMMENT ON COLUMN morbac.admin_rules.admin_target IS 'What can be administered: rules, roles, users, orgs, etc.';
|
||||
@@ -59,6 +59,7 @@ COMMENT ON FUNCTION morbac.cleanup_auth_cache() IS
|
||||
CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_org_id UUID;
|
||||
@@ -104,6 +105,7 @@ FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change();
|
||||
CREATE OR REPLACE FUNCTION morbac.refresh_on_hierarchy_change()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM morbac.refresh_hierarchy_cache();
|
||||
@@ -123,3 +125,22 @@ 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();
|
||||
|
||||
+13
-6
@@ -10,6 +10,11 @@
|
||||
-- - Each rule has an optional integer priority (NULL = 0, lowest)
|
||||
-- - When both a prohibition and a permission apply, the higher-priority rule wins
|
||||
-- - Tie goes to prohibition (modality precedence from the Multi-OrBAC paper)
|
||||
--
|
||||
-- Scope:
|
||||
-- - rules.scope controls which orgs a rule covers (self/subtree/descendants/...).
|
||||
-- Evaluated at query time via org_in_scope() — new orgs are covered automatically.
|
||||
-- - cross_org_rules.source_org_id is always required: user must hold the role there.
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
||||
p_user_id UUID,
|
||||
@@ -20,6 +25,7 @@ CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_rule RECORD;
|
||||
@@ -30,11 +36,11 @@ BEGIN
|
||||
FOR v_rule IN
|
||||
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||
FROM morbac.rules r
|
||||
WHERE r.org_id = p_org_id
|
||||
WHERE morbac.org_in_scope(p_org_id, r.org_id, r.scope)
|
||||
AND r.modality = 'prohibition'
|
||||
AND r.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||
AND r.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id))
|
||||
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, r.org_id))
|
||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||
ORDER BY COALESCE(r.priority, 0) DESC
|
||||
LOOP
|
||||
@@ -52,7 +58,7 @@ BEGIN
|
||||
AND cr.modality = 'prohibition'
|
||||
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
||||
ORDER BY COALESCE(cr.priority, 0) DESC
|
||||
LOOP
|
||||
@@ -68,11 +74,11 @@ BEGIN
|
||||
FOR v_rule IN
|
||||
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||
FROM morbac.rules r
|
||||
WHERE r.org_id = p_org_id
|
||||
WHERE morbac.org_in_scope(p_org_id, r.org_id, r.scope)
|
||||
AND r.modality = 'permission'
|
||||
AND r.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||
AND r.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id))
|
||||
AND r.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, r.org_id))
|
||||
AND morbac.is_rule_valid(r.valid_from, r.valid_until)
|
||||
ORDER BY COALESCE(r.priority, 0) DESC
|
||||
LOOP
|
||||
@@ -90,7 +96,7 @@ BEGIN
|
||||
AND cr.modality = 'permission'
|
||||
AND cr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity))
|
||||
AND cr.view IN (SELECT view FROM morbac.get_effective_views(p_view))
|
||||
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||
AND cr.role_id IN (SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id))
|
||||
AND morbac.is_rule_valid(cr.valid_from, cr.valid_until)
|
||||
ORDER BY COALESCE(cr.priority, 0) DESC
|
||||
LOOP
|
||||
@@ -129,6 +135,7 @@ CREATE OR REPLACE FUNCTION morbac.is_allowed(
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cached_result BOOLEAN;
|
||||
|
||||
+16
-3
@@ -8,14 +8,27 @@ CREATE TABLE morbac.config (
|
||||
COMMENT ON TABLE morbac.config IS 'Extension configuration - edit values to customize behavior';
|
||||
|
||||
INSERT INTO morbac.config (key, value, description) VALUES
|
||||
('cache_ttl_seconds', '300', 'Authorization cache time-to-live in seconds (default: 5 minutes)'),
|
||||
('hierarchy_max_depth', '10', 'Maximum depth for hierarchy traversal to prevent infinite loops'),
|
||||
('enable_audit_by_default', 'false', 'Whether to enable audit logging by default on installation');
|
||||
('cache_ttl_seconds', '300', 'Authorization cache time-to-live in seconds (default: 5 minutes)'),
|
||||
('hierarchy_max_depth', '10', 'Maximum depth for hierarchy traversal to prevent infinite loops'),
|
||||
('enable_audit_by_default', 'false', 'Whether to enable audit logging by default on installation'),
|
||||
-- System view names used in RLS policies on morbac tables.
|
||||
-- Override with morbac.set_config() to use your own naming conventions.
|
||||
-- The configured name must exist in morbac.views and your rules must reference it.
|
||||
('system_view.orgs', 'orgs', 'View name for morbac.orgs table access control'),
|
||||
('system_view.roles', 'roles', 'View name for morbac.roles table access control'),
|
||||
('system_view.rules', 'rules', 'View name for morbac.rules table access control'),
|
||||
('system_view.user_roles', 'user_roles', 'View name for morbac.user_roles table access control'),
|
||||
('system_view.contexts', 'contexts', 'View name for morbac.contexts table access control'),
|
||||
('system_view.activities', 'activities', 'View name for morbac.activities table access control'),
|
||||
('system_view.views', 'views', 'View name for morbac.views table access control'),
|
||||
('system_view.delegations', 'delegations', 'View name for morbac.delegations table access control'),
|
||||
('system_view.cross_org_rules', 'cross_org_rules', 'View name for morbac.cross_org_rules table access control');
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.get_config(p_key TEXT)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_value TEXT;
|
||||
|
||||
@@ -38,6 +38,7 @@ CREATE OR REPLACE FUNCTION morbac.eval_context(p_context_id UUID)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_evaluator REGPROC;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
-- Rules that grant access across organization boundaries
|
||||
-- Rules that grant access across organization boundaries.
|
||||
--
|
||||
-- source_org_id: the org where the user must hold role_id.
|
||||
-- target_org_id: the org where the resource resides.
|
||||
|
||||
CREATE TABLE morbac.cross_org_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -24,5 +27,5 @@ CREATE INDEX idx_cross_org_rules_target ON morbac.cross_org_rules(target_org_id)
|
||||
CREATE INDEX idx_cross_org_rules_temporal ON morbac.cross_org_rules(valid_from, valid_until);
|
||||
|
||||
COMMENT ON TABLE morbac.cross_org_rules IS 'Inter-organizational rules for cross-org access';
|
||||
COMMENT ON COLUMN morbac.cross_org_rules.source_org_id IS 'Organization where user has role';
|
||||
COMMENT ON COLUMN morbac.cross_org_rules.target_org_id IS 'Organization where resource resides';
|
||||
COMMENT ON COLUMN morbac.cross_org_rules.source_org_id IS 'Org where user holds the role';
|
||||
COMMENT ON COLUMN morbac.cross_org_rules.target_org_id IS 'Org where the resource resides';
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
-- Transitive closure and effective role/activity/view lookup functions
|
||||
--
|
||||
-- All functions that query morbac tables are SECURITY DEFINER so they run
|
||||
-- as the extension owner and bypass RLS on morbac tables. This prevents
|
||||
-- infinite recursion when RLS policies call is_allowed(), which in turn
|
||||
-- calls these functions.
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.get_org_ancestors(p_org_id UUID)
|
||||
RETURNS TABLE(org_id UUID, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -26,6 +32,7 @@ CREATE OR REPLACE FUNCTION morbac.get_org_descendants(p_org_id UUID)
|
||||
RETURNS TABLE(org_id UUID, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -64,6 +71,7 @@ CREATE OR REPLACE FUNCTION morbac.get_org_scope(
|
||||
RETURNS TABLE(org_id UUID, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
CASE p_scope
|
||||
@@ -131,6 +139,7 @@ CREATE OR REPLACE FUNCTION morbac.get_effective_roles(p_user_id UUID, p_org_id U
|
||||
RETURNS TABLE(role_id UUID, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -157,6 +166,7 @@ CREATE OR REPLACE FUNCTION morbac.get_inherited_roles(p_role_id UUID)
|
||||
RETURNS TABLE(role_id UUID, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -180,6 +190,7 @@ CREATE OR REPLACE FUNCTION morbac.get_effective_activities(p_activity TEXT)
|
||||
RETURNS TABLE(activity TEXT, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -203,6 +214,7 @@ CREATE OR REPLACE FUNCTION morbac.get_effective_views(p_view TEXT)
|
||||
RETURNS TABLE(view TEXT, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -226,6 +238,7 @@ CREATE OR REPLACE FUNCTION morbac.get_comprehensive_roles(p_user_id UUID, p_org_
|
||||
RETURNS TABLE(role_id UUID, source TEXT, depth INTEGER)
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
@@ -303,6 +316,7 @@ CREATE OR REPLACE FUNCTION morbac.eval_derived_role(
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_result BOOLEAN;
|
||||
@@ -317,3 +331,29 @@ $$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.eval_derived_role(REGPROC, UUID, UUID) IS
|
||||
'Evaluates a derived role condition function';
|
||||
|
||||
-- Check if p_target_org_id falls within the scope of p_rule_org_id.
|
||||
-- Used by the authorization engine to evaluate scoped rules at query time.
|
||||
CREATE OR REPLACE FUNCTION morbac.org_in_scope(
|
||||
p_target_org_id UUID,
|
||||
p_rule_org_id UUID,
|
||||
p_scope TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF p_scope = 'self' THEN
|
||||
RETURN p_target_org_id = p_rule_org_id;
|
||||
END IF;
|
||||
RETURN EXISTS(
|
||||
SELECT 1 FROM morbac.get_org_scope(p_rule_org_id, p_scope)
|
||||
WHERE org_id = p_target_org_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.org_in_scope(UUID, UUID, TEXT) IS
|
||||
'Returns TRUE if p_target_org_id is within get_org_scope(p_rule_org_id, p_scope). SECURITY DEFINER to bypass RLS on morbac.orgs.';
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
-- Simplified policy declaration using names instead of UUIDs
|
||||
|
||||
CREATE TABLE morbac.policy (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_name TEXT NOT NULL,
|
||||
role_name TEXT NOT NULL,
|
||||
activity TEXT NOT NULL,
|
||||
view TEXT NOT NULL,
|
||||
modality morbac.modality NOT NULL,
|
||||
context_name TEXT NOT NULL DEFAULT 'always',
|
||||
priority INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
compiled BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- NULL priority treated as -1 so two NULL-priority rows for the same tuple conflict
|
||||
CREATE UNIQUE INDEX idx_policy_unique
|
||||
ON morbac.policy(org_name, role_name, activity, view, modality, context_name, COALESCE(priority, -1));
|
||||
|
||||
CREATE INDEX idx_policy_not_compiled ON morbac.policy(compiled) WHERE NOT compiled;
|
||||
|
||||
COMMENT ON TABLE morbac.policy IS 'Policy DSL - simplified policy declaration using names';
|
||||
COMMENT ON COLUMN morbac.policy.compiled IS 'Whether this policy entry has been compiled into rules';
|
||||
COMMENT ON COLUMN morbac.policy.priority IS 'Optional rule priority (higher wins over lower; NULL = 0)';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.is_admin_allowed(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_admin_activity TEXT,
|
||||
p_admin_target TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_rule RECORD;
|
||||
BEGIN
|
||||
FOR v_rule IN
|
||||
SELECT ar.context_id
|
||||
FROM morbac.admin_rules ar
|
||||
WHERE ar.org_id = p_org_id
|
||||
AND ar.admin_activity = p_admin_activity
|
||||
AND ar.admin_target = p_admin_target
|
||||
AND ar.modality = 'prohibition'
|
||||
AND ar.role_id IN (
|
||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||
)
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
FOR v_rule IN
|
||||
SELECT ar.context_id
|
||||
FROM morbac.admin_rules ar
|
||||
WHERE ar.org_id = p_org_id
|
||||
AND ar.admin_activity = p_admin_activity
|
||||
AND ar.admin_target = p_admin_target
|
||||
AND ar.modality = 'permission'
|
||||
AND ar.role_id IN (
|
||||
SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id)
|
||||
)
|
||||
LOOP
|
||||
IF morbac.eval_context(v_rule.context_id) THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.is_admin_allowed(UUID, UUID, TEXT, TEXT) IS
|
||||
'Checks administration permissions for policy management operations';
|
||||
|
||||
-- Translates policy DSL entries into concrete rules by resolving names to IDs.
|
||||
-- Idempotent - safe to run multiple times.
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.compile_policy()
|
||||
RETURNS TABLE(
|
||||
compiled_count INTEGER,
|
||||
error_count INTEGER,
|
||||
errors TEXT[]
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_policy RECORD;
|
||||
v_org_id UUID;
|
||||
v_role_id UUID;
|
||||
v_context_id UUID;
|
||||
v_compiled INTEGER := 0;
|
||||
v_errors TEXT[] := ARRAY[]::TEXT[];
|
||||
v_error_count INTEGER := 0;
|
||||
BEGIN
|
||||
FOR v_policy IN
|
||||
SELECT * FROM morbac.policy WHERE NOT compiled
|
||||
LOOP
|
||||
BEGIN
|
||||
SELECT id INTO STRICT v_org_id
|
||||
FROM morbac.orgs
|
||||
WHERE name = v_policy.org_name;
|
||||
|
||||
SELECT id INTO STRICT v_role_id
|
||||
FROM morbac.roles
|
||||
WHERE org_id = v_org_id AND name = v_policy.role_name;
|
||||
|
||||
SELECT id INTO STRICT v_context_id
|
||||
FROM morbac.contexts
|
||||
WHERE name = v_policy.context_name;
|
||||
|
||||
INSERT INTO morbac.activities (name)
|
||||
VALUES (v_policy.activity)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
INSERT INTO morbac.views (name)
|
||||
VALUES (v_policy.view)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, priority)
|
||||
VALUES (v_org_id, v_role_id, v_policy.activity, v_policy.view, v_context_id, v_policy.modality, v_policy.priority)
|
||||
ON CONFLICT (org_id, role_id, activity, view, context_id, modality) DO NOTHING;
|
||||
|
||||
UPDATE morbac.policy SET compiled = TRUE WHERE id = v_policy.id;
|
||||
|
||||
v_compiled := v_compiled + 1;
|
||||
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
v_error_count := v_error_count + 1;
|
||||
v_errors := array_append(v_errors,
|
||||
format('Policy %s: %s', v_policy.id, SQLERRM));
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
RETURN QUERY SELECT v_compiled, v_error_count, v_errors;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.compile_policy() IS
|
||||
'Compiles policy DSL entries into concrete rules - idempotent and safe to run multiple times';
|
||||
@@ -77,6 +77,7 @@ CREATE OR REPLACE FUNCTION morbac.get_user_orgs(p_user_id UUID)
|
||||
RETURNS TABLE(org_id UUID)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
SELECT DISTINCT ur.org_id
|
||||
FROM morbac.user_roles ur
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- assign_role / revoke_role: convenience wrappers that enforce SoD and
|
||||
-- cardinality constraints. Access control is handled by RLS on morbac.user_roles.
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.assign_role(
|
||||
p_target_user_id UUID,
|
||||
p_role_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF morbac.check_sod_violation(p_target_user_id, p_role_id, p_org_id) THEN
|
||||
RAISE EXCEPTION 'Role assignment would violate Separation of Duty constraints';
|
||||
END IF;
|
||||
|
||||
-- INSERT triggers RLS policy on morbac.user_roles (create on user_roles view)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
VALUES (p_target_user_id, p_role_id, p_org_id)
|
||||
ON CONFLICT (user_id, role_id, org_id) DO NOTHING;
|
||||
|
||||
DECLARE
|
||||
v_error TEXT;
|
||||
BEGIN
|
||||
v_error := morbac.check_cardinality_violation(p_role_id);
|
||||
IF v_error IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Role assignment violates cardinality constraint: %', v_error;
|
||||
END IF;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.assign_role(UUID, UUID, UUID) IS
|
||||
'Assign role with SoD and cardinality validation. RLS on morbac.user_roles enforces authorization.';
|
||||
|
||||
CREATE OR REPLACE FUNCTION morbac.revoke_role(
|
||||
p_target_user_id UUID,
|
||||
p_role_id UUID,
|
||||
p_org_id UUID
|
||||
)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- DELETE triggers RLS policy on morbac.user_roles (delete on user_roles view)
|
||||
DELETE FROM morbac.user_roles
|
||||
WHERE user_id = p_target_user_id
|
||||
AND role_id = p_role_id
|
||||
AND org_id = p_org_id;
|
||||
|
||||
DECLARE
|
||||
v_error TEXT;
|
||||
BEGIN
|
||||
v_error := morbac.check_cardinality_violation(p_role_id, FALSE);
|
||||
IF v_error IS NOT NULL THEN
|
||||
RAISE WARNING 'Role revocation may violate cardinality constraint: %', v_error;
|
||||
END IF;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION morbac.revoke_role(UUID, UUID, UUID) IS
|
||||
'Revoke role with cardinality validation. RLS on morbac.user_roles enforces authorization.';
|
||||
+16
-2
@@ -1,4 +1,15 @@
|
||||
-- Core OrBAC rule relation: Rule(org, role, activity, view, context, modality)
|
||||
--
|
||||
-- scope controls which orgs this rule covers relative to org_id:
|
||||
-- 'self' — exact org only (default, current behavior)
|
||||
-- 'subtree' — org + all descendants
|
||||
-- 'descendants' — all descendants, excluding self
|
||||
-- 'children' — direct children only
|
||||
-- 'parent' — direct parent only
|
||||
-- 'ancestors' — all ancestors, excluding self
|
||||
-- 'lineage' — self + all ancestors
|
||||
-- 'root' — topmost ancestor only
|
||||
-- Evaluated at query time via org_in_scope() — new orgs are picked up automatically.
|
||||
|
||||
CREATE TABLE morbac.rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -8,14 +19,16 @@ CREATE TABLE morbac.rules (
|
||||
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
||||
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
||||
modality morbac.modality NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'self',
|
||||
priority INTEGER,
|
||||
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)
|
||||
UNIQUE(org_id, role_id, activity, view, context_id, modality, scope),
|
||||
CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from),
|
||||
CHECK (scope IN ('self', 'children', 'descendants', 'subtree', 'parent', 'ancestors', 'lineage', 'root'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rules_org_role ON morbac.rules(org_id, role_id);
|
||||
@@ -27,6 +40,7 @@ INCLUDE (role_id, context_id)
|
||||
WHERE is_active = true;
|
||||
|
||||
COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation';
|
||||
COMMENT ON COLUMN morbac.rules.scope IS 'Org scope: self (default), subtree, descendants, children, parent, ancestors, lineage, root. Evaluated at query time — new orgs are covered automatically.';
|
||||
COMMENT ON COLUMN morbac.rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation';
|
||||
COMMENT ON COLUMN morbac.rules.priority IS 'Optional rule priority (higher wins). NULL = 0. A permission with higher priority than a prohibition overrides it.';
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
-- RLS policies for morbac system tables.
|
||||
--
|
||||
-- is_allowed() and its internal callees are SECURITY DEFINER, so they run as
|
||||
-- the extension owner and bypass RLS. This prevents infinite recursion when
|
||||
-- these policies fire.
|
||||
--
|
||||
-- View names used in rules are config-driven (system_view.*).
|
||||
-- Override with morbac.set_config('system_view.orgs', 'my_orgs') etc.
|
||||
-- The new name must exist in morbac.views and your rules must reference it.
|
||||
--
|
||||
-- RLS is disabled for the superuser/extension owner by default (PostgreSQL
|
||||
-- behavior). Bootstrap the first organization and superuser role as the
|
||||
-- database owner before enabling this in production.
|
||||
|
||||
-- morbac.orgs
|
||||
-- Row's org context is the org itself.
|
||||
ALTER TABLE morbac.orgs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY orgs_select ON morbac.orgs FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), id, 'read',
|
||||
morbac.get_config('system_view.orgs')));
|
||||
|
||||
CREATE POLICY orgs_insert ON morbac.orgs FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'create',
|
||||
morbac.get_config('system_view.orgs')));
|
||||
|
||||
CREATE POLICY orgs_update ON morbac.orgs FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), id, 'update',
|
||||
morbac.get_config('system_view.orgs')));
|
||||
|
||||
CREATE POLICY orgs_delete ON morbac.orgs FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), id, 'delete',
|
||||
morbac.get_config('system_view.orgs')));
|
||||
|
||||
-- morbac.roles
|
||||
ALTER TABLE morbac.roles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY roles_select ON morbac.roles FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'read',
|
||||
morbac.get_config('system_view.roles')));
|
||||
|
||||
CREATE POLICY roles_insert ON morbac.roles FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), org_id, 'create',
|
||||
morbac.get_config('system_view.roles')));
|
||||
|
||||
CREATE POLICY roles_update ON morbac.roles FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'update',
|
||||
morbac.get_config('system_view.roles')));
|
||||
|
||||
CREATE POLICY roles_delete ON morbac.roles FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
||||
morbac.get_config('system_view.roles')));
|
||||
|
||||
-- morbac.rules
|
||||
ALTER TABLE morbac.rules ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY rules_select ON morbac.rules FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'read',
|
||||
morbac.get_config('system_view.rules')));
|
||||
|
||||
CREATE POLICY rules_insert ON morbac.rules FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), org_id, 'create',
|
||||
morbac.get_config('system_view.rules')));
|
||||
|
||||
CREATE POLICY rules_update ON morbac.rules FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'update',
|
||||
morbac.get_config('system_view.rules')));
|
||||
|
||||
CREATE POLICY rules_delete ON morbac.rules FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
||||
morbac.get_config('system_view.rules')));
|
||||
|
||||
-- morbac.user_roles
|
||||
ALTER TABLE morbac.user_roles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY user_roles_select ON morbac.user_roles FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'read',
|
||||
morbac.get_config('system_view.user_roles')));
|
||||
|
||||
CREATE POLICY user_roles_insert ON morbac.user_roles FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), org_id, 'create',
|
||||
morbac.get_config('system_view.user_roles')));
|
||||
|
||||
CREATE POLICY user_roles_delete ON morbac.user_roles FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
||||
morbac.get_config('system_view.user_roles')));
|
||||
|
||||
-- morbac.contexts (global — use current session org for writes)
|
||||
ALTER TABLE morbac.contexts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY contexts_select ON morbac.contexts FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'read',
|
||||
morbac.get_config('system_view.contexts')));
|
||||
|
||||
CREATE POLICY contexts_insert ON morbac.contexts FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'create',
|
||||
morbac.get_config('system_view.contexts')));
|
||||
|
||||
CREATE POLICY contexts_update ON morbac.contexts FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'update',
|
||||
morbac.get_config('system_view.contexts')));
|
||||
|
||||
CREATE POLICY contexts_delete ON morbac.contexts FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
|
||||
morbac.get_config('system_view.contexts')));
|
||||
|
||||
-- morbac.activities (global — use current session org for writes)
|
||||
ALTER TABLE morbac.activities ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY activities_select ON morbac.activities FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'read',
|
||||
morbac.get_config('system_view.activities')));
|
||||
|
||||
CREATE POLICY activities_insert ON morbac.activities FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'create',
|
||||
morbac.get_config('system_view.activities')));
|
||||
|
||||
CREATE POLICY activities_update ON morbac.activities FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'update',
|
||||
morbac.get_config('system_view.activities')));
|
||||
|
||||
CREATE POLICY activities_delete ON morbac.activities FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
|
||||
morbac.get_config('system_view.activities')));
|
||||
|
||||
-- morbac.views (global — use current session org for writes)
|
||||
ALTER TABLE morbac.views ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY views_select ON morbac.views FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'read',
|
||||
morbac.get_config('system_view.views')));
|
||||
|
||||
CREATE POLICY views_insert ON morbac.views FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'create',
|
||||
morbac.get_config('system_view.views')));
|
||||
|
||||
CREATE POLICY views_update ON morbac.views FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'update',
|
||||
morbac.get_config('system_view.views')));
|
||||
|
||||
CREATE POLICY views_delete ON morbac.views FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
|
||||
morbac.get_config('system_view.views')));
|
||||
|
||||
-- morbac.delegations
|
||||
ALTER TABLE morbac.delegations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY delegations_select ON morbac.delegations FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'read',
|
||||
morbac.get_config('system_view.delegations')));
|
||||
|
||||
CREATE POLICY delegations_insert ON morbac.delegations FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), org_id, 'create',
|
||||
morbac.get_config('system_view.delegations')));
|
||||
|
||||
CREATE POLICY delegations_update ON morbac.delegations FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'update',
|
||||
morbac.get_config('system_view.delegations')));
|
||||
|
||||
CREATE POLICY delegations_delete ON morbac.delegations FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
||||
morbac.get_config('system_view.delegations')));
|
||||
|
||||
-- morbac.cross_org_rules
|
||||
ALTER TABLE morbac.cross_org_rules ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY cross_org_rules_select ON morbac.cross_org_rules FOR SELECT
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), source_org_id, 'read',
|
||||
morbac.get_config('system_view.cross_org_rules')));
|
||||
|
||||
CREATE POLICY cross_org_rules_insert ON morbac.cross_org_rules FOR INSERT
|
||||
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), source_org_id, 'create',
|
||||
morbac.get_config('system_view.cross_org_rules')));
|
||||
|
||||
CREATE POLICY cross_org_rules_update ON morbac.cross_org_rules FOR UPDATE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), source_org_id, 'update',
|
||||
morbac.get_config('system_view.cross_org_rules')));
|
||||
|
||||
CREATE POLICY cross_org_rules_delete ON morbac.cross_org_rules FOR DELETE
|
||||
USING (morbac.is_allowed(morbac.current_user_id(), source_org_id, 'delete',
|
||||
morbac.get_config('system_view.cross_org_rules')));
|
||||
@@ -23,3 +23,17 @@ CREATE INDEX idx_view_hierarchy_junior ON morbac.view_hierarchy(junior_view);
|
||||
COMMENT ON TABLE morbac.view_hierarchy IS 'View hierarchy - senior views inherit from junior views';
|
||||
COMMENT ON COLUMN morbac.view_hierarchy.senior_view IS 'Senior view (more specific)';
|
||||
COMMENT ON COLUMN morbac.view_hierarchy.junior_view IS 'Junior view (more general)';
|
||||
|
||||
-- Default system view names — match system_view.* config keys.
|
||||
-- Override config values to rename; the new name must be seeded here too.
|
||||
INSERT INTO morbac.views (name, description) VALUES
|
||||
('orgs', 'Organizations table'),
|
||||
('roles', 'Roles table'),
|
||||
('rules', 'Authorization rules table'),
|
||||
('user_roles', 'User-role assignments table'),
|
||||
('contexts', 'Rule contexts table'),
|
||||
('activities', 'Activities table'),
|
||||
('views', 'Views table'),
|
||||
('delegations', 'Role delegations table'),
|
||||
('cross_org_rules', 'Cross-organization rules table')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
Reference in New Issue
Block a user