Skip to content

Commit 5213c41

Browse files
committed
datasette-public for datasette>=1.0a20
Refs simonw/datasette#2577
1 parent 4c15db6 commit 5213c41

File tree

3 files changed

+177
-72
lines changed

3 files changed

+177
-72
lines changed

datasette_public/__init__.py

Lines changed: 151 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from datasette import hookimpl, Forbidden, Response, NotFound, Permission
1+
from datasette import hookimpl, Forbidden, Response, NotFound
2+
from datasette.permissions import Action, PermissionSQL
3+
from datasette.resources import DatabaseResource, QueryResource, TableResource
24
from urllib.parse import quote_plus, unquote_plus
3-
from typing import Tuple
5+
from typing import Sequence, Tuple, Union
46

57
CREATE_TABLES_SQL = """
68
create table if not exists public_tables (
@@ -28,6 +30,46 @@
2830
);
2931
""".strip()
3032

33+
PermissionSpec = Union[str, Tuple[str, str], Tuple[str, str, str]]
34+
35+
36+
def _resource_for_action(action, parent=None, child=None):
37+
if action in ("datasette-public", "execute-sql", "view-database"):
38+
if parent is None:
39+
return None
40+
return DatabaseResource(database=parent)
41+
if action == "view-table":
42+
if parent is None or child is None:
43+
return None
44+
return TableResource(database=parent, table=child)
45+
if action == "view-query":
46+
if parent is None or child is None:
47+
return None
48+
return QueryResource(database=parent, query=child)
49+
return None
50+
51+
52+
async def _check_permissions_visibility(
53+
datasette, actor, permissions: Sequence[PermissionSpec]
54+
) -> Tuple[bool, bool]:
55+
for permission in permissions:
56+
if isinstance(permission, tuple):
57+
action = permission[0]
58+
parent = permission[1] if len(permission) > 1 else None
59+
child = permission[2] if len(permission) > 2 else None
60+
else:
61+
action = permission
62+
parent = child = None
63+
resource = _resource_for_action(action, parent, child)
64+
visible, private = await datasette.check_visibility(
65+
actor,
66+
action=action,
67+
resource=resource,
68+
)
69+
if visible:
70+
return visible, private
71+
return False, False
72+
3173

3274
@hookimpl
3375
def startup(datasette):
@@ -48,45 +90,63 @@ async def inner():
4890

4991

