kopia lustrzana https://github.com/simonw/datasette
/-/config page, closes #2254
rodzic
85a1dfe6e0
commit
1e901aa690
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue