/-/config page, closes #2254

pull/2257/head
Simon Willison 2024-02-06 12:33:46 -08:00
rodzic 85a1dfe6e0
commit 1e901aa690
5 zmienionych plików z 84 dodań i 23 usunięć

Wyświetl plik

@ -81,6 +81,7 @@ from .utils import (
tilde_decode,
to_css_class,
urlsafe_components,
redact_keys,
row_sql_params_pks,
)
from .utils.asgi import (
@ -1374,6 +1375,11 @@ class Datasette:
output.append(script)
return output
def _config(self):
return redact_keys(
self.config, ("secret", "key", "password", "token", "hash", "dsn")
)
def _routes(self):
routes = []
@ -1433,12 +1439,8 @@ class Datasette:
r"/-/settings(\.(?P<format>json))?$",
)
add_route(
permanent_redirect("/-/settings.json"),
r"/-/config.json",
)
add_route(
permanent_redirect("/-/settings"),
r"/-/config",
JsonDataView.as_view(self, "config.json", lambda: self._config()),
r"/-/config(\.(?P<format>json))?$",
)
add_route(
JsonDataView.as_view(self, "threads.json", self._threads),

Wyświetl plik

@ -17,6 +17,7 @@ import time
import types
import secrets
import shutil
from typing import Iterable
import urllib
import yaml
from .shutil_backport import copytree
@ -1327,3 +1328,30 @@ def move_plugins(source, destination):
recursive_move(source, destination)
prune_empty_dicts(source)
def redact_keys(original: dict, key_patterns: Iterable) -> dict:
"""
Recursively redact sensitive keys in a dictionary based on given patterns
:param original: The original dictionary
:param key_patterns: A list of substring patterns to redact
:return: A copy of the original dictionary with sensitive values redacted
"""
def redact(data):
if isinstance(data, dict):
return {
k: (
redact(v)
if not any(pattern in k for pattern in key_patterns)
else "***"
)
for k, v in data.items()
}
elif isinstance(data, list):
return [redact(item) for item in data]
else:
return data
return redact(original)

Wyświetl plik

@ -42,7 +42,7 @@ class JsonDataView(BaseView):
if self.ds.cors:
add_cors_headers(headers)
return Response(
json.dumps(data),
json.dumps(data, default=repr),
content_type="application/json; charset=utf-8",
headers=headers,
)
@ -53,7 +53,7 @@ class JsonDataView(BaseView):
request=request,
context={
"filename": self.filename,
"data_json": json.dumps(data, indent=4),
"data_json": json.dumps(data, indent=4, default=repr),
},
)

Wyświetl plik

@ -87,7 +87,7 @@ Shows a list of currently installed plugins and their versions. `Plugins example
Add ``?all=1`` to include details of the default plugins baked into Datasette.
.. _JsonDataView_config:
.. _JsonDataView_settings:
/-/settings
-----------
@ -105,6 +105,15 @@ Shows the :ref:`settings` for this instance of Datasette. `Settings example <htt
"sql_time_limit_ms": 1000
}
.. _JsonDataView_config:
/-/config
---------
Shows the :ref:`configuration <configuration>` for this instance of Datasette. This is generally the contents of the :ref:`datasette.yaml or datasette.json <configuration_reference>` file, which can include plugin configuration as well.
Any keys that include the one of the following substrings in their names will be returned as redacted ``***`` output, to help avoid accidentally leaking private configuration information: ``secret``, ``key``, ``password``, ``token``, ``hash``, ``dsn``.
.. _JsonDataView_databases:
/-/databases

Wyświetl plik

@ -846,20 +846,6 @@ async def test_settings_json(ds_client):
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path,expected_redirect",
(
("/-/config.json", "/-/settings.json"),
("/-/config", "/-/settings"),
),
)
async def test_config_redirects_to_settings(ds_client, path, expected_redirect):
response = await ds_client.get(path)
assert response.status_code == 301
assert response.headers["Location"] == expected_redirect
test_json_columns_default_expected = [
{"intval": 1, "strval": "s", "floatval": 0.5, "jsonval": '{"foo": "bar"}'}
]
@ -1039,3 +1025,39 @@ async def test_tilde_encoded_database_names(db_name):
# And the JSON for that database
response2 = await ds.client.get(path + ".json")
assert response2.status_code == 200
@pytest.mark.asyncio
@pytest.mark.parametrize(
"config,expected",
(
({}, {}),
({"plugins": {"datasette-foo": "bar"}}, {"plugins": {"datasette-foo": "bar"}}),
# Test redaction
(
{
"plugins": {
"datasette-auth": {"secret_key": "key"},
"datasette-foo": "bar",
"datasette-auth2": {"password": "password"},
"datasette-sentry": {
"dsn": "sentry:///foo",
},
}
},
{
"plugins": {
"datasette-auth": {"secret_key": "***"},
"datasette-foo": "bar",
"datasette-auth2": {"password": "***"},
"datasette-sentry": {"dsn": "***"},
}
},
),
),
)
async def test_config_json(config, expected):
"/-/config.json should return redacted configuration"
ds = Datasette(config=config)
response = await ds.client.get("/-/config.json")
assert response.json() == expected