check_visibility can now take multiple permissions into account

Closes #1829
This commit is contained in:
Simon Willison 2022-10-23 19:11:33 -07:00 committed by GitHub
commit 78dad236df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 196 additions and 82 deletions

View file

@ -1,5 +1,5 @@
import asyncio
from typing import Sequence, Union, Tuple
from typing import Sequence, Union, Tuple, Optional
import asgi_csrf
import collections
import datetime
@ -707,7 +707,7 @@ class Datasette:
Raises datasette.Forbidden() if any of the checks fail
"""
assert actor is None or isinstance(actor, dict)
assert actor is None or isinstance(actor, dict), "actor must be None or a dict"
for permission in permissions:
if isinstance(permission, str):
action = permission
@ -732,23 +732,34 @@ class Datasette:
else:
raise Forbidden(action)
async def check_visibility(self, actor, action, resource):
async def check_visibility(
self,
actor: dict,
action: Optional[str] = None,
resource: Optional[Union[str, Tuple[str, str]]] = None,
permissions: Optional[
Sequence[Union[Tuple[str, Union[str, Tuple[str, str]]], str]]
] = None,
):
"""Returns (visible, private) - visible = can you see it, private = can others see it too"""
visible = await self.permission_allowed(
actor,
action,
resource=resource,
default=True,
)
if not visible:
if permissions:
assert (
not action and not resource
), "Can't use action= or resource= with permissions="
else:
permissions = [(action, resource)]
try:
await self.ensure_permissions(actor, permissions)
except Forbidden:
return False, False
private = not await self.permission_allowed(
None,
action,
resource=resource,
default=True,
)
return visible, private
# User can see it, but can the anonymous user see it?
try:
await self.ensure_permissions(None, permissions)
except Forbidden:
# It's visible but private
return True, True
# It's visible to everyone
return True, False
async def execute(
self,

View file

@ -20,7 +20,7 @@
{% endblock %}
{% block content %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(database) }}">{{ table }}: {{ ', '.join(primary_key_values) }}</h1>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(database) }}">{{ table }}: {{ ', '.join(primary_key_values) }}{% if private %} 🔒{% endif %}</h1>
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

View file

@ -40,13 +40,16 @@ class DatabaseView(DataView):
raise NotFound("Database not found: {}".format(database_route))
database = db.name
await self.ds.ensure_permissions(
visible, private = await self.ds.check_visibility(
request.actor,
[
permissions=[
("view-database", database),
"view-instance",
],
)
if not visible:
raise Forbidden("You do not have permission to view this database")
metadata = (self.ds.metadata("databases") or {}).get(database, {})
self.ds.update_with_inherited_metadata(metadata)
@ -63,27 +66,27 @@ class DatabaseView(DataView):
views = []
for view_name in await db.view_names():
visible, private = await self.ds.check_visibility(
view_visible, view_private = await self.ds.check_visibility(
request.actor,
"view-table",
(database, view_name),
)
if visible:
if view_visible:
views.append(
{
"name": view_name,
"private": private,
"private": view_private,
}
)
tables = []
for table in table_counts:
visible, private = await self.ds.check_visibility(
table_visible, table_private = await self.ds.check_visibility(
request.actor,
"view-table",
(database, table),
)
if not visible:
if not table_visible:
continue
table_columns = await db.table_columns(table)
tables.append(
@ -95,7 +98,7 @@ class DatabaseView(DataView):
"hidden": table in hidden_table_names,
"fts_table": await db.fts_table(table),
"foreign_keys": all_foreign_keys[table],
"private": private,
"private": table_private,
}
)
@ -104,13 +107,13 @@ class DatabaseView(DataView):
for query in (
await self.ds.get_canned_queries(database, request.actor)
).values():
visible, private = await self.ds.check_visibility(
query_visible, query_private = await self.ds.check_visibility(
request.actor,
"view-query",
(database, query["name"]),
)
if visible:
canned_queries.append(dict(query, private=private))
if query_visible:
canned_queries.append(dict(query, private=query_private))
async def database_actions():
links = []
@ -130,15 +133,13 @@ class DatabaseView(DataView):
return (
{
"database": database,
"private": private,
"path": self.ds.urls.database(database),
"size": db.size,
"tables": tables,
"hidden_count": len([t for t in tables if t["hidden"]]),
"views": views,
"queries": canned_queries,
"private": not await self.ds.permission_allowed(
None, "view-database", database, default=True
),
"allow_execute_sql": await self.ds.permission_allowed(
request.actor, "execute-sql", database, default=True
),
@ -227,17 +228,17 @@ class QueryView(DataView):
private = False
if canned_query:
# Respect canned query permissions
await self.ds.ensure_permissions(
visible, private = await self.ds.check_visibility(
request.actor,
[
permissions=[
("view-query", (database, canned_query)),
("view-database", database),
"view-instance",
],
)
private = not await self.ds.permission_allowed(
None, "view-query", (database, canned_query), default=True
)
if not visible:
raise Forbidden("You do not have permission to view this query")
else:
await self.ds.ensure_permissions(request.actor, [("execute-sql", database)])

View file

@ -23,25 +23,25 @@ class IndexView(BaseView):
await self.ds.ensure_permissions(request.actor, ["view-instance"])
databases = []
for name, db in self.ds.databases.items():
visible, database_private = await self.ds.check_visibility(
database_visible, database_private = await self.ds.check_visibility(
request.actor,
"view-database",
name,
)
if not visible:
if not database_visible:
continue
table_names = await db.table_names()
hidden_table_names = set(await db.hidden_table_names())
views = []
for view_name in await db.view_names():
visible, private = await self.ds.check_visibility(
view_visible, view_private = await self.ds.check_visibility(
request.actor,
"view-table",
(name, view_name),
)
if visible:
views.append({"name": view_name, "private": private})
if view_visible:
views.append({"name": view_name, "private": view_private})
# Perform counts only for immutable or DBS with <= COUNT_TABLE_LIMIT tables
table_counts = {}

View file

@ -1,4 +1,4 @@
from datasette.utils.asgi import NotFound
from datasette.utils.asgi import NotFound, Forbidden
from datasette.database import QueryInterrupted
from .base import DataView
from datasette.utils import (
@ -21,14 +21,19 @@ class RowView(DataView):
except KeyError:
raise NotFound("Database not found: {}".format(database_route))
database = db.name
await self.ds.ensure_permissions(
# Ensure user has permission to view this row
visible, private = await self.ds.check_visibility(
request.actor,
[
permissions=[
("view-table", (database, table)),
("view-database", database),
"view-instance",
],
)
if not visible:
raise Forbidden("You do not have permission to view this table")
pk_values = urlsafe_components(request.url_vars["pks"])
try:
db = self.ds.get_database(route=database_route)
@ -55,6 +60,7 @@ class RowView(DataView):
for column in display_columns:
column["sortable"] = False
return {
"private": private,
"foreign_key_tables": await self.foreign_key_tables(
database, table, pk_values
),

View file

@ -28,7 +28,7 @@ from datasette.utils import (
urlsafe_components,
value_as_boolean,
)
from datasette.utils.asgi import BadRequest, NotFound
from datasette.utils.asgi import BadRequest, Forbidden, NotFound
from datasette.filters import Filters
from .base import DataView, DatasetteError, ureg
from .database import QueryView
@ -213,18 +213,16 @@ class TableView(DataView):
raise NotFound(f"Table not found: {table_name}")
# Ensure user has permission to view this table
await self.ds.ensure_permissions(
visible, private = await self.ds.check_visibility(
request.actor,
[
permissions=[
("view-table", (database_name, table_name)),
("view-database", database_name),
"view-instance",
],
)
private = not await self.ds.permission_allowed(
None, "view-table", (database_name, table_name), default=True
)
if not visible:
raise Forbidden("You do not have permission to view this table")
# Handle ?_filter_column and redirect, if present
redirect_params = filters_should_redirect(request.args)