-- ============================================================================= -- POLICY DSL TABLE -- ============================================================================= -- Simplified table for developers to declare policy -- Uses friendly 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', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), compiled BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE(org_name, role_name, activity, view, modality, context_name) ); 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.org_name IS 'Organization name (resolved during compilation)'; COMMENT ON COLUMN morbac.policy.role_name IS 'Role name (resolved during compilation)'; COMMENT ON COLUMN morbac.policy.activity IS 'Activity name'; COMMENT ON COLUMN morbac.policy.view IS 'View name'; COMMENT ON COLUMN morbac.policy.modality IS 'Deontic modality'; COMMENT ON COLUMN morbac.policy.context_name IS 'Context name (default: always)'; COMMENT ON COLUMN morbac.policy.compiled IS 'Whether this policy entry has been compiled into rules'; -- Helper function to check administration permissions 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 -- Check for admin prohibitions first 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; -- Check for admin permissions 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; -- Default deny END; $$; COMMENT ON FUNCTION morbac.is_admin_allowed(UUID, UUID, TEXT, TEXT) IS 'Checks administration permissions for policy management operations'; -- ============================================================================= -- POLICY COMPILER -- ============================================================================= -- Translates policy DSL entries into concrete rules -- Resolves 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 -- Process all uncompiled policy entries FOR v_policy IN SELECT * FROM morbac.policy WHERE NOT compiled LOOP BEGIN -- Resolve organization SELECT id INTO STRICT v_org_id FROM morbac.orgs WHERE name = v_policy.org_name; -- Resolve role within organization SELECT id INTO STRICT v_role_id FROM morbac.roles WHERE org_id = v_org_id AND name = v_policy.role_name; -- Resolve context SELECT id INTO STRICT v_context_id FROM morbac.contexts WHERE name = v_policy.context_name; -- Ensure activity exists INSERT INTO morbac.activities (name) VALUES (v_policy.activity) ON CONFLICT (name) DO NOTHING; -- Ensure view exists INSERT INTO morbac.views (name) VALUES (v_policy.view) ON CONFLICT (name) DO NOTHING; -- Insert rule (ignore if already exists) INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality) VALUES (v_org_id, v_role_id, v_policy.activity, v_policy.view, v_context_id, v_policy.modality) ON CONFLICT (org_id, role_id, activity, view, context_id, modality) DO NOTHING; -- Mark as compiled 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';