Merge lp:~maxiberta/canonical-identity-provider/readonly-improvements into lp:canonical-identity-provider/release
- readonly-improvements
- Merge into trunk
Proposed by
Maximiliano Bertacchini
Status: | Merged |
---|---|
Approved by: | Maximiliano Bertacchini |
Approved revision: | no longer in the source branch. |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | lp:~maxiberta/canonical-identity-provider/readonly-improvements |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
1396 lines (+170/-738) 19 files modified
django_project/settings_base.py (+0/-2) src/identityprovider/management/commands/readonly.py (+11/-49) src/identityprovider/readonly.py (+1/-92) src/identityprovider/templates/admin/readonly.html (+0/-66) src/identityprovider/templates/admin/readonly.txt (+0/-8) src/identityprovider/templates/admin/readonly_confirm.html (+0/-70) src/identityprovider/tests/test_auth.py (+4/-8) src/identityprovider/tests/test_command_readonly.py (+3/-52) src/identityprovider/tests/test_models_account.py (+5/-10) src/identityprovider/tests/test_models_openidmodels.py (+5/-14) src/identityprovider/tests/test_readonly.py (+2/-253) src/identityprovider/tests/test_signals.py (+10/-19) src/identityprovider/tests/utils.py (+13/-1) src/identityprovider/urls.py (+0/-12) src/identityprovider/views/readonly.py (+0/-64) src/webui/tests/test_templates.py (+3/-6) src/webui/tests/test_views_account.py (+64/-3) src/webui/tests/test_views_registration.py (+32/-2) src/webui/tests/test_views_ui.py (+17/-7) |
To merge this branch: | bzr merge lp:~maxiberta/canonical-identity-provider/readonly-improvements |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Daniel Manrique (community) | Approve | ||
Review via email: mp+374313@code.launchpad.net |
Commit message
Readonly cleanup: drop API for remote management of readonly flag (now handled via juju); simplify the 'readonly' management command.
Description of the change
Adds a number of tests around readonly mode that were missing.
Additionally, fix/workaround the readonly management command which was basically broken due to django's arg parser prefix matching "--set" to "--settings", which resulted in `manage readonly --set` crashing with AttributeError. This command is now intended for local development only (`manage readonly [set|clear]`).
To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Looks great, thanks for the added tests.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'django_project/settings_base.py' |
2 | --- django_project/settings_base.py 2019-09-25 20:39:50 +0000 |
3 | +++ django_project/settings_base.py 2019-10-22 14:04:10 +0000 |
4 | @@ -41,7 +41,6 @@ |
5 | API_HOST += ':%s' % parsed_url.port |
6 | API_URL = '/api/v2' |
7 | APPEND_SLASH = True |
8 | -APP_SERVERS = [] |
9 | AUTHENTICATION_BACKENDS = [ |
10 | 'identityprovider.auth.LaunchpadBackend', |
11 | # restore ModelBackend until admin login gets properly fixed |
12 | @@ -480,7 +479,6 @@ |
13 | 'canonical_raven.processors.SanitizeTimelineDjangoProcessor', |
14 | ], |
15 | } |
16 | -READONLY_SECRET = 'CHANGEME' |
17 | READ_ONLY_MODE = False |
18 | ROOT_URLCONF = 'django_project.urls' |
19 | SAML2IDP_CONFIG = { |
20 | |
21 | === modified file 'src/identityprovider/management/commands/readonly.py' |
22 | --- src/identityprovider/management/commands/readonly.py 2018-01-29 01:46:19 +0000 |
23 | +++ src/identityprovider/management/commands/readonly.py 2019-10-22 14:04:10 +0000 |
24 | @@ -1,66 +1,28 @@ |
25 | -# Copyright 2010-2018 Canonical Ltd. This software is licensed under the |
26 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
27 | # GNU Affero General Public License version 3 (see the file LICENSE). |
28 | |
29 | from __future__ import print_function |
30 | |
31 | -from django.conf import settings |
32 | from django.core.management.base import BaseCommand, CommandError |
33 | -from django.template import loader |
34 | |
35 | -from identityprovider.readonly import get_server_atts, update_server |
36 | +from identityprovider.readonly import ReadOnlyManager |
37 | |
38 | |
39 | class Command(BaseCommand): |
40 | help = ('Manage readonly mode.\n\n' |
41 | - 'options --set and --clear are all mutually exclusive.\n' |
42 | + 'Actions "set" and "clear" are all mutually exclusive.\n' |
43 | 'You can only choose one at a time.') |
44 | |
45 | def add_arguments(self, parser): |
46 | - parser.add_argument( |
47 | - '--list', action='store_true', dest='list_servers', default=False, |
48 | - help='List available application servers.') |
49 | - parser.add_argument( |
50 | - '--all', action='store_true', dest='all_servers', |
51 | - default=False, help='Select all servers.') |
52 | - parser.add_argument( |
53 | - '--set', action='store_const', const='set', |
54 | - dest='action', help='Set server to read-only.') |
55 | - parser.add_argument( |
56 | - '--clear', action='store_const', const='clear', |
57 | - dest='action', help='Set server to read-write.') |
58 | - parser.add_argument('servers', metavar='server', nargs='*') |
59 | + parser.add_argument('action', help='One of "set" or "clear".') |
60 | |
61 | def handle(self, *args, **options): |
62 | - servers = options.get('servers') |
63 | - list_servers = options.get('list_servers') |
64 | - all_servers = options.get('all_servers') |
65 | - action = options.get('action') |
66 | - |
67 | - # determine servers to act upon |
68 | - if all_servers: |
69 | - servers = [app['SERVER_ID'] for app in settings.APP_SERVERS] |
70 | - servers.sort() |
71 | + if options['action'] == 'set': |
72 | + ReadOnlyManager().set_readonly() |
73 | + self.stdout.write('Readonly mode set') |
74 | + elif options['action'] == 'clear': |
75 | + ReadOnlyManager().clear_readonly() |
76 | + self.stdout.write('Readonly mode cleared') |
77 | else: |
78 | - msgs = ( |
79 | - 'Enter at least one server, or specify the --all option.', |
80 | - 'Use --list to get a list of configured servers.', |
81 | - ) |
82 | - if not servers and not list_servers: |
83 | - raise CommandError('\n '.join(msgs)) |
84 | - |
85 | - # determine action to perform |
86 | - if action is not None: |
87 | - for server in servers: |
88 | - update_server(action, server) |
89 | - elif not list_servers: |
90 | - msg = 'Enter one of --set or --clear.' |
91 | + msg = 'Enter one of "set" or "clear".' |
92 | raise CommandError(msg) |
93 | - |
94 | - # list action is special as it can be combined with the other actions |
95 | - if list_servers: |
96 | - self.show_servers() |
97 | - |
98 | - def show_servers(self): |
99 | - """Provides a report about readonly status of all app servers.""" |
100 | - atts = get_server_atts(settings.APP_SERVERS) |
101 | - print(loader.render_to_string('admin/readonly.txt', atts)) |
102 | |
103 | === modified file 'src/identityprovider/readonly.py' |
104 | --- src/identityprovider/readonly.py 2018-01-28 12:43:01 +0000 |
105 | +++ src/identityprovider/readonly.py 2019-10-22 14:04:10 +0000 |
106 | @@ -1,11 +1,9 @@ |
107 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
108 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
109 | # GNU Affero General Public License version 3 (see the file LICENSE). |
110 | |
111 | -import json |
112 | import os |
113 | import stat |
114 | |
115 | -import requests |
116 | from django.conf import settings |
117 | |
118 | # When a request arrives, the middleware will: |
119 | @@ -31,95 +29,6 @@ |
120 | # automatic=True, as they manage manual overrides. |
121 | |
122 | |
123 | -def _remote_req(host, port=None, scheme=None, server_id=None, |
124 | - virtual_host=None, post=None): |
125 | - """Makes a request to a specific appserver. |
126 | - |
127 | - The first five arguments are the same as the keys for each dictionary in |
128 | - the APP_SERVERS setting: |
129 | - |
130 | - * host: The host at which we can reach the app server (can be an IP) |
131 | - * port: The port that should be used (optional) |
132 | - * scheme: The scheme to use to contact this app server (http/https) |
133 | - (defaults to http) |
134 | - * server_id: Some canonical name for this app server (optional) |
135 | - * virtual_host: The virtual host that should be used, for app servers that |
136 | - serve multiple sites (optional). |
137 | - |
138 | - If post is provided, it should be a sequence of 2-tuples that will be |
139 | - encoded in to POST data. |
140 | - """ |
141 | - if post is None: |
142 | - post = [] |
143 | - post.append(('secret', settings.READONLY_SECRET)) |
144 | - |
145 | - if scheme is None: |
146 | - scheme = 'http' |
147 | - if port is None: |
148 | - portstr = '' |
149 | - else: |
150 | - portstr = ':' + port |
151 | - url = '%s://%s%s/readonlydata' % (scheme, host, portstr) |
152 | - headers = {} |
153 | - if virtual_host is not None: |
154 | - headers['Host'] = virtual_host |
155 | - try: |
156 | - response = requests.post(url, headers=headers, data=post, timeout=5) |
157 | - data = response.content |
158 | - except requests.RequestException: |
159 | - data = None |
160 | - return data |
161 | - |
162 | - |
163 | -def lowercase_keys(dicts): |
164 | - """ Convert all keys in a list of dicts to lowercase. """ |
165 | - return [dict((k.lower(), v) for (k, v) in d.items()) for d in dicts] |
166 | - |
167 | - |
168 | -def get_server_atts(servers): |
169 | - """Provides a report about readonly status of all app servers.""" |
170 | - appservers = [] |
171 | - set_all_readonly = False |
172 | - clear_all_readonly = False |
173 | - for server in lowercase_keys(servers): |
174 | - datastr = _remote_req(**server) |
175 | - if datastr is None: |
176 | - data = {'reachable': False} |
177 | - else: |
178 | - try: |
179 | - data = json.loads(datastr) |
180 | - except ValueError: |
181 | - # This is probably caused by the remote database being out |
182 | - # of sync with our current database |
183 | - data = {'reachable': False} |
184 | - else: |
185 | - data['reachable'] = True |
186 | - if data.get('readonly'): |
187 | - clear_all_readonly = True |
188 | - else: |
189 | - set_all_readonly = True |
190 | - data['name'] = server['server_id'] |
191 | - appservers.append(data) |
192 | - atts = { |
193 | - 'appservers': appservers, |
194 | - 'clear_all_readonly': clear_all_readonly, |
195 | - 'set_all_readonly': set_all_readonly, |
196 | - } |
197 | - return atts |
198 | - |
199 | - |
200 | -def update_server(action, appserver=None): |
201 | - if appserver is None: |
202 | - appservers = settings.APP_SERVERS |
203 | - else: |
204 | - appservers = [server for server in settings.APP_SERVERS |
205 | - if server['SERVER_ID'] == appserver] |
206 | - appservers = lowercase_keys(appservers) |
207 | - for server in appservers: |
208 | - post = [('action', action)] |
209 | - _remote_req(post=post, **server) |
210 | - |
211 | - |
212 | class ReadOnlyManager(object): |
213 | |
214 | @property |
215 | |
216 | === removed file 'src/identityprovider/templates/admin/readonly.html' |
217 | --- src/identityprovider/templates/admin/readonly.html 2013-06-20 15:37:44 +0000 |
218 | +++ src/identityprovider/templates/admin/readonly.html 1970-01-01 00:00:00 +0000 |
219 | @@ -1,66 +0,0 @@ |
220 | -{% extends "admin/index.html" %} |
221 | -{% load i18n staticfiles %} |
222 | - |
223 | -{% comment %} |
224 | -Copyright 2010 Canonical Ltd. This software is licensed under the |
225 | -GNU Affero General Public License version 3 (see the file LICENSE). |
226 | -{% endcomment %} |
227 | - |
228 | -{% block breadcrumbs %} |
229 | -<div class="breadcrumbs"><a href="/admin/"> |
230 | -{% trans "Home" %}</a> › |
231 | -<a href="/admin/identityprovider/">Identityprovider</a> › |
232 | -{% trans "Readonly Admin" %} |
233 | -</div> |
234 | -{% endblock %} |
235 | - |
236 | -{% block content %} |
237 | -<div id="content-main"> |
238 | - <h1>{% trans "Readonly status per application server" %}</h1> |
239 | - {% for server in appservers %} |
240 | - <div class="module"> |
241 | - <table summary="DB connection on {{server.name}}."> |
242 | - <caption>{{server.name}}</caption> |
243 | - <tr><th scope="row"> |
244 | - {{server.state}} |
245 | -{% if server.reachable %} |
246 | - {% if server.readonly %}{% trans "In readonly mode" %}<ul> |
247 | - <li>{% trans "Manually disabled" %}</li> |
248 | - </ul> |
249 | - </th> |
250 | - <td> |
251 | - <a href="/readonly/{{server.name}}/clear/" class="addlink"> |
252 | - {% trans "Leave readonly" %} |
253 | - </a> |
254 | - </td> |
255 | - {% else %} |
256 | - {% trans "Operating normally" %} |
257 | - </th> |
258 | - <td> |
259 | - <a href="/readonly/{{server.name}}/set/" class="deletelink"> |
260 | - {% trans "Set readonly" %} |
261 | - </a> |
262 | - </td> |
263 | - {% endif %} |
264 | - {% else %}{% trans "Server is unreachable or out of sync" %} |
265 | - </th><td></td> |
266 | - {% endif %} |
267 | - </tr> |
268 | - </table> |
269 | - </div> |
270 | - {% endfor %} |
271 | - {% if clear_all_readonly %}<p> |
272 | - <a href="/readonly/clear/" class="addlink"> |
273 | - {% trans "Leave readonly on all appservers" %} |
274 | - </a> |
275 | - </p>{% endif %} |
276 | - {% if set_all_readonly %}<p> |
277 | - <a href="/readonly/set/" class="deletelink"> |
278 | - {% trans "Set readonly on all appservers" %} |
279 | - </a> |
280 | - </p>{% endif %} |
281 | -</div> |
282 | - |
283 | -{% endblock %} |
284 | - |
285 | -{% block sidebar %}{% endblock %} |
286 | |
287 | === removed file 'src/identityprovider/templates/admin/readonly.txt' |
288 | --- src/identityprovider/templates/admin/readonly.txt 2013-06-21 11:31:17 +0000 |
289 | +++ src/identityprovider/templates/admin/readonly.txt 1970-01-01 00:00:00 +0000 |
290 | @@ -1,8 +0,0 @@ |
291 | -{% comment %} |
292 | -Copyright 2010 Canonical Ltd. This software is licensed under the |
293 | -GNU Affero General Public License version 3 (see the file LICENSE). |
294 | - |
295 | -{% endcomment %}Readonly status per application server |
296 | -------------------------------------------------------------------------------- |
297 | -{% for server in appservers %} |
298 | - {{server.name}} -- {% if server.reachable %}{% if server.readonly %}In readonly mode{% else %}Operating normally{% endif %}{% else %}Server is unreachable or out of sync{% endif %}{% endfor %} |
299 | |
300 | === removed file 'src/identityprovider/templates/admin/readonly_confirm.html' |
301 | --- src/identityprovider/templates/admin/readonly_confirm.html 2013-06-20 15:41:21 +0000 |
302 | +++ src/identityprovider/templates/admin/readonly_confirm.html 1970-01-01 00:00:00 +0000 |
303 | @@ -1,70 +0,0 @@ |
304 | -{% extends "admin/index.html" %} |
305 | -{% load i18n %} |
306 | - |
307 | -{% comment %} |
308 | -Copyright 2010 Canonical Ltd. This software is licensed under the |
309 | -GNU Affero General Public License version 3 (see the file LICENSE). |
310 | -{% endcomment %} |
311 | - |
312 | -{% block breadcrumbs %} |
313 | -<div class="breadcrumbs"><a href="/admin/"> |
314 | -{% trans "Home" %}</a> › |
315 | -<a href="/admin/identityprovider/">Identityprovider</a> › |
316 | -<a href="/readonly">{% trans "Readonly Admin" %}</a> › |
317 | -{% trans "Confirm" %} |
318 | -</div> |
319 | -{% endblock %} |
320 | - |
321 | -{% block content %} |
322 | -<pre>{{ appservers }}</pre> |
323 | -<div id="content" class="colM"> |
324 | - <h1>{% trans "Are you sure?" %}</h1> |
325 | - {% if appserver %} |
326 | - <div class="system-message"> |
327 | - <p class="system-message-title"> |
328 | - {% blocktrans %}You're not operating on all appservers{% endblocktrans %} |
329 | - </p> |
330 | - <p> |
331 | - {% blocktrans %}Changing readonly mode on a single application server can lead to inconsistent states and unexpected behaviour.{% endblocktrans %} |
332 | - </p> |
333 | - </div> |
334 | - {% else %} |
335 | - {% ifequal action "clear" %} |
336 | - <div class="system-message"> |
337 | - <p class="system-message-title"> |
338 | - {% blocktrans %}Make sure the master database connection is enabled on all app servers <b>before</b> leaving readonly mode!{% endblocktrans %} |
339 | - </p> |
340 | - </div> |
341 | - {% endifequal %} |
342 | - {% endif %} |
343 | - |
344 | - <p> |
345 | - {% if action == "set" %} |
346 | - {% if appserver %} |
347 | - {% blocktrans %}You are about to enable readonly mode on appserver <b>{{ appserver }}</b>.{% endblocktrans %} |
348 | - {% else %} |
349 | - {% blocktrans %}You are about to enable readonly mode globally.{% endblocktrans %}</p><p> |
350 | - {% blocktrans %}All appservers will be passed to read-only mode if you confirm.{% endblocktrans %} |
351 | - {% endif %} |
352 | - {% elif action == "clear" %} |
353 | - {% if appserver %} |
354 | - {% blocktrans %}You are about to clear readonly mode on appserver <b>{{ appserver }}</b>.{% endblocktrans %}</p><p> |
355 | - {% blocktrans %}If you confirm, <b>{{ appserver }}</b> will attempt to resume normal operation with its master database connection.{% endblocktrans %}</p> |
356 | - {% else %} |
357 | - {% blocktrans %}You are about to clear readonly mode globally.{% endblocktrans %}</p><p> |
358 | - {% blocktrans %}If you confirm, all appservers will attempt to resume normal operation.{% endblocktrans %} |
359 | - {% endif %} |
360 | - {% endif %} |
361 | - </p> |
362 | - <form method="POST" action=""> |
363 | - {% csrf_token %} |
364 | - <input type="hidden" name="action" value="{{action}}" /> |
365 | - <input type="hidden" name="appserver" value="{{appserver}}" /> |
366 | - <input type="submit" value="{% trans "Yes, I'm sure" %}" /> |
367 | - {% trans "or" %} <a href="/readonly">{% trans "Cancel" %}</a> |
368 | - </form> |
369 | -</div> |
370 | - |
371 | -{% endblock %} |
372 | - |
373 | -{% block sidebar %}{% endblock %} |
374 | |
375 | === modified file 'src/identityprovider/tests/test_auth.py' |
376 | --- src/identityprovider/tests/test_auth.py 2018-11-29 12:02:42 +0000 |
377 | +++ src/identityprovider/tests/test_auth.py 2019-10-22 14:04:10 +0000 |
378 | @@ -50,9 +50,8 @@ |
379 | EmailStatus, |
380 | TokenScope, |
381 | ) |
382 | -from identityprovider.readonly import ReadOnlyManager |
383 | from identityprovider.tests import DEFAULT_API_PASSWORD, DEFAULT_USER_PASSWORD |
384 | -from identityprovider.tests.utils import SSOBaseTestCase |
385 | +from identityprovider.tests.utils import SSOBaseTestCase, readonly_enabled |
386 | from identityprovider.utils import ( |
387 | encrypt_launchpad_password, |
388 | get_cypher_migration_status, |
389 | @@ -350,12 +349,9 @@ |
390 | assert leak.password != self.account.encrypted_password |
391 | last_leak_check = self.account.accountpassword.last_leak_check |
392 | |
393 | - rm = ReadOnlyManager() |
394 | - rm.set_readonly() |
395 | - self.addCleanup(rm.clear_readonly) |
396 | - |
397 | - account = self.backend.authenticate( |
398 | - None, self.email, DEFAULT_USER_PASSWORD) |
399 | + with readonly_enabled(): |
400 | + account = self.backend.authenticate( |
401 | + None, self.email, DEFAULT_USER_PASSWORD) |
402 | # verify account was NOT flagged for password reset |
403 | self.assertFalse(account.need_password_reset) |
404 | # Verify the last_leak_check was NOT updated |
405 | |
406 | === modified file 'src/identityprovider/tests/test_command_readonly.py' |
407 | --- src/identityprovider/tests/test_command_readonly.py 2018-02-09 20:56:16 +0000 |
408 | +++ src/identityprovider/tests/test_command_readonly.py 2019-10-22 14:04:10 +0000 |
409 | @@ -1,4 +1,4 @@ |
410 | -# Copyright 2010-2018 Canonical Ltd. This software is licensed under the |
411 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
412 | # GNU Affero General Public License version 3 (see the file LICENSE). |
413 | |
414 | import shutil |
415 | @@ -7,13 +7,7 @@ |
416 | import tempfile |
417 | |
418 | from django.conf import settings |
419 | -from django.core.handlers.wsgi import WSGIHandler |
420 | from django.core.management import CommandError, call_command |
421 | -from wsgi_intercept import ( |
422 | - add_wsgi_intercept, |
423 | - remove_wsgi_intercept, |
424 | - requests_intercept, |
425 | -) |
426 | |
427 | from identityprovider.readonly import ReadOnlyManager |
428 | from identityprovider.tests.utils import SSOBaseTestCase |
429 | @@ -31,64 +25,21 @@ |
430 | self.patch(sys, 'stdout', new=StringIO.StringIO()) |
431 | self.patch(sys, 'stderr', new=StringIO.StringIO()) |
432 | |
433 | - self.servers = [ |
434 | - {'SERVER_ID': 'localhost', 'HOST': 'localhost', 'PORT': '8000'}, |
435 | - {'SERVER_ID': 'otherhost', 'HOST': 'localhost', 'PORT': '8001'}, |
436 | - ] |
437 | - _APP_SERVERS = settings.APP_SERVERS |
438 | - settings.APP_SERVERS = self.servers |
439 | - self.addCleanup(setattr, settings, 'APP_SERVERS', _APP_SERVERS) |
440 | - |
441 | _DBFAILOVER_FLAG_DIR = getattr(settings, 'DBFAILOVER_FLAG_DIR', None) |
442 | settings.DBFAILOVER_FLAG_DIR = tempfile.mkdtemp() |
443 | self.addCleanup(shutil.rmtree, settings.DBFAILOVER_FLAG_DIR, True) |
444 | self.addCleanup(setattr, settings, 'DBFAILOVER_FLAG_DIR', |
445 | _DBFAILOVER_FLAG_DIR) |
446 | |
447 | - # setup wsgi intercept mechanism to simulate wsgi server |
448 | - self.unset_env('http_proxy') |
449 | - self.unset_env('https_proxy') |
450 | - requests_intercept.install() |
451 | - self.addCleanup(requests_intercept.uninstall) |
452 | - for server in self.servers: |
453 | - add_wsgi_intercept(server['HOST'], int(server['PORT']), |
454 | - WSGIHandler) |
455 | - self.addCleanup(remove_wsgi_intercept, |
456 | - server['HOST'], int(server['PORT'])) |
457 | - |
458 | - def get_status(self): |
459 | - call_command('readonly', list_servers=True) |
460 | - sys.stdout.seek(0) |
461 | - output = sys.stdout.read() |
462 | - return output |
463 | - |
464 | def test_readonly(self): |
465 | self.assertRaises(CommandError, call_command, 'readonly') |
466 | |
467 | - def test_readonly_list_all(self): |
468 | - output = self.get_status() |
469 | - self.assertTrue(self.servers[0]['SERVER_ID'] in output) |
470 | - |
471 | def test_readonly_set(self): |
472 | - call_command('readonly', self.servers[0]['SERVER_ID'], action='set') |
473 | + call_command('readonly', 'set') |
474 | self.rm.check_readonly() |
475 | self.assertTrue(settings.READ_ONLY_MODE) |
476 | |
477 | def test_readonly_clear(self): |
478 | - call_command('readonly', self.servers[0]['SERVER_ID'], action='clear') |
479 | + call_command('readonly', 'clear') |
480 | self.rm.check_readonly() |
481 | self.assertFalse(settings.READ_ONLY_MODE) |
482 | - |
483 | - def test_readonly_set_all(self): |
484 | - call_command('readonly', action='set', all_servers=True) |
485 | - output = self.get_status() |
486 | - for server in settings.APP_SERVERS: |
487 | - expected = "%s -- In readonly mode" % server['SERVER_ID'] |
488 | - self.assertTrue(expected in output) |
489 | - |
490 | - def test_readonly_clear_all(self): |
491 | - call_command('readonly', action='clear', all_servers=True) |
492 | - output = self.get_status() |
493 | - for server in settings.APP_SERVERS: |
494 | - expected = "%s -- Operating normally" % server['SERVER_ID'] |
495 | - self.assertTrue(expected in output) |
496 | |
497 | === modified file 'src/identityprovider/tests/test_models_account.py' |
498 | --- src/identityprovider/tests/test_models_account.py 2018-02-14 14:05:59 +0000 |
499 | +++ src/identityprovider/tests/test_models_account.py 2019-10-22 14:04:10 +0000 |
500 | @@ -36,9 +36,8 @@ |
501 | TokenScope, |
502 | ) |
503 | from identityprovider.models.emailaddress import EmailAddress |
504 | -from identityprovider.readonly import ReadOnlyManager |
505 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
506 | -from identityprovider.tests.utils import SSOBaseTestCase |
507 | +from identityprovider.tests.utils import SSOBaseTestCase, readonly_enabled |
508 | from identityprovider.views.testing import MockLaunchpad |
509 | |
510 | |
511 | @@ -307,16 +306,14 @@ |
512 | self.assertTrue(account.last_login is not None) |
513 | |
514 | def test_set_last_login_when_readonly(self): |
515 | - readonly_manager = ReadOnlyManager() |
516 | account = self.factory.make_account() |
517 | account.last_login = last_login = datetime(2010, 01, 01) |
518 | self.assertEqual(account.last_login, last_login) |
519 | |
520 | assert not settings.READ_ONLY_MODE |
521 | - readonly_manager.set_readonly() |
522 | - self.addCleanup(readonly_manager.clear_readonly) |
523 | |
524 | - account.last_login = the_now = now() |
525 | + with readonly_enabled(): |
526 | + account.last_login = the_now = now() |
527 | self.assertEqual(account.last_login, last_login) |
528 | self.assertNotEqual(account.last_login, the_now) |
529 | |
530 | @@ -515,15 +512,13 @@ |
531 | self.assertEqual(account_password.password, 'invalid') |
532 | |
533 | def test_save_when_readonly(self): |
534 | - readonly_manager = ReadOnlyManager() |
535 | account = self.factory.make_account() |
536 | assert account.status == AccountStatus.ACTIVE |
537 | |
538 | assert not settings.READ_ONLY_MODE |
539 | - readonly_manager.set_readonly() |
540 | - self.addCleanup(readonly_manager.clear_readonly) |
541 | |
542 | - account.suspend() |
543 | + with readonly_enabled(): |
544 | + account.suspend() |
545 | # refresh account from db |
546 | account = Account.objects.get(id=account.id) |
547 | self.assertEqual(account.status, AccountStatus.ACTIVE) |
548 | |
549 | === modified file 'src/identityprovider/tests/test_models_openidmodels.py' |
550 | --- src/identityprovider/tests/test_models_openidmodels.py 2018-02-09 20:56:16 +0000 |
551 | +++ src/identityprovider/tests/test_models_openidmodels.py 2019-10-22 14:04:10 +0000 |
552 | @@ -19,8 +19,7 @@ |
553 | OpenIDRPConfig, |
554 | OpenIDRPSummary, |
555 | ) |
556 | -from identityprovider.readonly import ReadOnlyManager |
557 | -from identityprovider.tests.utils import SSOBaseTestCase |
558 | +from identityprovider.tests.utils import SSOBaseTestCase, readonly_enabled |
559 | |
560 | |
561 | class DjangoOpenIDStoreTestCase(SSOBaseTestCase): |
562 | @@ -192,13 +191,10 @@ |
563 | trust_root=self.trust_root) |
564 | |
565 | def test_authorize_when_readonly(self): |
566 | - rm = ReadOnlyManager() |
567 | - rm.set_readonly() |
568 | - self.addCleanup(rm.clear_readonly) |
569 | - |
570 | expires = now() |
571 | - OpenIDAuthorization.objects.authorize(self.account, self.trust_root, |
572 | - expires) |
573 | + with readonly_enabled(): |
574 | + OpenIDAuthorization.objects.authorize( |
575 | + self.account, self.trust_root, expires) |
576 | self.assertRaises( |
577 | OpenIDAuthorization.DoesNotExist, |
578 | OpenIDAuthorization.objects.get, account=self.account, |
579 | @@ -273,10 +269,7 @@ |
580 | return summary |
581 | |
582 | def test_record_when_readonly(self): |
583 | - rm = ReadOnlyManager() |
584 | - rm.set_readonly() |
585 | - |
586 | - try: |
587 | + with readonly_enabled(): |
588 | summary = OpenIDRPSummary.objects.record( |
589 | self.account, self.trust_root, self.openid_identity_url) |
590 | self.assertEqual(summary, None) |
591 | @@ -284,8 +277,6 @@ |
592 | OpenIDRPSummary.DoesNotExist, |
593 | OpenIDRPSummary.objects.get, account=self.account, |
594 | trust_root=self.trust_root, openid_identifier=None) |
595 | - finally: |
596 | - rm.clear_readonly() |
597 | |
598 | def test_record_with_existing_and_no_openid_identifier(self): |
599 | summary1 = self.create_summary() |
600 | |
601 | === modified file 'src/identityprovider/tests/test_readonly.py' |
602 | --- src/identityprovider/tests/test_readonly.py 2018-08-15 14:28:08 +0000 |
603 | +++ src/identityprovider/tests/test_readonly.py 2019-10-22 14:04:10 +0000 |
604 | @@ -1,23 +1,14 @@ |
605 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
606 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
607 | # GNU Affero General Public License version 3 (see the file LICENSE). |
608 | |
609 | -import json |
610 | import os |
611 | import shutil |
612 | import stat |
613 | import tempfile |
614 | |
615 | -import requests |
616 | -import responses |
617 | from django.conf import settings |
618 | -from django.urls import reverse |
619 | |
620 | -from identityprovider.readonly import ( |
621 | - ReadOnlyManager, |
622 | - _remote_req, |
623 | - get_server_atts, |
624 | - update_server, |
625 | -) |
626 | +from identityprovider.readonly import ReadOnlyManager |
627 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
628 | from identityprovider.tests.utils import SSOBaseTestCase |
629 | |
630 | @@ -32,7 +23,6 @@ |
631 | # readonlymode stays False |
632 | overrides = self.settings( |
633 | READ_ONLY_MODE=False, |
634 | - READONLY_SECRET='testsecret', |
635 | DBFAILOVER_FLAG_DIR=tempfile.mkdtemp(), |
636 | ) |
637 | overrides.enable() |
638 | @@ -42,57 +32,6 @@ |
639 | self.rm = ReadOnlyManager() |
640 | |
641 | |
642 | -class RemoteRequestTestCase(SSOBaseTestCase): |
643 | - msg = 'hello' |
644 | - host = 'myhost' |
645 | - scheme = 'https' |
646 | - vhost = 'http://foobar.baz' |
647 | - |
648 | - @responses.activate |
649 | - def test_plain_remote_req(self): |
650 | - responses.add( |
651 | - responses.POST, 'http://%s/readonlydata' % self.host, |
652 | - body=self.msg) |
653 | - server = {'host': self.host} |
654 | - self.assertEqual(self.msg, _remote_req(**server)) |
655 | - # This tests the PreparedRequest, which is before urllib3 inserts a |
656 | - # default Host header. |
657 | - self.assertNotIn('Host', responses.calls[0].request.headers) |
658 | - |
659 | - @responses.activate |
660 | - def test_https_remote_req(self): |
661 | - responses.add( |
662 | - responses.POST, '%s://%s/readonlydata' % (self.scheme, self.host), |
663 | - body=self.msg) |
664 | - server = {'host': self.host, 'scheme': self.scheme} |
665 | - self.assertEqual(self.msg, _remote_req(**server)) |
666 | - # This tests the PreparedRequest, which is before urllib3 inserts a |
667 | - # default Host header. |
668 | - self.assertNotIn('Host', responses.calls[0].request.headers) |
669 | - |
670 | - @responses.activate |
671 | - def test_vhost_remote_req(self): |
672 | - responses.add( |
673 | - responses.POST, '%s://%s/readonlydata' % (self.scheme, self.host), |
674 | - body=self.msg) |
675 | - server = { |
676 | - 'host': self.host, |
677 | - 'scheme': self.scheme, |
678 | - 'virtual_host': self.vhost, |
679 | - } |
680 | - self.assertEqual(self.msg, _remote_req(**server)) |
681 | - self.assertEqual( |
682 | - self.vhost, responses.calls[0].request.headers['Host']) |
683 | - |
684 | - @responses.activate |
685 | - def test_remote_req_error(self): |
686 | - responses.add( |
687 | - responses.POST, 'http://%s/readonlydata' % self.host, |
688 | - body=requests.RequestException('error')) |
689 | - server = {'host': self.host} |
690 | - self.assertEqual(None, _remote_req(**server)) |
691 | - |
692 | - |
693 | class ReadOnlyFlagFilesTestCase(ReadOnlyBaseTestCase): |
694 | |
695 | def test_flag_files_in_right_directory(self): |
696 | @@ -107,62 +46,6 @@ |
697 | self.assertTrue(mode & stat.S_IWGRP) |
698 | |
699 | |
700 | -class ReadOnlyDataTestCase(ReadOnlyBaseTestCase): |
701 | - |
702 | - url = reverse('readonly-data') |
703 | - |
704 | - def setUp(self): |
705 | - super(ReadOnlyDataTestCase, self).setUp() |
706 | - self.mock_logger = self.patch( |
707 | - 'identityprovider.views.readonly.logger') |
708 | - |
709 | - def test_requires_post(self): |
710 | - response = self.client.get(self.url) |
711 | - self.assertEqual(response.status_code, 405) |
712 | - |
713 | - response = self.client.delete(self.url) |
714 | - self.assertEqual(response.status_code, 405) |
715 | - |
716 | - response = self.client.put(self.url) |
717 | - self.assertEqual(response.status_code, 405) |
718 | - |
719 | - def test_missing_secret(self): |
720 | - response = self.client.post(self.url) |
721 | - |
722 | - self.assertEqual(response.status_code, 404) |
723 | - self.mock_logger.warning.assert_called_once_with( |
724 | - 'readonly_data request received with incorrect secret %r', None) |
725 | - |
726 | - def test_incorrect_secret(self): |
727 | - assert len(settings.READONLY_SECRET) > 0 |
728 | - secret = settings.READONLY_SECRET * 2 |
729 | - response = self.client.post(self.url, data=dict(secret=secret)) |
730 | - |
731 | - self.assertEqual(response.status_code, 404) |
732 | - self.mock_logger.warning.assert_called_once_with( |
733 | - 'readonly_data request received with incorrect secret %r', secret) |
734 | - |
735 | - def test_correct_secret(self): |
736 | - secret = settings.READONLY_SECRET |
737 | - response = self.client.post(self.url, data=dict(secret=secret)) |
738 | - |
739 | - self.assertEqual(response.status_code, 200) |
740 | - response = json.loads(response.content) |
741 | - self.assertEqual(response, dict(readonly=False)) |
742 | - |
743 | - def test_readonly_set_readonly(self): |
744 | - post = {'secret': settings.READONLY_SECRET, 'action': 'set'} |
745 | - response = self.client.post('/readonlydata', post) |
746 | - data = json.loads(response.content) |
747 | - self.assertTrue(data['readonly']) |
748 | - |
749 | - def test_readonly_clear_readonly(self): |
750 | - post = {'secret': settings.READONLY_SECRET, 'action': 'clear'} |
751 | - response = self.client.post('/readonlydata', post) |
752 | - data = json.loads(response.content) |
753 | - self.assertFalse(data['readonly']) |
754 | - |
755 | - |
756 | class ReadOnlyViewsTestCase(ReadOnlyBaseTestCase): |
757 | |
758 | def login_with_staff(self): |
759 | @@ -171,140 +54,6 @@ |
760 | assert self.client.login( |
761 | username=account.preferredemail.email, password='password') |
762 | |
763 | - def test_readonly_admin(self): |
764 | - self.login_with_staff() |
765 | - |
766 | - new_setting = [ |
767 | - {'SERVER_ID': 'localhost', 'SCHEME': 'http', |
768 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', |
769 | - 'PORT': '8000'} |
770 | - ] |
771 | - with self.settings(APP_SERVERS=new_setting): |
772 | - r = self.client.get('/readonly') |
773 | - |
774 | - self.assertTemplateUsed(r, 'admin/readonly.html') |
775 | - expected = {'appservers': [{'name': 'localhost', 'reachable': False}], |
776 | - 'clear_all_readonly': False, |
777 | - 'set_all_readonly': False} |
778 | - for item in r.context: |
779 | - for key, value in expected.items(): |
780 | - self.assertEqual(item[key], value) |
781 | - |
782 | - def test_get_server_atts_server_unreachable(self): |
783 | - servers = [{'SERVER_ID': 'localhost', 'SCHEME': 'http', |
784 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}] |
785 | - expected = {'appservers': [{'name': 'localhost', 'reachable': False}], |
786 | - 'clear_all_readonly': False, |
787 | - 'set_all_readonly': False} |
788 | - atts = get_server_atts(servers) |
789 | - self.assertEqual(atts, expected) |
790 | - |
791 | - @responses.activate |
792 | - def test_get_server_atts_data_error(self): |
793 | - responses.add( |
794 | - responses.POST, 'http://localhost:8000/readonlydata', body='{') |
795 | - |
796 | - servers = [{'SERVER_ID': 'localhost', 'SCHEME': 'http', |
797 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}] |
798 | - expected = {'appservers': [{'name': 'localhost', 'reachable': False}], |
799 | - 'clear_all_readonly': False, |
800 | - 'set_all_readonly': True} |
801 | - atts = get_server_atts(servers) |
802 | - self.assertEqual(atts, expected) |
803 | - |
804 | - @responses.activate |
805 | - def test_get_server_atts_readonly(self): |
806 | - responses.add( |
807 | - responses.POST, 'http://localhost:8000/readonlydata', |
808 | - json={'readonly': True}) |
809 | - |
810 | - servers = [{'SERVER_ID': 'localhost', 'SCHEME': 'http', |
811 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}] |
812 | - expected = {'appservers': [{'name': 'localhost', 'reachable': True, |
813 | - 'readonly': True}], |
814 | - 'clear_all_readonly': True, |
815 | - 'set_all_readonly': False} |
816 | - atts = get_server_atts(servers) |
817 | - self.assertEqual(atts, expected) |
818 | - |
819 | - def test_readonly_confirm_get(self): |
820 | - self.login_with_staff() |
821 | - |
822 | - r = self.client.get('/readonly/localhost/set') |
823 | - self.assertTemplateUsed(r, 'admin/readonly_confirm.html') |
824 | - self.assertEqual(r.context['appserver'], 'localhost') |
825 | - self.assertEqual(r.context['action'], 'set') |
826 | - |
827 | - @responses.activate |
828 | - def test_readonly_confirm_post(self): |
829 | - responses.add( |
830 | - responses.POST, 'http://localhost:8000/readonlydata', json={}) |
831 | - self.login_with_staff() |
832 | - |
833 | - new_setting = [ |
834 | - {'SERVER_ID': 'localhost', 'SCHEME': 'http', |
835 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', |
836 | - 'PORT': '8000'} |
837 | - ] |
838 | - with self.settings(APP_SERVERS=new_setting): |
839 | - r = self.client.post('/readonly/localhost/set') |
840 | - data = [call.request.body for call in responses.calls] |
841 | - self.assertEqual(data, [ |
842 | - "action=set&secret=%s" % settings.READONLY_SECRET |
843 | - ]) |
844 | - |
845 | - self.assertRedirects(r, '/readonly') |
846 | - |
847 | - @responses.activate |
848 | - def test_update_server_all_appservers(self): |
849 | - responses.add( |
850 | - responses.POST, 'http://localhost:8000/readonlydata', json={}) |
851 | - new_setting = [ |
852 | - {'SERVER_ID': 'localhost', 'SCHEME': 'http', |
853 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}, |
854 | - {'SERVER_ID': 'otherhost', 'SCHEME': 'http', |
855 | - 'HOST': 'otherhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}, |
856 | - ] |
857 | - with self.settings(APP_SERVERS=new_setting): |
858 | - update_server('set') |
859 | - data = [call.request.body for call in responses.calls] |
860 | - self.assertEqual(data, [ |
861 | - "action=set&secret=%s" % settings.READONLY_SECRET, |
862 | - "action=set&secret=%s" % settings.READONLY_SECRET |
863 | - ]) |
864 | - |
865 | - @responses.activate |
866 | - def test_update_server_one_appserver(self): |
867 | - responses.add( |
868 | - responses.POST, 'http://localhost:8000/readonlydata', json={}) |
869 | - new_setting = [ |
870 | - {'SERVER_ID': 'localhost', 'SCHEME': 'http', |
871 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}, |
872 | - {'SERVER_ID': 'otherhost', 'SCHEME': 'http', |
873 | - 'HOST': 'otherhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}, |
874 | - ] |
875 | - with self.settings(APP_SERVERS=new_setting): |
876 | - update_server('set', 'localhost') |
877 | - data = [call.request.body for call in responses.calls] |
878 | - self.assertEqual(data, [ |
879 | - "action=set&secret=%s" % settings.READONLY_SECRET |
880 | - ]) |
881 | - |
882 | - @responses.activate |
883 | - def test_update_server_one_connection(self): |
884 | - responses.add( |
885 | - responses.POST, 'http://localhost:8000/readonlydata', json={}) |
886 | - new_setting = [ |
887 | - {'SERVER_ID': 'localhost', 'SCHEME': 'http', |
888 | - 'HOST': 'localhost', 'VIRTUAL_HOST': '', 'PORT': '8000'}, |
889 | - ] |
890 | - with self.settings(APP_SERVERS=new_setting): |
891 | - update_server('set', 'localhost') |
892 | - data = [call.request.body for call in responses.calls] |
893 | - self.assertEqual(data, [ |
894 | - "action=set&secret=%s" % settings.READONLY_SECRET, |
895 | - ]) |
896 | - |
897 | def test_nop_db_in_readonly_mode(self): |
898 | account = self.factory.make_account() |
899 | |
900 | |
901 | === modified file 'src/identityprovider/tests/test_signals.py' |
902 | --- src/identityprovider/tests/test_signals.py 2018-05-28 19:44:56 +0000 |
903 | +++ src/identityprovider/tests/test_signals.py 2019-10-22 14:04:10 +0000 |
904 | @@ -21,7 +21,6 @@ |
905 | TokenScope, |
906 | ) |
907 | from identityprovider.models.emailaddress import EmailAddress |
908 | -from identityprovider.readonly import ReadOnlyManager |
909 | from identityprovider.signals import ( |
910 | invalidate_account_oauth_tokens, |
911 | invalidate_preferredemail, |
912 | @@ -34,7 +33,7 @@ |
913 | ) |
914 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
915 | from identityprovider.tests.test_auth import AuthLogTestCaseMixin |
916 | -from identityprovider.tests.utils import SSOBaseTestCase |
917 | +from identityprovider.tests.utils import SSOBaseTestCase, readonly_enabled |
918 | |
919 | |
920 | class SessionTokenOnLoginTestCase(SSOBaseTestCase): |
921 | @@ -107,34 +106,26 @@ |
922 | self.client.session.get(SESSION_TOKEN_KEY), token.key) |
923 | |
924 | def test_listener_read_only_mode_no_token(self): |
925 | - rm = ReadOnlyManager() |
926 | - rm.set_readonly() |
927 | - self.addCleanup(rm.clear_readonly) |
928 | - |
929 | - self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD) |
930 | + with readonly_enabled(): |
931 | + self.client.login( |
932 | + username=self.email, password=DEFAULT_USER_PASSWORD) |
933 | self.assertEqual(self.account.token_set.all().count(), 0) |
934 | self.assertEqual(self.client.session.get(SESSION_TOKEN_KEY), '') |
935 | |
936 | def test_listener_read_only_mode_previous_token_no_web_login(self): |
937 | self.account.create_oauth_token(token_name=SESSION_TOKEN_NAME) |
938 | - |
939 | - rm = ReadOnlyManager() |
940 | - rm.set_readonly() |
941 | - self.addCleanup(rm.clear_readonly) |
942 | - |
943 | - self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD) |
944 | + with readonly_enabled(): |
945 | + self.client.login( |
946 | + username=self.email, password=DEFAULT_USER_PASSWORD) |
947 | self.assertEqual( |
948 | self.client.session.get(SESSION_TOKEN_KEY), '') |
949 | |
950 | def test_listener_read_only_mode_previous_token(self): |
951 | token = self.account.create_oauth_token( |
952 | token_name=SESSION_TOKEN_NAME, scope=TokenScope.WEB_LOGIN) |
953 | - |
954 | - rm = ReadOnlyManager() |
955 | - rm.set_readonly() |
956 | - self.addCleanup(rm.clear_readonly) |
957 | - |
958 | - self.client.login(username=self.email, password=DEFAULT_USER_PASSWORD) |
959 | + with readonly_enabled(): |
960 | + self.client.login( |
961 | + username=self.email, password=DEFAULT_USER_PASSWORD) |
962 | self.assertEqual( |
963 | self.client.session.get(SESSION_TOKEN_KEY), token.key) |
964 | |
965 | |
966 | === modified file 'src/identityprovider/tests/utils.py' |
967 | --- src/identityprovider/tests/utils.py 2018-05-25 21:49:46 +0000 |
968 | +++ src/identityprovider/tests/utils.py 2019-10-22 14:04:10 +0000 |
969 | @@ -1,4 +1,4 @@ |
970 | -# Copyright 2010-2013 Canonical Ltd. This software is licensed under the |
971 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
972 | # GNU Affero General Public License version 3 (see the file LICENSE). |
973 | |
974 | import base64 |
975 | @@ -10,6 +10,7 @@ |
976 | import threading |
977 | import urllib |
978 | import urllib2 |
979 | +from contextlib import contextmanager |
980 | from cStringIO import StringIO |
981 | from datetime import timedelta |
982 | from importlib import import_module |
983 | @@ -33,6 +34,7 @@ |
984 | # same logic as prod systems |
985 | from identityprovider import crypto, signed, signals # noqa |
986 | from identityprovider.macaroon import MacaroonRequest |
987 | +from identityprovider.readonly import ReadOnlyManager |
988 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
989 | from identityprovider.tests.factory import SSOObjectFactory |
990 | from identityprovider.utils import generate_random_string |
991 | @@ -532,3 +534,13 @@ |
992 | self.assertIsInstance(thrown_exceptions[0], OperationalError) |
993 | self.assertIn( |
994 | "could not obtain lock on row", str(thrown_exceptions[0])) |
995 | + |
996 | + |
997 | +@contextmanager |
998 | +def readonly_enabled(): |
999 | + rm = ReadOnlyManager() |
1000 | + rm.set_readonly() |
1001 | + try: |
1002 | + yield |
1003 | + finally: |
1004 | + rm.clear_readonly() |
1005 | |
1006 | === modified file 'src/identityprovider/urls.py' |
1007 | --- src/identityprovider/urls.py 2018-02-15 11:27:21 +0000 |
1008 | +++ src/identityprovider/urls.py 2019-10-22 14:04:10 +0000 |
1009 | @@ -5,11 +5,6 @@ |
1010 | from django.conf.urls import include, url |
1011 | from django.views.generic import RedirectView |
1012 | |
1013 | -from identityprovider.views.readonly import ( |
1014 | - readonly_admin, |
1015 | - readonly_confirm, |
1016 | - readonly_data, |
1017 | -) |
1018 | from identityprovider.views.server import ( |
1019 | cancel, |
1020 | decide, |
1021 | @@ -55,13 +50,6 @@ |
1022 | RedirectView.as_view(url='/+id/%(identifier)s', permanent=False)), |
1023 | ] |
1024 | |
1025 | -urlpatterns += [ |
1026 | - url(r'^readonly$', readonly_admin), |
1027 | - url(r'^readonly/((?P<appserver>[A-Za-z0-9\-_.:]+)/)?' |
1028 | - r'(?P<action>set|clear)', readonly_confirm), |
1029 | - url(r'^readonlydata$', readonly_data, name='readonly-data'), |
1030 | -] |
1031 | - |
1032 | if settings.DEBUG: |
1033 | urlpatterns += [ |
1034 | url(r'^i18n/', include('django.conf.urls.i18n')), |
1035 | |
1036 | === removed file 'src/identityprovider/views/readonly.py' |
1037 | --- src/identityprovider/views/readonly.py 2018-02-09 20:56:16 +0000 |
1038 | +++ src/identityprovider/views/readonly.py 1970-01-01 00:00:00 +0000 |
1039 | @@ -1,64 +0,0 @@ |
1040 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
1041 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
1042 | - |
1043 | -import json |
1044 | -import logging |
1045 | - |
1046 | -from django.conf import settings |
1047 | -from django.contrib.admin.views.decorators import staff_member_required |
1048 | -from django.http import Http404, HttpResponse, HttpResponseRedirect |
1049 | -from django.shortcuts import render |
1050 | -from django.views.decorators.csrf import csrf_exempt |
1051 | -from django.views.decorators.http import require_POST |
1052 | - |
1053 | -from identityprovider.readonly import ( |
1054 | - ReadOnlyManager, |
1055 | - get_server_atts, |
1056 | - update_server, |
1057 | -) |
1058 | - |
1059 | - |
1060 | -logger = logging.getLogger(__name__) |
1061 | - |
1062 | - |
1063 | -@staff_member_required |
1064 | -def readonly_admin(request): |
1065 | - atts = get_server_atts(settings.APP_SERVERS) |
1066 | - return render(request, 'admin/readonly.html', atts) |
1067 | - |
1068 | - |
1069 | -@staff_member_required |
1070 | -def readonly_confirm(request, action, appserver=None): |
1071 | - if request.method == 'POST': |
1072 | - update_server(action, appserver) |
1073 | - return HttpResponseRedirect('/readonly') |
1074 | - context = { |
1075 | - 'appserver': appserver, |
1076 | - 'action': action, |
1077 | - } |
1078 | - return render(request, 'admin/readonly_confirm.html', context) |
1079 | - |
1080 | - |
1081 | -@csrf_exempt |
1082 | -@require_POST |
1083 | -def readonly_data(request): |
1084 | - """Provides data about the readonly status of this app server.""" |
1085 | - |
1086 | - secret = request.POST.get('secret') |
1087 | - if secret != settings.READONLY_SECRET: |
1088 | - logger.warning( |
1089 | - 'readonly_data request received with incorrect secret %r', secret) |
1090 | - raise Http404() |
1091 | - |
1092 | - action = request.POST.get('action') |
1093 | - romanager = ReadOnlyManager() |
1094 | - |
1095 | - if action == 'set': |
1096 | - romanager.set_readonly() |
1097 | - elif action == 'clear': |
1098 | - romanager.clear_readonly() |
1099 | - |
1100 | - result = { |
1101 | - 'readonly': settings.READ_ONLY_MODE, |
1102 | - } |
1103 | - return HttpResponse(json.dumps(result)) |
1104 | |
1105 | === modified file 'src/webui/tests/test_templates.py' |
1106 | --- src/webui/tests/test_templates.py 2018-05-28 20:15:33 +0000 |
1107 | +++ src/webui/tests/test_templates.py 2019-10-22 14:04:10 +0000 |
1108 | @@ -6,10 +6,10 @@ |
1109 | from pyquery import PyQuery |
1110 | |
1111 | from identityprovider.models.openidmodels import OpenIDRPConfig |
1112 | -from identityprovider.readonly import ReadOnlyManager |
1113 | from identityprovider.tests.utils import ( |
1114 | AuthenticatedTestCase, |
1115 | SSOBaseTestCase, |
1116 | + readonly_enabled, |
1117 | ) |
1118 | |
1119 | |
1120 | @@ -99,11 +99,8 @@ |
1121 | self.assertContains(response, "data-qa-id=\"create_account_form\"") |
1122 | |
1123 | def test_login_without_create_account_form(self): |
1124 | - rm = ReadOnlyManager() |
1125 | - rm.set_readonly() |
1126 | - self.addCleanup(rm.clear_readonly) |
1127 | - |
1128 | - response = self.client.get('/+login') |
1129 | + with readonly_enabled(): |
1130 | + response = self.client.get('/+login') |
1131 | self.assertNotContains(response, "data-qa-id=\"create_account_form\"") |
1132 | |
1133 | |
1134 | |
1135 | === modified file 'src/webui/tests/test_views_account.py' |
1136 | --- src/webui/tests/test_views_account.py 2019-06-18 20:13:31 +0000 |
1137 | +++ src/webui/tests/test_views_account.py 2019-10-22 14:04:10 +0000 |
1138 | @@ -1,4 +1,4 @@ |
1139 | -# Copyright 2010-2017 Canonical Ltd. This software is licensed under the |
1140 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
1141 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1142 | |
1143 | from __future__ import unicode_literals |
1144 | @@ -41,6 +41,7 @@ |
1145 | AuthenticatedTestCase, |
1146 | SSOBaseTestCase, |
1147 | assert_exhausted_warning, |
1148 | + readonly_enabled, |
1149 | ) |
1150 | from identityprovider.views.testing import MockLaunchpad |
1151 | from webui import decorators |
1152 | @@ -268,6 +269,17 @@ |
1153 | r = self.client.post(self.url, data) |
1154 | self.assertEqual(r.status_code, 302) |
1155 | |
1156 | + def test_index_edit_displayname_disabled_when_readonly(self): |
1157 | + old_displayname = self.account.displayname |
1158 | + data = {'displayname': "New Display Name", |
1159 | + 'preferred_email': self.account.preferredemail.id, |
1160 | + 'oldpassword': DEFAULT_USER_PASSWORD} |
1161 | + with readonly_enabled(): |
1162 | + r = self.client.post(self.url, data) |
1163 | + self.account.refresh_from_db() |
1164 | + self.assertEqual(r.status_code, 200) |
1165 | + self.assertEqual(old_displayname, self.account.displayname) |
1166 | + |
1167 | def test_index_edit_displayname_no_email(self): |
1168 | data = {'displayname': "New Display Name", |
1169 | 'preferred_email': self.account.preferredemail.id, |
1170 | @@ -427,6 +439,14 @@ |
1171 | r = self.client.get(self.url) |
1172 | self.assertContains(r, 'value="%s"' % bad_username) |
1173 | |
1174 | + @switches(USERNAME_UI=True) |
1175 | + def test_edit_username_disabled_when_readonly(self): |
1176 | + with readonly_enabled(): |
1177 | + r = self.post_username_change('new-username') |
1178 | + self.account.refresh_from_db() |
1179 | + self.assertEqual(r.status_code, 200) |
1180 | + self.assertIsNone(self.account.person) |
1181 | + |
1182 | def test_index_edit_password(self): |
1183 | oauth_tokens = self.account.token_set.all() |
1184 | # web login token should be there |
1185 | @@ -548,6 +568,23 @@ |
1186 | {'newemail': "very-new-email@example.com"}) |
1187 | self.assertEqual(r.status_code, 200) |
1188 | |
1189 | + def test_new_email_get_disabled_when_readonly(self): |
1190 | + with readonly_enabled(): |
1191 | + r = self.client.get(reverse('new_email')) |
1192 | + |
1193 | + self.assertEqual(r.status_code, 403) |
1194 | + self.assertTemplateUsed(r, 'readonly.html') |
1195 | + |
1196 | + def test_new_email_post_disabled_when_readonly(self): |
1197 | + with readonly_enabled(): |
1198 | + r = self.client.post( |
1199 | + reverse('new_email'), |
1200 | + {'newemail': "very-new-email@example.com"}, |
1201 | + ) |
1202 | + |
1203 | + self.assertEqual(r.status_code, 403) |
1204 | + self.assertTemplateUsed(r, 'readonly.html') |
1205 | + |
1206 | def test_new_email_post_with_token(self): |
1207 | url = reverse('new_email', kwargs=dict(token='thisissuperrando')) |
1208 | self.client.post(url, {'newemail': "very-new-email@example.com"}) |
1209 | @@ -695,6 +732,13 @@ |
1210 | self.assertContains( |
1211 | response, 'Your Ubuntu One account has now been deleted') |
1212 | |
1213 | + def test_delete_view_disabled_when_readonly(self): |
1214 | + data = {'password': DEFAULT_USER_PASSWORD} |
1215 | + with readonly_enabled(): |
1216 | + self.client.post('/+delete', data) |
1217 | + account = Account.objects.get(id=self.account.id) |
1218 | + self.assertNotEqual(account.status, AccountStatus.DELETED) |
1219 | + |
1220 | def test_confirm_password_before_changing(self): |
1221 | oauth_tokens = self.account.token_set.all() |
1222 | # web login token should be there |
1223 | @@ -1158,7 +1202,24 @@ |
1224 | r = self.client.get(reverse('applications')) |
1225 | self.assertTemplateUsed(r, 'account/applications.html') |
1226 | |
1227 | - def test_account_without_applications_does_not_renders_token_table(self): |
1228 | + def test_applications_get_disabled_when_readonly(self): |
1229 | + with readonly_enabled(): |
1230 | + r = self.client.get(reverse('applications')) |
1231 | + |
1232 | + self.assertEqual(r.status_code, 403) |
1233 | + self.assertTemplateUsed(r, 'readonly.html') |
1234 | + |
1235 | + def test_applications_post_disabled_when_readonly(self): |
1236 | + token = self.account.create_v1_oauth_token("Token-1") |
1237 | + |
1238 | + with readonly_enabled(): |
1239 | + r = self.client.post( |
1240 | + reverse('applications'), {'token_id': token.pk}, follow=True) |
1241 | + |
1242 | + self.assertEqual(r.status_code, 403) |
1243 | + self.assertTemplateUsed(r, 'readonly.html') |
1244 | + |
1245 | + def test_account_without_applications_does_not_render_token_table(self): |
1246 | r = self.client.get(reverse('applications')) |
1247 | # since user is logged in, the web login token exists, but we don't |
1248 | # want to list it |
1249 | @@ -1181,7 +1242,7 @@ |
1250 | reverse('applications'), {'token_id': t.pk}, follow=True) |
1251 | self.assertNotContains(r, t.pk) |
1252 | |
1253 | - def test_revoking_token_which_does_not_belogs_to_an_account(self): |
1254 | + def test_revoking_token_which_does_not_belong_to_an_account(self): |
1255 | account = self.factory.make_account(email="foo@x.com") |
1256 | token = account.create_oauth_token("Token") |
1257 | |
1258 | |
1259 | === modified file 'src/webui/tests/test_views_registration.py' |
1260 | --- src/webui/tests/test_views_registration.py 2019-06-18 20:13:31 +0000 |
1261 | +++ src/webui/tests/test_views_registration.py 2019-10-22 14:04:10 +0000 |
1262 | @@ -1,6 +1,6 @@ |
1263 | # coding: utf-8 |
1264 | |
1265 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
1266 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
1267 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1268 | |
1269 | from __future__ import unicode_literals |
1270 | @@ -33,7 +33,11 @@ |
1271 | AuthenticationDevice, |
1272 | ) |
1273 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
1274 | -from identityprovider.tests.utils import SSOBaseTestCase, TimelineActionMixin |
1275 | +from identityprovider.tests.utils import ( |
1276 | + SSOBaseTestCase, |
1277 | + TimelineActionMixin, |
1278 | + readonly_enabled, |
1279 | +) |
1280 | |
1281 | from webui.constants import ( |
1282 | EMAIL_EXISTS_ERROR, |
1283 | @@ -153,6 +157,13 @@ |
1284 | 'Please tell us your full name and choose a username and password:' |
1285 | ) |
1286 | |
1287 | + def test_get_when_readonly(self): |
1288 | + with readonly_enabled(): |
1289 | + response = self.client.get(self.URL) |
1290 | + |
1291 | + self.assertEqual(response.status_code, 403) |
1292 | + self.assertTemplateUsed(response, 'readonly.html') |
1293 | + |
1294 | def test_head(self): |
1295 | request = self.factory.make_request( |
1296 | self.URL, method='HEAD', user=AnonymousUser(), |
1297 | @@ -216,6 +227,13 @@ |
1298 | self.assertEqual( |
1299 | response.request.session['token_email'], self.TESTDATA['email']) |
1300 | |
1301 | + def test_post_disabled_when_readonly(self): |
1302 | + with readonly_enabled(): |
1303 | + response = self.client.post(self.URL, **self.TESTDATA) |
1304 | + |
1305 | + self.assertEqual(response.status_code, 403) |
1306 | + self.assertTemplateUsed(response, 'readonly.html') |
1307 | + |
1308 | @switches(USERNAME_UI=True) |
1309 | @override_settings(LP_API_URL='test.com') |
1310 | def test_post_success_with_username_with_feature_flag_enabled(self): |
1311 | @@ -502,6 +520,12 @@ |
1312 | self.assertEqual(ctx['form']['email'].value(), 'test@test.com') |
1313 | self.assertTemplateUsed(response, 'registration/forgot_password.html') |
1314 | |
1315 | + def test_when_readonly(self): |
1316 | + with readonly_enabled(): |
1317 | + response = self.get() |
1318 | + self.assertEqual(response.status_code, 403) |
1319 | + self.assertTemplateUsed(response, 'readonly.html') |
1320 | + |
1321 | def test_post_with_initial_data(self): |
1322 | data = dict(email='test@test.com', forgot_password='') |
1323 | response = self.post(data=data) |
1324 | @@ -624,6 +648,12 @@ |
1325 | self.assert_form_displayed(response, __all__=ERROR_TOO_MANY_REQUESTS) |
1326 | self.assert_stat_calls(['error.too_many_requests']) |
1327 | |
1328 | + def test_post_disabled_when_readonly(self): |
1329 | + with readonly_enabled(): |
1330 | + response = self.post(data=self.data) |
1331 | + self.assertEqual(response.status_code, 403) |
1332 | + self.assertTemplateUsed(response, 'readonly.html') |
1333 | + |
1334 | def test_method_not_allowed(self): |
1335 | response = self.put(data=self.data) |
1336 | self.assertEqual(response.status_code, 405) |
1337 | |
1338 | === modified file 'src/webui/tests/test_views_ui.py' |
1339 | --- src/webui/tests/test_views_ui.py 2019-05-10 19:53:55 +0000 |
1340 | +++ src/webui/tests/test_views_ui.py 2019-10-22 14:04:10 +0000 |
1341 | @@ -1,6 +1,6 @@ |
1342 | # coding: utf-8 |
1343 | |
1344 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
1345 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
1346 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1347 | |
1348 | from __future__ import unicode_literals |
1349 | @@ -55,12 +55,12 @@ |
1350 | AuthTokenType, |
1351 | EmailStatus, |
1352 | ) |
1353 | -from identityprovider.readonly import ReadOnlyManager |
1354 | from identityprovider.tests import DEFAULT_USER_PASSWORD |
1355 | from identityprovider.tests.test_auth import AuthLogTestCaseMixin |
1356 | from identityprovider.tests.utils import ( |
1357 | SSOBaseTestCase, |
1358 | TimelineActionMixin, |
1359 | + readonly_enabled, |
1360 | ) |
1361 | from identityprovider.utils import generate_random_string |
1362 | from identityprovider.validators import PASSWORD_LEAKED |
1363 | @@ -136,6 +136,20 @@ |
1364 | self.assertEqual(len(mail.outbox), 1) |
1365 | |
1366 | |
1367 | +class ReadOnlyViewsTestCase(BaseTestCase): |
1368 | + |
1369 | + def test_new_account_disabled_when_readonly(self): |
1370 | + self.account.status = AccountStatus.ACTIVE |
1371 | + self.account.save() |
1372 | + |
1373 | + with readonly_enabled(): |
1374 | + r = self.post_new_account(email=self.email) |
1375 | + |
1376 | + self.assertEqual(r.status_code, 403) |
1377 | + self.assertEqual(len(mail.outbox), 0) |
1378 | + self.assertTemplateUsed(r, 'readonly.html') |
1379 | + |
1380 | + |
1381 | class UIViewsBaseTestCase(BaseTestCase): |
1382 | |
1383 | def setUp(self): |
1384 | @@ -1518,11 +1532,7 @@ |
1385 | def test_twofactor_login_disabled_when_readonly(self): |
1386 | self.mock_site.return_value = False |
1387 | |
1388 | - rm = ReadOnlyManager() |
1389 | - rm.set_readonly() |
1390 | - self.addCleanup(rm.clear_readonly) |
1391 | - |
1392 | - with switches(TWOFACTOR=True): |
1393 | + with switches(TWOFACTOR=True), readonly_enabled(): |
1394 | self.do_login() |
1395 | response = self.client.get(self.url) |
1396 | self.assertEqual(response.status_code, 404) |
LGTM, maybe worth checking with Natalia to see if she can spot any remaining needs for that remote readonly API, but it definitely seems redundand in the Juju world which can go into each unit and run the command for us (fwiw other tools such as ansible could also do this so definitely an API for this feels overkill).