Merge lp:~jtv/maas/api-add-user into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1720
Proposed branch: lp:~jtv/maas/api-add-user
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 215 lines (+137/-0)
4 files modified
src/maasserver/api.py (+32/-0)
src/maasserver/api_utils.py (+23/-0)
src/maasserver/tests/test_api.py (+56/-0)
src/maasserver/tests/test_api_utils.py (+26/-0)
To merge this branch: bzr merge lp:~jtv/maas/api-add-user
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+192403@code.launchpad.net

Commit message

Allow creation of user accounts through the API.

Description of the change

Scott says his life will be easier with this capability. It's mainly for field-testing and such, so I did not bother with a secure way of exchanging the password. Instead, the name clearly indicates that it's not secure.

Jeroen

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

seems probably sufficent.
i'd save a step if i could specify ssh keys to that, but other than that that is fine.

this magically gets callable by the maas-cli?

Revision history for this message
Julian Edwards (julian-edwards) wrote :

> + production, unless you are confident that you can eavesdroppers from

Missing "prevent" ... ?

46 + if is_admin:
47 + User.objects.create_superuser(
48 + username=username, password=password, email=email)
49 + else:
50 + User.objects.create_user(
51 + username=username, password=password, email=email)
52 + return rc.ALL_OK

Do the create_* methods always succeed? Surely this needs some error checking? Or do they raise an exception?

Everything else looks great.

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 23 Oct 2013 21:49:45 you wrote:
> this magically gets callable by the maas-cli?

Yes.

Revision history for this message
Raphaël Badin (rvb) wrote :

