mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
datasette.create_token() method, closes #1951
This commit is contained in:
parent
d4cc1374f4
commit
fdf7c27b54
3 changed files with 113 additions and 31 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Sequence, Union, Tuple, Optional
|
from typing import Sequence, Union, Tuple, Optional, Dict, Iterable
|
||||||
import asgi_csrf
|
import asgi_csrf
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
|
|
@ -16,6 +16,7 @@ import re
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -465,6 +466,45 @@ class Datasette:
|
||||||
def unsign(self, signed, namespace="default"):
|
def unsign(self, signed, namespace="default"):
|
||||||
return URLSafeSerializer(self._secret, namespace).loads(signed)
|
return URLSafeSerializer(self._secret, namespace).loads(signed)
|
||||||
|
|
||||||
|
def create_token(
|
||||||
|
self,
|
||||||
|
actor_id: str,
|
||||||
|
*,
|
||||||
|
expires_after: Optional[int] = None,
|
||||||
|
restrict_all: Optional[Iterable[str]] = None,
|
||||||
|
restrict_database: Optional[Dict[str, Iterable[str]]] = None,
|
||||||
|
restrict_resource: Optional[Dict[str, Dict[str, Iterable[str]]]] = None,
|
||||||
|
):
|
||||||
|
token = {"a": actor_id, "token": "dstok", "t": int(time.time())}
|
||||||
|
if expires_after:
|
||||||
|
token["d"] = expires_after
|
||||||
|
|
||||||
|
def abbreviate_action(action):
|
||||||
|
# rename to abbr if possible
|
||||||
|
permission = self.permissions.get(action)
|
||||||
|
if not permission:
|
||||||
|
return action
|
||||||
|
return permission.abbr or action
|
||||||
|
|
||||||
|
if expires_after:
|
||||||
|
token["d"] = expires_after
|
||||||
|
if restrict_all or restrict_database or restrict_resource:
|
||||||
|
token["_r"] = {}
|
||||||
|
if restrict_all:
|
||||||
|
token["_r"]["a"] = [abbreviate_action(a) for a in restrict_all]
|
||||||
|
if restrict_database:
|
||||||
|
token["_r"]["d"] = {}
|
||||||
|
for database, actions in restrict_database.items():
|
||||||
|
token["_r"]["d"][database] = [abbreviate_action(a) for a in actions]
|
||||||
|
if restrict_resource:
|
||||||
|
token["_r"]["r"] = {}
|
||||||
|
for database, resources in restrict_resource.items():
|
||||||
|
for resource, actions in resources.items():
|
||||||
|
token["_r"]["r"].setdefault(database, {})[resource] = [
|
||||||
|
abbreviate_action(a) for a in actions
|
||||||
|
]
|
||||||
|
return "dstok_{}".format(self.sign(token, namespace="token"))
|
||||||
|
|
||||||
def get_database(self, name=None, route=None):
|
def get_database(self, name=None, route=None):
|
||||||
if route is not None:
|
if route is not None:
|
||||||
matches = [db for db in self.databases.values() if db.route == route]
|
matches = [db for db in self.databases.values() if db.route == route]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ from runpy import run_module
|
||||||
import shutil
|
import shutil
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from .app import (
|
from .app import (
|
||||||
OBSOLETE_SETTINGS,
|
OBSOLETE_SETTINGS,
|
||||||
|
|
@ -730,41 +729,40 @@ def create_token(
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(ds.invoke_startup())
|
loop.run_until_complete(ds.invoke_startup())
|
||||||
|
|
||||||
def fix_action(action):
|
# Warn about any unknown actions
|
||||||
# Warn if invalid, rename to abbr if possible
|
actions = []
|
||||||
permission = ds.permissions.get(action)
|
actions.extend(alls)
|
||||||
if not permission:
|
actions.extend([p[1] for p in databases])
|
||||||
# Output red message
|
actions.extend([p[2] for p in resources])
|
||||||
|
for action in actions:
|
||||||
|
if not ds.permissions.get(action):
|
||||||
click.secho(
|
click.secho(
|
||||||
f" Unknown permission: {action} ",
|
f" Unknown permission: {action} ",
|
||||||
fg="red",
|
fg="red",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
return action
|
|
||||||
return permission.abbr or action
|
|
||||||
|
|
||||||
bits = {"a": id, "token": "dstok", "t": int(time.time())}
|
restrict_database = {}
|
||||||
if expires_after:
|
for database, action in databases:
|
||||||
bits["d"] = expires_after
|
restrict_database.setdefault(database, []).append(action)
|
||||||
if alls or databases or resources:
|
restrict_resource = {}
|
||||||
bits["_r"] = {}
|
for database, resource, action in resources:
|
||||||
if alls:
|
restrict_resource.setdefault(database, {}).setdefault(resource, []).append(
|
||||||
bits["_r"]["a"] = [fix_action(a) for a in alls]
|
action
|
||||||
if databases:
|
)
|
||||||
bits["_r"]["d"] = {}
|
|
||||||
for database, action in databases:
|
token = ds.create_token(
|
||||||
bits["_r"]["d"].setdefault(database, []).append(fix_action(action))
|
id,
|
||||||
if resources:
|
expires_after=expires_after,
|
||||||
bits["_r"]["r"] = {}
|
restrict_all=alls,
|
||||||
for database, table, action in resources:
|
restrict_database=restrict_database,
|
||||||
bits["_r"]["r"].setdefault(database, {}).setdefault(table, []).append(
|
restrict_resource=restrict_resource,
|
||||||
fix_action(action)
|
)
|
||||||
)
|
click.echo(token)
|
||||||
token = ds.sign(bits, namespace="token")
|
|
||||||
click.echo("dstok_{}".format(token))
|
|
||||||
if debug:
|
if debug:
|
||||||
|
encoded = token[len("dstok_") :]
|
||||||
click.echo("\nDecoded:\n")
|
click.echo("\nDecoded:\n")
|
||||||
click.echo(json.dumps(ds.unsign(token, namespace="token"), indent=2))
|
click.echo(json.dumps(ds.unsign(encoded, namespace="token"), indent=2))
|
||||||
|
|
||||||
|
|
||||||
pm.hook.register_commands(cli=cli)
|
pm.hook.register_commands(cli=cli)
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,50 @@ The following example runs three checks in a row, similar to :ref:`datasette_ens
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.create_token(actor_id, expires_after=None, restrict_all=None, restrict_database=None, restrict_resource=None)
|
||||||
|
--------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
``actor_id`` - string
|
||||||
|
The ID of the actor to create a token for.
|
||||||
|
|
||||||
|
``expires_after`` - int, optional
|
||||||
|
The number of seconds after which the token should expire.
|
||||||
|
|
||||||
|
``restrict_all`` - iterable, optional
|
||||||
|
A list of actions that this token should be restricted to across all databases and resources.
|
||||||
|
|
||||||
|
``restrict_database`` - dict, optional
|
||||||
|
For restricting actions within specific databases, e.g. ``{"mydb": ["view-table", "view-query"]}``.
|
||||||
|
|
||||||
|
``restrict_resource`` - dict, optional
|
||||||
|
For restricting actions to specific resources (tables, SQL views and :ref:`canned_queries`) within a database. For example: ``{"mydb": {"mytable": ["insert-row", "update-row"]}}``.
|
||||||
|
|
||||||
|
This method returns a signed :ref:`API token <CreateTokenView>` of the format ``dstok_...`` which can be used to authenticate requests to the Datasette API.
|
||||||
|
|
||||||
|
All tokens must have an ``actor_id`` string indicating the ID of the actor which the token will act on behalf of.
|
||||||
|
|
||||||
|
Tokens default to lasting forever, but can be set to expire after a given number of seconds using the ``expires_after`` argument. The following code creates a token for ``user1`` that will expire after an hour:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
token = datasette.create_token(
|
||||||
|
actor_id="user1",
|
||||||
|
expires_after=3600,
|
||||||
|
)
|
||||||
|
|
||||||
|
The three ``restrict_*`` arguments can be used to create a token that has additional restrictions beyond what the associated actor is allowed to do.
|
||||||
|
|
||||||
|
The following example creates a token that can access ``view-instance`` and ``view-table`` across everything, can additionally use ``view-query`` for anything in the ``docs`` database and is allowed to execute ``insert-row`` and ``update-row`` in the ``attachments`` table in that database:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
token = datasette.create_token(
|
||||||
|
actor_id="user1",
|
||||||
|
restrict_all=("view-instance", "view-table"),
|
||||||
|
restrict_database={"docs": ("view-query",)},
|
||||||
|
restrict_resource={"docs": {"attachments": ("insert-row", "update-row")}},
|
||||||
|
)
|
||||||
|
|
||||||
.. _datasette_get_database:
|
.. _datasette_get_database:
|
||||||
|
|
||||||
.get_database(name)
|
.get_database(name)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue