Merge lp:~bloodearnest/django-preflight/optional-settings into lp:django-preflight

Proposed by Simon Davy
Status: Merged
Approved by: Simon Davy
Approved revision: 31
Merged at revision: 28
Proposed branch: lp:~bloodearnest/django-preflight/optional-settings
Merge into: lp:django-preflight
Diff against target: 179 lines (+65/-5)
4 files modified
preflight/conf.py (+1/-0)
preflight/models.py (+28/-5)
preflight/templates/preflight/overview.html (+4/-0)
preflight/tests.py (+32/-0)
To merge this branch: bzr merge lp:~bloodearnest/django-preflight/optional-settings
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Łukasz Czyżykowski (community) Approve
Ricardo Kirkner (community) Approve
Review via email: mp+169842@code.launchpad.net

Commit message

- Allow disabling setting dump on overview page.
- Default PREFLIGHT_ENABLE_SETTINGS to False, and improve secret cleansing.

Description of the change

Make settings optional via config

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Branch looks good, but can we please make the default be False?

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Expanding a bit on my former comment: having settings enabled by default makes much easier to expose sensitive data through the preflight overview page, because the gather_settings function is not smart about hidding settings inside a dict.

So, if we will leave the "show settings" defaulting to True, I would please request that preflight will copy/reuse the logic in django/views/debug.py:26:

def cleanse_setting(key, value):
    """Cleanse an individual setting key/value of sensitive content.

    If the value is a dictionary, recursively cleanse the keys in
    that dictionary.
    """
    try:
        if HIDDEN_SETTINGS.search(key):
            cleansed = '********************'
        else:
            if isinstance(value, dict):
                cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items())
            else:
                cleansed = value
    except TypeError:
        # If the key isn't regex-able, just return as-is.
        cleansed = value
    return cleansed

review: Needs Fixing
29. By Simon Davy

Default PREFLIGHT_ENABLE_SETTINGS to False, and improve secret cleansing

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) :
review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

09:35 < nessita> bloodearnest, branch looks great, I'm approving. Any chance you can add a test where a setting inside a dict is expected to be masked?
09:35 < nessita> such as the database setting
09:39 < bloodearnest> nessita, good idea, will do
09:39 < nessita> bloodearnest, thanks, will leave my vote in there

review: Approve
30. By Simon Davy

added test for new cleansing code

31. By Simon Davy

