Merge lp:~anso/nova/authn_and_authz into lp:~hudson-openstack/nova/trunk

Proposed by termie
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
Reviewer Review Type Date Requested Status
Jay Pipes (community) Needs Information
Review via email: mp+52119@code.launchpad.net

Description of the change

A prototype / demo authn and authz system. Further discussion of the concepts here are in http://plansthis.com/auth

To post a comment you must log in.
Revision history for this message
termie (termie) wrote :

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.

Revision history for this message
Jay Pipes (jaypipes) wrote :
Download full text (4.2 KiB)

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_flagfile()
131 + flags.FLAGS(sys.argv)
132 + logging.setup()
133 + service.serve()
134 + service.wait()

will work...

3) In bin/stack:

165 +gflags.DEFINE_string('password', 'user1', 'Direct API password')

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_before:
195 + token = do_authenticate()

do_authenticate() returns token, but it returns it like so:

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']

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://wiki.openstack.org/openstack-authn? Jorge and Khaled suggest using the X-Authorization header and mandating that the authentication service set the X-Identity-Status header. What were your thoughts on this?

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.DEFINE_string('authn_topic', 'authn', 'topic to listen for authn on')

Why are we adding more AMQP communication for authentication? Perhaps I'm just not understanding the various service/manager/driver/adapter abstractions going on here, but it seems odd to be delegating authentication requests to the message queue that another service is listening on?

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)

So, we have an AuthnManager that inherits from manager.Manager and yet has a self.auth_manager member that is a manager_auth.AuthManager? Yikes, that just got really confusing. Could we document this relationship in code comments? I'm having a really tough time understanding how all the managers relate to each other.

290 +_url = "http://127.0.0.1:9001"
291 +_cat...

Read more...

review: Needs Information

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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