Merge lp:~blake-rouse/maas/fix-1583670 into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 5122
Proposed branch: lp:~blake-rouse/maas/fix-1583670
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 246 lines (+90/-18)
5 files modified
src/maasserver/api/maas.py (+2/-0)
src/maasserver/api/tests/test_api.py (+4/-14)
src/maasserver/api/tests/test_maas.py (+79/-2)
src/maasserver/tests/test_forms_config.py (+3/-0)
src/maasserver/urls_api.py (+2/-2)
To merge this branch: bzr merge lp:~blake-rouse/maas/fix-1583670
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+297081@code.launchpad.net

Commit message

Allow get-config to work as a standard user.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

There are a few config items that should not be exposed to ordinary
users:

- omapi_key
- rpc_region_certificate (unused right now)
- rpc_shared_secret

TBH, I'm not sure we want to share these with admins either.

It might be prudent for MaasHandler to use a whitelist to limit read and
write access to a subset of all config keys, i.e. configuration is not
readable or writable by default.

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Uhm... did you test the branch?

That is already handled and yes administrators cannot access those either.

Revision history for this message
Gavin Panella (allenap) wrote :

> Uhm... did you test the branch?

There are no tests for this. I've written some: lp:~allenap/maas/blake-rouse--fix-1583670

>
> That is already handled and yes administrators cannot access those either.

You're correct, but without tests it doesn't count :)

In writing the tests I discovered that "commissioning_osystem" is not available to configure via the Web API but it ought to be.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

I merged your branch.

Revision history for this message
Gavin Panella (allenap) wrote :

Looks good then :)

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.3 MiB)

