Merge lp:~allenap/maas/rpc-give-shared-secret-to-accepted-cluster into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Rejected
Rejected by: Gavin Panella
Proposed branch: lp:~allenap/maas/rpc-give-shared-secret-to-accepted-cluster
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 237 lines (+79/-25)
4 files modified
src/maasserver/api/node_groups.py (+5/-2)
src/maasserver/api/tests/test_register.py (+7/-2)
src/provisioningserver/start_cluster_controller.py (+33/-6)
src/provisioningserver/tests/test_start_cluster_controller.py (+34/-15)
To merge this branch: bzr merge lp:~allenap/maas/rpc-give-shared-secret-to-accepted-cluster
Reviewer Review Type Date Requested Status
Christian Reis (community) Disapprove
Raphaël Badin (community) Approve
Review via email: mp+236777@code.launchpad.net

Commit message

When a cluster is accepted, give it the shared secret with which to authenticate RPC connections.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good; couple of remarks but nothing major.

review: Approve
Revision history for this message
Christian Reis (kiko) wrote :

This is crazy. We are not going to land something which sends a password over the wire, over my dead body.

review: Disapprove
Revision history for this message
Andres Rodriguez (andreserl) wrote :
Download full text (9.9 KiB)

Can we send the secret in base 64 at the very least? I know it is not
encryption but it is better than sending it over clear text.
On Oct 1, 2014 3:33 PM, "Gavin Panella" <email address hidden> wrote:

