mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2025-05-13 14:44:29 -04:00
redact passwords, keys, and secret tokens in admin UI
This commit is contained in:
parent
8667ed29f1
commit
027c029316
6 changed files with 183 additions and 4 deletions
|
@ -112,7 +112,7 @@ CONFIG_SCHEMA: Dict[str, ConfigDefaultDict] = {
|
||||||
'LDAP_FIRSTNAME_ATTR': {'type': str, 'default': None},
|
'LDAP_FIRSTNAME_ATTR': {'type': str, 'default': None},
|
||||||
'LDAP_LASTNAME_ATTR': {'type': str, 'default': None},
|
'LDAP_LASTNAME_ATTR': {'type': str, 'default': None},
|
||||||
'LDAP_EMAIL_ATTR': {'type': str, 'default': None},
|
'LDAP_EMAIL_ATTR': {'type': str, 'default': None},
|
||||||
'LDAP_CREATE_SUPERUSER': {'type': bool, 'default': False},
|
'LDAP_CREATE_SUPERUSER': {'type': bool, 'default': False},
|
||||||
},
|
},
|
||||||
|
|
||||||
'ARCHIVE_METHOD_TOGGLES': {
|
'ARCHIVE_METHOD_TOGGLES': {
|
||||||
|
@ -265,7 +265,7 @@ CONFIG_ALIASES = {
|
||||||
for key, default in section.items()
|
for key, default in section.items()
|
||||||
for alias in default.get('aliases', ())
|
for alias in default.get('aliases', ())
|
||||||
}
|
}
|
||||||
USER_CONFIG = {key for section in CONFIG_SCHEMA.values() for key in section.keys()}
|
USER_CONFIG = {key: section[key] for section in CONFIG_SCHEMA.values() for key in section.keys()}
|
||||||
|
|
||||||
def get_real_name(key: str) -> str:
|
def get_real_name(key: str) -> str:
|
||||||
"""get the current canonical name for a given deprecated config key"""
|
"""get the current canonical name for a given deprecated config key"""
|
||||||
|
|
|
@ -13,9 +13,11 @@ from django.utils.safestring import mark_safe
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django import forms
|
from django import forms
|
||||||
# monkey patch django-signals-webhooks to change how it shows up in Admin UI
|
|
||||||
|
|
||||||
from signal_webhooks.apps import DjangoSignalWebhooksConfig
|
from signal_webhooks.apps import DjangoSignalWebhooksConfig
|
||||||
from signal_webhooks.admin import WebhookAdmin, WebhookModel
|
from signal_webhooks.admin import WebhookAdmin, WebhookModel
|
||||||
|
|
||||||
from ..util import htmldecode, urldecode, ansi_to_html
|
from ..util import htmldecode, urldecode, ansi_to_html
|
||||||
|
|
||||||
from core.models import Snapshot, ArchiveResult, Tag
|
from core.models import Snapshot, ArchiveResult, Tag
|
||||||
|
@ -117,6 +119,16 @@ archivebox_admin.register(APIToken)
|
||||||
archivebox_admin.register(WebhookModel, WebhookAdmin)
|
archivebox_admin.register(WebhookModel, WebhookAdmin)
|
||||||
archivebox_admin.disable_action('delete_selected')
|
archivebox_admin.disable_action('delete_selected')
|
||||||
|
|
||||||
|
|
||||||
|
# patch admin with methods to add data views
|
||||||
|
from admin_data_views.admin import get_app_list, admin_data_index_view, get_admin_data_urls, get_urls
|
||||||
|
|
||||||
|
archivebox_admin.get_app_list = get_app_list.__get__(archivebox_admin, ArchiveBoxAdmin)
|
||||||
|
archivebox_admin.admin_data_index_view = admin_data_index_view.__get__(archivebox_admin, ArchiveBoxAdmin)
|
||||||
|
archivebox_admin.get_admin_data_urls = get_admin_data_urls.__get__(archivebox_admin, ArchiveBoxAdmin)
|
||||||
|
archivebox_admin.get_urls = get_urls(archivebox_admin.get_urls).__get__(archivebox_admin, ArchiveBoxAdmin)
|
||||||
|
|
||||||
|
|
||||||
class ArchiveResultInline(admin.TabularInline):
|
class ArchiveResultInline(admin.TabularInline):
|
||||||
model = ArchiveResult
|
model = ArchiveResult
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,22 @@ class CoreConfig(AppConfig):
|
||||||
name = 'core'
|
name = 'core'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
# register our custom admin as the primary django admin
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin import sites
|
||||||
|
from core.admin import archivebox_admin
|
||||||
|
|
||||||
|
admin.site = archivebox_admin
|
||||||
|
sites.site = archivebox_admin
|
||||||
|
|
||||||
|
|
||||||
|
# register signal handlers
|
||||||
from .auth import register_signals
|
from .auth import register_signals
|
||||||
|
|
||||||
register_signals()
|
register_signals()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# from django.contrib.admin.apps import AdminConfig
|
||||||
|
# class CoreAdminConfig(AdminConfig):
|
||||||
|
# default_site = "core.admin.get_admin_site"
|
||||||
|
|
|
@ -64,6 +64,8 @@ INSTALLED_APPS = [
|
||||||
'core',
|
'core',
|
||||||
'api',
|
'api',
|
||||||
|
|
||||||
|
'admin_data_views',
|
||||||
|
|
||||||
'signal_webhooks',
|
'signal_webhooks',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
]
|
]
|
||||||
|
@ -416,3 +418,20 @@ SIGNAL_WEBHOOKS = {
|
||||||
"api.models.APIToken": ...,
|
"api.models.APIToken": ...,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ADMIN_DATA_VIEWS = {
|
||||||
|
"NAME": "configuration",
|
||||||
|
"URLS": [
|
||||||
|
{
|
||||||
|
"route": "live/",
|
||||||
|
"view": "core.views.live_config_list_view",
|
||||||
|
"name": "live",
|
||||||
|
"items": {
|
||||||
|
"route": "<str:key>/",
|
||||||
|
"view": "core.views.live_config_value_view",
|
||||||
|
"name": "live_config_value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
__package__ = 'archivebox.core'
|
__package__ = 'archivebox.core'
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
|
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.http import HttpResponse, Http404
|
from django.http import HttpRequest, HttpResponse, Http404
|
||||||
from django.utils.html import format_html, mark_safe
|
from django.utils.html import format_html, mark_safe
|
||||||
from django.views import View, static
|
from django.views import View, static
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
@ -14,6 +16,10 @@ from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from admin_data_views.typing import TableContext, ItemContext
|
||||||
|
from admin_data_views.utils import render_with_table_view, render_with_item_view, ItemLink
|
||||||
|
|
||||||
|
|
||||||
from core.models import Snapshot
|
from core.models import Snapshot
|
||||||
from core.forms import AddLinkForm
|
from core.forms import AddLinkForm
|
||||||
|
|
||||||
|
@ -26,6 +32,10 @@ from ..config import (
|
||||||
COMMIT_HASH,
|
COMMIT_HASH,
|
||||||
FOOTER_INFO,
|
FOOTER_INFO,
|
||||||
SNAPSHOTS_PER_PAGE,
|
SNAPSHOTS_PER_PAGE,
|
||||||
|
CONFIG,
|
||||||
|
CONFIG_SCHEMA,
|
||||||
|
DYNAMIC_CONFIG_SCHEMA,
|
||||||
|
USER_CONFIG,
|
||||||
)
|
)
|
||||||
from ..main import add
|
from ..main import add
|
||||||
from ..util import base_url, ansi_to_html
|
from ..util import base_url, ansi_to_html
|
||||||
|
@ -312,3 +322,124 @@ class HealthCheckView(View):
|
||||||
content_type='text/plain',
|
content_type='text/plain',
|
||||||
status=200
|
status=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_config_section(key: str) -> str:
|
||||||
|
matching_sections = [
|
||||||
|
name for name, opts in CONFIG_SCHEMA.items() if key in opts
|
||||||
|
]
|
||||||
|
section = matching_sections[0] if matching_sections else 'DYNAMIC'
|
||||||
|
return section
|
||||||
|
|
||||||
|
def find_config_default(key: str) -> str:
|
||||||
|
default_val = USER_CONFIG.get(key, {}).get('default', lambda: None)
|
||||||
|
if isinstance(default_val, Callable):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
default_val = repr(default_val)
|
||||||
|
return default_val
|
||||||
|
|
||||||
|
def find_config_type(key: str) -> str:
|
||||||
|
if key in USER_CONFIG:
|
||||||
|
return USER_CONFIG[key]['type'].__name__
|
||||||
|
elif key in DYNAMIC_CONFIG_SCHEMA:
|
||||||
|
return type(CONFIG[key]).__name__
|
||||||
|
return 'str'
|
||||||
|
|
||||||
|
def key_is_safe(key: str) -> bool:
|
||||||
|
for term in ('key', 'password', 'secret', 'token'):
|
||||||
|
if term in key.lower():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@render_with_table_view
|
||||||
|
def live_config_list_view(request: HttpRequest, **kwargs) -> TableContext:
|
||||||
|
|
||||||
|
assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
|
||||||
|
|
||||||
|
rows = {
|
||||||
|
"Section": [],
|
||||||
|
"Key": [],
|
||||||
|
"Type": [],
|
||||||
|
"Value": [],
|
||||||
|
"Default": [],
|
||||||
|
# "Documentation": [],
|
||||||
|
"Aliases": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for section in CONFIG_SCHEMA.keys():
|
||||||
|
for key in CONFIG_SCHEMA[section].keys():
|
||||||
|
rows['Section'].append(section.replace('_', ' ').title().replace(' Config', ''))
|
||||||
|
rows['Key'].append(ItemLink(key, key=key))
|
||||||
|
rows['Type'].append(mark_safe(f'<code>{find_config_type(key)}</code>'))
|
||||||
|
rows['Value'].append(mark_safe(f'<code>{CONFIG[key]}</code>') if key_is_safe(key) else '******** (redacted)')
|
||||||
|
rows['Default'].append(mark_safe(f'<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+%27{key}%27&type=code"><code style="text-decoration: underline">{find_config_default(key) or 'See here...'}</code></a>'))
|
||||||
|
# rows['Documentation'].append(mark_safe(f'Wiki: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">{key}</a>'))
|
||||||
|
rows['Aliases'].append(', '.join(CONFIG_SCHEMA[section][key].get('aliases', [])))
|
||||||
|
|
||||||
|
section = 'DYNAMIC'
|
||||||
|
for key in DYNAMIC_CONFIG_SCHEMA.keys():
|
||||||
|
rows['Section'].append(section.replace('_', ' ').title().replace(' Config', ''))
|
||||||
|
rows['Key'].append(ItemLink(key, key=key))
|
||||||
|
rows['Type'].append(mark_safe(f'<code>{find_config_type(key)}</code>'))
|
||||||
|
rows['Value'].append(mark_safe(f'<code>{CONFIG[key]}</code>') if key_is_safe(key) else '******** (redacted)')
|
||||||
|
rows['Default'].append(mark_safe(f'<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+%27{key}%27&type=code"><code style="text-decoration: underline">{find_config_default(key) or 'See here...'}</code></a>'))
|
||||||
|
# rows['Documentation'].append(mark_safe(f'Wiki: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">{key}</a>'))
|
||||||
|
rows['Aliases'].append(ItemLink(key, key=key) if key in USER_CONFIG else '')
|
||||||
|
|
||||||
|
return TableContext(
|
||||||
|
title="Computed Configuration Values",
|
||||||
|
table=rows,
|
||||||
|
)
|
||||||
|
|
||||||
|
@render_with_item_view
|
||||||
|
def live_config_value_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
|
||||||
|
|
||||||
|
assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
|
||||||
|
|
||||||
|
aliases = USER_CONFIG.get(key, {}).get("aliases", [])
|
||||||
|
|
||||||
|
return ItemContext(
|
||||||
|
slug=key,
|
||||||
|
title=key,
|
||||||
|
data=[
|
||||||
|
{
|
||||||
|
"name": mark_safe(f'data / ArchiveBox.conf [{find_config_section(key)}] <b><code style="color: lightgray">{key}</code></b>' if key in USER_CONFIG else f'[DYNAMIC CONFIG] <b><code style="color: lightgray">{key}</code></b> <small>(calculated at runtime)</small>'),
|
||||||
|
"description": None,
|
||||||
|
"fields": {
|
||||||
|
'Key': key,
|
||||||
|
'Type': find_config_type(key),
|
||||||
|
'Value': CONFIG[key] if key_is_safe(key) else '********',
|
||||||
|
},
|
||||||
|
"help_texts": {
|
||||||
|
'Key': mark_safe(f'''
|
||||||
|
<a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">Documentation</a>
|
||||||
|
<span style="display: {"inline" if aliases else "none"}">
|
||||||
|
Aliases: {", ".join(aliases)}
|
||||||
|
</span>
|
||||||
|
'''),
|
||||||
|
'Type': mark_safe(f'''
|
||||||
|
<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+%27{key}%27&type=code">
|
||||||
|
See full definition in <code>archivebox/config.py</code>...
|
||||||
|
</a>
|
||||||
|
'''),
|
||||||
|
'Value': mark_safe(f'''
|
||||||
|
{'<b style="color: red">Value is redacted for your security. (Passwords, secrets, API tokens, etc. cannot be viewed in the Web UI)</b><br/><br/>' if not key_is_safe(key) else ''}
|
||||||
|
Default: <a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+%27{key}%27&type=code">
|
||||||
|
<code>{find_config_default(key) or 'See 1here...'}</code>
|
||||||
|
</a>
|
||||||
|
<br/><br/>
|
||||||
|
<p style="display: {"block" if key in USER_CONFIG else "none"}">
|
||||||
|
<i>To change this value, edit <code>data/ArchiveBox.conf</code> or run:</i>
|
||||||
|
<br/><br/>
|
||||||
|
<code>archivebox config --set {key}="{
|
||||||
|
val.strip("'")
|
||||||
|
if (val := find_config_default(key)) else
|
||||||
|
(repr(CONFIG[key] if key_is_safe(key) else '********')).strip("'")
|
||||||
|
}"</code>
|
||||||
|
</p>
|
||||||
|
'''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
|
@ -36,6 +36,7 @@ dependencies = [
|
||||||
# - scihubdl
|
# - scihubdl
|
||||||
# - See Github issues for more...
|
# - See Github issues for more...
|
||||||
"django-signal-webhooks>=0.3.0",
|
"django-signal-webhooks>=0.3.0",
|
||||||
|
"django-admin-data-views>=0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
homepage = "https://github.com/ArchiveBox/ArchiveBox"
|
homepage = "https://github.com/ArchiveBox/ArchiveBox"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue