2020-06-01 05:00:36 +00:00
|
|
|
from datasette import hookimpl
|
2020-06-07 21:23:16 +00:00
|
|
|
from datasette.utils import actor_matches_allow
|
2022-10-26 04:26:12 +00:00
|
|
|
import click
|
2022-10-26 02:18:41 +00:00
|
|
|
import itsdangerous
|
2022-10-26 04:26:12 +00:00
|
|
|
import json
|
2022-10-26 02:18:41 +00:00
|
|
|
import time
|
2020-06-01 05:00:36 +00:00
|
|
|
|
|
|
|
|
2020-06-08 22:09:57 +00:00
|
|
|
@hookimpl(tryfirst=True)
|
2020-06-08 18:59:11 +00:00
|
|
|
def permission_allowed(datasette, actor, action, resource):
|
2020-06-18 23:22:33 +00:00
|
|
|
async def inner():
|
2022-10-27 03:57:02 +00:00
|
|
|
if action in ("permissions-debug", "debug-menu", "insert-row"):
|
2020-06-18 23:22:33 +00:00
|
|
|
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":
|
2020-12-21 19:48:06 +00:00
|
|
|
if resource == "_internal" and (actor is None or actor.get("id") != "root"):
|
2020-12-18 22:34:05 +00:00
|
|
|
return False
|
2020-06-18 23:22:33 +00:00
|
|
|
database_allow = datasette.metadata("allow", database=resource)
|
|
|
|
if database_allow is None:
|
2020-06-30 22:49:06 +00:00
|
|
|
return None
|
2020-06-18 23:22:33 +00:00
|
|
|
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:
|
2020-06-30 22:49:06 +00:00
|
|
|
return None
|
2020-06-18 23:22:33 +00:00
|
|
|
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:
|
2020-06-30 22:49:06 +00:00
|
|
|
return None
|
2020-06-07 21:30:39 +00:00
|
|
|
return actor_matches_allow(actor, allow)
|
2020-06-18 23:22:33 +00:00
|
|
|
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:
|
2020-06-30 22:49:06 +00:00
|
|
|
return None
|
2020-06-18 23:22:33 +00:00
|
|
|
return actor_matches_allow(actor, database_allow_sql)
|
|
|
|
|
|
|
|
return inner
|
2022-10-26 02:18:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
@hookimpl
|
|
|
|
def actor_from_request(datasette, request):
|
|
|
|
prefix = "dstok_"
|
2022-10-26 02:55:47 +00:00
|
|
|
if not datasette.setting("allow_signed_tokens"):
|
|
|
|
return None
|
2022-10-26 21:13:31 +00:00
|
|
|
max_signed_tokens_ttl = datasette.setting("max_signed_tokens_ttl")
|
2022-10-26 02:18:41 +00:00
|
|
|
authorization = request.headers.get("authorization")
|
|
|
|
if not authorization:
|
|
|
|
return None
|
|
|
|
if not authorization.startswith("Bearer "):
|
|
|
|
return None
|
|
|
|
token = authorization[len("Bearer ") :]
|
|
|
|
if not token.startswith(prefix):
|
|
|
|
return None
|
|
|
|
token = token[len(prefix) :]
|
|
|
|
try:
|
|
|
|
decoded = datasette.unsign(token, namespace="token")
|
|
|
|
except itsdangerous.BadSignature:
|
|
|
|
return None
|
2022-10-26 21:13:31 +00:00
|
|
|
if "t" not in decoded:
|
|
|
|
# Missing timestamp
|
|
|
|
return None
|
|
|
|
created = decoded["t"]
|
|
|
|
if not isinstance(created, int):
|
|
|
|
# Invalid timestamp
|
|
|
|
return None
|
|
|
|
duration = decoded.get("d")
|
|
|
|
if duration is not None and not isinstance(duration, int):
|
|
|
|
# Invalid duration
|
|
|
|
return None
|
|
|
|
if (duration is None and max_signed_tokens_ttl) or (
|
|
|
|
duration is not None
|
|
|
|
and max_signed_tokens_ttl
|
|
|
|
and duration > max_signed_tokens_ttl
|
|
|
|
):
|
|
|
|
duration = max_signed_tokens_ttl
|
|
|
|
if duration:
|
|
|
|
if time.time() - created > duration:
|
|
|
|
# Expired
|
2022-10-26 02:18:41 +00:00
|
|
|
return None
|
2022-10-26 21:13:31 +00:00
|
|
|
actor = {"id": decoded["a"], "token": "dstok"}
|
|
|
|
if duration:
|
|
|
|
actor["token_expires"] = created + duration
|
|
|
|
return actor
|
2022-10-26 04:26:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
@hookimpl
|
|
|
|
def register_commands(cli):
|
|
|
|
from datasette.app import Datasette
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
@click.argument("id")
|
|
|
|
@click.option(
|
|
|
|
"--secret",
|
|
|
|
help="Secret used for signing the API tokens",
|
|
|
|
envvar="DATASETTE_SECRET",
|
|
|
|
required=True,
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"-e",
|
|
|
|
"--expires-after",
|
|
|
|
help="Token should expire after this many seconds",
|
|
|
|
type=int,
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--debug",
|
|
|
|
help="Show decoded token",
|
|
|
|
is_flag=True,
|
|
|
|
)
|
|
|
|
def create_token(id, secret, expires_after, debug):
|
|
|
|
"Create a signed API token for the specified actor ID"
|
|
|
|
ds = Datasette(secret=secret)
|
2022-10-26 21:13:31 +00:00
|
|
|
bits = {"a": id, "token": "dstok", "t": int(time.time())}
|
2022-10-26 04:26:12 +00:00
|
|
|
if expires_after:
|
2022-10-26 21:13:31 +00:00
|
|
|
bits["d"] = expires_after
|
2022-10-26 04:26:12 +00:00
|
|
|
token = ds.sign(bits, namespace="token")
|
|
|
|
click.echo("dstok_{}".format(token))
|
|
|
|
if debug:
|
|
|
|
click.echo("\nDecoded:\n")
|
|
|
|
click.echo(json.dumps(ds.unsign(token, namespace="token"), indent=2))
|