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 import click
from markupsafe import Markup from markupsafe import Markup
from itsdangerous import URLSafeSerializer
import jinja2 import jinja2
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader, escape from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader, escape
from jinja2.environment import Template from jinja2.environment import Template
@ -163,12 +164,14 @@ class Datasette:
static_mounts=None, static_mounts=None,
memory=False, memory=False,
config=None, config=None,
secret=None,
version_note=None, version_note=None,
config_dir=None, config_dir=None,
): ):
assert config_dir is None or isinstance( assert config_dir is None or isinstance(
config_dir, Path config_dir, Path
), "config_dir= should be a pathlib.Path" ), "config_dir= should be a pathlib.Path"
self._secret = secret or os.urandom(32).hex()
self.files = tuple(files) + tuple(immutables or []) self.files = tuple(files) + tuple(immutables or [])
if config_dir: if config_dir:
self.files += tuple([str(p) for p in config_dir.glob("*.db")]) self.files += tuple([str(p) for p in config_dir.glob("*.db")])
@ -281,6 +284,12 @@ class Datasette:
self._register_renderers() 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): def get_database(self, name=None):
if name is None: if name is None:
return next(iter(self.databases.values())) 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", help="Set config option using configname:value datasette.readthedocs.io/en/latest/config.html",
multiple=True, 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("--version-note", help="Additional note to show on /-/versions")
@click.option("--help-config", is_flag=True, help="Show available config options") @click.option("--help-config", is_flag=True, help="Show available config options")
def serve( def serve(
@ -317,6 +322,7 @@ def serve(
static, static,
memory, memory,
config, config,
secret,
version_note, version_note,
help_config, help_config,
return_instance=False, return_instance=False,
@ -362,6 +368,7 @@ def serve(
static_mounts=static, static_mounts=static,
config=dict(config), config=dict(config),
memory=memory, memory=memory,
secret=secret,
version_note=version_note, version_note=version_note,
) )

View file

@ -29,6 +29,9 @@ Options:
--config CONFIG Set config option using configname:value --config CONFIG Set config option using configname:value
datasette.readthedocs.io/en/latest/config.html 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 --version-note TEXT Additional note to show on /-/versions
--help-config Show available config options --help-config Show available config options
--help Show this message and exit. --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. 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: .. _internals_database:
Database class Database class

View file

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

View file

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

View file

@ -1,6 +1,7 @@
""" """
Tests for the datasette.app.Datasette class Tests for the datasette.app.Datasette class
""" """
from itsdangerous import BadSignature
from .fixtures import app_client from .fixtures import app_client
import pytest import pytest
@ -21,3 +22,14 @@ def test_get_database_no_argument(datasette):
# Returns the first available database: # Returns the first available database:
db = datasette.get_database() db = datasette.get_database()
assert "fixtures" == db.name 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 ":"))