274 lines
10 KiB
Python
274 lines
10 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
#
|
|
# 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.
|
|
|
|
from swift.common import utils as swift_utils
|
|
from swift.common.middleware import acl as swift_acl
|
|
from swift.common.swob import HTTPNotFound, HTTPForbidden, HTTPUnauthorized
|
|
from swift.common.swob import Request
|
|
from swift.common.utils import register_swift_info
|
|
from enforcer import get_enforcer
|
|
|
|
|
|
class SwiftPolicy(object):
|
|
"""Swift middleware to handle Keystone authorization based
|
|
openstack policy.json format
|
|
|
|
In Swift's proxy-server.conf add this middleware to your pipeline::
|
|
|
|
[pipeline:main]
|
|
pipeline = catch_errors cache authtoken swiftpolicy proxy-server
|
|
|
|
Make sure you have the authtoken middleware before the
|
|
swiftpolicy middleware.
|
|
|
|
The authtoken middleware will take care of validating the user and
|
|
swiftpolicy will authorize access.
|
|
|
|
The authtoken middleware is shipped directly with keystone it
|
|
does not have any other dependences than itself so you can either
|
|
install it by copying the file directly in your python path or by
|
|
installing keystone.
|
|
|
|
If support is required for unvalidated users (as with anonymous
|
|
access) or for formpost/staticweb/tempurl middleware, authtoken will
|
|
need to be configured with ``delay_auth_decision`` set to true. See
|
|
the Keystone documentation for more detail on how to configure the
|
|
authtoken middleware.
|
|
|
|
In proxy-server.conf you will need to have the setting account
|
|
auto creation to true::
|
|
|
|
[app:proxy-server]
|
|
account_autocreate = true
|
|
|
|
And add a swift authorization filter section, such as::
|
|
|
|
[filter:swiftpolicy]
|
|
use = egg:swiftpolicy#swiftpolicy
|
|
operator_roles = admin, swiftoperator
|
|
policy = /path/to/policy.json
|
|
|
|
This maps tenants to account in Swift.
|
|
|
|
The user whose able to give ACL / create Containers permissions
|
|
will be the one that are inside the ``operator_roles``
|
|
setting which by default includes the admin and the swiftoperator
|
|
roles.
|
|
|
|
If you need to have a different reseller_prefix to be able to
|
|
mix different auth servers you can configure the option
|
|
``reseller_prefix`` in your swiftpolicy entry like this::
|
|
|
|
reseller_prefix = NEWAUTH
|
|
|
|
:param app: The next WSGI app in the pipeline
|
|
:param conf: The dict of configuration values
|
|
"""
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
self.logger = swift_utils.get_logger(conf, log_route='swiftpolicy')
|
|
self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_').strip()
|
|
if self.reseller_prefix and self.reseller_prefix[-1] != '_':
|
|
self.reseller_prefix += '_'
|
|
self.operator_roles = conf.get('operator_roles',
|
|
'admin, swiftoperator').lower()
|
|
self.reseller_admin_role = conf.get('reseller_admin_role',
|
|
'ResellerAdmin').lower()
|
|
config_is_admin = conf.get('is_admin', "false").lower()
|
|
self.is_admin = swift_utils.config_true_value(config_is_admin)
|
|
config_overrides = conf.get('allow_overrides', 't').lower()
|
|
self.allow_overrides = swift_utils.config_true_value(config_overrides)
|
|
self.policy_file = conf.get('policy', None)
|
|
|
|
def __call__(self, environ, start_response):
|
|
identity = self._keystone_identity(environ)
|
|
|
|
# Check if one of the middleware like tempurl or formpost have
|
|
# set the swift.authorize_override environ and want to control the
|
|
# authentication
|
|
if (self.allow_overrides and
|
|
environ.get('swift.authorize_override', False)):
|
|
msg = 'Authorizing from an overriding middleware (i.e: tempurl)'
|
|
self.logger.debug(msg)
|
|
return self.app(environ, start_response)
|
|
|
|
if identity:
|
|
self.logger.debug('Using identity: %r', identity)
|
|
environ['keystone.identity'] = identity
|
|
environ['REMOTE_USER'] = identity.get('tenant')
|
|
environ['swift.authorize'] = self.authorize
|
|
# Check reseller_request again poicy
|
|
if self.check_action('reseller_request', environ):
|
|
environ['reseller_request'] = True
|
|
else:
|
|
self.logger.debug('Authorizing as anonymous')
|
|
environ['swift.authorize'] = self.authorize
|
|
|
|
environ['swift.clean_acl'] = swift_acl.clean_acl
|
|
|
|
return self.app(environ, start_response)
|
|
|
|
def _keystone_identity(self, environ):
|
|
"""Extract the identity from the Keystone auth component."""
|
|
# In next release, we would add user id in env['keystone.identity'] by
|
|
# using _integral_keystone_identity to replace current
|
|
# _keystone_identity. The purpose of keeping it in this release it for
|
|
# back compatibility.
|
|
if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed':
|
|
return
|
|
roles = []
|
|
if 'HTTP_X_ROLES' in environ:
|
|
roles = environ['HTTP_X_ROLES'].split(',')
|
|
identity = {'user': environ.get('HTTP_X_USER_NAME'),
|
|
'tenant': (environ.get('HTTP_X_TENANT_ID'),
|
|
environ.get('HTTP_X_TENANT_NAME')),
|
|
'roles': roles}
|
|
return identity
|
|
|
|
def _integral_keystone_identity(self, environ):
|
|
"""Extract the identity from the Keystone auth component."""
|
|
if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed':
|
|
return
|
|
roles = []
|
|
if 'HTTP_X_ROLES' in environ:
|
|
roles = environ['HTTP_X_ROLES'].split(',')
|
|
identity = {'user': (environ.get('HTTP_X_USER_ID'),
|
|
environ.get('HTTP_X_USER_NAME')),
|
|
'tenant': (environ.get('HTTP_X_TENANT_ID'),
|
|
environ.get('HTTP_X_TENANT_NAME')),
|
|
'roles': roles}
|
|
return identity
|
|
|
|
def _get_account_for_tenant(self, tenant_id):
|
|
return '%s%s' % (self.reseller_prefix, tenant_id)
|
|
|
|
def get_creds(self, environ):
|
|
req = Request(environ)
|
|
try:
|
|
parts = req.split_path(1, 4, True)
|
|
_, account, _, _ = parts
|
|
except ValueError:
|
|
account = None
|
|
|
|
env_identity = self._integral_keystone_identity(environ)
|
|
if not env_identity:
|
|
# user identity is not confirmed. (anonymous?)
|
|
creds = {
|
|
'identity': None,
|
|
'is_authoritative': (account and
|
|
account.startswith(self.reseller_prefix))
|
|
}
|
|
return creds
|
|
|
|
tenant_id, tenant_name = env_identity['tenant']
|
|
user_id, user_name = env_identity['user']
|
|
roles = [r.strip() for r in env_identity.get('roles', [])]
|
|
account = self._get_account_for_tenant(tenant_id)
|
|
is_admin = (tenant_name == user_name)
|
|
|
|
creds = {
|
|
"identity": env_identity,
|
|
"roles": roles,
|
|
"account": account,
|
|
"tenant_id": tenant_id,
|
|
"tenant_name": tenant_name,
|
|
"user_id": user_id,
|
|
"user_name": user_name,
|
|
"is_admin": is_admin
|
|
}
|
|
return creds
|
|
|
|
def get_target(self, environ):
|
|
req = Request(environ)
|
|
try:
|
|
parts = req.split_path(1, 4, True)
|
|
version, account, container, obj = parts
|
|
except ValueError:
|
|
version = account = container = obj = None
|
|
|
|
referrers, acls = swift_acl.parse_acl(getattr(req, 'acl', None))
|
|
target = {
|
|
"req": req,
|
|
"method": req.method.lower(),
|
|
"version": version,
|
|
"account": account,
|
|
"container": container,
|
|
"object": obj,
|
|
"acls": acls,
|
|
"referrers": referrers
|
|
}
|
|
return target
|
|
|
|
@staticmethod
|
|
def get_action(method, parts):
|
|
version, account, container, obj = parts
|
|
action = method.lower() + "_"
|
|
if obj:
|
|
action += "object"
|
|
elif container:
|
|
action += "container"
|
|
elif account:
|
|
action += "account"
|
|
|
|
return action
|
|
|
|
def check_action(self, action, environ):
|
|
creds = self.get_creds(environ)
|
|
target = self.get_target(environ)
|
|
enforcer = get_enforcer(self.operator_roles,
|
|
self.reseller_admin_role,
|
|
self.is_admin,
|
|
self.logger,
|
|
self.policy_file)
|
|
self.logger.debug("enforce action '%s'", action)
|
|
return enforcer.enforce(action, target, creds)
|
|
|
|
def authorize(self, req):
|
|
try:
|
|
parts = req.split_path(1, 4, True)
|
|
except ValueError:
|
|
return HTTPNotFound(request=req)
|
|
|
|
env = req.environ
|
|
action = self.get_action(req.method, parts)
|
|
|
|
if self.check_action(action, env):
|
|
if self.check_action("swift_owner", env):
|
|
req.environ['swift_owner'] = True
|
|
return
|
|
return self.denied_response(req)
|
|
|
|
def denied_response(self, req):
|
|
"""Deny WSGI Response.
|
|
|
|
Returns a standard WSGI response callable with the status of 403 or 401
|
|
depending on whether the REMOTE_USER is set or not.
|
|
"""
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
"""Returns a WSGI filter app for use with paste.deploy."""
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
register_swift_info('swiftpolicy')
|
|
|
|
def auth_filter(app):
|
|
return SwiftPolicy(app, conf)
|
|
return auth_filter
|