5092
@hookimpl
51-
def register_permissions():
93+
def register_actions():
5294
return [
53-
Permission(
95+
Action(
5496
name="datasette-public",
55-
abbr=None,
56-
description="Make tables and databases public/private",
57-
takes_database=True,
58-
takes_resource=False,
59-
default=False,
97+
description="Make tables and databases public or private",
98+
resource_class=DatabaseResource,
6099
),
61100
]
62101

63102

64-
@hookimpl
65-
def permission_allowed(datasette, action, actor, resource):
66-
async def inner():
67-
# Root actor can always edit public status
68-
if actor and actor.get("id") == "root" and action == "datasette-public":
69-
return True
70-
if action == "execute-sql" and not actor:
71-
# We now have an opinion on execute-sql for anonymous users
72-
_, allow_sql = await database_privacy_settings(datasette, resource)
73-
return allow_sql
74-
if action not in ("view-table", "view-database", "view-query"):
75-
return None
76-
if action == "view-table" and await table_is_public(
77-
datasette, resource[0], resource[1]
78-
):
79-
return True
80-
if action == "view-database":
81-
is_public, _ = await database_privacy_settings(datasette, resource)
82-
if is_public:
83-
return True
84-
if action == "view-query" and await query_is_public(
85-
datasette, resource[0], resource[1]
86-
):
87-
return True
88-
89-
return inner
103+
@hookimpl(specname="permission_resources_sql")
104+
def permission_resources_sql(datasette, actor, action):
105+
if action == "view-database":
106+
return PermissionSQL(
107+
sql="""
108+
select database_name as parent,
109+
null as child,
110+
1 as allow,
111+
'datasette-public database is public' as reason
112+
from public_databases
113+
""",
114+
)
115+
if action == "view-table":
116+
return PermissionSQL(
117+
sql="""
118+
select database_name as parent,
119+
table_name as child,
120+
1 as allow,
121+
'datasette-public table is public' as reason
122+
from public_tables
123+
""",
124+
)
125+
if action == "view-query":
126+
return PermissionSQL(
127+
sql="""
128+
select database_name as parent,
129+
query_name as child,
130+
1 as allow,
131+
'datasette-public query is public' as reason
132+
from public_queries
133+
""",
134+
)
135+
if action == "execute-sql":
136+
return PermissionSQL(
137+
sql="""
138+
select database_name as parent,
139+
null as child,
140+
case when allow_sql = 1 then 1 else 0 end as allow,
141+
case
142+
when allow_sql = 1 then 'datasette-public SQL enabled'
143+
else 'datasette-public SQL disabled'
144+
end as reason
145+
from public_databases
146+
where :actor is null
147+
""",
148+
)
149+
return None
90150

91151

92152
async def table_is_public(datasette, database_name, table_name):
@@ -120,14 +180,21 @@ async def query_is_public(datasette, database_name, query_name):
120180

121181

122182
@hookimpl
123-
def table_actions(datasette, actor, database, table):
183+
def table_actions(datasette, actor, database, table, request):
124184
async def inner():
125-
if not await datasette.permission_allowed(
126-
actor, "datasette-public", resource=database
185+
resource = _resource_for_action("datasette-public", database)
186+
if not resource:
187+
return
188+
if not await datasette.allowed(
189+
action="datasette-public",
190+
resource=resource,
191+
actor=actor,
127192
):
128193
return
129-
database_visible, database_private = await datasette.check_visibility(
130-
actor, permissions=[("view-database", database), "view-instance"]
194+
database_visible, database_private = await _check_permissions_visibility(
195+
datasette,
196+
actor,
197+
[("view-database", database), "view-instance"],
131198
)
132199
if database_visible and not database_private:
133200
return
@@ -155,14 +222,21 @@ async def inner():
155222

156223

157224
@hookimpl
158-
def database_actions(datasette, actor, database):
225+
def database_actions(datasette, actor, database, request):
159226
async def inner():
160-
if not await datasette.permission_allowed(
161-
actor, "datasette-public", resource=database
227+
resource = _resource_for_action("datasette-public", database)
228+
if not resource:
229+
return
230+
if not await datasette.allowed(
231+
action="datasette-public",
232+
resource=resource,
233+
actor=actor,
162234
):
163235
return
164-
instance_visible, instance_private = await datasette.check_visibility(
165-
actor, permissions=["view-instance"]
236+
instance_visible, instance_private = await _check_permissions_visibility(
237+
datasette,
238+
actor,
239+
["view-instance"],
166240
)
167241
if instance_visible and not instance_private:
168242
return
@@ -186,21 +260,28 @@ async def inner():
186260

187261

188262
@hookimpl
189-
def view_actions(datasette, actor, database, view):
190-
return table_actions(datasette, actor, database, view)
263+
def view_actions(datasette, actor, database, view, request):
264+
return table_actions(datasette, actor, database, view, request)
191265

192266

193267
@hookimpl
194-
def query_actions(datasette, actor, database, query_name):
268+
def query_actions(datasette, actor, database, query_name, request, sql, params):
195269
async def inner():
196-
if not await datasette.permission_allowed(
197-
actor, "datasette-public", resource=database
270+
resource = _resource_for_action("datasette-public", database)
271+
if not resource:
272+
return
273+
if not await datasette.allowed(
274+
action="datasette-public",
275+
resource=resource,
276+
actor=actor,
198277
):
199278
return
200-
database_visible, database_private = await datasette.check_visibility(
201-
actor, permissions=[("view-database", database), "view-instance"]
279+
database_visible, database_private = await _check_permissions_visibility(
280+
datasette,
281+
actor,
282+
[("view-database", database), "view-instance"],
202283
)
203-
if (not database_visible) or database_private:
284+
if (not database_visible) or (not database_private):
204285
return
205286
is_private = not await query_is_public(datasette, database, query_name)
206287
return [
@@ -221,8 +302,11 @@ async def inner():
221302

222303

223304
async def check_permissions(datasette, request, database):
224-
if not await datasette.permission_allowed(
225-
request.actor, "datasette-public", resource=database
305+
resource = _resource_for_action("datasette-public", database)
306+
if not resource or not await datasette.allowed(
307+
action="datasette-public",
308+
resource=resource,
309+
actor=request.actor,
226310
):
227311
raise Forbidden("Permission denied for changing table privacy")
228312

@@ -336,8 +420,10 @@ async def change_table_privacy(request, datasette):
336420

337421
is_private = not await table_is_public(datasette, database_name, table)
338422

339-
database_visible, database_private = await datasette.check_visibility(
340-
request.actor, permissions=[("view-database", database_name), "view-instance"]
423+
database_visible, database_private = await _check_permissions_visibility(
424+
datasette,
425+
request.actor,
426+
[("view-database", database_name), "view-instance"],
341427
)
342428
database_is_public = database_visible and not database_private
343429

@@ -441,8 +527,10 @@ async def change_database_privacy(request, datasette):
441527

442528
is_public, allow_sql = await database_privacy_settings(datasette, database_name)
443529

444-
instance_visible, instance_private = await datasette.check_visibility(
445-
request.actor, permissions=["view-instance"]
530+
instance_visible, instance_private = await _check_permissions_visibility(
531+
datasette,
532+
request.actor,
533+
["view-instance"],
446534
)
447535
instance_is_public = instance_visible and not instance_private
448536

@@ -566,8 +654,10 @@ async def change_query_privacy(request, datasette):
566654

567655
is_private = not await query_is_public(datasette, database_name, query)
568656

569-
database_visible, database_private = await datasette.check_visibility(
570-
request.actor, permissions=[("view-database", database_name), "view-instance"]
657+
database_visible, database_private = await _check_permissions_visibility(
658+
datasette,
659+
request.actor,
660+
[("view-database", database_name), "view-instance"],
571661
)
572662
database_is_public = database_visible and not database_private
573663

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ version = "0.3a4"
44
description = "Make selected Datasette databases and tables visible to the public"
55
readme = "README.md"
66
authors = [{name = "Simon Willison"}]
7-
license = {text = "Apache-2.0"}
7+
license = "Apache-2.0"
88
classifiers=[
99
"Framework :: Datasette",
10-
"License :: OSI Approved :: Apache Software License"
1110
]
12-
requires-python = ">=3.9"
11+
requires-python = ">=3.10"
1312
dependencies = [
14-
"datasette==1.0a19",
13+
"datasette>=1.0a21",
1514
]
1615

1716
[project.urls]

0 commit comments

Comments
 (0)