Merge lp:~hazmat/pyjuju/rapi-login into lp:~hazmat/pyjuju/rapi-rollup
- rapi-login
- Merge into rapi-rollup
Status: | Merged |
---|---|
Merged at revision: | 624 |
Proposed branch: | lp:~hazmat/pyjuju/rapi-login |
Merge into: | lp:~hazmat/pyjuju/rapi-rollup |
Diff against target: |
256 lines (+84/-12) 6 files modified
juju/errors.py (+4/-0) juju/rapi/context.py (+9/-2) juju/rapi/tests/common.py (+1/-0) juju/rapi/tests/test_context.py (+3/-1) juju/rapi/transport/tests/test_ws.py (+47/-2) juju/rapi/transport/ws.py (+20/-7) |
To merge this branch: | bzr merge lp:~hazmat/pyjuju/rapi-login |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu | Pending | ||
Review via email: mp+140268@code.launchpad.net |
Commit message
Description of the change
Require login before api usage.
This update requires login before using the ws api and before the delta stream is active.
Kapil Thangavelu (hazmat) wrote : | # |
Nicola Larosa (teknico) wrote : | # |
Code looks good, on the face of it (I'm not familiar with this
codebase).
I dislike one- or two-letter names, even in tests, but that's just
personal preference.
Benjamin Saller (bcsaller) wrote : | # |
I think I'm a little shy on context here given the comments below. If
the goals are in keeping with the comments then this seems ok, if we are
looking for real ACL access over the tree, it wasn't clear to make that
worked as expected.
https:/
File juju/agents/api.py (right):
https:/
juju/agents/
Its not possible for someone to register another 'admin' like name here
or will the first added always appear first? This seems an odd way of
tracking this. Since this codebase only supports admin anyway wouldn't
this be acls[0] anyway or are there others?
https:/
juju/agents/
all=True)])
I don't recall the details of the security branch, but this copies it
from /initialized to /login basically? I only have vague guesses as to
why this is the process, you need to create it during init to set up the
agents and then add it to the login tree which will in the future
support additional logins?
https:/
File juju/rapi/
https:/
juju/rapi/
This does the ACL check on the login node and and uses that as the
success boolean. Are the other nodes protected with the same ACL? Its
not clear where that's being managed here.
https:/
File juju/rapi/
https:/
juju/rapi/
Again it looks like the ACL check works here, but I'm not sure if the
way this is wired in provides any actual access control outside of that
node. Is that a non-goal with this iteration?
Kapil Thangavelu (hazmat) wrote : | # |
replies to ben's comments.
https:/
File juju/agents/api.py (right):
https:/
juju/agents/
On 2012/12/17 18:36:59, benjamin.saller wrote:
> Its not possible for someone to register another 'admin' like name
here or will
> the first added always appear first? This seems an odd way of tracking
this.
> Since this codebase only supports admin anyway wouldn't this be
acls[0] anyway
> or are there others?
at the moment the only time this node is created is if it doesn't exist,
at the client side. there is no other client that will modify it once
its in place. the initialized node is the marker node for the rest of
the system to go forward once the environment details have been loaded.
ie. while its true that multiple 'admin' users could potentially be
created with different passwords, this node is WORM and thus suitable
for this use.
https:/
juju/agents/
all=True)])
On 2012/12/17 18:36:59, benjamin.saller wrote:
> I don't recall the details of the security branch, but this copies it
from
> /initialized to /login basically?
This copies the admin identity (which is a hash of the secret of the
user id) with a more restrictive acl grant to a newly created login
node. Effectively to read this node you must have authenticated with the
admin credentials.
This code also exists in initialized, but our current usage/deployment
mode is via charm so the initialize code from this branch will never be
called, hence this lazy init technique for the api agent startup.
> I only have vague guesses as to why this is
> the process, you need to create it during init to set up the agents
and then add
> it to the login tree which will in the future support additional
logins?
hopefully the previous comments make it clear.
https:/
File juju/rapi/
https:/
juju/rapi/
On 2012/12/17 18:36:59, benjamin.saller wrote:
> This does the ACL check on the login node and and uses that as the
success
> boolean. Are the other nodes protected with the same ACL? Its not
clear where
> that's being managed here.
access to the node is only possible if the username and password are
correct, ie. match the admin identity used for the node acl.
https:/
File juju/rapi/
https:/
juju/rapi/
On 2012/12/17 18:36:59, benjamin.saller wrote:
> Again it looks like the ACL check works here, but I'm not sure if the
way this
> is wired in provides any actual access control outside of that node.
Is that non-goal with this iteration?
this isn't about access control at the node tree level, that's the
security branch work which is *much* larger ...
Kapil Thangavelu (hazmat) wrote : | # |
Please take a look.
Kapil Thangavelu (hazmat) wrote : | # |
Please take a look.
Benjamin Saller (bcsaller) wrote : | # |
LGTM
I didn't see a test around the login call returning the added
unauthorized error though.
https:/
File juju/rapi/
https:/
juju/rapi/
== "_login":
func.__name__ has a bad smell to it but fair enough
https:/
File juju/rapi/
https:/
juju/rapi/
Is there an use case for a logout/inverse to this stopping the delta? I
suspect not at this time.
Kapil Thangavelu (hazmat) wrote : | # |
thanks for the review
https:/
File juju/rapi/
https:/
juju/rapi/
== "_login":
On 2013/01/18 14:04:23, bcsaller wrote:
> func.__name__ has a bad smell to it but fair enough
its already playing with func.. not many other options outside of
decorating every other function with another attribute for perm, which
still amounts to the same, ie func inspection.
https:/
File juju/rapi/
https:/
juju/rapi/
On 2013/01/18 14:04:23, bcsaller wrote:
> Is there an use case for a logout/inverse to this stopping the delta?
I suspect
> not at this time.
logout not possible with the underlying zk auth mechanism, wrt to use
not at the moment outside of disconnect.
ideally stream sub/unsub would be explicit api methods, but doing so
without gui support first would break the gui.
Preview Diff
1 | === modified file 'juju/errors.py' |
2 | --- juju/errors.py 2012-08-28 16:09:47 +0000 |
3 | +++ juju/errors.py 2013-01-17 23:19:20 +0000 |
4 | @@ -12,6 +12,10 @@ |
5 | """ |
6 | |
7 | |
8 | +class Unauthorized(JujuError): |
9 | + """The user is not authorized to perform the operation.""" |
10 | + |
11 | + |
12 | class IncompatibleVersion(JujuError): |
13 | """Raised when there is a mismatch in versions using the topology. |
14 | |
15 | |
16 | === modified file 'juju/rapi/context.py' |
17 | --- juju/rapi/context.py 2012-12-20 04:43:20 +0000 |
18 | +++ juju/rapi/context.py 2013-01-17 23:19:20 +0000 |
19 | @@ -1,6 +1,8 @@ |
20 | from StringIO import StringIO |
21 | from twisted.internet.defer import ( |
22 | - maybeDeferred, succeed, returnValue, inlineCallbacks) |
23 | + fail, maybeDeferred, succeed, returnValue, inlineCallbacks) |
24 | + |
25 | +from juju.errors import Unauthorized |
26 | |
27 | from juju.rapi.cmd import add_relation |
28 | from juju.rapi.cmd import add_unit |
29 | @@ -67,6 +69,7 @@ |
30 | self.client = client |
31 | self.provider = provider |
32 | self.log = LogBuffer() |
33 | + self.authenticated = False |
34 | |
35 | def connect(self, *args, **kw): |
36 | if self.client.connected: |
37 | @@ -80,7 +83,10 @@ |
38 | self.client.close() |
39 | |
40 | def _invoke(self, func, *args, **kw): |
41 | - d = maybeDeferred(func, *args, **kw) |
42 | + if not self.authenticated and not func.__name__ == "_login": |
43 | + d = fail(Unauthorized("Login required.")) |
44 | + else: |
45 | + d = maybeDeferred(func, *args, **kw) |
46 | return d.addCallback( |
47 | lambda r: {'result': r, 'log': self.log.reset()} |
48 | ).addErrback(self._on_error) |
49 | @@ -233,6 +239,7 @@ |
50 | self.log.error("Invalid credentials") |
51 | raise |
52 | else: |
53 | + self.authenticated = True |
54 | self.log.info("Login success") |
55 | returnValue(True) |
56 | |
57 | |
58 | === modified file 'juju/rapi/tests/common.py' |
59 | --- juju/rapi/tests/common.py 2012-09-27 12:02:51 +0000 |
60 | +++ juju/rapi/tests/common.py 2013-01-17 23:19:20 +0000 |
61 | @@ -34,6 +34,7 @@ |
62 | "cloudy", {"default-series": "precise", |
63 | "storage-directory": self.makeDir()}) |
64 | self.context = APIContext(self.client, self.provider) |
65 | + self.context.authenticated = True |
66 | self.context.charm_cache_dir = self.makeDir() |
67 | yield self.client.create("/environment", TEST_ENV) |
68 | |
69 | |
70 | === modified file 'juju/rapi/tests/test_context.py' |
71 | --- juju/rapi/tests/test_context.py 2012-12-20 04:43:20 +0000 |
72 | +++ juju/rapi/tests/test_context.py 2013-01-17 23:19:20 +0000 |
73 | @@ -182,12 +182,14 @@ |
74 | principal = Principal(u, p) |
75 | self.client.create( |
76 | "/login", acls=[make_ace(principal.get_token(), all=True)]) |
77 | + self.context.authenticated = False |
78 | result = yield self.context.login(u, "hello") |
79 | self.assertEqual(result['log'], [('error', 'Invalid credentials')]) |
80 | self.assertEqual(result['err'], True) |
81 | result = yield self.context.login(u, p) |
82 | self.assertEqual(result['result'], True) |
83 | self.assertEqual(result['log'], [('info', 'Login success')]) |
84 | + self.assertTrue(self.context.authenticated) |
85 | yield self.client.delete("/login") |
86 | |
87 | @inlineCallbacks |
88 | @@ -205,7 +207,7 @@ |
89 | 'public-address': None}}, |
90 | 'charm': 'cs:precise/wordpress-3', |
91 | 'relations': {}}}, |
92 | - 'machines': {0: {'instance-id': 'pending'}}} |
93 | + 'machines': {0: {'instance-id': 'pending'}}} |
94 | self.assertEqual(response['result'], stat) |
95 | |
96 | response = yield self.context.expose('wordpress') |
97 | |
98 | === modified file 'juju/rapi/transport/tests/test_ws.py' |
99 | --- juju/rapi/transport/tests/test_ws.py 2012-10-04 15:58:43 +0000 |
100 | +++ juju/rapi/transport/tests/test_ws.py 2013-01-17 23:19:20 +0000 |
101 | @@ -1,14 +1,16 @@ |
102 | import json |
103 | import logging |
104 | |
105 | - |
106 | from twisted.internet.defer import inlineCallbacks, Deferred |
107 | |
108 | from juju.rapi.transport.ws import WebSocketAPIFactory |
109 | from juju.providers.dummy import MachineProvider |
110 | |
111 | +from juju.state.auth import make_ace |
112 | +from juju.state.topology import InternalTopology |
113 | +from juju.state.security import Principal |
114 | + |
115 | from juju.rapi.tests.common import ContextTestBase |
116 | -from juju.state.topology import InternalTopology |
117 | from juju.state.tests.common import StateTestBase |
118 | from juju.tests.common import get_test_zookeeper_address |
119 | |
120 | @@ -112,7 +114,10 @@ |
121 | @inlineCallbacks |
122 | def test_status(self): |
123 | d = self.transport.watch_kv('op', 'status') |
124 | + |
125 | self.ws.connectionMade() |
126 | + self.ws.context.authenticated = True |
127 | + |
128 | self.ws.dataReceived(json.dumps(dict( |
129 | op='status'))) |
130 | msg = yield d |
131 | @@ -139,6 +144,44 @@ |
132 | return super(DeltaSocketTest, self).tearDown() |
133 | |
134 | @inlineCallbacks |
135 | + def test_delta_after_login(self): |
136 | + """No delta events are sent till after login. |
137 | + """ |
138 | + # Setup for login and service deploy |
139 | + u, p = "admin", "cekret" |
140 | + self.client.create("/login") |
141 | + yield self.mock_store_charms(["precise/wordpress"]) |
142 | + self.mocker.replay() |
143 | + |
144 | + self.ws.connectionMade() |
145 | + yield self.context.deploy("precise/wordpress") |
146 | + |
147 | + # Even if we pump the delta stream now, the connected client |
148 | + # sees nothing till after login. |
149 | + yield self.stream_manager.pump() |
150 | + |
151 | + greeting = self.transport.stream[-1] |
152 | + self.assertEqual(greeting, |
153 | + {u'state': u'login-required', |
154 | + u'version': 0, |
155 | + u'extensions': [], |
156 | + u'ready': True, |
157 | + u'default_series': u'precise', |
158 | + u'provider_type': u'dummy'}) |
159 | + |
160 | + login_d = self.transport.watch_kv('op', 'login') |
161 | + delta_d = self.transport.watch_kv('op', 'delta') |
162 | + |
163 | + self.ws.dataReceived(json.dumps(dict( |
164 | + op='login', user=u, password=p))) |
165 | + |
166 | + yield login_d |
167 | + yield delta_d |
168 | + |
169 | + delta = self.transport.stream[-1] |
170 | + self.assertEqual(delta['op'], 'delta') |
171 | + |
172 | + @inlineCallbacks |
173 | def test_delta_stream(self): |
174 | yield self.mock_store_charms(["precise/wordpress", "precise/mysql"]) |
175 | self.mocker.replay() |
176 | @@ -146,6 +189,7 @@ |
177 | # There's always an implicit initial pump of current state made |
178 | # on connection. But no initial state for this test. |
179 | self.ws.connectionMade() |
180 | + self.ws.context.authenticated = True |
181 | |
182 | # Modify state and send events out. |
183 | yield self.context.deploy("precise/mysql") |
184 | @@ -153,6 +197,7 @@ |
185 | yield self.stream_manager.pump() |
186 | |
187 | delta = self.transport.stream[-1] |
188 | + |
189 | self.assertEqual(delta['op'], 'delta') |
190 | self.assertEqual(delta['result'], [ |
191 | [u'service', u'add', {u'id': 'wordpress', |
192 | |
193 | === modified file 'juju/rapi/transport/ws.py' |
194 | --- juju/rapi/transport/ws.py 2012-10-04 15:58:43 +0000 |
195 | +++ juju/rapi/transport/ws.py 2013-01-17 23:19:20 +0000 |
196 | @@ -46,18 +46,26 @@ |
197 | """Process the next message off the websocket channel. |
198 | """ |
199 | self.transport.write(json.dumps( |
200 | - {"version": 0, "extensions": [], "ready": True, |
201 | + {"version": 0, |
202 | + "extensions": [], |
203 | + "ready": True, |
204 | + "state": "login-required", |
205 | "provider_type": self.provider.provider_type, |
206 | "default_series": self.provider.config.get("default-series")})) |
207 | |
208 | log.info("Sent greeting") |
209 | |
210 | - # Push the current state. |
211 | - self.publish_changes() |
212 | - |
213 | + subscribed = False |
214 | while 1: |
215 | if not self.active: |
216 | break |
217 | + |
218 | + # Enable delta with initial push of current after login. |
219 | + if self.context.authenticated and not subscribed: |
220 | + subscribed = True |
221 | + self.publish_changes() |
222 | + self.factory.stream_manager.add(self) |
223 | + |
224 | msg = yield self.queue.get() |
225 | if msg is None: |
226 | break |
227 | @@ -123,6 +131,8 @@ |
228 | |
229 | def handle_msg_error(self, err): |
230 | # TODO: return back a request/op structure with error info. |
231 | + # ... errors generally handled by context, if we get here the |
232 | + # connection is hosed. |
233 | log.warning("Error %s" % err) |
234 | self.active = False |
235 | self.transport.write(json.dumps( |
236 | @@ -173,8 +183,12 @@ |
237 | self.zk_hosts = zk_hosts |
238 | self.provider = provider |
239 | self.numClients = 0 |
240 | - self.stream_manager = DeltaStreamManager( |
241 | - APIContext(ManagedClient(self.zk_hosts), self.provider)) |
242 | + |
243 | + # Setup an internal context soley for use by the stream manager. |
244 | + stream_context = APIContext( |
245 | + ManagedClient(self.zk_hosts), self.provider) |
246 | + stream_context.authenticated = True |
247 | + self.stream_manager = DeltaStreamManager(stream_context) |
248 | |
249 | # The common usage for twisted's native ws support is via protocol |
250 | # wrapper mounted onto an http server. This usage prevents |
251 | @@ -193,5 +207,4 @@ |
252 | def buildProtocol(self, addr=None): |
253 | p = self.protocol(self.zk_hosts, self.provider) |
254 | p.factory = self |
255 | - self.stream_manager.add(p) |
256 | return p |
Reviewers: mp+140268_ code.launchpad. net,
Message:
Please take a look.
Description:
Add a login api
Login support using admin credentials/secret
https:/ /code.launchpad .net/~hazmat/ juju/rapi- login/+ merge/140268
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/6935068/
Affected files: context. py tests/test_ context. py initialize. py tests/test_ initialize. py
A [revision details]
M juju/agents/api.py
M juju/rapi/
M juju/rapi/
M juju/state/
M juju/state/
Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: <email address hidden>
+New revision: <email address hidden>
Index: juju/agents/api.py api.py' transport. ws import WebSocketAPIFactory
=== modified file 'juju/agents/
--- juju/agents/api.py 2012-09-10 03:43:29 +0000
+++ juju/agents/api.py 2012-12-12 19:30:20 +0000
@@ -11,6 +11,7 @@
from juju.errors import JujuError
from juju.lib.websockets import WebSocketsResource
from juju.rapi.
+from juju.state.auth import make_ace
from twisted.web.static import Data
log. debug(" Received environment data.")
self. ws_factory = WebSocketAPIFac tory( 'zookeeper_ servers' ], self.provider) 'zookeeper_ servers' ], self.provider) factory. startFactory( )
from twisted.web.server import Site
@@ -32,7 +33,7 @@
root = Data("Juju API Server\n", "text/plain")
- self.config[
+ self.config[
yield self.ws_
@@ -79,10 +80,30 @@
+ # Lazy initialize login nodes if not present. e_login( ) fig()
config. parse(environme nt_data)
returnValue( config. get_default( ))
+ yield self._initializ
+
config = EnvironmentsCon
+ @inlineCallbacks login(self) : get_children( "/") get_acl( "/initialized" ) .startswith( 'admin' ): ace(admin_ acl['id' ], all=True)])
super( APIEndpointAgen t, self).configure (options) get("port" ):
+ def _initialize_
+ children = yield self.client.
+ if "login" in children:
+ return
+
+ acls, stat = yield self.client.
+ admin_acl = None
+
+ for a in acls:
+ if a['id']
+ admin_acl = a
+ break
+
+ yield self.client.create(
+ "/login", acls=[make_
+
def configure(self, options):
if not options.
Index: juju/rapi/ context. py context. py' context. py 2012-10-12 20:49:45 +0000 context. py 2012-12-12 19:30:20 +0000 internet. defer import maybeDeferred, succeed internet. defer import (
=== modified file 'juju/rapi/
--- juju/rapi/
+++ juju/rapi/
@@ -1,5 +1,6 @@
from StringIO import StringIO
-from twisted.
+from twisted.
+ maybeDeferred, succeed, returnValue, inlineCallbacks)
from juju.rapi.cmd import add_relation
from juju.rapi.cmd import add_unit
@@ -22,7 +23,10 @@
from juju.rapi import rest
+from juju.state.security import Principal
+
...