New plugin hook: canned_queries(), refs #852

This commit is contained in:
Simon Willison 2020-06-18 16:22:33 -07:00
commit 6c26345836
12 changed files with 203 additions and 50 deletions

View file

@ -387,18 +387,28 @@ class Datasette:
).hexdigest()[:6]
return self._app_css_hash
def get_canned_queries(self, database_name):
async def get_canned_queries(self, database_name, actor):
queries = self.metadata("queries", database=database_name, fallback=False) or {}
names = queries.keys()
return [self.get_canned_query(database_name, name) for name in names]
for more_queries in pm.hook.canned_queries(
datasette=self, database=database_name, actor=actor,
):
if callable(more_queries):
more_queries = more_queries()
if asyncio.iscoroutine(more_queries):
more_queries = await more_queries
queries.update(more_queries or {})
# Fix any {"name": "select ..."} queries to be {"name": {"sql": "select ..."}}
for key in queries:
if not isinstance(queries[key], dict):
queries[key] = {"sql": queries[key]}
# Also make sure "name" is available:
queries[key]["name"] = key
return queries
def get_canned_query(self, database_name, query_name):
queries = self.metadata("queries", database=database_name, fallback=False) or {}
async def get_canned_query(self, database_name, query_name, actor):
queries = await self.get_canned_queries(database_name, actor)
query = queries.get(query_name)
if query:
if not isinstance(query, dict):
query = {"sql": query}
query["name"] = query_name
return query
def update_with_inherited_metadata(self, metadata):

View file

@ -4,41 +4,42 @@ from datasette.utils import actor_matches_allow
@hookimpl(tryfirst=True)
def permission_allowed(datasette, actor, action, resource):
if action == "permissions-debug":
if actor and actor.get("id") == "root":
return True
elif action == "view-instance":
allow = datasette.metadata("allow")
if allow is not None:
async def inner():
if action == "permissions-debug":
if actor and actor.get("id") == "root":
return True
elif action == "view-instance":
allow = datasette.metadata("allow")
if allow is not None:
return actor_matches_allow(actor, allow)
elif action == "view-database":
database_allow = datasette.metadata("allow", database=resource)
if database_allow is None:
return True
return actor_matches_allow(actor, database_allow)
elif action == "view-table":
database, table = resource
tables = datasette.metadata("tables", database=database) or {}
table_allow = (tables.get(table) or {}).get("allow")
if table_allow is None:
return True
return actor_matches_allow(actor, table_allow)
elif action == "view-query":
# Check if this query has a "allow" block in metadata
database, query_name = resource
query = await datasette.get_canned_query(database, query_name, actor)
assert query is not None
allow = query.get("allow")
if allow is None:
return True
return actor_matches_allow(actor, allow)
elif action == "view-database":
database_allow = datasette.metadata("allow", database=resource)
if database_allow is None:
return True
return actor_matches_allow(actor, database_allow)
elif action == "view-table":
database, table = resource
tables = datasette.metadata("tables", database=database) or {}
table_allow = (tables.get(table) or {}).get("allow")
if table_allow is None:
return True
return actor_matches_allow(actor, table_allow)
elif action == "view-query":
# Check if this query has a "allow" block in metadata
database, query_name = resource
queries_metadata = datasette.metadata("queries", database=database)
assert query_name in queries_metadata
if isinstance(queries_metadata[query_name], str):
return True
allow = queries_metadata[query_name].get("allow")
if allow is None:
return True
return actor_matches_allow(actor, allow)
elif action == "execute-sql":
# Use allow_sql block from database block, or from top-level
database_allow_sql = datasette.metadata("allow_sql", database=resource)
if database_allow_sql is None:
database_allow_sql = datasette.metadata("allow_sql")
if database_allow_sql is None:
return True
return actor_matches_allow(actor, database_allow_sql)
elif action == "execute-sql":
# Use allow_sql block from database block, or from top-level
database_allow_sql = datasette.metadata("allow_sql", database=resource)
if database_allow_sql is None:
database_allow_sql = datasette.metadata("allow_sql")
if database_allow_sql is None:
return True
return actor_matches_allow(actor, database_allow_sql)
return inner

View file

@ -78,3 +78,8 @@ def actor_from_request(datasette, request):
@hookspec
def permission_allowed(datasette, actor, action, resource):
"Check if actor is allowed to perfom this action - return True, False or None"
@hookspec
def canned_queries(datasette, database, actor):
"Return a dictonary of canned query definitions or an awaitable function that returns them"

View file

@ -70,7 +70,9 @@ class DatabaseView(DataView):
tables.sort(key=lambda t: (t["hidden"], t["name"]))
canned_queries = []
for query in self.ds.get_canned_queries(database):
for query in (
await self.ds.get_canned_queries(database, request.actor)
).values():
visible, private = await check_visibility(
self.ds, request.actor, "view-query", (database, query["name"]),
)

View file

@ -223,7 +223,9 @@ class TableView(RowTableShared):
async def post(self, request, db_name, table_and_format):
# Handle POST to a canned query
canned_query = self.ds.get_canned_query(db_name, table_and_format)
canned_query = await self.ds.get_canned_query(
db_name, table_and_format, request.actor
)
assert canned_query, "You may only POST to a canned query"
return await QueryView(self.ds).data(
request,
@ -247,7 +249,7 @@ class TableView(RowTableShared):
_next=None,
_size=None,
):
canned_query = self.ds.get_canned_query(database, table)
canned_query = await self.ds.get_canned_query(database, table, request.actor)
if canned_query:
return await QueryView(self.ds).data(
request,