diff options
| author | Michael Gundlach <michael.gundlach@rackspace.com> | 2010-09-01 12:42:06 -0400 |
|---|---|---|
| committer | Michael Gundlach <michael.gundlach@rackspace.com> | 2010-09-01 12:42:06 -0400 |
| commit | 83df968cfb050bdb6bac981dfcc2d0b1c3dd80db (patch) | |
| tree | dd8763a3fe358c5bca2c852c3b31bbf18f33a079 /nova | |
| parent | 8de182446993ac24e7b8fba12342f8adb3e179d4 (diff) | |
| download | nova-83df968cfb050bdb6bac981dfcc2d0b1c3dd80db.tar.gz nova-83df968cfb050bdb6bac981dfcc2d0b1c3dd80db.tar.xz nova-83df968cfb050bdb6bac981dfcc2d0b1c3dd80db.zip | |
Delete rbac.py, moving @rbac decorator knowledge into api.ec2.Authorizer WSGI middleware.
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/api/ec2/__init__.py | 64 | ||||
| -rw-r--r-- | nova/api/ec2/admin.py | 31 | ||||
| -rw-r--r-- | nova/api/ec2/cloud.py | 30 | ||||
| -rw-r--r-- | nova/auth/rbac.py | 69 |
4 files changed, 60 insertions, 134 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 87a72ca7c..aee9915d0 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -122,14 +122,70 @@ class Router(wsgi.Application): class Authorization(wsgi.Middleware): """ - Verify that ec2.controller and ec2.action in WSGI environ may be executed - in ec2.context. + Return a 401 if ec2.controller and ec2.action in WSGI environ may not be + executed in ec2.context. """ + def __init__(self, application): + super(Authorization, self).__init__(application) + self.action_roles = { + 'CloudController': { + 'DescribeAvailabilityzones': ['all'], + 'DescribeRegions': ['all'], + 'DescribeSnapshots': ['all'], + 'DescribeKeyPairs': ['all'], + 'CreateKeyPair': ['all'], + 'DeleteKeyPair': ['all'], + 'DescribeSecurityGroups': ['all'], + 'CreateSecurityGroup': ['netadmin'], + 'DeleteSecurityGroup': ['netadmin'], + 'GetConsoleOutput': ['projectmanager', 'sysadmin'], + 'DescribeVolumes': ['projectmanager', 'sysadmin'], + 'CreateVolume': ['projectmanager', 'sysadmin'], + 'AttachVolume': ['projectmanager', 'sysadmin'], + 'DetachVolume': ['projectmanager', 'sysadmin'], + 'DescribeInstances': ['all'], + 'DescribeAddresses': ['all'], + 'AllocateAddress': ['netadmin'], + 'ReleaseAddress': ['netadmin'], + 'AssociateAddress': ['netadmin'], + 'DisassociateAddress': ['netadmin'], + 'RunInstances': ['projectmanager', 'sysadmin'], + 'TerminateInstances': ['projectmanager', 'sysadmin'], + 'RebootInstances': ['projectmanager', 'sysadmin'], + 'DeleteVolume': ['projectmanager', 'sysadmin'], + 'DescribeImages': ['all'], + 'DeregisterImage': ['projectmanager', 'sysadmin'], + 'RegisterImage': ['projectmanager', 'sysadmin'], + 'DescribeImageAttribute': ['all'], + 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + }, + 'AdminController': { + # All actions have the same permission: [] (the default) + # admins will be allowed to run them + # all others will get HTTPUnauthorized. + }, + } + @webob.dec.wsgify def __call__(self, req): - #TODO(gundlach): put rbac information here. - return self.application + context = req.environ['ec2.context'] + controller_name = req.environ['ec2.controller'].__name__ + action = req.environ['ec2.action'] + allowed_roles = self.action_roles[controller_name].get(action, []) + if self._matches_any_role(context, allowed_roles): + return self.application + else: + raise webob.exc.HTTPUnauthorized() + + def _matches_any_role(self, context, roles): + """Return True if any role in roles is allowed in context.""" + if 'all' in roles: + return True + if 'none' in roles: + return False + return any(context.project.has_role(context.user.id, role) + for role in roles) class Executor(wsg.Application): diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py index d6f622755..f0c643bbd 100644 --- a/nova/api/ec2/admin.py +++ b/nova/api/ec2/admin.py @@ -57,46 +57,27 @@ def host_dict(host): return {} -def admin_only(target): - """Decorator for admin-only API calls""" - def wrapper(*args, **kwargs): - """Internal wrapper method for admin-only API calls""" - context = args[1] - if context.user.is_admin(): - return target(*args, **kwargs) - else: - return {} - - return wrapper - - class AdminController(object): """ API Controller for users, hosts, nodes, and workers. - Trivial admin_only wrapper will be replaced with RBAC, - allowing project managers to administer project users. """ def __str__(self): return 'AdminController' - @admin_only def describe_user(self, _context, name, **_kwargs): """Returns user data, including access and secret keys.""" return user_dict(manager.AuthManager().get_user(name)) - @admin_only def describe_users(self, _context, **_kwargs): """Returns all users - should be changed to deal with a list.""" return {'userSet': [user_dict(u) for u in manager.AuthManager().get_users()] } - @admin_only def register_user(self, _context, name, **_kwargs): """Creates a new user, and returns generated credentials.""" return user_dict(manager.AuthManager().create_user(name)) - @admin_only def deregister_user(self, _context, name, **_kwargs): """Deletes a single user (NOT undoable.) Should throw an exception if the user has instances, @@ -106,13 +87,11 @@ class AdminController(object): return True - @admin_only def describe_roles(self, context, project_roles=True, **kwargs): """Returns a list of allowed roles.""" roles = manager.AuthManager().get_roles(project_roles) return { 'roles': [{'role': r} for r in roles]} - @admin_only def describe_user_roles(self, context, user, project=None, **kwargs): """Returns a list of roles for the given user. Omitting project will return any global roles that the user has. @@ -121,7 +100,6 @@ class AdminController(object): roles = manager.AuthManager().get_user_roles(user, project=project) return { 'roles': [{'role': r} for r in roles]} - @admin_only def modify_user_role(self, context, user, role, project=None, operation='add', **kwargs): """Add or remove a role for a user and project.""" @@ -134,7 +112,6 @@ class AdminController(object): return True - @admin_only def generate_x509_for_user(self, _context, name, project=None, **kwargs): """Generates and returns an x509 certificate for a single user. Is usually called from a client that will wrap this with @@ -146,19 +123,16 @@ class AdminController(object): user = manager.AuthManager().get_user(name) return user_dict(user, base64.b64encode(project.get_credentials(user))) - @admin_only def describe_project(self, context, name, **kwargs): """Returns project data, including member ids.""" return project_dict(manager.AuthManager().get_project(name)) - @admin_only def describe_projects(self, context, user=None, **kwargs): """Returns all projects - should be changed to deal with a list.""" return {'projectSet': [project_dict(u) for u in manager.AuthManager().get_projects(user=user)]} - @admin_only def register_project(self, context, name, manager_user, description=None, member_users=None, **kwargs): """Creates a new project""" @@ -169,20 +143,17 @@ class AdminController(object): description=None, member_users=None)) - @admin_only def deregister_project(self, context, name): """Permanently deletes a project.""" manager.AuthManager().delete_project(name) return True - @admin_only def describe_project_members(self, context, name, **kwargs): project = manager.AuthManager().get_project(name) result = { 'members': [{'member': m} for m in project.member_ids]} return result - @admin_only def modify_project_member(self, context, user, project, operation, **kwargs): """Add or remove a user from a project.""" if operation =='add': @@ -193,7 +164,6 @@ class AdminController(object): raise exception.ApiError('operation must be add or remove') return True - @admin_only def describe_hosts(self, _context, **_kwargs): """Returns status info for all nodes. Includes: * Disk Space @@ -205,7 +175,6 @@ class AdminController(object): """ return {'hostSet': [host_dict(h) for h in model.Host.all()]} - @admin_only def describe_host(self, _context, name, **_kwargs): """Returns status info for single node.""" return host_dict(model.Host.lookup(name)) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 05fbf3861..566887c1a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -32,7 +32,6 @@ from nova import exception from nova import flags from nova import rpc from nova import utils -from nova.auth import rbac from nova.auth import manager from nova.compute import model from nova.compute.instance_types import INSTANCE_TYPES @@ -163,18 +162,15 @@ class CloudController(object): data['product-codes'] = i['product_codes'] return data - @rbac.allow('all') def describe_availability_zones(self, context, **kwargs): return {'availabilityZoneInfo': [{'zoneName': 'nova', 'zoneState': 'available'}]} - @rbac.allow('all') def describe_regions(self, context, region_name=None, **kwargs): # TODO(vish): region_name is an array. Support filtering return {'regionInfo': [{'regionName': 'nova', 'regionUrl': FLAGS.ec2_url}]} - @rbac.allow('all') def describe_snapshots(self, context, snapshot_id=None, @@ -190,7 +186,6 @@ class CloudController(object): 'volumeSize': 0, 'description': 'fixme'}]} - @rbac.allow('all') def describe_key_pairs(self, context, key_name=None, **kwargs): key_pairs = context.user.get_key_pairs() if not key_name is None: @@ -208,35 +203,29 @@ class CloudController(object): return {'keypairsSet': result} - @rbac.allow('all') def create_key_pair(self, context, key_name, **kwargs): data = _gen_key(context.user.id, key_name) return {'keyName': key_name, 'keyFingerprint': data['fingerprint'], 'keyMaterial': data['private_key']} - @rbac.allow('all') def delete_key_pair(self, context, key_name, **kwargs): context.user.delete_key_pair(key_name) # aws returns true even if the key doens't exist return True - @rbac.allow('all') def describe_security_groups(self, context, group_names, **kwargs): groups = {'securityGroupSet': []} # Stubbed for now to unblock other things. return groups - @rbac.allow('netadmin') def create_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('netadmin') def delete_security_group(self, context, group_name, **kwargs): return True - @rbac.allow('projectmanager', 'sysadmin') def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances instance = self._get_instance(context, instance_id[0]) @@ -250,7 +239,6 @@ class CloudController(object): else: return None - @rbac.allow('projectmanager', 'sysadmin') def describe_volumes(self, context, **kwargs): volumes = [] for volume in self.volumes: @@ -284,7 +272,6 @@ class CloudController(object): v['attachmentSet'] = [{}] return v - @rbac.allow('projectmanager', 'sysadmin') def create_volume(self, context, size, **kwargs): # TODO(vish): refactor this to create the volume object here and tell service to create it result = rpc.call(FLAGS.volume_topic, {"method": "create_volume", @@ -324,7 +311,6 @@ class CloudController(object): return volume raise exception.NotFound('Volume %s could not be found' % volume_id) - @rbac.allow('projectmanager', 'sysadmin') def attach_volume(self, context, volume_id, instance_id, device, **kwargs): volume = self._get_volume(context, volume_id) if volume['status'] == "attached": @@ -348,7 +334,6 @@ class CloudController(object): 'status': volume['attach_status'], 'volumeId': volume_id} - @rbac.allow('projectmanager', 'sysadmin') def detach_volume(self, context, volume_id, **kwargs): volume = self._get_volume(context, volume_id) instance_id = volume.get('instance_id', None) @@ -381,7 +366,6 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] - @rbac.allow('all') def describe_instances(self, context, **kwargs): return self._format_describe_instances(context) @@ -442,7 +426,6 @@ class CloudController(object): return list(reservations.values()) - @rbac.allow('all') def describe_addresses(self, context, **kwargs): return self.format_addresses(context) @@ -465,7 +448,6 @@ class CloudController(object): addresses.append(address_rv) return {'addressesSet': addresses} - @rbac.allow('netadmin') def allocate_address(self, context, **kwargs): network_topic = self._get_network_topic(context) public_ip = rpc.call(network_topic, @@ -474,7 +456,6 @@ class CloudController(object): "project_id": context.project.id}}) return {'addressSet': [{'publicIp': public_ip}]} - @rbac.allow('netadmin') def release_address(self, context, public_ip, **kwargs): # NOTE(vish): Should we make sure this works? network_topic = self._get_network_topic(context) @@ -483,7 +464,6 @@ class CloudController(object): "args": {"elastic_ip": public_ip}}) return {'releaseResponse': ["Address released."]} - @rbac.allow('netadmin') def associate_address(self, context, instance_id, public_ip, **kwargs): instance = self._get_instance(context, instance_id) address = self._get_address(context, public_ip) @@ -495,7 +475,6 @@ class CloudController(object): "instance_id": instance['instance_id']}}) return {'associateResponse': ["Address associated."]} - @rbac.allow('netadmin') def disassociate_address(self, context, public_ip, **kwargs): address = self._get_address(context, public_ip) network_topic = self._get_network_topic(context) @@ -514,7 +493,6 @@ class CloudController(object): "project_id": context.project.id}}) return '%s.%s' %(FLAGS.network_topic, host) - @rbac.allow('projectmanager', 'sysadmin') def run_instances(self, context, **kwargs): # make sure user can access the image # vpn image is private so it doesn't show up on lists @@ -587,7 +565,6 @@ class CloudController(object): # TODO: Make Network figure out the network name from ip. return self._format_run_instances(context, reservation_id) - @rbac.allow('projectmanager', 'sysadmin') def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") network_topic = self._get_network_topic(context) @@ -628,7 +605,6 @@ class CloudController(object): instance.destroy() return True - @rbac.allow('projectmanager', 'sysadmin') def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for i in instance_id: @@ -638,7 +614,6 @@ class CloudController(object): "args": {"instance_id": i}}) return True - @rbac.allow('projectmanager', 'sysadmin') def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized volume = self._get_volume(context, volume_id) @@ -648,19 +623,16 @@ class CloudController(object): "args": {"volume_id": volume_id}}) return True - @rbac.allow('all') def describe_images(self, context, image_id=None, **kwargs): # The objectstore does its own authorization for describe imageSet = images.list(context, image_id) return {'imagesSet': imageSet} - @rbac.allow('projectmanager', 'sysadmin') def deregister_image(self, context, image_id, **kwargs): # FIXME: should the objectstore be doing these authorization checks? images.deregister(context, image_id) return {'imageId': image_id} - @rbac.allow('projectmanager', 'sysadmin') def register_image(self, context, image_location=None, **kwargs): # FIXME: should the objectstore be doing these authorization checks? if image_location is None and kwargs.has_key('name'): @@ -670,7 +642,6 @@ class CloudController(object): return {'imageId': image_id} - @rbac.allow('all') def describe_image_attribute(self, context, image_id, attribute, **kwargs): if attribute != 'launchPermission': raise exception.ApiError('attribute not supported: %s' % attribute) @@ -683,7 +654,6 @@ class CloudController(object): result['launchPermission'].append({'group': 'all'}) return result - @rbac.allow('projectmanager', 'sysadmin') def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): # TODO(devcamcar): Support users and groups other than 'all'. if attribute != 'launchPermission': diff --git a/nova/auth/rbac.py b/nova/auth/rbac.py deleted file mode 100644 index d157f44b3..000000000 --- a/nova/auth/rbac.py +++ /dev/null @@ -1,69 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Role-based access control decorators to use fpr wrapping other -methods with.""" - -from nova import exception - - -def allow(*roles): - """Allow the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - return func(self, context, *args, **kwargs) - raise exception.NotAuthorized() - - return wrapped_func - - return wrap - - -def deny(*roles): - """Deny the given roles access the wrapped function.""" - - def wrap(func): # pylint: disable-msg=C0111 - - def wrapped_func(self, context, *args, - **kwargs): # pylint: disable-msg=C0111 - if context.user.is_superuser(): - return func(self, context, *args, **kwargs) - for role in roles: - if __matches_role(context, role): - raise exception.NotAuthorized() - return func(self, context, *args, **kwargs) - - return wrapped_func - - return wrap - - -def __matches_role(context, role): - """Check if a role is allowed.""" - if role == 'all': - return True - if role == 'none': - return False - return context.project.has_role(context.user.id, role) |
