mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Implements a new ensure_permission() method that is a convenience wrapper around allowed() that raises Forbidden instead of returning False. Changes: - Added ensure_permission() method to datasette/app.py - Updated all views to use ensure_permission() instead of the pattern: if not await self.ds.allowed(...): raise Forbidden(...) - Updated docs/internals.rst to document the new method - Removed old ensure_permissions() documentation (that method was already removed) The new method simplifies permission enforcement in views and makes the code more concise and consistent.
This commit is contained in:
parent
6df364cb2c
commit
fabcfd68ad
5 changed files with 74 additions and 45 deletions
|
|
@ -1310,6 +1310,39 @@ class Datasette:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def ensure_permission(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
action: str,
|
||||||
|
resource: "Resource" = None,
|
||||||
|
actor: dict | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Check if actor can perform action on resource, raising Forbidden if not.
|
||||||
|
|
||||||
|
This is a convenience wrapper around allowed() that raises Forbidden
|
||||||
|
instead of returning False. Use this when you want to enforce a permission
|
||||||
|
check and halt execution if it fails.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from datasette.resources import TableResource
|
||||||
|
|
||||||
|
# Will raise Forbidden if actor cannot view the table
|
||||||
|
await datasette.ensure_permission(
|
||||||
|
action="view-table",
|
||||||
|
resource=TableResource(database="analytics", table="users"),
|
||||||
|
actor=request.actor
|
||||||
|
)
|
||||||
|
|
||||||
|
# For instance-level actions, resource can be omitted:
|
||||||
|
await datasette.ensure_permission(
|
||||||
|
action="permissions-debug",
|
||||||
|
actor=request.actor
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
if not await self.allowed(action=action, resource=resource, actor=actor):
|
||||||
|
raise Forbidden(action)
|
||||||
|
|
||||||
async def execute(
|
async def execute(
|
||||||
self,
|
self,
|
||||||
db_name,
|
db_name,
|
||||||
|
|
|
||||||
|
|
@ -370,12 +370,11 @@ async def database_download(request, datasette):
|
||||||
from datasette.resources import DatabaseResource
|
from datasette.resources import DatabaseResource
|
||||||
|
|
||||||
database = tilde_decode(request.url_vars["database"])
|
database = tilde_decode(request.url_vars["database"])
|
||||||
if not await datasette.allowed(
|
await datasette.ensure_permission(
|
||||||
action="view-database-download",
|
action="view-database-download",
|
||||||
resource=DatabaseResource(database=database),
|
resource=DatabaseResource(database=database),
|
||||||
actor=request.actor,
|
actor=request.actor,
|
||||||
):
|
)
|
||||||
raise Forbidden("view-database-download")
|
|
||||||
try:
|
try:
|
||||||
db = datasette.get_database(route=database)
|
db = datasette.get_database(route=database)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
@ -544,12 +543,11 @@ class QueryView(View):
|
||||||
raise Forbidden("You do not have permission to view this query")
|
raise Forbidden("You do not have permission to view this query")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not await datasette.allowed(
|
await datasette.ensure_permission(
|
||||||
action="execute-sql",
|
action="execute-sql",
|
||||||
resource=DatabaseResource(database=database),
|
resource=DatabaseResource(database=database),
|
||||||
actor=request.actor,
|
actor=request.actor,
|
||||||
):
|
)
|
||||||
raise Forbidden("execute-sql")
|
|
||||||
|
|
||||||
# Flattened because of ?sql=&name1=value1&name2=value2 feature
|
# Flattened because of ?sql=&name1=value1&name2=value2 feature
|
||||||
params = {key: request.args.get(key) for key in request.args}
|
params = {key: request.args.get(key) for key in request.args}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,7 @@ class IndexView(BaseView):
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
as_format = request.url_vars["format"]
|
as_format = request.url_vars["format"]
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
|
||||||
|
|
||||||
# Get all allowed databases and tables in bulk
|
# Get all allowed databases and tables in bulk
|
||||||
allowed_databases = await self.ds.allowed_resources(
|
allowed_databases = await self.ds.allowed_resources(
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ class JsonDataView(BaseView):
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
if self.permission:
|
if self.permission:
|
||||||
if not await self.ds.allowed(action=self.permission, actor=request.actor):
|
await self.ds.ensure_permission(action=self.permission, actor=request.actor)
|
||||||
raise Forbidden(self.permission)
|
|
||||||
if self.needs_request:
|
if self.needs_request:
|
||||||
data = self.data_callback(request)
|
data = self.data_callback(request)
|
||||||
else:
|
else:
|
||||||
|
|
@ -55,8 +54,7 @@ class JsonDataView(BaseView):
|
||||||
|
|
||||||
class PatternPortfolioView(View):
|
class PatternPortfolioView(View):
|
||||||
async def get(self, request, datasette):
|
async def get(self, request, datasette):
|
||||||
if not await datasette.allowed(action="view-instance", actor=request.actor):
|
await datasette.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
|
||||||
return Response.html(
|
return Response.html(
|
||||||
await datasette.render_template(
|
await datasette.render_template(
|
||||||
"patterns.html",
|
"patterns.html",
|
||||||
|
|
@ -114,10 +112,8 @@ class PermissionsDebugView(BaseView):
|
||||||
has_json_alternate = False
|
has_json_alternate = False
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
await self.ds.ensure_permission(action="permissions-debug", actor=request.actor)
|
||||||
if not await self.ds.allowed(action="permissions-debug", actor=request.actor):
|
|
||||||
raise Forbidden("Permission denied")
|
|
||||||
filter_ = request.args.get("filter") or "all"
|
filter_ = request.args.get("filter") or "all"
|
||||||
permission_checks = list(reversed(self.ds._permission_checks))
|
permission_checks = list(reversed(self.ds._permission_checks))
|
||||||
if filter_ == "exclude-yours":
|
if filter_ == "exclude-yours":
|
||||||
|
|
@ -153,10 +149,8 @@ class PermissionsDebugView(BaseView):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
await self.ds.ensure_permission(action="permissions-debug", actor=request.actor)
|
||||||
if not await self.ds.allowed(action="permissions-debug", actor=request.actor):
|
|
||||||
raise Forbidden("Permission denied")
|
|
||||||
vars = await request.post_vars()
|
vars = await request.post_vars()
|
||||||
actor = json.loads(vars["actor"])
|
actor = json.loads(vars["actor"])
|
||||||
permission = vars["permission"]
|
permission = vars["permission"]
|
||||||
|
|
@ -362,10 +356,8 @@ class PermissionRulesView(BaseView):
|
||||||
has_json_alternate = False
|
has_json_alternate = False
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
await self.ds.ensure_permission(action="permissions-debug", actor=request.actor)
|
||||||
if not await self.ds.allowed(action="permissions-debug", actor=request.actor):
|
|
||||||
raise Forbidden("Permission denied")
|
|
||||||
|
|
||||||
# Check if this is a request for JSON (has .json extension)
|
# Check if this is a request for JSON (has .json extension)
|
||||||
as_format = request.url_vars.get("format")
|
as_format = request.url_vars.get("format")
|
||||||
|
|
@ -607,13 +599,11 @@ class MessagesDebugView(BaseView):
|
||||||
has_json_alternate = False
|
has_json_alternate = False
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
|
||||||
return await self.render(["messages_debug.html"], request)
|
return await self.render(["messages_debug.html"], request)
|
||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
if not await self.ds.allowed(action="view-instance", actor=request.actor):
|
await self.ds.ensure_permission(action="view-instance", actor=request.actor)
|
||||||
raise Forbidden("view-instance")
|
|
||||||
post = await request.post_vars()
|
post = await request.post_vars()
|
||||||
message = post.get("message", "")
|
message = post.get("message", "")
|
||||||
message_type = post.get("message_type") or "INFO"
|
message_type = post.get("message_type") or "INFO"
|
||||||
|
|
|
||||||
|
|
@ -416,30 +416,39 @@ The method returns ``True`` if the permission is granted, ``False`` if denied.
|
||||||
|
|
||||||
For legacy string/tuple based permission checking, use :ref:`datasette_permission_allowed` instead.
|
For legacy string/tuple based permission checking, use :ref:`datasette_permission_allowed` instead.
|
||||||
|
|
||||||
.. _datasette_ensure_permissions:
|
.. _datasette_ensure_permission:
|
||||||
|
|
||||||
await .ensure_permissions(actor, permissions)
|
await .ensure_permission(action, resource=None, actor=None)
|
||||||
---------------------------------------------
|
------------------------------------------------------------
|
||||||
|
|
||||||
``actor`` - dictionary
|
``action`` - string
|
||||||
|
The action to check. See :ref:`permissions` for a list of available actions.
|
||||||
|
|
||||||
|
``resource`` - Resource object (optional)
|
||||||
|
The resource to check the permission against. Must be an instance of ``InstanceResource``, ``DatabaseResource``, or ``TableResource`` from the ``datasette.resources`` module. If omitted, defaults to ``InstanceResource()`` for instance-level permissions.
|
||||||
|
|
||||||
|
``actor`` - dictionary (optional)
|
||||||
The authenticated actor. This is usually ``request.actor``.
|
The authenticated actor. This is usually ``request.actor``.
|
||||||
|
|
||||||
``permissions`` - list
|
This is a convenience wrapper around :ref:`datasette_allowed` that raises a ``datasette.Forbidden`` exception if the permission check fails. Use this when you want to enforce a permission check and halt execution if the actor is not authorized.
|
||||||
A list of permissions to check. Each permission in that list can be a string ``action`` name or a 2-tuple of ``(action, resource)``.
|
|
||||||
|
|
||||||
This method allows multiple permissions to be checked at once. It raises a ``datasette.Forbidden`` exception if any of the checks are denied before one of them is explicitly granted.
|
Example:
|
||||||
|
|
||||||
This is useful when you need to check multiple permissions at once. For example, an actor should be able to view a table if either one of the following checks returns ``True`` or not a single one of them returns ``False``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
await datasette.ensure_permissions(
|
from datasette.resources import TableResource
|
||||||
request.actor,
|
|
||||||
[
|
# Will raise Forbidden if actor cannot view the table
|
||||||
("view-table", (database, table)),
|
await datasette.ensure_permission(
|
||||||
("view-database", database),
|
action="view-table",
|
||||||
"view-instance",
|
resource=TableResource(database="fixtures", table="cities"),
|
||||||
],
|
actor=request.actor
|
||||||
|
)
|
||||||
|
|
||||||
|
# For instance-level actions, resource can be omitted:
|
||||||
|
await datasette.ensure_permission(
|
||||||
|
action="permissions-debug",
|
||||||
|
actor=request.actor
|
||||||
)
|
)
|
||||||
|
|
||||||
.. _datasette_check_visibility:
|
.. _datasette_check_visibility:
|
||||||
|
|
@ -473,7 +482,7 @@ This example checks if the user can access a specific table, and sets ``private`
|
||||||
resource=(database, table),
|
resource=(database, table),
|
||||||
)
|
)
|
||||||
|
|
||||||
The following example runs three checks in a row, similar to :ref:`datasette_ensure_permissions`. If any of the checks are denied before one of them is explicitly granted then ``visible`` will be ``False``. ``private`` will be ``True`` if an anonymous user would not be able to view the resource.
|
The following example runs three checks in a row. If any of the checks are denied before one of them is explicitly granted then ``visible`` will be ``False``. ``private`` will be ``True`` if an anonymous user would not be able to view the resource.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue