datasette.sign() and datasette.unsign() methods, refs #785

This commit is contained in:
Simon Willison 2020-05-31 15:42:08 -07:00
commit fa27e44fe0
7 changed files with 61 additions and 0 deletions

View file

@ -14,6 +14,7 @@ from pathlib import Path
import click
from markupsafe import Markup
from itsdangerous import URLSafeSerializer
import jinja2
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader, escape
from jinja2.environment import Template
@ -163,12 +164,14 @@ class Datasette:
static_mounts=None,
memory=False,
config=None,
secret=None,
version_note=None,
config_dir=None,
):
assert config_dir is None or isinstance(
config_dir, Path
), "config_dir= should be a pathlib.Path"
self._secret = secret or os.urandom(32).hex()
self.files = tuple(files) + tuple(immutables or [])
if config_dir:
self.files += tuple([str(p) for p in config_dir.glob("*.db")])
@ -281,6 +284,12 @@ class Datasette:
self._register_renderers()
def sign(self, value, namespace="default"):
return URLSafeSerializer(self._secret, namespace).dumps(value)
def unsign(self, signed, namespace="default"):
return URLSafeSerializer(self._secret, namespace).loads(signed)
def get_database(self, name=None):
if name is None:
return next(iter(self.databases.values()))

View file

@ -299,6 +299,11 @@ def package(
help="Set config option using configname:value datasette.readthedocs.io/en/latest/config.html",
multiple=True,
)
@click.option(
"--secret",
help="Secret used for signing secure values, such as signed cookies",
envvar="DATASETTE_SECRET",
)
@click.option("--version-note", help="Additional note to show on /-/versions")
@click.option("--help-config", is_flag=True, help="Show available config options")
def serve(
@ -317,6 +322,7 @@ def serve(
static,
memory,
config,
secret,
version_note,
help_config,
return_instance=False,
@ -362,6 +368,7 @@ def serve(
static_mounts=static,
config=dict(config),
memory=memory,
secret=secret,
version_note=version_note,
)

View file

@ -29,6 +29,9 @@ Options:
--config CONFIG Set config option using configname:value
datasette.readthedocs.io/en/latest/config.html
--secret TEXT Secret used for signing secure values, such as signed
cookies
--version-note TEXT Additional note to show on /-/versions
--help-config Show available config options
--help Show this message and exit.

View file

@ -183,6 +183,34 @@ Use ``is_memory`` if the connection is to an in-memory SQLite database.
This removes a database that has been previously added. ``name=`` is the unique name of that database, also used in the URL for it.
.. _datasette_sign:
.sign(value, namespace="default")
---------------------------------
``value`` - any serializable type
The value to be signed.
``namespace`` - string, optional
An alternative namespace, see the `itsdangerous salt documentation <https://itsdangerous.palletsprojects.com/en/1.1.x/serializer/#the-salt>`__.
Utility method for signing values, such that you can safely pass data to and from an untrusted environment. This is a wrapper around the `itsdangerous <https://itsdangerous.palletsprojects.com/>`__ library.
This method returns a signed string, which can be decoded and verified using :ref:`datasette_unsign`.
.. _datasette_unsign:
.unsign(value, namespace="default")
-----------------------------------
``signed`` - any serializable type
The signed string that was created using :ref:`datasette_sign`.
``namespace`` - string, optional
The alternative namespace, if one was used.
Returns the original, decoded object that was passed to :ref:`datasette_sign`. If the signature is not valid this raises a ``itsdangerous.BadSignature`` exception.
.. _internals_database:
Database class

View file

@ -55,6 +55,7 @@ setup(
"janus>=0.4,<0.6",
"PyYAML~=5.3",
"mergedeep>=1.1.1,<1.4.0",
"itsdangerous~=1.1",
],
entry_points="""
[console_scripts]

View file

@ -75,6 +75,7 @@ def test_metadata_yaml():
static=[],
memory=False,
config=[],
secret=None,
version_note=None,
help_config=False,
return_instance=True,

View file

@ -1,6 +1,7 @@
"""
Tests for the datasette.app.Datasette class
"""
from itsdangerous import BadSignature
from .fixtures import app_client
import pytest
@ -21,3 +22,14 @@ def test_get_database_no_argument(datasette):
# Returns the first available database:
db = datasette.get_database()
assert "fixtures" == db.name
@pytest.mark.parametrize("value", ["hello", 123, {"key": "value"}])
@pytest.mark.parametrize("namespace", [None, "two"])
def test_sign_unsign(datasette, value, namespace):
extra_args = [namespace] if namespace else []
signed = datasette.sign(value, *extra_args)
assert value != signed
assert value == datasette.unsign(signed, *extra_args)
with pytest.raises(BadSignature):
datasette.unsign(signed[:-1] + ("!" if signed[-1] != "!" else ":"))