> Gavin Panella has proposed merging
> lp:~allenap/maas/rpc-give-shared-secret-to-accepted-cluster into lp:maas.
>
> Commit message:
> When a cluster is accepted, give it the shared secret with which to
> authenticate RPC connections.
>
> Requested reviews:
> MAAS Maintainers (maas-maintainers)
>
> For more details, see:
>
> https://code.launchpad.net/~allenap/maas/rpc-give-shared-secret-to-accepted-cluster/+merge/236777
> --
>
> https://code.launchpad.net/~allenap/maas/rpc-give-shared-secret-to-accepted-cluster/+merge/236777
> You are subscribed to branch lp:maas.
>
> === modified file 'src/maasserver/api/node_groups.py'
> --- src/maasserver/api/node_groups.py 2014-09-24 14:20:51 +0000
> +++ src/maasserver/api/node_groups.py 2014-10-01 20:32:40 +0000
> @@ -29,6 +29,7 @@
> from django.http import HttpResponse
> from django.shortcuts import get_object_or_404
> from formencode import validators
> +from maasserver import security
> from maasserver.api.logger import maaslog
> from maasserver.api.support import (
> admin_method,
> @@ -115,10 +116,12 @@
> The response is based on the status of the `nodegroup` after
> registration,
> and whether it had already been registered before the call.
>
> - If the nodegroup was accepted, this returns an empty 200 response.
> + If the nodegroup was accepted, this returns a JSON-encoded response
> + containing the shared-secret to be used for RPC communications.
> """
> if nodegroup.status == NODEGROUP_STATUS.ACCEPTED:
> - return {} # Previously returned task-runner credentials.
> + secret_hex = security.get_shared_secret().encode("hex")
> + return {"secret": secret_hex.decode("ascii")}
> elif nodegroup.status == NODEGROUP_STATUS.REJECTED:
> raise PermissionDenied('Rejected cluster.')
> elif nodegroup.status == NODEGROUP_STATUS.PENDING:
>
> === renamed file 'src/maasserver/api/tests/test_nodegroup.py' =>
> 'src/maasserver/api/tests/test_node_groups.py'
> === modified file 'src/maasserver/api/tests/test_register.py'
> --- src/maasserver/api/tests/test_register.py 2014-09-24 14:20:51 +0000
> +++ src/maasserver/api/tests/test_register.py 2014-10-01 20:32:40 +0000
> @@ -26,6 +26,8 @@
> )
> from django.core.urlresolvers import reverse
> from django.test.client import RequestFactory
> +from fixtures import EnvironmentVariableFixture
> +from maasserver import security
> from maasserver.api import node_groups as nodegroups_module
> from maasserver.api.node_groups import (
> compose_nodegroup_register_response,
> @@ -172,11 +174,14 @@
> class TestComposeNodegroupRegisterResponse(MAASServerTestCase):
> """Tests for `compose_nodegroup_register_response`."""
>
> - def test_returns_empty_response_if_accepted(self):
> + def test_returns_shared_secret_if_accepted(self):
> + self.useFixture(EnvironmentVariableFixture(
> + "MAAS_ROOT", self.make_dir()))
> + secret_hex = security.get_...

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

> This is crazy. We are not going to land something which sends a
> password over the wire, over my dead body.

That's why I haven't landed it yet :-/

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

> Can we send the secret in base 64 at the very least? I know it is not
> encryption but it is better than sending it over clear text.

As it is it's going over base16, which is way more 133t and 5ekure than
base64 ;)

Unmerged revisions

3144. By Gavin Panella

Save secret from register call in start_cluster_controller.

3143. By Gavin Panella

Return the shared secret to an accepted cluster.

3142. By Gavin Panella

Rename test module to match the module it's testing.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/node_groups.py'
2--- src/maasserver/api/node_groups.py 2014-09-24 14:20:51 +0000
3+++ src/maasserver/api/node_groups.py 2014-10-01 20:32:40 +0000
4@@ -29,6 +29,7 @@
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404
7 from formencode import validators
8+from maasserver import security
9 from maasserver.api.logger import maaslog
10 from maasserver.api.support import (
11 admin_method,
12@@ -115,10 +116,12 @@
13 The response is based on the status of the `nodegroup` after registration,
14 and whether it had already been registered before the call.
15
16- If the nodegroup was accepted, this returns an empty 200 response.
17+ If the nodegroup was accepted, this returns a JSON-encoded response
18+ containing the shared-secret to be used for RPC communications.
19 """
20 if nodegroup.status == NODEGROUP_STATUS.ACCEPTED:
21- return {} # Previously returned task-runner credentials.
22+ secret_hex = security.get_shared_secret().encode("hex")
23+ return {"secret": secret_hex.decode("ascii")}
24 elif nodegroup.status == NODEGROUP_STATUS.REJECTED:
25 raise PermissionDenied('Rejected cluster.')
26 elif nodegroup.status == NODEGROUP_STATUS.PENDING:
27
28=== renamed file 'src/maasserver/api/tests/test_nodegroup.py' => 'src/maasserver/api/tests/test_node_groups.py'
29=== modified file 'src/maasserver/api/tests/test_register.py'
30--- src/maasserver/api/tests/test_register.py 2014-09-24 14:20:51 +0000
31+++ src/maasserver/api/tests/test_register.py 2014-10-01 20:32:40 +0000
32@@ -26,6 +26,8 @@
33 )
34 from django.core.urlresolvers import reverse
35 from django.test.client import RequestFactory
36+from fixtures import EnvironmentVariableFixture
37+from maasserver import security
38 from maasserver.api import node_groups as nodegroups_module
39 from maasserver.api.node_groups import (
40 compose_nodegroup_register_response,
41@@ -172,11 +174,14 @@
42 class TestComposeNodegroupRegisterResponse(MAASServerTestCase):
43 """Tests for `compose_nodegroup_register_response`."""
44
45- def test_returns_empty_response_if_accepted(self):
46+ def test_returns_shared_secret_if_accepted(self):
47+ self.useFixture(EnvironmentVariableFixture(
48+ "MAAS_ROOT", self.make_dir()))
49+ secret_hex = security.get_shared_secret().encode("hex")
50 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
51 existed = factory.pick_bool()
52 self.assertEqual(
53- {},
54+ {"secret": secret_hex.decode("ascii")},
55 compose_nodegroup_register_response(nodegroup, existed))
56
57 def test_returns_forbidden_if_rejected(self):
58
59=== modified file 'src/provisioningserver/start_cluster_controller.py'
60--- src/provisioningserver/start_cluster_controller.py 2014-09-23 20:16:54 +0000
61+++ src/provisioningserver/start_cluster_controller.py 2014-10-01 20:32:40 +0000
62@@ -19,6 +19,7 @@
63
64 import httplib
65 import json
66+from sys import stderr
67 from time import sleep
68 from urllib2 import (
69 HTTPError,
70@@ -33,6 +34,7 @@
71 from provisioningserver.cluster_config import get_cluster_uuid
72 from provisioningserver.logger import get_maas_logger
73 from provisioningserver.network import discover_networks
74+from provisioningserver.security import set_shared_secret_on_filesystem
75
76
77 maaslog = get_maas_logger("cluster")
78@@ -46,6 +48,9 @@
79 """For use by :class:`MainScript`."""
80 parser.add_argument(
81 'server_url', metavar='URL', help="URL to the MAAS region controller.")
82+ parser.add_argument(
83+ '--debug', action="store_true", help="Show debugging output.")
84+ parser.set_defaults(debug=False)
85
86
87 def log_error(exception):
88@@ -59,7 +64,7 @@
89 return MAASClient(NoAuth(), MAASDispatcher(), server_url)
90
91
92-def register(server_url):
93+def maybe_register(server_url):
94 """Register with the region controller.
95
96 Offers this machine to the region controller as a potential cluster
97@@ -108,14 +113,36 @@
98 raise AssertionError("Unexpected return code: %r" % status_code)
99
100
101+def register(server_url):
102+ """Poll the region controller until this node is accepted or rejected.
103+
104+ :returns: The secret returned from the region controller.
105+ """
106+ while True:
107+ response = maybe_register(server_url)
108+ if response is not None:
109+ secret_hex = response["secret"].encode("ascii")
110+ return secret_hex.decode("hex")
111+ # Bad luck. Wait a minute, then try again.
112+ sleep(60)
113+
114+
115 def run(args):
116 """Start the cluster controller.
117
118 If this system is still awaiting approval as a cluster controller, this
119 command will keep looping until it gets a definite answer.
120 """
121- maaslog.info("Starting cluster controller %s." % get_cluster_uuid())
122- connection_details = register(args.server_url)
123- while connection_details is None:
124- sleep(60)
125- connection_details = register(args.server_url)
126+ try:
127+ cluster_uuid = get_cluster_uuid()
128+ maaslog.info("Starting cluster controller %s.", cluster_uuid)
129+ secret = register(args.server_url)
130+ set_shared_secret_on_filesystem(secret)
131+ except Exception as error:
132+ if args.debug:
133+ raise
134+ else:
135+ print(unicode(error), file=stderr)
136+ raise SystemExit(2)
137+ else:
138+ raise SystemExit(0)
139
140=== modified file 'src/provisioningserver/tests/test_start_cluster_controller.py'
141--- src/provisioningserver/tests/test_start_cluster_controller.py 2014-09-23 20:21:50 +0000
142+++ src/provisioningserver/tests/test_start_cluster_controller.py 2014-10-01 20:32:40 +0000
143@@ -26,6 +26,7 @@
144 from fixtures import EnvironmentVariableFixture
145 from maastesting.factory import factory
146 from provisioningserver import start_cluster_controller
147+from provisioningserver.security import get_shared_secret_from_filesystem
148 from provisioningserver.testing.testcase import PservTestCase
149 from testtools.matchers import StartsWith
150
151@@ -48,13 +49,13 @@
152 )
153
154
155-FakeArgs = namedtuple('FakeArgs', ['server_url'])
156-
157-
158-def make_args(server_url=None):
159+FakeArgs = namedtuple('FakeArgs', ['server_url', 'debug'])
160+
161+
162+def make_args(server_url=None, debug=True):
163 if server_url is None:
164 server_url = make_url('region')
165- return FakeArgs(server_url)
166+ return FakeArgs(server_url, debug)
167
168
169 class FakeURLOpenResponse:
170@@ -84,10 +85,10 @@
171 start_cluster_controller, 'get_cluster_uuid')
172 get_uuid.return_value = factory.make_UUID()
173
174- def make_connection_details(self):
175- return {
176- 'BROKER_URL': make_url('broker'),
177- }
178+ def make_connection_details(self, secret=None):
179+ if secret is None:
180+ secret = factory.make_bytes()
181+ return {'secret': secret.encode("hex").decode("ascii")}
182
183 def parse_headers_and_body(self, headers, body):
184 """Parse ingredients of a web request.
185@@ -109,9 +110,9 @@
186 fake.return_value = FakeURLOpenResponse(content, status=http_code)
187 return fake
188
189- def prepare_success_response(self):
190+ def prepare_success_response(self, secret=None):
191 """Prepare to return connection details from API request."""
192- details = self.make_connection_details()
193+ details = self.make_connection_details(secret)
194 self.prepare_response(httplib.OK, json.dumps(details))
195 return details
196
197@@ -131,14 +132,21 @@
198 self.prepare_success_response()
199 parser = ArgumentParser()
200 start_cluster_controller.add_arguments(parser)
201- self.assertIsNone(
202- start_cluster_controller.run(
203- parser.parse_args((make_url(),))))
204+
205+ sysexit = self.assertRaises(
206+ SystemExit, start_cluster_controller.run,
207+ parser.parse_args((make_url(),)))
208+ self.assertEqual(0, sysexit.code)
209
210 def test_uses_given_url(self):
211 url = make_url('region')
212 self.prepare_success_response()
213- start_cluster_controller.run(make_args(server_url=url))
214+
215+ sysexit = self.assertRaises(
216+ SystemExit, start_cluster_controller.run,
217+ make_args(server_url=url))
218+ self.assertEqual(0, sysexit.code)
219+
220 (args, kwargs) = MAASDispatcher.dispatch_query.call_args
221 self.assertThat(args[0], StartsWith(url + 'api/1.0/nodegroups/'))
222
223@@ -180,3 +188,14 @@
224 self.assertEqual(
225 start_cluster_controller.get_cluster_uuid.return_value,
226 post['uuid'])
227+
228+ def test_register_saves_secret(self):
229+ secret = factory.make_bytes()
230+ self.prepare_success_response(secret)
231+
232+ sysexit = self.assertRaises(
233+ SystemExit, start_cluster_controller.run, make_args())
234+ self.assertEqual(0, sysexit.code)
235+
236+ self.assertEqual(
237+ secret, get_shared_secret_from_filesystem())