mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
/-/config page, closes #2254
This commit is contained in:
parent
85a1dfe6e0
commit
1e901aa690
5 changed files with 84 additions and 23 deletions
|
|
@ -81,6 +81,7 @@ from .utils import (
|
||||||
tilde_decode,
|
tilde_decode,
|
||||||
to_css_class,
|
to_css_class,
|
||||||
urlsafe_components,
|
urlsafe_components,
|
||||||
|
redact_keys,
|
||||||
row_sql_params_pks,
|
row_sql_params_pks,
|
||||||
)
|
)
|
||||||
from .utils.asgi import (
|
from .utils.asgi import (
|
||||||
|
|
@ -1374,6 +1375,11 @@ class Datasette:
|
||||||
output.append(script)
|
output.append(script)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _config(self):
|
||||||
|
return redact_keys(
|
||||||
|
self.config, ("secret", "key", "password", "token", "hash", "dsn")
|
||||||
|
)
|
||||||
|
|
||||||
def _routes(self):
|
def _routes(self):
|
||||||
routes = []
|
routes = []
|
||||||
|
|
||||||
|
|
@ -1433,12 +1439,8 @@ class Datasette:
|
||||||
r"/-/settings(\.(?P<format>json))?$",
|
r"/-/settings(\.(?P<format>json))?$",
|
||||||
)
|
)
|
||||||
add_route(
|
add_route(
|
||||||
permanent_redirect("/-/settings.json"),
|
JsonDataView.as_view(self, "config.json", lambda: self._config()),
|
||||||
r"/-/config.json",
|
r"/-/config(\.(?P<format>json))?$",
|
||||||
)
|
|
||||||
add_route(
|
|
||||||
permanent_redirect("/-/settings"),
|
|
||||||
r"/-/config",
|
|
||||||
)
|
)
|
||||||
add_route(
|
add_route(
|
||||||
JsonDataView.as_view(self, "threads.json", self._threads),
|
JsonDataView.as_view(self, "threads.json", self._threads),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import time
|
||||||
import types
|
import types
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Iterable
|
||||||
import urllib
|
import urllib
|
||||||
import yaml
|
import yaml
|
||||||
from .shutil_backport import copytree
|
from .shutil_backport import copytree
|
||||||
|
|
@ -1327,3 +1328,30 @@ def move_plugins(source, destination):
|
||||||
|
|
||||||
recursive_move(source, destination)
|
recursive_move(source, destination)
|
||||||
prune_empty_dicts(source)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class JsonDataView(BaseView):
|
||||||
if self.ds.cors:
|
if self.ds.cors:
|
||||||
add_cors_headers(headers)
|
add_cors_headers(headers)
|
||||||
return Response(
|
return Response(
|
||||||
json.dumps(data),
|
json.dumps(data, default=repr),
|
||||||
content_type="application/json; charset=utf-8",
|
content_type="application/json; charset=utf-8",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
@ -53,7 +53,7 @@ class JsonDataView(BaseView):
|
||||||
request=request,
|
request=request,
|
||||||
context={
|
context={
|
||||||
"filename": self.filename,
|
"filename": self.filename,
|
||||||
"data_json": json.dumps(data, indent=4),
|
"data_json": json.dumps(data, indent=4, default=repr),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Add ``?all=1`` to include details of the default plugins baked into Datasette.
|
||||||
|
|
||||||
.. _JsonDataView_config:
|
.. _JsonDataView_settings:
|
||||||
|
|
||||||
/-/settings
|
/-/settings
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -105,6 +105,15 @@ Shows the :ref:`settings` for this instance of Datasette. `Settings example <htt
|
||||||
"sql_time_limit_ms": 1000
|
"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:
|
.. _JsonDataView_databases:
|
||||||
|
|
||||||
/-/databases
|
/-/databases
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
test_json_columns_default_expected = [
|
||||||
{"intval": 1, "strval": "s", "floatval": 0.5, "jsonval": '{"foo": "bar"}'}
|
{"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
|
# And the JSON for that database
|
||||||
response2 = await ds.client.get(path + ".json")
|
response2 = await ds.client.get(path + ".json")
|
||||||
assert response2.status_code == 200
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue