* Issue 2429 indicates the possiblity of an open redirect
The 404 processing ends up redirecting a request with multiple path
slashes to that site, i.e.
https://my-site//shedcode.co.uk will redirect to https://shedcode.co.uk
This commit uses a regular expression to remove the multiple leading
slashes before redirecting.
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
Simplified Action by moving takes_child/takes_parent logic to Resource
- Removed InstanceResource - global actions are now simply those with resource_class=None
- Resource.parent_class - Replaced parent_name: str with parent_class: type[Resource] | None for direct class references
- Simplified Action dataclass - No more redundant fields, everything is derived from the Resource class structure
- Validation - The __init_subclass__ method now checks parent_class.parent_class to enforce the 2-level hierarchy
Closes#2563
* Add keyset pagination to allowed_resources()
This replaces the unbounded list return with PaginatedResources,
which supports efficient keyset pagination for handling thousands
of resources.
Closes#2560
Changes:
- allowed_resources() now returns PaginatedResources instead of list
- Added limit (1-1000, default 100) and next (keyset token) parameters
- Added include_reasons parameter (replaces allowed_resources_with_reasons)
- Removed allowed_resources_with_reasons() method entirely
- PaginatedResources.all() async generator for automatic pagination
- Uses tilde-encoding for tokens (matching table pagination)
- Updated all callers to use .resources accessor
- Updated documentation with new API and examples
The PaginatedResources object has:
- resources: List of Resource objects for current page
- next: Token for next page (None if no more results)
- all(): Async generator that yields all resources across pages
Example usage:
page = await ds.allowed_resources("view-table", actor, limit=100)
for table in page.resources:
print(table.child)
# Iterate all pages automatically
async for table in page.all():
print(table.child)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>