New PermissionSQL.restriction_sql mechanism for actor restrictions

Implement INTERSECT-based actor restrictions to prevent permission bypass

Actor restrictions are now implemented as SQL filters using INTERSECT rather
than as deny/allow permission rules. This ensures restrictions act as hard
limits that cannot be overridden by other permission plugins or config blocks.

Previously, actor restrictions (_r in actor dict) were implemented by 
generating permission rules with deny/allow logic. This approach had a 
critical flaw: database-level config allow blocks could bypass table-level 
restrictions, granting access to tables not in the actor's allowlist.

The new approach separates concerns:

- Permission rules determine what's allowed based on config and plugins
- Restriction filters limit the result set to only allowlisted resources
- Restrictions use INTERSECT to ensure all restriction criteria are met
- Database-level restrictions (parent, NULL) properly match all child tables

Implementation details:

- Added restriction_sql field to PermissionSQL dataclass
- Made PermissionSQL.sql optional to support restriction-only plugins
- Updated actor_restrictions_sql() to return restriction filters instead of rules
- Modified SQL builders to apply restrictions via INTERSECT and EXISTS clauses

Closes #2572
This commit is contained in:
Simon Willison 2025-11-03 14:17:51 -08:00 committed by GitHub
commit 18fd373a8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 760 additions and 113 deletions

View file

@ -1630,6 +1630,16 @@ async def test_hook_register_actions_with_custom_resources():
reason="user2 granted view-document-collection"
)
# Default allow for view-document-collection (like other view-* actions)
if action == "view-document-collection":
return PermissionSQL.allow(
reason="default allow for view-document-collection"
)
# Default allow for view-document (like other view-* actions)
if action == "view-document":
return PermissionSQL.allow(reason="default allow for view-document")
# Register the plugin temporarily
plugin = TestPlugin()
pm.register(plugin, name="test_custom_resources_plugin")