Merge lp:~maxiberta/canonical-identity-provider/readonly-improvements into lp:canonical-identity-provider/release

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
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
Daniel Manrique (roadmr) wrote :

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).

review: Approve
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> &rsaquo;
231-<a href="/admin/identityprovider/">Identityprovider</a> &rsaquo;
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> &rsaquo;
315-<a href="/admin/identityprovider/">Identityprovider</a> &rsaquo;
316-<a href="/readonly">{% trans "Readonly Admin" %}</a> &rsaquo;
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)