The attempt to merge lp:~blake-rouse/maas/fix-1583670 into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [94.5 kB]
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [94.5 kB]
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Fetched 189 kB in 0s (442 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6.03+dfsg-11...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/maas.py'
2--- src/maasserver/api/maas.py 2016-03-28 13:54:47 +0000
3+++ src/maasserver/api/maas.py 2016-06-14 18:49:36 +0000
4@@ -12,6 +12,7 @@
5 from django.http import HttpResponse
6 from formencode import validators
7 from maasserver.api.support import (
8+ admin_method,
9 operation,
10 OperationsHandler,
11 )
12@@ -31,6 +32,7 @@
13 api_doc_section_name = "MAAS server"
14 create = read = update = delete = None
15
16+ @admin_method
17 @operation(idempotent=False)
18 def set_config(self, request):
19 """Set a config value.
20
21=== modified file 'src/maasserver/api/tests/test_api.py'
22--- src/maasserver/api/tests/test_api.py 2016-05-24 22:05:45 +0000
23+++ src/maasserver/api/tests/test_api.py 2016-06-14 18:49:36 +0000
24@@ -298,19 +298,19 @@
25 class MAASAPIAnonTest(APITestCase.ForAnonymous):
26 """The MAAS' handler is not accessible to anon users."""
27
28- def test_anon_get_config_forbidden(self):
29+ def test_anon_get_config_unauthorized(self):
30 response = self.client.get(
31 reverse('maas_handler'),
32 {'op': 'get_config'})
33
34- self.assertEqual(http.client.FORBIDDEN, response.status_code)
35+ self.assertEqual(http.client.UNAUTHORIZED, response.status_code)
36
37- def test_anon_set_config_forbidden(self):
38+ def test_anon_set_config_unauthorized(self):
39 response = self.client.post(
40 reverse('maas_handler'),
41 {'op': 'set_config'})
42
43- self.assertEqual(http.client.FORBIDDEN, response.status_code)
44+ self.assertEqual(http.client.UNAUTHORIZED, response.status_code)
45
46
47 class MAASAPIVersioningTest(APITestCase.ForAnonymousAndUserAndAdmin):
48@@ -343,13 +343,6 @@
49 self.assertEqual(
50 '/api/2.0/maas/', reverse('maas_handler'))
51
52- def test_simple_user_get_config_forbidden(self):
53- response = self.client.get(
54- reverse('maas_handler'),
55- {'op': 'get_config'})
56-
57- self.assertEqual(http.client.FORBIDDEN, response.status_code)
58-
59 def test_simple_user_set_config_forbidden(self):
60 response = self.client.post(
61 reverse('maas_handler'),
62@@ -358,7 +351,6 @@
63 self.assertEqual(http.client.FORBIDDEN, response.status_code)
64
65 def test_get_config_requires_name_param(self):
66- self.become_admin()
67 response = self.client.get(
68 reverse('maas_handler'),
69 {
70@@ -369,7 +361,6 @@
71 self.assertEqual(b"No provided name!", response.content)
72
73 def test_get_config_returns_config(self):
74- self.become_admin()
75 name = 'maas_name'
76 value = factory.make_string()
77 Config.objects.set_config(name, value)
78@@ -386,7 +377,6 @@
79 self.assertEqual(value, parsed_result)
80
81 def test_get_config_rejects_unknown_config_item(self):
82- self.become_admin()
83 name = factory.make_string()
84 value = factory.make_string()
85 Config.objects.set_config(name, value)
86
87=== modified file 'src/maasserver/api/tests/test_maas.py'
88--- src/maasserver/api/tests/test_maas.py 2016-05-24 21:29:53 +0000
89+++ src/maasserver/api/tests/test_maas.py 2016-06-14 18:49:36 +0000
90@@ -6,10 +6,16 @@
91 __all__ = []
92
93 import http.client
94+import json
95+from operator import itemgetter
96
97 from django.conf import settings
98 from django.core.urlresolvers import reverse
99-from maasserver.models.config import Config
100+from maasserver.forms_settings import CONFIG_ITEMS_KEYS
101+from maasserver.models.config import (
102+ Config,
103+ DEFAULT_CONFIG,
104+)
105 from maasserver.testing.api import APITestCase
106 from maasserver.testing.factory import factory
107 from maasserver.testing.osystems import (
108@@ -17,12 +23,38 @@
109 make_usable_osystem,
110 patch_usable_osystems,
111 )
112+from maastesting.matchers import DocTestMatches
113+from maastesting.testcase import MAASTestCase
114+from testtools.content import text_content
115+from testtools.matchers import (
116+ AfterPreprocessing,
117+ Equals,
118+ MatchesAll,
119+ MatchesDict,
120+ MatchesListwise,
121+ MatchesStructure,
122+)
123+
124+# Names forbidden for use via the Web API.
125+FORBIDDEN_NAMES = {
126+ "omapi_key", "rpc_region_certificate",
127+ "rpc_shared_secret", "commissioning_osystem",
128+}
129+
130+
131+class TestForbiddenNames(MAASTestCase):
132+
133+ def test_forbidden_names(self):
134+ # The difference between the set of possible configuration keys and
135+ # those permitted via the Web API is small but important to security.
136+ self.assertThat(
137+ set(DEFAULT_CONFIG).difference(CONFIG_ITEMS_KEYS),
138+ Equals(FORBIDDEN_NAMES))
139
140
141 class MAASHandlerAPITest(APITestCase.ForUser):
142
143 def test_get_config_default_distro_series(self):
144- self.become_admin()
145 default_distro_series = factory.make_name("distro_series")
146 Config.objects.set_config(
147 "default_distro_series", default_distro_series)
148@@ -69,3 +101,48 @@
149 })
150 self.assertEqual(
151 http.client.BAD_REQUEST, response.status_code, response.content)
152+
153+ def assertInvalidConfigurationSetting(self, name, response):
154+ self.addDetail(
155+ "Response for op={get,set}_config&name=%s" % name, text_content(
156+ response.serialize().decode(settings.DEFAULT_CHARSET)))
157+ self.expectThat(
158+ response, MatchesAll(
159+ # An HTTP 400 response,
160+ MatchesStructure(
161+ status_code=Equals(http.client.BAD_REQUEST)),
162+ # with a JSON body,
163+ AfterPreprocessing(
164+ itemgetter("Content-Type"),
165+ Equals("application/json")),
166+ # containing a serialised ValidationError.
167+ AfterPreprocessing(
168+ lambda response: json.loads(
169+ response.content.decode(settings.DEFAULT_CHARSET)),
170+ MatchesDict({
171+ name: MatchesListwise([
172+ DocTestMatches(
173+ name + " is not a valid config setting "
174+ "(valid settings are: ...)."),
175+ ]),
176+ })),
177+ first_only=True,
178+ ))
179+
180+ def test_get_config_forbidden_config_items(self):
181+ for name in FORBIDDEN_NAMES:
182+ response = self.client.get(
183+ reverse('maas_handler'), {
184+ "op": "get_config", "name": name,
185+ })
186+ self.assertInvalidConfigurationSetting(name, response)
187+
188+ def test_set_config_forbidden_config_items(self):
189+ self.become_admin()
190+ for name in FORBIDDEN_NAMES:
191+ response = self.client.post(
192+ reverse('maas_handler'), {
193+ "op": "set_config", "name": name,
194+ "value": factory.make_name("nonsense"),
195+ })
196+ self.assertInvalidConfigurationSetting(name, response)
197
198=== modified file 'src/maasserver/tests/test_forms_config.py'
199--- src/maasserver/tests/test_forms_config.py 2015-12-01 18:12:59 +0000
200+++ src/maasserver/tests/test_forms_config.py 2016-06-14 18:49:36 +0000
201@@ -70,6 +70,9 @@
202 def test_form_loads_initial_values_from_default_value(self):
203 value = factory.make_string()
204 DEFAULT_CONFIG['field1'] = value
205+ # Remove the added config from the DEFAULT_CONFIG or it will
206+ # break other tests.
207+ self.addCleanup(lambda: DEFAULT_CONFIG.pop('field1'))
208 form = TestOptionForm()
209
210 self.assertItemsEqual(['field1'], form.initial)
211
212=== modified file 'src/maasserver/urls_api.py'
213--- src/maasserver/urls_api.py 2016-05-23 09:16:07 +0000
214+++ src/maasserver/urls_api.py 2016-06-14 18:49:36 +0000
215@@ -166,6 +166,7 @@
216 )
217
218
219+maas_handler = RestrictedResource(MaasHandler, authentication=api_auth)
220 account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
221 boot_resource_handler = RestrictedResource(
222 BootResourceHandler, authentication=api_auth)
223@@ -266,7 +267,6 @@
224
225
226 # Admin handlers.
227-maas_handler = AdminRestrictedResource(MaasHandler, authentication=api_auth)
228 commissioning_script_handler = AdminRestrictedResource(
229 CommissioningScriptHandler, authentication=api_auth)
230 commissioning_scripts_handler = AdminRestrictedResource(
231@@ -296,6 +296,7 @@
232 # API URLs for logged-in users.
233 urlpatterns += patterns(
234 '',
235+ url(r'^maas/$', maas_handler, name='maas_handler'),
236 url(r'^nodes/(?P<system_id>[^/]+)/blockdevices/$',
237 blockdevices_handler, name='blockdevices_handler'),
238 url(r'^nodes/(?P<system_id>[^/]+)/blockdevices/(?P<device_id>[^/]+)/$',
239@@ -458,7 +459,6 @@
240 # API URLs for admin users.
241 urlpatterns += patterns(
242 '',
243- url(r'^maas/$', maas_handler, name='maas_handler'),
244 url(
245 r'^commissioning-scripts/$', commissioning_scripts_handler,
246 name='commissioning_scripts_handler'),