Merge lp:~anso/nova/authn_and_authz into lp:~hudson-openstack/nova/trunk
- authn_and_authz
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~anso/nova/authn_and_authz |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
1525 lines (+880/-95) 15 files modified
bin/nova-authn (+71/-0) bin/nova-authz (+54/-0) bin/nova-direct-api (+2/-1) bin/stack (+28/-5) etc/nova-api.conf (+6/-3) nova/api/authn.py (+208/-0) nova/api/authz.py (+223/-0) nova/api/direct.py (+6/-0) nova/api/ec2/__init__.py (+144/-51) nova/api/openstack/auth.py (+33/-19) nova/api/openstack/servers.py (+51/-14) nova/compute/api.py (+49/-0) nova/context.py (+3/-1) nova/flags.py (+1/-0) nova/tests/test_access.py (+1/-1) |
To merge this branch: | bzr merge lp:~anso/nova/authn_and_authz |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Needs Information | ||
Review via email: mp+52119@code.launchpad.net |
Commit message
Description of the change
A prototype / demo authn and authz system. Further discussion of the concepts here are in http://
Jay Pipes (jaypipes) wrote : | # |
Hi guys!
I'm not sure how much specifics I should get into here, since you say it's mostly for discussion, so please excuse me if I have gone into too much of a discussion of the implementation details!
1) Any reason bin/nova-authn and bin/nova-authz don't use paste.config like the other bin/ servers?
2) It seems that bin/nova-authz was not completed? Am I missing something? I don't think that this code:
129 +if __name__ == '__main__':
130 + utils.default_
131 + flags.FLAGS(
132 + logging.setup()
133 + service.serve()
134 + service.wait()
will work...
3) In bin/stack:
165 +gflags.
We've run into some problems in the unit tests where the user and key were both "user1" :) Could we avoid problems by making the password "pass1" or something like that?
194 + if not token and authenticate_
195 + token = do_authenticate()
do_authenticate() returns token, but it returns it like so:
182 + rv = do_request('authn', 'authenticate', params,
183 + host=FLAGS.
184 + port=FLAGS.
185 + authenticate_
186 + return rv['auth'
This will raise KeyError if the authentication doesn't work, correct? Probably best to trap for this in do_request(). That said, I believe a better solution would be to have the do_request() call in do_authenticate() trap for the 401 that *should* be returned if a user does not authenticate, no?
4)
Have you guys taken a look at this blueprint: http://
5) In etc/nova-api.conf
226 +pipeline = logrequest authenticate authn adminrequest authorizer ec2executor
and:
245 +pipeline = faultwrap auth authn ratelimit osapiapp
Seems to be a be confusing having (authenticate and authn) or (auth and authn) in same pipeline...
6) About nova/api/authn.py
This file and its contents are difficult for me to understand. I have the following issues/questions about it:
287 +flags.
Why are we adding more AMQP communication for authentication? Perhaps I'm just not understanding the various service/
304 +class AuthnManager(
305 + """Manages token based authentication."""
306 + def __init__(self, auth_manager=None, *args, **kwargs):
307 + if not auth_manager:
308 + auth_manager = manager_
309 + self.auth_manager = auth_manager
310 + super(AuthnManager, self)._
So, we have an AuthnManager that inherits from manager.Manager and yet has a self.auth_manager member that is a manager_
290 +_url = "http://
291 +_cat...
Unmerged revisions
- 770. By Vish Ishaya
-
make authn work with os api
- 769. By Vish Ishaya
-
make ec2 api use authn
- 768. By Vish Ishaya
-
simplify deepmerge
- 767. By Vish Ishaya
-
move where 'account:' is added to owner
- 766. By Vish Ishaya
-
Change NotFound exceptions into NotAuthorized
- 765. By Vish Ishaya
-
Changed account to owner in authz because it is clearer. Added docstrings. Added get_acl method
- 764. By Vish Ishaya
-
add docstrings and fix wrappers to check against owner
- 763. By Vish Ishaya
-
pass roles as roles and avoid exception in wrapper
- 762. By Vish Ishaya
-
add decorator and decorate compute methods
- 761. By Vish Ishaya
-
Add docstrings and and allow set_acl
Preview Diff
1 | === added file 'bin/nova-authn' |
2 | --- bin/nova-authn 1970-01-01 00:00:00 +0000 |
3 | +++ bin/nova-authn 2011-03-03 20:14:39 +0000 |
4 | @@ -0,0 +1,71 @@ |
5 | +#!/usr/bin/env python |
6 | +# pylint: disable-msg=C0103 |
7 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
8 | + |
9 | +# Copyright 2010 United States Government as represented by the |
10 | +# Administrator of the National Aeronautics and Space Administration. |
11 | +# All Rights Reserved. |
12 | +# |
13 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
14 | +# you may not use this file except in compliance with the License. |
15 | +# You may obtain a copy of the License at |
16 | +# |
17 | +# http://www.apache.org/licenses/LICENSE-2.0 |
18 | +# |
19 | +# Unless required by applicable law or agreed to in writing, software |
20 | +# distributed under the License is distributed on an "AS IS" BASIS, |
21 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
22 | +# See the License for the specific language governing permissions and |
23 | +# limitations under the License. |
24 | + |
25 | +"""Starter script for Nova Authentication.""" |
26 | + |
27 | +import gettext |
28 | +import os |
29 | +import sys |
30 | + |
31 | +# If ../nova/__init__.py exists, add ../ to Python search path, so that |
32 | +# it will override what happens to be installed in /usr/(local/)lib/python... |
33 | +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
34 | + os.pardir, |
35 | + os.pardir)) |
36 | +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): |
37 | + sys.path.insert(0, possible_topdir) |
38 | + |
39 | +gettext.install('nova', unicode=1) |
40 | + |
41 | +from nova import context |
42 | +from nova import flags |
43 | +from nova import log as logging |
44 | +from nova import service |
45 | +from nova import utils |
46 | +from nova import wsgi |
47 | +from nova.api import direct |
48 | +from nova.api import authn |
49 | + |
50 | + |
51 | +FLAGS = flags.FLAGS |
52 | +flags.DEFINE_integer('authn_port', 9001, 'Direct Auth port') |
53 | +flags.DEFINE_string('authn_host', '0.0.0.0', 'Direct Auth host') |
54 | +flags.DEFINE_string('authn_manager', 'nova.api.authn.AuthnManager', |
55 | + 'manager to use for authn') |
56 | +flags.DEFINE_flag(flags.HelpFlag()) |
57 | +flags.DEFINE_flag(flags.HelpshortFlag()) |
58 | +flags.DEFINE_flag(flags.HelpXMLFlag()) |
59 | + |
60 | + |
61 | +if __name__ == '__main__': |
62 | + utils.default_flagfile() |
63 | + FLAGS(sys.argv) |
64 | + logging.setup() |
65 | + |
66 | + direct.register_service('authn', authn.API()) |
67 | + router = direct.Router() |
68 | + with_req = direct.PostParamsMiddleware(router) |
69 | + with_ctx = direct.EmptyContextMiddleware(with_req) |
70 | + |
71 | + service.serve() |
72 | + server = wsgi.Server() |
73 | + server.start(with_ctx, FLAGS.authn_port, host=FLAGS.authn_host) |
74 | + server.wait() |
75 | + service.wait() |
76 | |
77 | === added file 'bin/nova-authz' |
78 | --- bin/nova-authz 1970-01-01 00:00:00 +0000 |
79 | +++ bin/nova-authz 2011-03-03 20:14:39 +0000 |
80 | @@ -0,0 +1,54 @@ |
81 | +#!/usr/bin/env python |
82 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
83 | + |
84 | +# Copyright 2010 United States Government as represented by the |
85 | +# Administrator of the National Aeronautics and Space Administration. |
86 | +# All Rights Reserved. |
87 | +# |
88 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
89 | +# not use this file except in compliance with the License. You may obtain |
90 | +# a copy of the License at |
91 | +# |
92 | +# http://www.apache.org/licenses/LICENSE-2.0 |
93 | +# |
94 | +# Unless required by applicable law or agreed to in writing, software |
95 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
96 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
97 | +# License for the specific language governing permissions and limitations |
98 | +# under the License. |
99 | + |
100 | +"""Starter script for Nova Authz.""" |
101 | + |
102 | +import eventlet |
103 | +eventlet.monkey_patch() |
104 | + |
105 | +import gettext |
106 | +import os |
107 | +import sys |
108 | + |
109 | +# If ../nova/__init__.py exists, add ../ to Python search path, so that |
110 | +# it will override what happens to be installed in /usr/(local/)lib/python... |
111 | +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
112 | + os.pardir, |
113 | + os.pardir)) |
114 | +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): |
115 | + sys.path.insert(0, possible_topdir) |
116 | + |
117 | +gettext.install('nova', unicode=1) |
118 | + |
119 | +from nova import flags |
120 | +from nova import log as logging |
121 | +from nova import service |
122 | +from nova import utils |
123 | + |
124 | + |
125 | +flags.DEFINE_string('authz_manager', 'nova.api.authz.AuthzManager', |
126 | + 'manager to user for authz') |
127 | + |
128 | + |
129 | +if __name__ == '__main__': |
130 | + utils.default_flagfile() |
131 | + flags.FLAGS(sys.argv) |
132 | + logging.setup() |
133 | + service.serve() |
134 | + service.wait() |
135 | |
136 | === modified file 'bin/nova-direct-api' |
137 | --- bin/nova-direct-api 2011-02-23 23:26:52 +0000 |
138 | +++ bin/nova-direct-api 2011-03-03 20:14:39 +0000 |
139 | @@ -39,6 +39,7 @@ |
140 | from nova import utils |
141 | from nova import wsgi |
142 | from nova.api import direct |
143 | +from nova.api import authn |
144 | from nova.compute import api as compute_api |
145 | |
146 | |
147 | @@ -60,7 +61,7 @@ |
148 | router = direct.Router() |
149 | with_json = direct.JsonParamsMiddleware(router) |
150 | with_req = direct.PostParamsMiddleware(with_json) |
151 | - with_auth = direct.DelegatedAuthMiddleware(with_req) |
152 | + with_auth = authn.AuthnMiddleware(with_req) |
153 | |
154 | server = wsgi.Server() |
155 | server.start(with_auth, FLAGS.direct_port, host=FLAGS.direct_host) |
156 | |
157 | === modified file 'bin/stack' |
158 | --- bin/stack 2011-01-18 15:24:20 +0000 |
159 | +++ bin/stack 2011-03-03 20:14:39 +0000 |
160 | @@ -45,7 +45,10 @@ |
161 | gflags.DEFINE_string('host', '127.0.0.1', 'Direct API host') |
162 | gflags.DEFINE_integer('port', 8001, 'Direct API host') |
163 | gflags.DEFINE_string('user', 'user1', 'Direct API username') |
164 | -gflags.DEFINE_string('project', 'proj1', 'Direct API project') |
165 | +gflags.DEFINE_string('password', 'user1', 'Direct API password') |
166 | +gflags.DEFINE_string('account', 'proj1', 'Direct API project') |
167 | +gflags.DEFINE_string('authn_host', '127.0.0.1', 'Authn Host') |
168 | +gflags.DEFINE_string('authn_port', '9001', 'Authn Port') |
169 | |
170 | |
171 | USAGE = """usage: stack [options] <controller> <method> [arg1=value arg2=value] |
172 | @@ -95,15 +98,35 @@ |
173 | return '\n'.join(out) |
174 | |
175 | |
176 | -def do_request(controller, method, params=None): |
177 | +def do_authenticate(): |
178 | + params = {'user_id': FLAGS.user, |
179 | + 'account_id': FLAGS.account, |
180 | + 'password': FLAGS.password} |
181 | + |
182 | + rv = do_request('authn', 'authenticate', params, |
183 | + host=FLAGS.authn_host, |
184 | + port=FLAGS.authn_port, |
185 | + authenticate_before=False) |
186 | + return rv['auth']['token']['id'] |
187 | + |
188 | + |
189 | +def do_request(controller, method, params=None, token=None, host=None, |
190 | + port=None, authenticate_before=True): |
191 | + host = host and host or FLAGS.host |
192 | + port = port and port or FLAGS.port |
193 | + |
194 | + if not token and authenticate_before: |
195 | + token = do_authenticate() |
196 | + |
197 | if params: |
198 | data = urllib.urlencode(params) |
199 | else: |
200 | data = None |
201 | |
202 | - url = 'http://%s:%s/%s/%s' % (FLAGS.host, FLAGS.port, controller, method) |
203 | - headers = {'X-OpenStack-User': FLAGS.user, |
204 | - 'X-OpenStack-Project': FLAGS.project} |
205 | + url = 'http://%s:%s/%s/%s' % (host, port, controller, method) |
206 | + headers = {} |
207 | + if token: |
208 | + headers['X-OpenStack-Token'] = token |
209 | |
210 | req = urllib2.Request(url, data, headers) |
211 | try: |
212 | |
213 | === modified file 'etc/nova-api.conf' |
214 | --- etc/nova-api.conf 2011-02-18 15:02:55 +0000 |
215 | +++ etc/nova-api.conf 2011-03-03 20:14:39 +0000 |
216 | @@ -19,11 +19,11 @@ |
217 | /1.0: ec2metadata |
218 | |
219 | [pipeline:ec2cloud] |
220 | -pipeline = logrequest authenticate cloudrequest authorizer ec2executor |
221 | +pipeline = logrequest authenticate authn cloudrequest ec2executor |
222 | #pipeline = logrequest ec2lockout authenticate cloudrequest authorizer ec2executor |
223 | |
224 | [pipeline:ec2admin] |
225 | -pipeline = logrequest authenticate adminrequest authorizer ec2executor |
226 | +pipeline = logrequest authenticate authn adminrequest authorizer ec2executor |
227 | |
228 | [pipeline:ec2metadata] |
229 | pipeline = logrequest ec2md |
230 | @@ -40,6 +40,9 @@ |
231 | [filter:authenticate] |
232 | paste.filter_factory = nova.api.ec2:Authenticate.factory |
233 | |
234 | +[filter:authn] |
235 | +paste.filter_factory = nova.api.authn:AuthnMiddleware.factory |
236 | + |
237 | [filter:cloudrequest] |
238 | controller = nova.api.ec2.cloud.CloudController |
239 | paste.filter_factory = nova.api.ec2:Requestify.factory |
240 | @@ -70,7 +73,7 @@ |
241 | /v1.0: openstackapi |
242 | |
243 | [pipeline:openstackapi] |
244 | -pipeline = faultwrap auth ratelimit osapiapp |
245 | +pipeline = faultwrap auth authn ratelimit osapiapp |
246 | |
247 | [filter:faultwrap] |
248 | paste.filter_factory = nova.api.openstack:FaultWrapper.factory |
249 | |
250 | === added file 'nova/api/authn.py' |
251 | --- nova/api/authn.py 1970-01-01 00:00:00 +0000 |
252 | +++ nova/api/authn.py 2011-03-03 20:14:39 +0000 |
253 | @@ -0,0 +1,208 @@ |
254 | +#!/usr/bin/env python |
255 | + |
256 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
257 | + |
258 | +# Copyright 2010 United States Government as represented by the |
259 | +# Administrator of the National Aeronautics and Space Administration. |
260 | +# All Rights Reserved. |
261 | +# |
262 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
263 | +# you may not use this file except in compliance with the License. |
264 | +# You may obtain a copy of the License at |
265 | +# |
266 | +# http://www.apache.org/licenses/LICENSE-2.0 |
267 | +# |
268 | +# Unless required by applicable law or agreed to in writing, software |
269 | +# distributed under the License is distributed on an "AS IS" BASIS, |
270 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
271 | +# See the License for the specific language governing permissions and |
272 | +# limitations under the License. |
273 | + |
274 | +import uuid |
275 | + |
276 | +from nova import context |
277 | +from nova import exception |
278 | +from nova import flags |
279 | +from nova import log as logging |
280 | +from nova import manager |
281 | +from nova import rpc |
282 | +from nova import wsgi |
283 | +from nova.auth import manager as manager_auth |
284 | + |
285 | + |
286 | +FLAGS = flags.FLAGS |
287 | +flags.DEFINE_string('authn_topic', 'authn', 'topic to listen for authn on') |
288 | +LOG = logging.getLogger("nova.auth") |
289 | + |
290 | +_url = "http://127.0.0.1:9001" |
291 | +_catalog = {"nova": [{"region": "nova", |
292 | + "v1Default": "true", |
293 | + "publicUrl": _url, |
294 | + "privateUrl": _url}], |
295 | + "swift": [{"region": "nova", |
296 | + "v1Default": "true", |
297 | + "publicUrl": _url, |
298 | + "privateUrl": _url}], |
299 | + "cdn": [{"region": "nova", |
300 | + "v1Default": "true", |
301 | + "publicUrl": _url, |
302 | + "privateUrl": _url}]} |
303 | + |
304 | +class AuthnManager(manager.Manager): |
305 | + """Manages token based authentication.""" |
306 | + def __init__(self, auth_manager=None, *args, **kwargs): |
307 | + if not auth_manager: |
308 | + auth_manager = manager_auth.AuthManager() |
309 | + self.auth_manager = auth_manager |
310 | + super(AuthnManager, self).__init__(*args, **kwargs) |
311 | + |
312 | + def get_accounts(self, context, user_id, password): |
313 | + """Retrieves a list of accounts for a given user_id, password""" |
314 | + user = self.auth_manager.get_user(user_id) |
315 | + if not user: |
316 | + raise exception.NotAuthorized("invalid user") |
317 | + if password != user.secret: |
318 | + raise exception.NotAuthorized("invalid password") |
319 | + projects = self.auth_manager.get_projects() |
320 | + return {"accounts" : [project.id for project in projects]} |
321 | + |
322 | + |
323 | + def authenticate(self, context, user_id, password, account_id): |
324 | + """Athenticates a user and password. |
325 | + |
326 | + :param context: the context of the request |
327 | + :param user_id: the user to auth |
328 | + :param password: the password to auth |
329 | + :param account_id: the id of the account to use |
330 | + :returns: dictionary containing token and service catalog |
331 | + |
332 | + """ |
333 | + try: |
334 | + user = self.auth_manager.get_user(user_id) |
335 | + except exception.NotFound: |
336 | + raise exception.NotAuthorized("invalid user") |
337 | + # TODO(vish): clearly this shouldn't be plaintext matching |
338 | + # against secret, but rather using its own field |
339 | + # and a hash of the password. |
340 | + if password != user.secret: |
341 | + raise exception.NotAuthorized("invalid password") |
342 | + try: |
343 | + project = self.auth_manager.get_project(account_id) |
344 | + except exception.NotFound: |
345 | + raise exception.NotAuthorized("invalid account") |
346 | + # NOTE(vish): the below doesn't seem to be necessary, because |
347 | + # authz will take care of it |
348 | + #if not self.auth_manager.is_member(project, user): |
349 | + # raise exception.NotAuthorized("can't access account") |
350 | + token = self._generate_token(context, user_id, account_id) |
351 | + catalog = self._generate_catalog(context, user_id, account_id) |
352 | + return {"auth": {"token" : token, "serviceCatalog": catalog}} |
353 | + |
354 | + def validate_token(self, context, token): |
355 | + """Validates token and returns a dictionary of metadata |
356 | + |
357 | + :param context: the context of the request |
358 | + :param token: the token identifier |
359 | + :returns: dictionary containing user, account, groups |
360 | + """ |
361 | + elevated = context.elevated() |
362 | + token = self.db.auth_token_get(elevated, token) |
363 | + # TODO(vish): actually validate the token we receive |
364 | + user_id, _sep, account_id = token["user_id"].partition(":") |
365 | + user = self.auth_manager.get_user(user_id) |
366 | + project = self.auth_manager.get_project(account_id) |
367 | + groups = [] |
368 | + groups.append("user:%s" % user_id) |
369 | + # NOTE(vish): There is no need to actually use something like |
370 | + # roles for the auth system. The roles are added |
371 | + # because this is the system nova currently uses. |
372 | + # It is provided primarily as an example to aid |
373 | + # understanding of the flexibility of the system. |
374 | + if self.auth_manager.is_project_member(user, project): |
375 | + groups.append("account:%s" % account_id) |
376 | + # TODO(vish): The next two could easily be converted to actual |
377 | + # roles in the database instead of requiring |
378 | + # special treatment. |
379 | + if self.auth_manager.is_project_manager(user, project): |
380 | + groups.append("role:project_manager") |
381 | + if self.auth_manager.is_admin(user): |
382 | + groups.append("role:admin") |
383 | + |
384 | + # NOTE(vish): This should be a union, but intersection is the |
385 | + # way the current nova implementation works. |
386 | + global_roles = set(self.auth_manager.get_user_roles(user)) |
387 | + local_roles = set(self.auth_manager.get_user_roles(user, project)) |
388 | + roles = global_roles.intersection(local_roles) |
389 | + groups = groups + ["role:%s" % role for role in roles] |
390 | + # NOTE(vish): We should possibly pass these in group format |
391 | + # as in "user" : "user:%s" % user_id |
392 | + return {"user" : user_id, |
393 | + "account" : account_id, |
394 | + "groups": groups} |
395 | + |
396 | + def _generate_token(self, context, user_id, account_id): |
397 | + # TODO(vish): This |
398 | + tok = {"token_hash": uuid.uuid4().hex, |
399 | + "user_id": "%s:%s" % (user_id, account_id)} |
400 | + elevated = context.elevated() |
401 | + tok_ref = self.db.auth_token_create(elevated, tok) |
402 | + return {"id": tok_ref['token_hash'], |
403 | + "expires": "2011-11-01T03:32:15-05:00"} |
404 | + |
405 | + def _generate_catalog(self, context, user_id, account_id): |
406 | + global _catalog |
407 | + catalog = {} |
408 | + for service_name, endpoints in _catalog.iteritems(): |
409 | + out_endpoints = [] |
410 | + for endpoint in endpoints: |
411 | + out_endpoint = dict(endpoint) |
412 | + for url in ["publicUrl", "privateUrl"]: |
413 | + out = endpoint[url].replace("$account_id", account_id) |
414 | + out = endpoint[url].replace("$user_id", user_id) |
415 | + out_endpoint[url] = out |
416 | + out_endpoints.append(out_endpoint) |
417 | + catalog[service_name] = out_endpoints |
418 | + return catalog |
419 | + |
420 | + |
421 | +class API(object): |
422 | + def authenticate(self, context, user_id, password, account_id): |
423 | + return rpc.call(context, FLAGS.authn_topic, |
424 | + {'method': 'authenticate', |
425 | + 'args': {'user_id': user_id, |
426 | + 'password': password, |
427 | + 'account_id': account_id}}) |
428 | + |
429 | + def validate_token(self, context, token): |
430 | + return rpc.call(context, FLAGS.authn_topic, |
431 | + {'method': 'validate_token', |
432 | + 'args': {'token': token}}) |
433 | + |
434 | + |
435 | +class AuthnMiddleware(wsgi.Middleware): |
436 | + def process_request(self, request): |
437 | + api = API() |
438 | + |
439 | + token = request.headers.get('X-OpenStack-Token') |
440 | + if not token: |
441 | + token = request.headers.get('X-Auth-Token') |
442 | + empty = context.RequestContext(None, None) |
443 | + valid = api.validate_token(empty, token) |
444 | + if not valid: |
445 | + raise Exception('invalid token') |
446 | + remote_address = request.remote_addr |
447 | + if FLAGS.use_forwarded_for: |
448 | + remote_address = request.headers.get('X-Forwarded-For', |
449 | + remote_address) |
450 | + context_ref = context.RequestContext(valid['user'], |
451 | + valid['account'], |
452 | + groups=valid['groups'], |
453 | + remote_address=remote_address) |
454 | + request.environ['openstack.context'] = context_ref |
455 | + # TODO(vish): the line below is compatibility for api.openstack, |
456 | + # which expects the context to be in nova.context |
457 | + request.environ['nova.context'] = context_ref |
458 | + uname = valid['user'] |
459 | + aname = valid['account'] |
460 | + msg = _('Authenticated Request For %(uname)s:%(aname)s)') % locals() |
461 | + LOG.audit(msg, context=request.environ['openstack.context']) |
462 | |
463 | === added file 'nova/api/authz.py' |
464 | --- nova/api/authz.py 1970-01-01 00:00:00 +0000 |
465 | +++ nova/api/authz.py 2011-03-03 20:14:39 +0000 |
466 | @@ -0,0 +1,223 @@ |
467 | +#!/usr/bin/env python |
468 | + |
469 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
470 | + |
471 | +# Copyright 2010 United States Government as represented by the |
472 | +# Administrator of the National Aeronautics and Space Administration. |
473 | +# All Rights Reserved. |
474 | +# |
475 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
476 | +# you may not use this file except in compliance with the License. |
477 | +# You may obtain a copy of the License at |
478 | +# |
479 | +# http://www.apache.org/licenses/LICENSE-2.0 |
480 | +# |
481 | +# Unless required by applicable law or agreed to in writing, software |
482 | +# distributed under the License is distributed on an "AS IS" BASIS, |
483 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
484 | +# See the License for the specific language governing permissions and |
485 | +# limitations under the License. |
486 | + |
487 | +from nova import exception |
488 | +from nova import flags |
489 | +from nova import rpc |
490 | +from nova import manager |
491 | +from nova import utils |
492 | + |
493 | + |
494 | +FLAGS = flags.FLAGS |
495 | +flags.DEFINE_string('authz_driver', 'nova.api.authz.TrivialAccountPolicy', |
496 | + 'which driver to use to backend authz requests') |
497 | + |
498 | + |
499 | +def check_acl(context, action, owner, target=None): |
500 | + """Helper method to call check_acl on authz""" |
501 | + authorized = rpc.call(context, |
502 | + FLAGS.authz_topic, |
503 | + {"method": "check_acl", |
504 | + "args": {"action": action, |
505 | + "owner": owner, |
506 | + "groups": context.groups, |
507 | + "target": target}}) |
508 | + if not authorized: |
509 | + raise exception.NotAuthorized("Failed ACL") |
510 | + |
511 | + |
512 | +def authorize(fn): |
513 | + """Decorator to auth action with no target.""" |
514 | + def wrapper(self, context, *args, **kwargs): |
515 | + # NOTE(vish): Wrapping the account should be removed |
516 | + # once account is passed in "group" format |
517 | + owner = "account:%s" % context.project_id |
518 | + check_acl(context, fn.func_name, owner) |
519 | + return fn(self, context, *args, **kwargs) |
520 | + wrapper.func_name = fn.func_name |
521 | + return wrapper |
522 | + |
523 | + |
524 | +class AuthzManager(manager.Manager): |
525 | + """Manages Authorization to actions and target objects. |
526 | + |
527 | + Proxies requests to a driver for implementation details. |
528 | + |
529 | + """ |
530 | + |
531 | + def __init__(self, authz_driver=None, *args, **kwargs): |
532 | + if not authz_driver: |
533 | + authz_driver = FLAGS.authz_driver |
534 | + self.driver = utils.import_object(authz_driver) |
535 | + super(AuthzManager, self).__init__(*args, **kwargs) |
536 | + |
537 | + def check_acl(self, context, action, owner, groups, target=None): |
538 | + """Check acl for a given action, owner, groups. |
539 | + |
540 | + The manager expects the client service to pass in a list of groups |
541 | + to be authenticated against the particular action and target. These |
542 | + driver is expected to return true if any of the groups passed in is |
543 | + allowed to perform the action on the target. |
544 | + |
545 | + The groups are expected to be retrieved by the service when it |
546 | + authenticates the request. Context is passed in case a specific |
547 | + implementation wants to pass additional data from the authn service. |
548 | + |
549 | + :param context: the current context. |
550 | + :param action: string representing action to check acls for |
551 | + :param owner: the owner of the target. If target is not specified, |
552 | + then the owner represents the account context in which |
553 | + the action is being performed. For example, when |
554 | + creating an object, this will be the owner of the new |
555 | + object that is being created. |
556 | + :param groups: a list of strings representing the groups that the |
557 | + actor is a part of. |
558 | + :param target: a string representing a unique id of the object |
559 | + to check acls for. |
560 | + :returns: True on successful check. |
561 | + |
562 | + """ |
563 | + return self.driver.check_acl(context, action, owner, groups, target) |
564 | + |
565 | + def get_acl(self, context, action, owner, target=None): |
566 | + """Get the current acls for a given action and owner. |
567 | + |
568 | + :param context: the current context. |
569 | + :param action: string representing action to get acls for |
570 | + :param owner: the owner to get acls for. |
571 | + :param target: a string representing a unique id of the object |
572 | + to get acls for. If this is None, the get |
573 | + will return acls for all objects that don't have |
574 | + specified rules. |
575 | + :returns: True on successful override. Raises on Error |
576 | + |
577 | + """ |
578 | + return self.driver.get_acl(context, action, owner, target) |
579 | + |
580 | + def set_acl(self, context, action, owner, groups, target=None): |
581 | + """Override the default acl for a given action, owner, group. |
582 | + |
583 | + Note that overrides are not added to the existing roles. If you |
584 | + wish to add a permission, it is necessary to call get_acl first |
585 | + to get the existing list of acls, append to it, and set_acl using |
586 | + the list. |
587 | + |
588 | + :param context: the current context. |
589 | + :param action: string representing action to override acls for |
590 | + :param owner: the owner to override acls for. Global rules are set |
591 | + by policy, so this cannot be None |
592 | + :param groups: a list of strings representing the groups that should |
593 | + be authorized to perform the action |
594 | + :param target: a string representing a unique id of the object |
595 | + to override acls for. If this is None, the override |
596 | + will apply to all objects that don't have specified |
597 | + rules. |
598 | + :returns: True on successful override. Raises on Error |
599 | + |
600 | + """ |
601 | + return self.driver.set_acl(context, action, owner, groups, target) |
602 | + |
603 | + |
604 | +class TrivialAccountPolicy(object): |
605 | + """Ultra simple policy that just makes sure the owner is in group.""" |
606 | + def check_acl(self, context, action, owner, groups, target=None): |
607 | + allowed = self.get_acl(context, action, owner, target) |
608 | + return bool(set(allowed).intersection(set(groups))) |
609 | + |
610 | + def get_acl(self, context, action, owner, target=None): |
611 | + return owner |
612 | + |
613 | + def set_acl(self, context, action, owner, groups, target=None): |
614 | + raise NotImplemented() |
615 | + |
616 | +def _deepmerge(left, right): |
617 | + """Utility function to deep merge two dictionaries.""" |
618 | + if isinstance(left, dict) and isinstance(right, dict): |
619 | + rval = dict(left) |
620 | + for k, v in right.iteritems(): |
621 | + result = _deepmerge(left.get(k), right.get(k)) |
622 | + if result: |
623 | + rval[k] = result |
624 | + return rval |
625 | + return right or left |
626 | + |
627 | +class SimplePolicy(object): |
628 | + """In memory policy engine that allows defaults and overrides.""" |
629 | + policy = { |
630 | + "default": ["$owner"], |
631 | + "set_acl": ["role:project_manager"], |
632 | + "reboot": {"default": ["role:sysadmin"]}, |
633 | + "delete": {"default": ["role:sysadmin"]}, |
634 | + } |
635 | + |
636 | + overrides = { |
637 | + "account:baz": { |
638 | + "get_all": {"default": ["role:sysadmin"]}, |
639 | + "reboot": {"1": ["user:foo"]}, |
640 | + "delete": {"default": [], "1": ["user:foo"]}, |
641 | + } |
642 | + } |
643 | + |
644 | + def check_acl(self, context, action, owner, groups, target=None): |
645 | + """Checks for allowed groups using policy.""" |
646 | + allowed = self.get_acl(context, action, owner, target) |
647 | + return bool(set(allowed).intersection(set(groups))) |
648 | + |
649 | + def get_acl(self, context, action, owner, target=None): |
650 | + allowed = self._get_groups_for_action(action, owner, target) |
651 | + return [x.replace("$owner", owner) for x in allowed] |
652 | + |
653 | + def set_acl(self, context, action, owner, groups, target=None): |
654 | + if not target: |
655 | + target = "default" |
656 | + new_overrides = {owner: {action: {target: groups}}} |
657 | + overrides = _deepmerge(self.overrides, new_overrides) |
658 | + return True |
659 | + |
660 | + def _get_groups_for_action(self, action, owner, target=None): |
661 | + """Helper method to get all groups using policy and overrides. |
662 | + |
663 | + First we do a deep merge with the default policy and any |
664 | + overrides that have been set. Then we attempt to find the most |
665 | + specific set of groups for our particular owner, action, target |
666 | + combination. The following rules will apply (later rule wins): |
667 | + |
668 | + * policy.default |
669 | + * overrides.owner.default |
670 | + * policy.action.default |
671 | + * overrides.owner.action.default |
672 | + * overrides.owner.action.target |
673 | + |
674 | + Note that specifically, overriding the default acl for an owner |
675 | + does will not change the acl for a specific action if it is |
676 | + specified in policy.action.default |
677 | + |
678 | + """ |
679 | + local_policy = _deepmerge(self.policy, self.overrides.get(owner)) |
680 | + groups = local_policy["default"] |
681 | + if local_policy.get(action) is not None: |
682 | + if local_policy[action].get("default") is not None: |
683 | + groups = local_policy[action]["default"] |
684 | + if target: |
685 | + target = str(target) |
686 | + if local_policy[action].get(target) is not None: |
687 | + groups = local_policy[action][target] |
688 | + return groups |
689 | + |
690 | |
691 | === modified file 'nova/api/direct.py' |
692 | --- nova/api/direct.py 2011-01-19 22:46:31 +0000 |
693 | +++ nova/api/direct.py 2011-03-03 20:14:39 +0000 |
694 | @@ -72,6 +72,12 @@ |
695 | request.environ['openstack.context'] = context_ref |
696 | |
697 | |
698 | +class EmptyContextMiddleware(wsgi.Middleware): |
699 | + def process_request(self, request): |
700 | + context_ref = context.RequestContext(None, None) |
701 | + request.environ['openstack.context'] = context_ref |
702 | + |
703 | + |
704 | class JsonParamsMiddleware(wsgi.Middleware): |
705 | def process_request(self, request): |
706 | if 'json' not in request.params: |
707 | |
708 | === modified file 'nova/api/ec2/__init__.py' |
709 | --- nova/api/ec2/__init__.py 2011-02-19 21:39:44 +0000 |
710 | +++ nova/api/ec2/__init__.py 2011-03-03 20:14:39 +0000 |
711 | @@ -28,11 +28,13 @@ |
712 | from nova import exception |
713 | from nova import flags |
714 | from nova import log as logging |
715 | +from nova import rpc |
716 | from nova import utils |
717 | from nova import wsgi |
718 | from nova.api.ec2 import apirequest |
719 | from nova.api.ec2 import cloud |
720 | from nova.auth import manager |
721 | +from nova.auth import signer |
722 | |
723 | |
724 | FLAGS = flags.FLAGS |
725 | @@ -50,6 +52,20 @@ |
726 | 'Memcached servers or None for in process cache.') |
727 | |
728 | |
729 | +def _error(req, context, code, message): |
730 | + LOG.error("%s: %s", code, message, context=context) |
731 | + resp = webob.Response() |
732 | + resp.status = 400 |
733 | + resp.headers['Content-Type'] = 'text/xml' |
734 | + resp.body = str('<?xml version="1.0"?>\n' |
735 | + '<Response><Errors><Error><Code>%s</Code>' |
736 | + '<Message>%s</Message></Error></Errors>' |
737 | + '<RequestID>%s</RequestID></Response>' % |
738 | + (utils.utf8(code), utils.utf8(message), |
739 | + utils.utf8(context.request_id))) |
740 | + return resp |
741 | + |
742 | + |
743 | class RequestLogging(wsgi.Middleware): |
744 | """Access-Log akin logging for all EC2 API requests.""" |
745 | |
746 | @@ -65,7 +81,7 @@ |
747 | if controller: |
748 | controller = controller.__class__.__name__ |
749 | action = request.environ.get('ec2.action', None) |
750 | - ctxt = request.environ.get('ec2.context', None) |
751 | + ctxt = request.environ.get('openstack.context', None) |
752 | delta = utils.utcnow() - start |
753 | seconds = delta.seconds |
754 | microseconds = delta.microseconds |
755 | @@ -139,7 +155,7 @@ |
756 | |
757 | class Authenticate(wsgi.Middleware): |
758 | |
759 | - """Authenticate an EC2 request and add 'ec2.context' to WSGI environ.""" |
760 | + """Authenticate an EC2 request and to WSGI environ.""" |
761 | |
762 | @webob.dec.wsgify |
763 | def __call__(self, req): |
764 | @@ -157,32 +173,116 @@ |
765 | |
766 | # Authenticate the request. |
767 | try: |
768 | - (user, project) = manager.AuthManager().authenticate( |
769 | - access, |
770 | - signature, |
771 | - auth_params, |
772 | - req.method, |
773 | - req.host, |
774 | - req.path) |
775 | + (user, password, account) = self._validate_sig(access, |
776 | + signature, |
777 | + auth_params, |
778 | + req.method, |
779 | + req.host, |
780 | + req.path) |
781 | # Be explicit for what exceptions are 403, the rest bubble as 500 |
782 | except (exception.NotFound, exception.NotAuthorized) as ex: |
783 | - LOG.audit(_("Authentication Failure: %s"), unicode(ex)) |
784 | - raise webob.exc.HTTPForbidden() |
785 | + LOG.info(_('SignatureDoesNotMatch raised: %s'), unicode(ex)) |
786 | + message = _('The request signature we calculated does not match ' |
787 | + 'the signature you provided. Check your AWS Secret ' |
788 | + 'Access Key and signing method. Consult the service ' |
789 | + 'documentation for details.') |
790 | + # NOTE(vish): Clients don't seem to respond very well to 4xx errors |
791 | + # so we return a special error message. |
792 | + ctxt = context.get_admin_context() |
793 | + return _error(req, ctxt, 'SignatureDoesNotMatch', message) |
794 | |
795 | - # Authenticated! |
796 | - remote_address = req.remote_addr |
797 | - if FLAGS.use_forwarded_for: |
798 | - remote_address = req.headers.get('X-Forwarded-For', remote_address) |
799 | - ctxt = context.RequestContext(user=user, |
800 | - project=project, |
801 | - remote_address=remote_address) |
802 | - req.environ['ec2.context'] = ctxt |
803 | - uname = user.name |
804 | - pname = project.name |
805 | - msg = _('Authenticated Request For %(uname)s:%(pname)s)') % locals() |
806 | - LOG.audit(msg, context=req.environ['ec2.context']) |
807 | + # TODO(vish): An empty context should be created as the first |
808 | + # middleware and other information should be added |
809 | + rv = rpc.call(context.get_admin_context(), |
810 | + FLAGS.authn_topic, |
811 | + {"method": "authenticate", |
812 | + "args": {"user_id": user, |
813 | + "password": password, |
814 | + "account_id": account}}) |
815 | + req.headers['X-OpenStack-Token'] = rv['auth']['token']['id'] |
816 | return self.application |
817 | |
818 | + def _validate_sig(self, access, signature, params, verb='GET', |
819 | + server_string='127.0.0.1:8773', path='/', |
820 | + check_type='ec2', headers=None): |
821 | + """Authenticates AWS request using access key and signature |
822 | + |
823 | + If the project is not specified, attempts to authenticate to |
824 | + a project with the same name as the user. This way, older tools |
825 | + that have no project knowledge will still work. |
826 | + |
827 | + @type access: str |
828 | + @param access: Access key for user in the form "access:project". |
829 | + |
830 | + @type signature: str |
831 | + @param signature: Signature of the request. |
832 | + |
833 | + @type params: list of str |
834 | + @param params: Web paramaters used for the signature. |
835 | + |
836 | + @type verb: str |
837 | + @param verb: Web request verb ('GET' or 'POST'). |
838 | + |
839 | + @type server_string: str |
840 | + @param server_string: Web request server string. |
841 | + |
842 | + @type path: str |
843 | + @param path: Web request path. |
844 | + |
845 | + @type check_type: str |
846 | + @param check_type: Type of signature to check. 'ec2' for EC2, 's3' for |
847 | + S3. Any other value will cause signature not to be |
848 | + checked. |
849 | + |
850 | + @type headers: list |
851 | + @param headers: HTTP headers passed with the request (only needed for |
852 | + s3 signature checks) |
853 | + |
854 | + @rtype: tuple (User, Project) |
855 | + @return: User and project that the request represents. |
856 | + """ |
857 | + # TODO(vish): check for valid timestamp |
858 | + (access_key, _sep, project_id) = access.partition(':') |
859 | + |
860 | + LOG.debug(_('Looking up user: %r'), access_key) |
861 | + # TODO(vish): this should be making calls through authn |
862 | + # instead of making AuthManager calls directly |
863 | + auth_manager = manager.AuthManager() |
864 | + user = auth_manager.get_user_from_access_key(access_key) |
865 | + LOG.debug('user: %r', user) |
866 | + if user == None: |
867 | + LOG.audit(_("Failed authorization for access key %s"), access_key) |
868 | + raise exception.NotFound(_('No user found for access key %s') |
869 | + % access_key) |
870 | + |
871 | + # NOTE(vish): if we stop using project name as id we need better |
872 | + # logic to find a default project for user |
873 | + if project_id == '': |
874 | + LOG.debug(_("Using project id = user id (%s)"), user.id) |
875 | + project_id = user.id |
876 | + |
877 | + if check_type == 's3': |
878 | + sign = signer.Signer(user.secret.encode()) |
879 | + expected_signature = sign.s3_authorization(headers, verb, path) |
880 | + LOG.debug('user.secret: %s', user.secret) |
881 | + LOG.debug('expected_signature: %s', expected_signature) |
882 | + LOG.debug('signature: %s', signature) |
883 | + if signature != expected_signature: |
884 | + LOG.audit(_("Invalid signature for user %s"), user.name) |
885 | + raise exception.NotAuthorized(_('Signature does not match')) |
886 | + elif check_type == 'ec2': |
887 | + # NOTE(vish): hmac can't handle unicode, so encode ensures that |
888 | + # secret isn't unicode |
889 | + expected_signature = signer.Signer(user.secret.encode()).generate( |
890 | + params, verb, server_string, path) |
891 | + LOG.debug('user.secret: %s', user.secret) |
892 | + LOG.debug('expected_signature: %s', expected_signature) |
893 | + LOG.debug('signature: %s', signature) |
894 | + if signature != expected_signature: |
895 | + LOG.audit(_("Invalid signature for user %s"), user.name) |
896 | + raise exception.NotAuthorized(_('Signature does not match')) |
897 | + return (user.id, user.secret, project_id) |
898 | + |
899 | |
900 | class Requestify(wsgi.Middleware): |
901 | |
902 | @@ -271,7 +371,7 @@ |
903 | |
904 | @webob.dec.wsgify |
905 | def __call__(self, req): |
906 | - context = req.environ['ec2.context'] |
907 | + context = req.environ['openstack.context'] |
908 | controller = req.environ['ec2.request'].controller.__class__.__name__ |
909 | action = req.environ['ec2.request'].action |
910 | allowed_roles = self.action_roles[controller].get(action, ['none']) |
911 | @@ -298,50 +398,57 @@ |
912 | |
913 | """Execute an EC2 API request. |
914 | |
915 | - Executes 'ec2.action' upon 'ec2.controller', passing 'ec2.context' and |
916 | - 'ec2.action_args' (all variables in WSGI environ.) Returns an XML |
917 | + Executes 'ec2.action' upon 'ec2.controller', passing 'openstack.context' |
918 | + and 'ec2.action_args' (all variables in WSGI environ.) Returns an XML |
919 | response, or a 400 upon failure. |
920 | """ |
921 | |
922 | @webob.dec.wsgify |
923 | def __call__(self, req): |
924 | - context = req.environ['ec2.context'] |
925 | + context = req.environ['openstack.context'] |
926 | api_request = req.environ['ec2.request'] |
927 | result = None |
928 | try: |
929 | result = api_request.invoke(context) |
930 | + except exception.NotAuthorized as ex: |
931 | + LOG.info(_('NotAuthorized raised: %s'), unicode(ex), |
932 | + context=context) |
933 | + message = _('You are not authorized for that action') |
934 | + # NOTE(vish): Clients don't seem to respond very well to 4xx errors |
935 | + # so we return a NotAuthorized message. |
936 | + return _error(req, context, type(ex).__name__, message) |
937 | except exception.InstanceNotFound as ex: |
938 | LOG.info(_('InstanceNotFound raised: %s'), unicode(ex), |
939 | context=context) |
940 | ec2_id = cloud.id_to_ec2_id(ex.instance_id) |
941 | message = _('Instance %s not found') % ec2_id |
942 | - return self._error(req, context, type(ex).__name__, message) |
943 | + return _error(req, context, type(ex).__name__, message) |
944 | except exception.VolumeNotFound as ex: |
945 | LOG.info(_('VolumeNotFound raised: %s'), unicode(ex), |
946 | context=context) |
947 | ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x') |
948 | message = _('Volume %s not found') % ec2_id |
949 | - return self._error(req, context, type(ex).__name__, message) |
950 | + return _error(req, context, type(ex).__name__, message) |
951 | except exception.NotFound as ex: |
952 | LOG.info(_('NotFound raised: %s'), unicode(ex), context=context) |
953 | - return self._error(req, context, type(ex).__name__, unicode(ex)) |
954 | + return _error(req, context, type(ex).__name__, unicode(ex)) |
955 | except exception.ApiError as ex: |
956 | LOG.exception(_('ApiError raised: %s'), unicode(ex), |
957 | context=context) |
958 | if ex.code: |
959 | - return self._error(req, context, ex.code, unicode(ex)) |
960 | + return _error(req, context, ex.code, unicode(ex)) |
961 | else: |
962 | - return self._error(req, context, type(ex).__name__, |
963 | + return _error(req, context, type(ex).__name__, |
964 | unicode(ex)) |
965 | except Exception as ex: |
966 | extra = {'environment': req.environ} |
967 | LOG.exception(_('Unexpected error raised: %s'), unicode(ex), |
968 | extra=extra, context=context) |
969 | - return self._error(req, |
970 | - context, |
971 | - 'UnknownError', |
972 | - _('An unknown error has occurred. ' |
973 | - 'Please try your request again.')) |
974 | + return _error(req, |
975 | + context, |
976 | + 'UnknownError', |
977 | + _('An unknown error has occurred. ' |
978 | + 'Please try your request again.')) |
979 | else: |
980 | resp = webob.Response() |
981 | resp.status = 200 |
982 | @@ -349,20 +456,6 @@ |
983 | resp.body = str(result) |
984 | return resp |
985 | |
986 | - def _error(self, req, context, code, message): |
987 | - LOG.error("%s: %s", code, message, context=context) |
988 | - resp = webob.Response() |
989 | - resp.status = 400 |
990 | - resp.headers['Content-Type'] = 'text/xml' |
991 | - resp.body = str('<?xml version="1.0"?>\n' |
992 | - '<Response><Errors><Error><Code>%s</Code>' |
993 | - '<Message>%s</Message></Error></Errors>' |
994 | - '<RequestID>%s</RequestID></Response>' % |
995 | - (utils.utf8(code), utils.utf8(message), |
996 | - utils.utf8(context.request_id))) |
997 | - return resp |
998 | - |
999 | - |
1000 | class Versions(wsgi.Application): |
1001 | |
1002 | @webob.dec.wsgify |
1003 | |
1004 | === modified file 'nova/api/openstack/auth.py' |
1005 | --- nova/api/openstack/auth.py 2011-03-18 02:09:46 +0000 |
1006 | +++ nova/api/openstack/auth.py 2011-03-03 20:14:39 +0000 |
1007 | @@ -29,6 +29,7 @@ |
1008 | from nova import exception |
1009 | from nova import flags |
1010 | from nova import manager |
1011 | +from nova import rpc |
1012 | from nova import utils |
1013 | from nova import wsgi |
1014 | from nova.api.openstack import faults |
1015 | @@ -51,13 +52,6 @@ |
1016 | if not self.has_authentication(req): |
1017 | return self.authenticate(req) |
1018 | |
1019 | - user = self.get_user_by_authentication(req) |
1020 | - |
1021 | - if not user: |
1022 | - return faults.Fault(webob.exc.HTTPUnauthorized()) |
1023 | - |
1024 | - project = self.auth.get_project(FLAGS.default_project) |
1025 | - req.environ['nova.context'] = context.RequestContext(user, project) |
1026 | return self.application |
1027 | |
1028 | def has_authentication(self, req): |
1029 | @@ -79,19 +73,39 @@ |
1030 | except KeyError: |
1031 | return faults.Fault(webob.exc.HTTPUnauthorized()) |
1032 | |
1033 | - token, user = self._authorize_user(username, key, req) |
1034 | - if user and token: |
1035 | - res = webob.Response() |
1036 | - res.headers['X-Auth-Token'] = token.token_hash |
1037 | - res.headers['X-Server-Management-Url'] = \ |
1038 | - token.server_management_url |
1039 | - res.headers['X-Storage-Url'] = token.storage_url |
1040 | - res.headers['X-CDN-Management-Url'] = token.cdn_management_url |
1041 | - res.content_type = 'text/plain' |
1042 | - res.status = '204' |
1043 | - return res |
1044 | - else: |
1045 | + # TODO(vish): An empty context should be created as the first |
1046 | + # middleware and other information should be added |
1047 | + ctxt = context.get_admin_context() |
1048 | + # NOTE(vish): account not passed in yet, so just get accounts |
1049 | + # and use the first one |
1050 | + try: |
1051 | + rv = rpc.call(ctxt, |
1052 | + FLAGS.authn_topic, |
1053 | + {"method": "get_accounts", |
1054 | + "args": {"user_id": username, |
1055 | + "password": key}}) |
1056 | + account = rv["accounts"][0] |
1057 | + rv = rpc.call(ctxt, |
1058 | + FLAGS.authn_topic, |
1059 | + {"method": "authenticate", |
1060 | + "args": {"user_id": username, |
1061 | + "password": key, |
1062 | + "account_id": account}}) |
1063 | + except exception.RemoteError: |
1064 | return faults.Fault(webob.exc.HTTPUnauthorized()) |
1065 | + res = webob.Response() |
1066 | + res.headers['X-OpenStack-Token'] = rv['auth']['token']['id'] |
1067 | + res.headers['X-Auth-Token'] = rv['auth']['token']['id'] |
1068 | + res.headers['X-Server-Management-Url'] = \ |
1069 | + req.url |
1070 | + # rv['auth']['serviceCatalog']['nova'][0]['publicUrl'] |
1071 | + res.headers['X-Storage-Url'] = \ |
1072 | + rv['auth']['serviceCatalog']['swift'][0]['publicUrl'] |
1073 | + res.headers['X-CDN-Management-Url'] = \ |
1074 | + rv['auth']['serviceCatalog']['cdn'][0]['publicUrl'] |
1075 | + res.content_type = 'text/plain' |
1076 | + res.status = '204' |
1077 | + return res |
1078 | |
1079 | def authorize_token(self, token_hash): |
1080 | """ retrieves user information from the datastore given a token |
1081 | |
1082 | === modified file 'nova/api/openstack/servers.py' |
1083 | --- nova/api/openstack/servers.py 2011-03-01 16:53:19 +0000 |
1084 | +++ nova/api/openstack/servers.py 2011-03-03 20:14:39 +0000 |
1085 | @@ -118,7 +118,10 @@ |
1086 | |
1087 | entity_maker - either _translate_detail_keys or _translate_keys |
1088 | """ |
1089 | - instance_list = self.compute_api.get_all(req.environ['nova.context']) |
1090 | + try: |
1091 | + instance_list = self.compute_api.get_all(req.environ['nova.context']) |
1092 | + except exception.NotAuthorized: |
1093 | + return faults.Fault(exc.HTTPUnauthorized()) |
1094 | limited_list = common.limited(instance_list, req) |
1095 | res = [entity_maker(inst)['server'] for inst in limited_list] |
1096 | return dict(servers=res) |
1097 | @@ -130,6 +133,8 @@ |
1098 | return _translate_detail_keys(instance) |
1099 | except exception.NotFound: |
1100 | return faults.Fault(exc.HTTPNotFound()) |
1101 | + except exception.NotAuthorized: |
1102 | + return faults.Fault(exc.HTTPUnauthorized()) |
1103 | |
1104 | def delete(self, req, id): |
1105 | """ Destroys a server """ |
1106 | @@ -137,6 +142,8 @@ |
1107 | self.compute_api.delete(req.environ['nova.context'], id) |
1108 | except exception.NotFound: |
1109 | return faults.Fault(exc.HTTPNotFound()) |
1110 | + except exception.NotAuthorized: |
1111 | + return faults.Fault(exc.HTTPUnauthorized()) |
1112 | return exc.HTTPAccepted() |
1113 | |
1114 | def create(self, req): |
1115 | @@ -166,18 +173,21 @@ |
1116 | for k, v in env['server']['metadata'].items(): |
1117 | metadata.append({'key': k, 'value': v}) |
1118 | |
1119 | - instances = self.compute_api.create( |
1120 | - context, |
1121 | - instance_types.get_by_flavor_id(env['server']['flavorId']), |
1122 | - image_id, |
1123 | - kernel_id=kernel_id, |
1124 | - ramdisk_id=ramdisk_id, |
1125 | - display_name=env['server']['name'], |
1126 | - display_description=env['server']['name'], |
1127 | - key_name=key_pair['name'], |
1128 | - key_data=key_pair['public_key'], |
1129 | - metadata=metadata, |
1130 | - onset_files=env.get('onset_files', [])) |
1131 | + try: |
1132 | + instances = self.compute_api.create( |
1133 | + context, |
1134 | + instance_types.get_by_flavor_id(env['server']['flavorId']), |
1135 | + image_id, |
1136 | + kernel_id=kernel_id, |
1137 | + ramdisk_id=ramdisk_id, |
1138 | + display_name=env['server']['name'], |
1139 | + display_description=env['server']['name'], |
1140 | + key_name=key_pair['name'], |
1141 | + key_data=key_pair['public_key'], |
1142 | + metadata=metadata, |
1143 | + onset_files=env.get('onset_files', [])) |
1144 | + except exception.NotAuthorized: |
1145 | + return faults.Fault(exc.HTTPUnauthorized()) |
1146 | return _translate_keys(instances[0]) |
1147 | |
1148 | def update(self, req, id): |
1149 | @@ -192,6 +202,8 @@ |
1150 | update_dict['admin_pass'] = inst_dict['server']['adminPass'] |
1151 | try: |
1152 | self.compute_api.set_admin_password(ctxt, id) |
1153 | + except exception.NotAuthorized: |
1154 | + return faults.Fault(exc.HTTPUnauthorized()) |
1155 | except exception.TimeoutException, e: |
1156 | return exc.HTTPRequestTimeout() |
1157 | if 'name' in inst_dict['server']: |
1158 | @@ -200,6 +212,8 @@ |
1159 | self.compute_api.update(ctxt, id, **update_dict) |
1160 | except exception.NotFound: |
1161 | return faults.Fault(exc.HTTPNotFound()) |
1162 | + except exception.NotAuthorized: |
1163 | + return faults.Fault(exc.HTTPUnauthorized()) |
1164 | return exc.HTTPNoContent() |
1165 | |
1166 | def action(self, req, id): |
1167 | @@ -215,6 +229,8 @@ |
1168 | # TODO(gundlach): pass reboot_type, support soft reboot in |
1169 | # virt driver |
1170 | self.compute_api.reboot(req.environ['nova.context'], id) |
1171 | + except exception.NotAuthorized: |
1172 | + return faults.Fault(exc.HTTPUnauthorized()) |
1173 | except: |
1174 | return faults.Fault(exc.HTTPUnprocessableEntity()) |
1175 | return exc.HTTPAccepted() |
1176 | @@ -243,6 +259,8 @@ |
1177 | context = req.environ['nova.context'] |
1178 | try: |
1179 | self.compute_api.unlock(context, id) |
1180 | + except exception.NotAuthorized: |
1181 | + return faults.Fault(exc.HTTPUnauthorized()) |
1182 | except: |
1183 | readable = traceback.format_exc() |
1184 | LOG.exception(_("Compute.api::unlock %s"), readable) |
1185 | @@ -257,6 +275,8 @@ |
1186 | context = req.environ['nova.context'] |
1187 | try: |
1188 | self.compute_api.get_lock(context, id) |
1189 | + except exception.NotAuthorized: |
1190 | + return faults.Fault(exc.HTTPUnauthorized()) |
1191 | except: |
1192 | readable = traceback.format_exc() |
1193 | LOG.exception(_("Compute.api::get_lock %s"), readable) |
1194 | @@ -271,6 +291,8 @@ |
1195 | context = req.environ['nova.context'] |
1196 | try: |
1197 | self.compute_api.reset_network(context, id) |
1198 | + except exception.NotAuthorized: |
1199 | + return faults.Fault(exc.HTTPUnauthorized()) |
1200 | except: |
1201 | readable = traceback.format_exc() |
1202 | LOG.exception(_("Compute.api::reset_network %s"), readable) |
1203 | @@ -285,6 +307,8 @@ |
1204 | context = req.environ['nova.context'] |
1205 | try: |
1206 | self.compute_api.inject_network_info(context, id) |
1207 | + except exception.NotAuthorized: |
1208 | + return faults.Fault(exc.HTTPUnauthorized()) |
1209 | except: |
1210 | readable = traceback.format_exc() |
1211 | LOG.exception(_("Compute.api::inject_network_info %s"), readable) |
1212 | @@ -296,6 +320,8 @@ |
1213 | ctxt = req.environ['nova.context'] |
1214 | try: |
1215 | self.compute_api.pause(ctxt, id) |
1216 | + except exception.NotAuthorized: |
1217 | + return faults.Fault(exc.HTTPUnauthorized()) |
1218 | except: |
1219 | readable = traceback.format_exc() |
1220 | LOG.exception(_("Compute.api::pause %s"), readable) |
1221 | @@ -307,6 +333,8 @@ |
1222 | ctxt = req.environ['nova.context'] |
1223 | try: |
1224 | self.compute_api.unpause(ctxt, id) |
1225 | + except exception.NotAuthorized: |
1226 | + return faults.Fault(exc.HTTPUnauthorized()) |
1227 | except: |
1228 | readable = traceback.format_exc() |
1229 | LOG.exception(_("Compute.api::unpause %s"), readable) |
1230 | @@ -318,6 +346,8 @@ |
1231 | context = req.environ['nova.context'] |
1232 | try: |
1233 | self.compute_api.suspend(context, id) |
1234 | + except exception.NotAuthorized: |
1235 | + return faults.Fault(exc.HTTPUnauthorized()) |
1236 | except: |
1237 | readable = traceback.format_exc() |
1238 | LOG.exception(_("compute.api::suspend %s"), readable) |
1239 | @@ -329,6 +359,8 @@ |
1240 | context = req.environ['nova.context'] |
1241 | try: |
1242 | self.compute_api.resume(context, id) |
1243 | + except exception.NotAuthorized: |
1244 | + return faults.Fault(exc.HTTPUnauthorized()) |
1245 | except: |
1246 | readable = traceback.format_exc() |
1247 | LOG.exception(_("compute.api::resume %s"), readable) |
1248 | @@ -364,12 +396,17 @@ |
1249 | int(id)) |
1250 | except exception.NotFound: |
1251 | return faults.Fault(exc.HTTPNotFound()) |
1252 | + except exception.NotAuthorized: |
1253 | + return faults.Fault(exc.HTTPUnauthorized()) |
1254 | return exc.HTTPAccepted() |
1255 | |
1256 | def diagnostics(self, req, id): |
1257 | """Permit Admins to retrieve server diagnostics.""" |
1258 | ctxt = req.environ["nova.context"] |
1259 | - return self.compute_api.get_diagnostics(ctxt, id) |
1260 | + try: |
1261 | + return self.compute_api.get_diagnostics(ctxt, id) |
1262 | + except exception.NotAuthorized: |
1263 | + return faults.Fault(exc.HTTPUnauthorized()) |
1264 | |
1265 | def actions(self, req, id): |
1266 | """Permit Admins to retrieve server actions.""" |
1267 | |
1268 | === modified file 'nova/compute/api.py' |
1269 | --- nova/compute/api.py 2011-03-03 01:50:48 +0000 |
1270 | +++ nova/compute/api.py 2011-03-03 20:14:39 +0000 |
1271 | @@ -35,6 +35,7 @@ |
1272 | from nova import volume |
1273 | from nova.compute import instance_types |
1274 | from nova.db import base |
1275 | +from nova.api import authz |
1276 | |
1277 | FLAGS = flags.FLAGS |
1278 | LOG = logging.getLogger('nova.compute.api') |
1279 | @@ -44,6 +45,25 @@ |
1280 | """Default function to generate a hostname given an instance reference.""" |
1281 | return str(instance_id) |
1282 | |
1283 | +def authorize_instance(fn): |
1284 | + """Decorator to auth action on an instance.""" |
1285 | + def wrapper(self, context, *args, **kwargs): |
1286 | + # TODO(vish): We have to elevate when getting the owner, since |
1287 | + # the action authorization system, needs to check |
1288 | + # the owner of the instance. This means that |
1289 | + # some calls will be authorized but fail at the db |
1290 | + # layer. We should evaluate whether the db layer |
1291 | + # auth is really necessary. |
1292 | + elevated = context.elevated() |
1293 | + instance_id = kwargs.get('instance_id') |
1294 | + instance_ref = self.db.instance_get(elevated, instance_id) |
1295 | + # TODO(vish): This wrapper should be removed once we are storing |
1296 | + # opaque owner strings |
1297 | + owner = "account:%s" % instance_ref['project_id'] |
1298 | + authz.authorize_acl(context, fn.func_name, owner, instance_id) |
1299 | + return fn(self, context, *args, **kwargs) |
1300 | + wrapper.func_name = fn.func_name |
1301 | + return wrapper |
1302 | |
1303 | class API(base.Base): |
1304 | """API for interacting with the compute manager.""" |
1305 | @@ -80,6 +100,7 @@ |
1306 | topic, |
1307 | {"method": "get_network_topic", "args": {'fake': 1}}) |
1308 | |
1309 | + @authz.authorize |
1310 | def create(self, context, instance_type, |
1311 | image_id, kernel_id=None, ramdisk_id=None, |
1312 | min_count=1, max_count=1, |
1313 | @@ -299,6 +320,7 @@ |
1314 | {"method": "refresh_security_group_members", |
1315 | "args": {"security_group_id": group_id}}) |
1316 | |
1317 | + @authorize_instance |
1318 | def update(self, context, instance_id, **kwargs): |
1319 | """Updates the instance in the datastore. |
1320 | |
1321 | @@ -314,6 +336,7 @@ |
1322 | rv = self.db.instance_update(context, instance_id, kwargs) |
1323 | return dict(rv.iteritems()) |
1324 | |
1325 | + @authorize_instance |
1326 | def delete(self, context, instance_id): |
1327 | LOG.debug(_("Going to try to terminate %s"), instance_id) |
1328 | try: |
1329 | @@ -341,16 +364,21 @@ |
1330 | else: |
1331 | self.db.instance_destroy(context, instance_id) |
1332 | |
1333 | + @authorize_instance |
1334 | def get(self, context, instance_id): |
1335 | """Get a single instance with the given ID.""" |
1336 | rv = self.db.instance_get(context, instance_id) |
1337 | return dict(rv.iteritems()) |
1338 | |
1339 | + @authz.authorize |
1340 | def get_all(self, context, project_id=None, reservation_id=None, |
1341 | fixed_ip=None): |
1342 | """Get all instances, possibly filtered by one of the |
1343 | given parameters. If there is no filter and the context is |
1344 | an admin, it will retreive all instances in the system.""" |
1345 | + # NOTE(vish): get_all not in the context of a project doesn't |
1346 | + # work anymore, which is probably fine because |
1347 | + # it doesn't scale at all anyway. |
1348 | if reservation_id is not None: |
1349 | return self.db.instance_get_all_by_reservation(context, |
1350 | reservation_id) |
1351 | @@ -404,6 +432,7 @@ |
1352 | kwargs = {'method': method, 'args': params} |
1353 | return rpc.call(context, queue, kwargs) |
1354 | |
1355 | + @authorize_instance |
1356 | def snapshot(self, context, instance_id, name): |
1357 | """Snapshot the given instance. |
1358 | |
1359 | @@ -416,18 +445,22 @@ |
1360 | params=params) |
1361 | return image_meta |
1362 | |
1363 | + @authorize_instance |
1364 | def reboot(self, context, instance_id): |
1365 | """Reboot the given instance.""" |
1366 | self._cast_compute_message('reboot_instance', context, instance_id) |
1367 | |
1368 | + @authorize_instance |
1369 | def pause(self, context, instance_id): |
1370 | """Pause the given instance.""" |
1371 | self._cast_compute_message('pause_instance', context, instance_id) |
1372 | |
1373 | + @authorize_instance |
1374 | def unpause(self, context, instance_id): |
1375 | """Unpause the given instance.""" |
1376 | self._cast_compute_message('unpause_instance', context, instance_id) |
1377 | |
1378 | + @authorize_instance |
1379 | def get_diagnostics(self, context, instance_id): |
1380 | """Retrieve diagnostics for the given instance.""" |
1381 | return self._call_compute_message( |
1382 | @@ -435,34 +468,42 @@ |
1383 | context, |
1384 | instance_id) |
1385 | |
1386 | + @authorize_instance |
1387 | def get_actions(self, context, instance_id): |
1388 | """Retrieve actions for the given instance.""" |
1389 | return self.db.instance_get_actions(context, instance_id) |
1390 | |
1391 | + @authorize_instance |
1392 | def suspend(self, context, instance_id): |
1393 | """suspend the instance with instance_id""" |
1394 | self._cast_compute_message('suspend_instance', context, instance_id) |
1395 | |
1396 | + @authorize_instance |
1397 | def resume(self, context, instance_id): |
1398 | """resume the instance with instance_id""" |
1399 | self._cast_compute_message('resume_instance', context, instance_id) |
1400 | |
1401 | + @authorize_instance |
1402 | def rescue(self, context, instance_id): |
1403 | """Rescue the given instance.""" |
1404 | self._cast_compute_message('rescue_instance', context, instance_id) |
1405 | |
1406 | + @authorize_instance |
1407 | def unrescue(self, context, instance_id): |
1408 | """Unrescue the given instance.""" |
1409 | self._cast_compute_message('unrescue_instance', context, instance_id) |
1410 | |
1411 | + @authorize_instance |
1412 | def set_admin_password(self, context, instance_id): |
1413 | """Set the root/admin password for the given instance.""" |
1414 | self._cast_compute_message('set_admin_password', context, instance_id) |
1415 | |
1416 | + @authorize_instance |
1417 | def inject_file(self, context, instance_id): |
1418 | """Write a file to the given instance.""" |
1419 | self._cast_compute_message('inject_file', context, instance_id) |
1420 | |
1421 | + @authorize_instance |
1422 | def get_ajax_console(self, context, instance_id): |
1423 | """Get a url to an AJAX Console""" |
1424 | instance = self.get(context, instance_id) |
1425 | @@ -476,25 +517,30 @@ |
1426 | return {'url': '%s/?token=%s' % (FLAGS.ajax_console_proxy_url, |
1427 | output['token'])} |
1428 | |
1429 | + @authorize_instance |
1430 | def get_console_output(self, context, instance_id): |
1431 | """Get console output for an an instance""" |
1432 | return self._call_compute_message('get_console_output', |
1433 | context, |
1434 | instance_id) |
1435 | |
1436 | + @authorize_instance |
1437 | def lock(self, context, instance_id): |
1438 | """lock the instance with instance_id""" |
1439 | self._cast_compute_message('lock_instance', context, instance_id) |
1440 | |
1441 | + @authorize_instance |
1442 | def unlock(self, context, instance_id): |
1443 | """unlock the instance with instance_id""" |
1444 | self._cast_compute_message('unlock_instance', context, instance_id) |
1445 | |
1446 | + @authorize_instance |
1447 | def get_lock(self, context, instance_id): |
1448 | """return the boolean state of (instance with instance_id)'s lock""" |
1449 | instance = self.get(context, instance_id) |
1450 | return instance['locked'] |
1451 | |
1452 | + @authorize_instance |
1453 | def reset_network(self, context, instance_id): |
1454 | """ |
1455 | Reset networking on the instance. |
1456 | @@ -502,6 +548,7 @@ |
1457 | """ |
1458 | self._cast_compute_message('reset_network', context, instance_id) |
1459 | |
1460 | + @authorize_instance |
1461 | def inject_network_info(self, context, instance_id): |
1462 | """ |
1463 | Inject network info for the instance. |
1464 | @@ -509,6 +556,7 @@ |
1465 | """ |
1466 | self._cast_compute_message('inject_network_info', context, instance_id) |
1467 | |
1468 | + @authorize_instance |
1469 | def attach_volume(self, context, instance_id, volume_id, device): |
1470 | if not re.match("^/dev/[a-z]d[a-z]+$", device): |
1471 | raise exception.ApiError(_("Invalid device specified: %s. " |
1472 | @@ -536,6 +584,7 @@ |
1473 | "volume_id": volume_id}}) |
1474 | return instance |
1475 | |
1476 | + @authorize_instance |
1477 | def associate_floating_ip(self, context, instance_id, address): |
1478 | instance = self.get(context, instance_id) |
1479 | self.network_api.associate_floating_ip(context, address, |
1480 | |
1481 | === modified file 'nova/context.py' |
1482 | --- nova/context.py 2011-01-31 00:04:52 +0000 |
1483 | +++ nova/context.py 2011-03-03 20:14:39 +0000 |
1484 | @@ -29,7 +29,8 @@ |
1485 | |
1486 | class RequestContext(object): |
1487 | def __init__(self, user, project, is_admin=None, read_deleted=False, |
1488 | - remote_address=None, timestamp=None, request_id=None): |
1489 | + remote_address=None, timestamp=None, request_id=None, |
1490 | + groups=None): |
1491 | if hasattr(user, 'id'): |
1492 | self._user = user |
1493 | self.user_id = user.id |
1494 | @@ -60,6 +61,7 @@ |
1495 | chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-' |
1496 | request_id = ''.join([random.choice(chars) for x in xrange(20)]) |
1497 | self.request_id = request_id |
1498 | + self.groups = groups and groups or [] |
1499 | |
1500 | @property |
1501 | def user(self): |
1502 | |
1503 | === modified file 'nova/flags.py' |
1504 | --- nova/flags.py 2011-02-25 01:04:25 +0000 |
1505 | +++ nova/flags.py 2011-03-03 20:14:39 +0000 |
1506 | @@ -266,6 +266,7 @@ |
1507 | DEFINE_integer('s3_port', 3333, 's3 port') |
1508 | DEFINE_string('s3_host', '$my_ip', 's3 host (for infrastructure)') |
1509 | DEFINE_string('s3_dmz', '$my_ip', 's3 dmz ip (for instances)') |
1510 | +DEFINE_string('authz_topic', 'authz', 'the topic authz nodes listen on') |
1511 | DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') |
1512 | DEFINE_string('console_topic', 'console', |
1513 | 'the topic console proxy nodes listen on') |
1514 | |
1515 | === modified file 'nova/tests/test_access.py' |
1516 | --- nova/tests/test_access.py 2011-01-12 19:20:05 +0000 |
1517 | +++ nova/tests/test_access.py 2011-03-03 20:14:39 +0000 |
1518 | @@ -41,7 +41,7 @@ |
1519 | class AccessTestCase(test.TestCase): |
1520 | def _env_for(self, ctxt, action): |
1521 | env = {} |
1522 | - env['ec2.context'] = ctxt |
1523 | + env['openstack.context'] = ctxt |
1524 | env['ec2.request'] = FakeApiRequest(action) |
1525 | return env |
1526 |
This hasn't been rebased onto trunk yet, so there may be some spurious conflicts in the diff, we wanted the branch to remain intact while we work on it, we will resolve conflicts again before actually proposing for review. For the moment this is just to get launchpad to generate a bit of a diff so that people can look over the code more easily.