minor tweak for django 1.1/1.2 support

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'preflight/conf.py'
2--- preflight/conf.py 2013-05-17 14:51:29 +0000
3+++ preflight/conf.py 2013-06-18 13:38:25 +0000
4@@ -4,3 +4,4 @@
5 BASE_TEMPLATE = getattr(
6 settings, 'PREFLIGHT_BASE_TEMPLATE', "admin/base.html")
7 TABLE_CLASS = getattr(settings, 'PREFLIGHT_TABLE_CLASS', "listing")
8+ENABLE_SETTINGS = getattr(settings, 'PREFLIGHT_ENABLE_SETTINGS', False)
9
10=== modified file 'preflight/models.py'
11--- preflight/models.py 2013-06-10 12:57:48 +0000
12+++ preflight/models.py 2013-06-18 13:38:25 +0000
13@@ -8,6 +8,8 @@
14 from django.conf import settings
15 from django.views.debug import HIDDEN_SETTINGS
16
17+from .conf import ENABLE_SETTINGS
18+
19 REGISTRY = []
20
21
22@@ -130,7 +132,31 @@
23 return "not found"
24
25
26+# taken from django.debug.views.py at 7902fd74f1
27+def cleanse_setting(key, value, hidden):
28+ """Cleanse an individual setting key/value of sensitive content.
29+
30+ If the value is a dictionary, recursively cleanse the keys in
31+ that dictionary.
32+ """
33+ try:
34+ if hidden.search(key):
35+ cleansed = '******************'
36+ else:
37+ if isinstance(value, dict):
38+ cleansed = dict((k, cleanse_setting(k, v, hidden))
39+ for k,v in value.items())
40+ else:
41+ cleansed = value
42+ except TypeError:
43+ # If the key isn't regex-able, just return as-is.
44+ cleansed = value
45+ return cleansed
46+
47 def gather_settings():
48+ if not ENABLE_SETTINGS:
49+ return []
50+
51 names = sorted([x for x in settings._wrapped.__dict__
52 if x.isupper() and not x.startswith('_')])
53 settings_list = []
54@@ -140,13 +166,10 @@
55 else:
56 hidden_settings = HIDDEN_SETTINGS
57 for name in names:
58- if hidden_settings.search(name):
59- value = '******************'
60- else:
61- value = getattr(settings, name, None)
62+ value = getattr(settings, name, None)
63 settings_list.append({
64 'name': name,
65- 'value': value,
66+ 'value': cleanse_setting(name, value, hidden_settings),
67 'location': locate_setting(name),
68 })
69 return settings_list
70
71=== modified file 'preflight/templates/preflight/overview.html'
72--- preflight/templates/preflight/overview.html 2013-06-04 15:00:12 +0000
73+++ preflight/templates/preflight/overview.html 2013-06-18 13:38:25 +0000
74@@ -16,7 +16,9 @@
75 <li><a href="#{{ application.name|slugify }}">{{ application.name }}</a></li>
76 {% endfor %}
77 <li><a href="#versions">Versions</a></li>
78+ {% if settings %}
79 <li><a href="#settings">Settings</a></li>
80+ {% endif %}
81 {% if switches %}
82 <li><a href="#switches">Switches</a></li>
83 {% endif %}
84@@ -72,6 +74,7 @@
85 </tbody>
86 </table>
87
88+ {% if settings %}
89 <a href="#links" style="text-decoration: none;">
90 <h1 id="settings">{% trans "Settings" %}</h1>
91 </a>
92@@ -82,6 +85,7 @@
93 {% if setting.location %}<dd>Location: {{ setting.location}}</dd>{% endif %}
94 {% endfor %}
95 </dl>
96+ {% endif %}
97
98 {% if switches %}
99 <a href="#links" style="text-decoration: none;">
100
101=== modified file 'preflight/tests.py'
102--- preflight/tests.py 2013-06-10 12:57:48 +0000
103+++ preflight/tests.py 2013-06-18 13:38:25 +0000
104@@ -26,8 +26,10 @@
105 from .models import (
106 Application,
107 Check,
108+ HIDDEN_SETTINGS,
109 REGISTRY,
110 authenticate,
111+ cleanse_setting,
112 gather_checks,
113 gather_switches,
114 gather_gargoyle,
115@@ -322,6 +324,8 @@
116
117
118 class SettingsTestCase(TestCase):
119+
120+ @patch('preflight.models.ENABLE_SETTINGS', True)
121 @patch('preflight.models.settings')
122 def test_gather_settings_no_location(self, mock_settings):
123 mock_settings._wrapped.FOO = 'bar'
124@@ -331,6 +335,7 @@
125 expected = [{'name': 'FOO', 'value': 'bar', 'location': ''}]
126 self.assertEqual(expected, settings)
127
128+ @patch('preflight.models.ENABLE_SETTINGS', True)
129 @patch('preflight.models.settings')
130 def test_gather_settings_with_location(self, mock_settings):
131 mock_settings._wrapped.FOO = 'bar'
132@@ -343,6 +348,7 @@
133 expected = [{'name': 'FOO', 'value': 'bar', 'location': '/tmp/baz'}]
134 self.assertEqual(expected, settings)
135
136+ @patch('preflight.models.ENABLE_SETTINGS', True)
137 @patch('preflight.models.settings')
138 def test_gather_settings_hidden(self, mock_settings):
139 mock_settings._wrapped.FOO_ZOGGLES = 'bar'
140@@ -352,6 +358,7 @@
141 expected = [{'name': 'FOO_ZOGGLES', 'value': '*' * 18, 'location': ''}]
142 self.assertEqual(expected, settings)
143
144+ @patch('preflight.models.ENABLE_SETTINGS', True)
145 @patch('preflight.models.settings')
146 def test_gather_settings_omitted(self, mock_settings):
147 mock_settings._wrapped._FOO = 'bar'
148@@ -360,6 +367,31 @@
149 settings = gather_settings()
150 self.assertEqual([], settings)
151
152+ def test_gather_settings_disabled(self):
153+ self.assertEqual(gather_settings(), [])
154+
155+ def test_gargoyle_template_no_settings(self):
156+ context = {
157+ "settings": [],
158+ "preflight_base_template": BASE_TEMPLATE,
159+ "preflight_table_class": TABLE_CLASS,
160+ }
161+ response = render_to_string('preflight/overview.html', context)
162+ dom = PyQuery(response)
163+
164+ settings_header = dom.find('#settings')
165+ self.assertEqual(settings_header, [])
166+
167+ def test_cleanse_setting(self):
168+ key = 'DATABASES'
169+ value = {
170+ 'default': {
171+ 'PASSWORD': 'secret'
172+ }
173+ }
174+ result = cleanse_setting(key, value, HIDDEN_SETTINGS)
175+ self.assertFalse('secret' in result['default']['PASSWORD'])
176+
177
178 @skipIf(not settings.USE_GARGOYLE, 'skipping for Django 1.1')
179 class GargoyleTestCase(TestCase):

Subscribers

People subscribed via source and target branches