@@ -103,13 +103,49 @@ def _maintain_privilege_sql(role_expression: str) -> str:
103103 )
104104
105105
106+ def _privileged_outbox_view_sql (role_expression : str ) -> str :
107+ """Build a probe for caller-selectable views that can bypass outbox RLS."""
108+ return (
109+ "EXISTS ("
110+ "SELECT 1 FROM pg_catalog.pg_class AS exposed_view "
111+ "JOIN pg_catalog.pg_namespace AS exposed_view_schema "
112+ "ON exposed_view_schema.oid OPERATOR(pg_catalog.=) exposed_view.relnamespace "
113+ "JOIN pg_catalog.pg_roles AS exposed_view_owner "
114+ "ON exposed_view_owner.oid OPERATOR(pg_catalog.=) exposed_view.relowner "
115+ "JOIN pg_catalog.pg_rewrite AS exposed_view_rule "
116+ "ON exposed_view_rule.ev_class OPERATOR(pg_catalog.=) exposed_view.oid "
117+ "JOIN pg_catalog.pg_depend AS exposed_view_dependency "
118+ "ON exposed_view_dependency.classid OPERATOR(pg_catalog.=) "
119+ "'pg_catalog.pg_rewrite'::pg_catalog.regclass "
120+ "AND exposed_view_dependency.objid OPERATOR(pg_catalog.=) exposed_view_rule.oid "
121+ "AND exposed_view_dependency.refclassid OPERATOR(pg_catalog.=) "
122+ "'pg_catalog.pg_class'::pg_catalog.regclass "
123+ "AND exposed_view_dependency.refobjid OPERATOR(pg_catalog.=) admitted_relation.oid "
124+ "WHERE exposed_view.relkind::pg_catalog.text OPERATOR(pg_catalog.=) 'v' "
125+ "AND exposed_view_schema.nspname NOT LIKE 'pg\\ _%' ESCAPE '\\ ' "
126+ "AND exposed_view_schema.nspname OPERATOR(pg_catalog.<>) 'information_schema' "
127+ "AND NOT COALESCE("
128+ "exposed_view.reloptions OPERATOR(pg_catalog.@>) "
129+ "ARRAY['security_invoker=true']::pg_catalog.text[], false) "
130+ "AND pg_catalog.has_schema_privilege("
131+ f"{ role_expression } , exposed_view_schema.oid, 'USAGE') "
132+ "AND pg_catalog.has_table_privilege("
133+ f"{ role_expression } , exposed_view.oid, 'SELECT') "
134+ "AND pg_catalog.has_any_column_privilege("
135+ "exposed_view_owner.oid, admitted_relation.oid, 'SELECT') "
136+ "AND (exposed_view_owner.rolsuper OR exposed_view_owner.rolbypassrls)"
137+ ")"
138+ )
139+
140+
106141def _require_rls_application_role (cursor : Any ) -> None :
107142 """Reject unsafe runtime roles or drifted canonical RLS policy authority."""
108143 maintain_selectable = _maintain_privilege_sql ("selectable_role.oid" )
109144 maintain_delegated_dml = _maintain_privilege_sql ("delegated_dml_role.oid" )
110145 maintain_definer = _maintain_privilege_sql ("definer_role.oid" )
111146 maintain_definer_admin = _maintain_privilege_sql ("definer_admin_role.oid" )
112147 maintain_definer_admin_set = _maintain_privilege_sql ("definer_admin_set_role.oid" )
148+ privileged_outbox_view = _privileged_outbox_view_sql ("selectable_role.oid" )
113149 cursor .execute (
114150 "SELECT admitted_role.rolsuper "
115151 "OR NOT admitted_relation.relrowsecurity "
@@ -310,6 +346,8 @@ def _require_rls_application_role(cursor: Any) -> None:
310346 "definer_admin_set_role.oid, admitted_relation.oid, 'TRIGGER')))"
311347 "))"
312348 ")) "
349+ "OR "
350+ f"{ privileged_outbox_view } "
313351 "OR pg_catalog.has_any_column_privilege("
314352 "selectable_role.oid, admitted_relation.oid, 'SELECT WITH GRANT OPTION') "
315353 "OR pg_catalog.has_any_column_privilege("
@@ -657,15 +695,15 @@ def load_in_transaction(
657695 can return to application code. Both the effective ``CURRENT_USER`` and the
658696 authenticated ``SESSION_USER`` role-selection closure must remain ordinary RLS
659697 subjects without outbox-owner, destructive, replication, database/role
660- administration, delegable DML, relation-programming, or executable privileged
661- user-schema ``SECURITY DEFINER`` authority, while the canonical relation still
662- has RLS enabled and forced with the sole reviewed tenant policy semantics. The
663- live admission is checked before tenant state is bound or durable rows are
664- touched. Security-critical function, relation, and policy authority is
665- explicitly schema-qualified, and ``ONLY`` prevents inherited relations from
666- widening the canonical durable row source if an inheritance edge appears after
667- migration admission. The outbox does not mutate or inherit the caller
668- transaction's ``search_path``.
698+ administration, delegable DML, relation-programming, caller-selectable
699+ privileged-view, or executable privileged user-schema ``SECURITY DEFINER``
700+ authority, while the canonical relation still has RLS enabled and forced with
701+ the sole reviewed tenant policy semantics. The live admission is checked before
702+ tenant state is bound or durable rows are touched. Security-critical function,
703+ relation, and policy authority is explicitly schema-qualified, and ``ONLY``
704+ prevents inherited relations from widening the canonical durable row source if
705+ an inheritance edge appears after migration admission. The outbox does not
706+ mutate or inherit the caller transaction's ``search_path``.
669707 """
670708 if type (for_update ) is not bool :
671709 raise ValidationError (
0 commit comments