launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #07030
[Merge] lp:~rvb/maas/maas-error-auth-enlist into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-error-auth-enlist into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #975267 in MAAS: "When an admin registers a node with invalid data, an error 500 is triggered."
https://bugs.launchpad.net/maas/+bug/975267
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-error-auth-enlist/+merge/101124
This branch fixes a bug triggered when an admin user enlists a node and provides wrong data that won't validate. The main change is this branch is that api.py:create_node raises a validation error if the form is not valid instead of returning an bad request http response; this is to prevent any further processing. I refactored all the enlistment tests: we only tested anon enlistment and now most of the enlistment tests are run: for an anon user, for a normal user and for an admin user.
Drive-by fix: added missing signal cleanup.
--
https://code.launchpad.net/~rvb/maas/maas-error-auth-enlist/+merge/101124
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-error-auth-enlist into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-04-05 14:32:23 +0000
+++ src/maasserver/api.py 2012-04-06 16:01:25 +0000
@@ -408,17 +408,15 @@
The node will be in the Declared state.
:param request: The http request for this node to be created.
- :return: A suitable return value for the handler receiving the "new"
- request that this implements.
- :rtype: :class:`maasserver.models.Node` or
- :class:`django.http.HttpResponseBadRequest`.
+ :return: A `Node`.
+ :rtype: :class:`maasserver.models.Node`.
+ :raises: ValidationError
"""
form = NodeWithMACAddressesForm(request.data)
if form.is_valid():
return form.save()
else:
- return HttpResponseBadRequest(
- form.errors, content_type='application/json')
+ raise ValidationError(form.errors)
@api_operations
=== modified file 'src/maasserver/testing/testcase.py'
--- src/maasserver/testing/testcase.py 2012-04-02 16:04:34 +0000
+++ src/maasserver/testing/testcase.py 2012-04-06 16:01:25 +0000
@@ -10,6 +10,7 @@
__metaclass__ = type
__all__ = [
+ 'AdminLoggedInTestCase',
'LoggedInTestCase',
'TestCase',
'TestModelTestCase',
@@ -43,3 +44,10 @@
"""Promote the logged-in user to admin."""
self.logged_in_user.is_superuser = True
self.logged_in_user.save()
+
+
+class AdminLoggedInTestCase(LoggedInTestCase):
+
+ def setUp(self):
+ super(AdminLoggedInTestCase, self).setUp()
+ self.become_admin()
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-04-05 14:32:23 +0000
+++ src/maasserver/tests/test_api.py 2012-04-06 16:01:25 +0000
@@ -45,6 +45,7 @@
from maasserver.testing.factory import factory
from maasserver.testing.oauthclient import OAuthAuthenticatedClient
from maasserver.testing.testcase import (
+ AdminLoggedInTestCase,
LoggedInTestCase,
TestCase,
)
@@ -94,8 +95,7 @@
extract_constraints(QueryDict('name=%s' % name)))
-class AnonymousEnlistmentAPITest(APIv10TestMixin, TestCase):
- # Nodes can be enlisted anonymously.
+class EnlistmentAPITest(APIv10TestMixin):
def test_POST_new_creates_node(self):
# The API allows a Node to be created.
@@ -119,25 +119,6 @@
self.assertEqual(2, diane.after_commissioning_action)
self.assertEqual(architecture, diane.architecture)
- def test_POST_new_anonymous_creates_node_in_declared_state(self):
- # Upon anonymous enlistment, a node goes into the Declared
- # state. Deliberate approval is required before we start
- # reinstalling the system, wiping its disks etc.
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': factory.getRandomString(),
- 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
- })
- self.assertEqual(httplib.OK, response.status_code)
- system_id = json.loads(response.content)['system_id']
- self.assertEqual(
- NODE_STATUS.DECLARED,
- Node.objects.get(system_id=system_id).status)
-
def test_POST_new_power_type_defaults_to_asking_config(self):
architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
response = self.client.post(
@@ -194,25 +175,6 @@
system_id=json.loads(response.content)['system_id'])
self.assertEqual('node-aabbccddeeff.local', node.hostname)
- def test_POST_returns_limited_fields(self):
- architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': 'diane',
- 'architecture': architecture,
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
- })
- parsed_result = json.loads(response.content)
- self.assertItemsEqual(
- [
- 'hostname', 'system_id', 'macaddress_set', 'architecture',
- 'status'
- ],
- list(parsed_result))
-
def test_POST_fails_without_operation(self):
# If there is no operation ('op=operation_name') specified in the
# request data, a 'Bad request' response is returned.
@@ -259,6 +221,7 @@
self.fail("post_save should not have been called")
post_save.connect(node_created, sender=Node)
+ self.addCleanup(post_save.disconnect, node_created, sender=Node)
self.client.post(
self.get_uri('nodes/'),
{
@@ -318,6 +281,32 @@
self.assertIn('application/json', response['Content-Type'])
self.assertItemsEqual(['architecture'], parsed_result)
+
+class NonAdminEnlistmentAPITest(EnlistmentAPITest):
+
+ def test_POST_new_anonymous_creates_node_in_declared_state(self):
+ # Upon anonymous enlistment, a node goes into the Declared
+ # state. Deliberate approval is required before we start
+ # reinstalling the system, wiping its disks etc.
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
+ })
+ self.assertEqual(httplib.OK, response.status_code)
+ system_id = json.loads(response.content)['system_id']
+ self.assertEqual(
+ NODE_STATUS.DECLARED,
+ Node.objects.get(system_id=system_id).status)
+
+
+class AnonymousEnlistmentAPITest(NonAdminEnlistmentAPITest, TestCase):
+ # Nodes can be enlisted anonymously.
+
def test_POST_accept_not_allowed(self):
# An anonymous user is not allowed to accept an anonymously
# enlisted node. That would defeat the whole purpose of holding
@@ -329,6 +318,100 @@
(httplib.UNAUTHORIZED, "You must be logged in to accept nodes."),
(response.status_code, response.content))
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'hostname': factory.getRandomString(),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status',
+ ],
+ list(parsed_result))
+
+
+class SimpleUserLoggedInEnlistmentAPITest(NonAdminEnlistmentAPITest,
+ LoggedInTestCase):
+ # Nodes can be enlisted when logged-in.
+
+ def test_POST_accept_not_allowed(self):
+ # An anonymous user is not allowed to accept an anonymously
+ # enlisted node. That would defeat the whole purpose of holding
+ # those nodes for approval.
+ node_id = factory.make_node(status=NODE_STATUS.DECLARED).system_id
+ response = self.client.post(
+ self.get_uri('nodes/'), {'op': 'accept', 'nodes': [node_id]})
+ self.assertEqual(
+ (httplib.FORBIDDEN,
+ "You don't have the required permission to accept the "
+ "following node(s): %s." % node_id),
+ (response.status_code, response.content))
+
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status', 'resource_uri',
+ ],
+ list(parsed_result))
+
+
+class AdminLoggedInEnlistmentAPITest(EnlistmentAPITest,
+ AdminLoggedInTestCase):
+ # Nodes can be enlisted when logged-in as an admin user.
+
+ def test_POST_admin_creates_node_in_ready_state(self):
+ # When an admin user enlists a node, it goes into the Ready state.
+ # This will change once we start doing proper commissioning.
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
+ })
+ self.assertEqual(httplib.OK, response.status_code)
+ system_id = json.loads(response.content)['system_id']
+ self.assertEqual(
+ NODE_STATUS.READY, Node.objects.get(system_id=system_id).status)
+
+ def test_POST_returns_limited_fields(self):
+ response = self.client.post(
+ self.get_uri('nodes/'),
+ {
+ 'op': 'new',
+ 'hostname': factory.getRandomString(),
+ 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
+ 'after_commissioning_action': '2',
+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
+ })
+ parsed_result = json.loads(response.content)
+ self.assertItemsEqual(
+ [
+ 'hostname', 'system_id', 'macaddress_set', 'architecture',
+ 'status', 'resource_uri',
+ ],
+ list(parsed_result))
+
class AnonymousIsRegisteredAPITest(APIv10TestMixin, TestCase):
@@ -779,24 +862,6 @@
NODE_STATUS.DECLARED,
Node.objects.get(system_id=system_id).status)
- def test_POST_new_when_admin_creates_node_in_ready_state(self):
- # When an admin user enlists a node, it goes into the Ready state.
- # This will change once we start doing proper commissioning.
- self.become_admin()
- response = self.client.post(
- self.get_uri('nodes/'),
- {
- 'op': 'new',
- 'hostname': factory.getRandomString(),
- 'architecture': factory.getRandomChoice(ARCHITECTURE_CHOICES),
- 'after_commissioning_action': '2',
- 'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
- })
- self.assertEqual(httplib.OK, response.status_code)
- system_id = json.loads(response.content)['system_id']
- self.assertEqual(
- NODE_STATUS.READY, Node.objects.get(system_id=system_id).status)
-
def test_GET_list_lists_nodes(self):
# The api allows for fetching the list of Nodes.
node1 = factory.make_node()