I'm a bit surprised that you decided to put this on the MAAS handler. I think it really deserves both a UserHandler and a UsersHandler. Right now, the UI has functions (to manage users) not available through the API and we probably should fix that.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api.py'
2--- src/maasserver/api.py 2013-10-18 09:32:57 +0000
3+++ src/maasserver/api.py 2013-10-24 03:23:13 +0000
4@@ -105,6 +105,7 @@
5 import bson
6 from celery.app import app_or_default
7 from django.conf import settings
8+from django.contrib.auth.models import User
9 from django.core.exceptions import (
10 PermissionDenied,
11 ValidationError,
12@@ -129,6 +130,7 @@
13 OperationsHandler,
14 )
15 from maasserver.api_utils import (
16+ extract_bool,
17 extract_oauth_key,
18 get_list_from_dict_or_multidict,
19 get_mandatory_param,
20@@ -1967,6 +1969,36 @@
21 # about the available configuration items.
22 get_config.__doc__ %= get_config_doc(indentation=8)
23
24+ @operation(idempotent=False)
25+ def unsafe_create_user(self, request):
26+ """Create a MAAS user account.
27+
28+ This is not safe: the password is sent in plaintext. Avoid it for
29+ production, unless you are confident that you can prevent eavesdroppers
30+ from observing the request.
31+
32+ :param username: Identifier-style username for the new user.
33+ :type username: unicode
34+ :param email: Email address for the new user.
35+ :type email: unicode
36+ :param password: Password for the new user.
37+ :type password: unicode
38+ :param is_admin: Whether the new user is to be an administrator.
39+ :type is_admin: bool ('0' for False, '1' for True)
40+ """
41+ username = get_mandatory_param(request.data, 'username')
42+ email = get_mandatory_param(request.data, 'email')
43+ password = get_mandatory_param(request.data, 'password')
44+ is_admin = extract_bool(get_mandatory_param(request.data, 'is_admin'))
45+
46+ if is_admin:
47+ User.objects.create_superuser(
48+ username=username, password=password, email=email)
49+ else:
50+ User.objects.create_user(
51+ username=username, password=password, email=email)
52+ return rc.ALL_OK
53+
54 @classmethod
55 def resource_uri(cls, *args, **kwargs):
56 return ('maas_handler', [])
57
58=== modified file 'src/maasserver/api_utils.py'
59--- src/maasserver/api_utils.py 2013-10-07 09:12:40 +0000
60+++ src/maasserver/api_utils.py 2013-10-24 03:23:13 +0000
61@@ -13,6 +13,7 @@
62
63 __metaclass__ = type
64 __all__ = [
65+ 'extract_bool',
66 'extract_oauth_key',
67 'get_list_from_dict_or_multidict',
68 'get_mandatory_param',
69@@ -28,6 +29,28 @@
70 from piston.models import Token
71
72
73+def extract_bool(value):
74+ """Extract a boolean from an API request argument.
75+
76+ Boolean arguments to API requests are passed as string values: "0" for
77+ False, or "1" for True. This helper converts the string value to the
78+ native Boolean value.
79+
80+ :param value: Value of a request parameter.
81+ :type value: unicode
82+ :return: Boolean value encoded in `value`.
83+ :rtype bool:
84+ :raise ValueError: If `value` is not an accepted encoding of a boolean.
85+ """
86+ assert isinstance(value, unicode)
87+ if value == '0':
88+ return False
89+ elif value == '1':
90+ return True
91+ else:
92+ raise ValueError("Not a valid Boolean value (0 or 1): '%s'" % value)
93+
94+
95 def get_mandatory_param(data, key, validator=None):
96 """Get the parameter from the provided data dict or raise a ValidationError
97 if this parameter is not present.
98
99=== modified file 'src/maasserver/tests/test_api.py'
100--- src/maasserver/tests/test_api.py 2013-10-18 16:35:07 +0000
101+++ src/maasserver/tests/test_api.py 2013-10-24 03:23:13 +0000
102@@ -21,6 +21,7 @@
103
104 from apiclient.maas_client import MAASClient
105 from django.conf import settings
106+from django.contrib.auth.models import User
107 from django.core.urlresolvers import reverse
108 from fixtures import EnvironmentVariableFixture
109 from maasserver import api
110@@ -441,6 +442,61 @@
111 ),
112 (response.status_code, json.loads(response.content)))
113
114+ def test_unsafe_create_user_creates_user(self):
115+ self.become_admin()
116+ username = factory.make_name('user')
117+ email = factory.getRandomEmail()
118+ password = factory.getRandomString()
119+
120+ response = self.client.post(
121+ self.get_uri('maas/'),
122+ {
123+ 'op': 'unsafe_create_user',
124+ 'username': username,
125+ 'email': email,
126+ 'password': password,
127+ 'is_admin': '0',
128+ })
129+ self.assertEqual(httplib.OK, response.status_code, response.content)
130+
131+ created_user = User.objects.get(username=username)
132+ self.assertEqual(email, created_user.email)
133+ self.assertEqual(False, created_user.is_superuser)
134+
135+ def test_unsafe_create_user_creates_admin(self):
136+ self.become_admin()
137+ username = factory.make_name('user')
138+ email = factory.getRandomEmail()
139+ password = factory.getRandomString()
140+
141+ response = self.client.post(
142+ self.get_uri('maas/'),
143+ {
144+ 'op': 'unsafe_create_user',
145+ 'username': username,
146+ 'email': email,
147+ 'password': password,
148+ 'is_admin': '1',
149+ })
150+ self.assertEqual(httplib.OK, response.status_code, response.content)
151+
152+ created_user = User.objects.get(username=username)
153+ self.assertEqual(email, created_user.email)
154+ self.assertEqual(True, created_user.is_superuser)
155+
156+ def test_unsafe_create_user_requires_admin(self):
157+ response = self.client.post(
158+ self.get_uri('maas/'),
159+ {
160+ 'op': 'unsafe_create_user',
161+ 'username': factory.make_name('user'),
162+ 'email': factory.getRandomEmail(),
163+ 'password': factory.getRandomString(),
164+ 'is_admin': '1' if factory.getRandomBoolean() else '0',
165+ })
166+ self.assertEqual(
167+ httplib.FORBIDDEN, response.status_code, response.content)
168+
169
170 class APIErrorsTest(APIv10TestMixin, TransactionTestCase):
171
172
173=== modified file 'src/maasserver/tests/test_api_utils.py'
174--- src/maasserver/tests/test_api_utils.py 2013-10-07 09:12:40 +0000
175+++ src/maasserver/tests/test_api_utils.py 2013-10-24 03:23:13 +0000
176@@ -18,6 +18,7 @@
177
178 from django.http import QueryDict
179 from maasserver.api_utils import (
180+ extract_bool,
181 extract_oauth_key,
182 extract_oauth_key_from_auth_header,
183 get_oauth_token,
184@@ -28,6 +29,31 @@
185 from maasserver.testing.testcase import MAASServerTestCase
186
187
188+class TestExtractBool(MAASServerTestCase):
189+ def test_asserts_against_raw_bytes(self):
190+ self.assertRaises(AssertionError, extract_bool, b'0')
191+
192+ def test_asserts_against_None(self):
193+ self.assertRaises(AssertionError, extract_bool, None)
194+
195+ def test_asserts_against_number(self):
196+ self.assertRaises(AssertionError, extract_bool, 0)
197+
198+ def test_0_means_False(self):
199+ self.assertEquals(extract_bool('0'), False)
200+
201+ def test_1_means_True(self):
202+ self.assertEquals(extract_bool('1'), True)
203+
204+ def test_rejects_other_numeric_strings(self):
205+ self.assertRaises(ValueError, extract_bool, '00')
206+ self.assertRaises(ValueError, extract_bool, '2')
207+ self.assertRaises(ValueError, extract_bool, '-1')
208+
209+ def test_rejects_empty_string(self):
210+ self.assertRaises(ValueError, extract_bool, '')
211+
212+
213 class TestGetOverridedQueryDict(MAASServerTestCase):
214
215 def test_returns_QueryDict(self):