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
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2013-10-18 09:32:57 +0000
+++ src/maasserver/api.py 2013-10-24 03:23:13 +0000
@@ -105,6 +105,7 @@
105import bson105import bson
106from celery.app import app_or_default106from celery.app import app_or_default
107from django.conf import settings107from django.conf import settings
108from django.contrib.auth.models import User
108from django.core.exceptions import (109from django.core.exceptions import (
109 PermissionDenied,110 PermissionDenied,
110 ValidationError,111 ValidationError,
@@ -129,6 +130,7 @@
129 OperationsHandler,130 OperationsHandler,
130 )131 )
131from maasserver.api_utils import (132from maasserver.api_utils import (
133 extract_bool,
132 extract_oauth_key,134 extract_oauth_key,
133 get_list_from_dict_or_multidict,135 get_list_from_dict_or_multidict,
134 get_mandatory_param,136 get_mandatory_param,
@@ -1967,6 +1969,36 @@
1967 # about the available configuration items.1969 # about the available configuration items.
1968 get_config.__doc__ %= get_config_doc(indentation=8)1970 get_config.__doc__ %= get_config_doc(indentation=8)
19691971
1972 @operation(idempotent=False)
1973 def unsafe_create_user(self, request):
1974 """Create a MAAS user account.
1975
1976 This is not safe: the password is sent in plaintext. Avoid it for
1977 production, unless you are confident that you can prevent eavesdroppers
1978 from observing the request.
1979
1980 :param username: Identifier-style username for the new user.
1981 :type username: unicode
1982 :param email: Email address for the new user.
1983 :type email: unicode
1984 :param password: Password for the new user.
1985 :type password: unicode
1986 :param is_admin: Whether the new user is to be an administrator.
1987 :type is_admin: bool ('0' for False, '1' for True)
1988 """
1989 username = get_mandatory_param(request.data, 'username')
1990 email = get_mandatory_param(request.data, 'email')
1991 password = get_mandatory_param(request.data, 'password')
1992 is_admin = extract_bool(get_mandatory_param(request.data, 'is_admin'))
1993
1994 if is_admin:
1995 User.objects.create_superuser(
1996 username=username, password=password, email=email)
1997 else:
1998 User.objects.create_user(
1999 username=username, password=password, email=email)
2000 return rc.ALL_OK
2001
1970 @classmethod2002 @classmethod
1971 def resource_uri(cls, *args, **kwargs):2003 def resource_uri(cls, *args, **kwargs):
1972 return ('maas_handler', [])2004 return ('maas_handler', [])
19732005
=== modified file 'src/maasserver/api_utils.py'
--- src/maasserver/api_utils.py 2013-10-07 09:12:40 +0000
+++ src/maasserver/api_utils.py 2013-10-24 03:23:13 +0000
@@ -13,6 +13,7 @@
1313
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 'extract_bool',
16 'extract_oauth_key',17 'extract_oauth_key',
17 'get_list_from_dict_or_multidict',18 'get_list_from_dict_or_multidict',
18 'get_mandatory_param',19 'get_mandatory_param',
@@ -28,6 +29,28 @@
28from piston.models import Token29from piston.models import Token
2930
3031
32def extract_bool(value):
33 """Extract a boolean from an API request argument.
34
35 Boolean arguments to API requests are passed as string values: "0" for
36 False, or "1" for True. This helper converts the string value to the
37 native Boolean value.
38
39 :param value: Value of a request parameter.
40 :type value: unicode
41 :return: Boolean value encoded in `value`.
42 :rtype bool:
43 :raise ValueError: If `value` is not an accepted encoding of a boolean.
44 """
45 assert isinstance(value, unicode)
46 if value == '0':
47 return False
48 elif value == '1':
49 return True
50 else:
51 raise ValueError("Not a valid Boolean value (0 or 1): '%s'" % value)
52
53
31def get_mandatory_param(data, key, validator=None):54def get_mandatory_param(data, key, validator=None):
32 """Get the parameter from the provided data dict or raise a ValidationError55 """Get the parameter from the provided data dict or raise a ValidationError
33 if this parameter is not present.56 if this parameter is not present.
3457
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2013-10-18 16:35:07 +0000
+++ src/maasserver/tests/test_api.py 2013-10-24 03:23:13 +0000
@@ -21,6 +21,7 @@
2121
22from apiclient.maas_client import MAASClient22from apiclient.maas_client import MAASClient
23from django.conf import settings23from django.conf import settings
24from django.contrib.auth.models import User
24from django.core.urlresolvers import reverse25from django.core.urlresolvers import reverse
25from fixtures import EnvironmentVariableFixture26from fixtures import EnvironmentVariableFixture
26from maasserver import api27from maasserver import api
@@ -441,6 +442,61 @@
441 ),442 ),
442 (response.status_code, json.loads(response.content)))443 (response.status_code, json.loads(response.content)))
443444
445 def test_unsafe_create_user_creates_user(self):
446 self.become_admin()
447 username = factory.make_name('user')
448 email = factory.getRandomEmail()
449 password = factory.getRandomString()
450
451 response = self.client.post(
452 self.get_uri('maas/'),
453 {
454 'op': 'unsafe_create_user',
455 'username': username,
456 'email': email,
457 'password': password,
458 'is_admin': '0',
459 })
460 self.assertEqual(httplib.OK, response.status_code, response.content)
461
462 created_user = User.objects.get(username=username)
463 self.assertEqual(email, created_user.email)
464 self.assertEqual(False, created_user.is_superuser)
465
466 def test_unsafe_create_user_creates_admin(self):
467 self.become_admin()
468 username = factory.make_name('user')
469 email = factory.getRandomEmail()
470 password = factory.getRandomString()
471
472 response = self.client.post(
473 self.get_uri('maas/'),
474 {
475 'op': 'unsafe_create_user',
476 'username': username,
477 'email': email,
478 'password': password,
479 'is_admin': '1',
480 })
481 self.assertEqual(httplib.OK, response.status_code, response.content)
482
483 created_user = User.objects.get(username=username)
484 self.assertEqual(email, created_user.email)
485 self.assertEqual(True, created_user.is_superuser)
486
487 def test_unsafe_create_user_requires_admin(self):
488 response = self.client.post(
489 self.get_uri('maas/'),
490 {
491 'op': 'unsafe_create_user',
492 'username': factory.make_name('user'),
493 'email': factory.getRandomEmail(),
494 'password': factory.getRandomString(),
495 'is_admin': '1' if factory.getRandomBoolean() else '0',
496 })
497 self.assertEqual(
498 httplib.FORBIDDEN, response.status_code, response.content)
499
444500
445class APIErrorsTest(APIv10TestMixin, TransactionTestCase):501class APIErrorsTest(APIv10TestMixin, TransactionTestCase):
446502
447503
=== modified file 'src/maasserver/tests/test_api_utils.py'
--- src/maasserver/tests/test_api_utils.py 2013-10-07 09:12:40 +0000
+++ src/maasserver/tests/test_api_utils.py 2013-10-24 03:23:13 +0000
@@ -18,6 +18,7 @@
1818
19from django.http import QueryDict19from django.http import QueryDict
20from maasserver.api_utils import (20from maasserver.api_utils import (
21 extract_bool,
21 extract_oauth_key,22 extract_oauth_key,
22 extract_oauth_key_from_auth_header,23 extract_oauth_key_from_auth_header,
23 get_oauth_token,24 get_oauth_token,
@@ -28,6 +29,31 @@
28from maasserver.testing.testcase import MAASServerTestCase29from maasserver.testing.testcase import MAASServerTestCase
2930
3031
32class TestExtractBool(MAASServerTestCase):
33 def test_asserts_against_raw_bytes(self):
34 self.assertRaises(AssertionError, extract_bool, b'0')
35
36 def test_asserts_against_None(self):
37 self.assertRaises(AssertionError, extract_bool, None)
38
39 def test_asserts_against_number(self):
40 self.assertRaises(AssertionError, extract_bool, 0)
41
42 def test_0_means_False(self):
43 self.assertEquals(extract_bool('0'), False)
44
45 def test_1_means_True(self):
46 self.assertEquals(extract_bool('1'), True)
47
48 def test_rejects_other_numeric_strings(self):
49 self.assertRaises(ValueError, extract_bool, '00')
50 self.assertRaises(ValueError, extract_bool, '2')
51 self.assertRaises(ValueError, extract_bool, '-1')
52
53 def test_rejects_empty_string(self):
54 self.assertRaises(ValueError, extract_bool, '')
55
56
31class TestGetOverridedQueryDict(MAASServerTestCase):57class TestGetOverridedQueryDict(MAASServerTestCase):
3258
33 def test_returns_QueryDict(self):59 def test_returns_QueryDict(self):