如何想用使用 Swift的服务,都需要经过认证鉴权,例如,某用户想上传一个文件X,首先该用户需要有权限进入到系统中,然后他需要有可以上传文件的权限,早期版本Swift有自己的实现认证鉴权的程序tempauth,在/swift/common/middlleware/下你可以找到这个python文件,但是后期,openstack退出了自己的认证鉴权的模块keystone,提供统一的接口,这样服务向外暴露的只是认证鉴权的url。通常我们使用keystone作为 swift鉴权模块,因为它功能强大,同时可以支持其他服务。
之前的文章中,我分析过请求的流程,一个请求->auth_token->swift_auth->handler_request,其中 auth_token,swift_auth 分别在keystone/middleware/ 中,auth_token作为通用的模块,提供给nova,glance,swift等等组件的服务,同时也可以支持亚马逊的API ,swift_auth,是针对swift鉴权的模块,在最新的版本中, swift_auth已经集成到了swift中,命名为keystoneauth.py 其实它们基本上就是一个文件。
值得注意的是,swift需要启动auth_token服务,也就是keystone的代码,所以,通常swift proxy和keystone会安装在同一台机器上,如果不安装在一台机器上,则需要在安装swift proxy 的机器上安装auth_token及相关的代码。
我本身没有研究 keystone这个项目,所以对一些东西也不是很熟悉,但是keystone确实是一个功能很强大的鉴权模块,比如支持ssl,acl(防盗链)等,我咨询过源码贡献者,所以下面的源码分析有一定的借鉴意义。
下面是源码分析:
def __call__(self, env, start_response):
"""Handle incoming request.
Authenticate send downstream on success. Reject request if
we can't authenticate.
"""
LOG.debug('Authenticating user token')
try:
self._remove_auth_headers(env)#在已有的env环境中中,删除token相关的headers。
user_token = self._get_user_token_from_header(env)#从header中获取user_token
token_info = self._validate_user_token(user_token)#验证user_token
user_headers = self._build_user_headers(token_info)#制作token的headers
self._add_headers(env, user_headers)#把新生成的headers添加到env中,
return self.app(env, start_response)
except InvalidUserToken:
if self.delay_auth_decision:
LOG.info('Invalid user token - deferring reject downstream')
self._add_headers(env, {'X-Identity-Status': 'Invalid'})
return self.app(env, start_response)
else:
LOG.info('Invalid user token - rejecting request')
return self._reject_request(env, start_response)
except ServiceError, e:
LOG.critical('Unable to obtain admin token: %s' % e)
resp = webob.exc.HTTPServiceUnavailable()
return resp(env, start_response)
在env中 删除token相关的header
def _remove_auth_headers(self, env):
"""Remove headers so a user can't fake authentication.#删除headers这样一个用户就不能伪装认证
:param env: wsgi request environment
"""
auth_headers = (#把这些头转化成env相应的格式 然后删除它们,保证env没有与identity相关的数据
'X-Identity-Status',
'X-Tenant-Id',
'X-Tenant-Name',
'X-User-Id',
'X-User-Name',
'X-Roles',
'X-Service-Catalog',
# Deprecated
'X-User',
'X-Tenant',
'X-Role',
)
LOG.debug('Removing headers from request environment: %s' %
','.join(auth_headers))
self._remove_headers(env, auth_headers)
从env中获取user_token
def _get_user_token_from_header(self, env):
"""Get token id from request.
:param env: wsgi request environment
:return token id
:raises InvalidUserToken if no token is provided in request
"""
token = self._get_header(env, 'X-Auth-Token',#获取token
self._get_header(env, 'X-Storage-Token'))
if token:
return token
else:
LOG.warn("Unable to find authentication token in headers: %s", env)
raise InvalidUserToken('Unable to find token in headers')
验证user_token流程
def _validate_user_token(self, user_token, retry=True):
"""Authenticate user using PKI
:param user_token: user's token id
:param retry: Ignored, as it is not longer relevant
:return uncrypted body of the token if the token is valid
:raise InvalidUserToken if token is rejected
:no longer raises ServiceError since it no longer makes RPC
"""
try:
cached = self._cache_get(user_token)#在缓存中查找user_token
if cached:#如果缓存缓存中存在,说明已经在鉴权有效期内
return cached
if (len(user_token) > cms.UUID_TOKEN_LENGTH):#如果大于32位
verified = self.verify_signed_token(user_token)#ssl功能
data = json.loads(verified)
else:#如果不大于32位—》去keystone鉴权
data = self.verify_uuid_token(user_token, retry)
self._cache_put(user_token, data)#放到缓存中,此处有bug,因为跳出程序后还会进行缓存。
return data
except Exception as e:
LOG.debug('Token validation failure.', exc_info=True)
self._cache_store_invalid(user_token)
LOG.warn("Authorization failed for token %s", user_token)
raise InvalidUserToken('Token authorization failed')
def verify_uuid_token(self, user_token, retry=True):
"""Authenticate user token with keystone.
:param user_token: user's token id
:param retry: flag that forces the middleware to retry
user authentication when an indeterminate
response is received. Optional.
:return token object received from keystone on success
:raise InvalidUserToken if token is rejected
:raise ServiceError if unable to authenticate token
"""
headers = {'X-Auth-Token': self.get_admin_token()}#在proxy-server.conf配置文件中,配置的admin_token
response, data = self._json_request('GET',#发送请求到keystone,得到响应,和响应data
'/v2.0/tokens/%s' % user_token,
additional_headers=headers)
if response.status == 200:#200表示成功,缓存数据,然后返回。
self._cache_put(user_token, data)
return data
if response.status == 404:
# FIXME(ja): I'm assuming the 404 status means that user_token is
# invalid - not that the admin_token is invalid
self._cache_store_invalid(user_token)#user_token已经无效
LOG.warn("Authorization failed for token %s", user_token)
raise InvalidUserToken('Token authorization failed')
if response.status == 401:
LOG.info('Keystone rejected admin token %s, resetting', headers)
self.admin_token = None
else:
LOG.error('Bad response code while validating token: %s' %
response.status)
if retry:
LOG.info('Retrying validation')
return self._validate_user_token(user_token, False)
else:
LOG.warn("Invalid user token: %s. Keystone response: %s.",
user_token, data)
raise InvalidUserToken()
转换token对象到header 中
def _build_user_headers(self, token_info):
"""Convert token object into headers.
Build headers that represent authenticated user:
* X_IDENTITY_STATUS: Confirmed or Invalid
* X_TENANT_ID: id of tenant if tenant is present
* X_TENANT_NAME: name of tenant if tenant is present
* X_USER_ID: id of user
* X_USER_NAME: name of user
* X_ROLES: list of roles
* X_SERVICE_CATALOG: service catalog
Additional (deprecated) headers include:
* X_USER: name of user
* X_TENANT: For legacy compatibility before we had ID and Name
* X_ROLE: list of roles
:param token_info: token object returned by keystone on authentication
:raise InvalidUserToken when unable to parse token object
"""
user = token_info['access']['user']#根据token_info,获取user,token,roles
token = token_info['access']['token']
roles = ','.join([role['name'] for role in user.get('roles', [])])
def get_tenant_info():
"""Returns a (tenant_id, tenant_name) tuple from context."""
def essex():
"""Essex puts the tenant ID and name on the token."""
return (token['tenant']['id'], token['tenant']['name'])
def pre_diablo():
"""Pre-diablo, Keystone only provided tenantId."""
return (token['tenantId'], token['tenantId'])
def default_tenant():
"""Assume the user's default tenant."""
return (user['tenantId'], user['tenantName'])
for method in [essex, pre_diablo, default_tenant]:
try:
return method()
except KeyError:
pass
raise InvalidUserToken('Unable to determine tenancy.')
tenant_id, tenant_name = get_tenant_info()#获取tenant_id,tenant_name
user_id = user['id']
user_name = user['name']
rval = {#生成一个关于Identity信息的字典。
'X-Identity-Status': 'Confirmed',
'X-Tenant-Id': tenant_id,
'X-Tenant-Name': tenant_name,
'X-User-Id': user_id,
'X-User-Name': user_name,
'X-Roles': roles,
# Deprecated
'X-User': user_name,
'X-Tenant': tenant_name,
'X-Role': roles,
}
try:
catalog = token_info['access']['serviceCatalog']
rval['X-Service-Catalog'] = jsonutils.dumps(catalog)#catalog使用json格式
except KeyError:
pass
return rval
#然后把生成的user_token 添加到env中,
这样鉴权的过程就结束了,下一步是swift_auth.py/keystoneauth.py
我们在handle_request中会发现程序在执行最终操作之前,才调用鉴权函数,这是因为,如果有权限,我们就使出这个钩子(鉴权),这样之后的就不需要鉴权了。
def __call__(self, environ, start_response):
identity = self._keystone_identity(environ)#先从环境中获取identity信息
# 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#如果使用其他的中间件设置了swift.authorize
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#设置句柄
else:
self.logger.debug('Authorizing as anonymous')
environ['swift.authorize'] = self.authorize_anonymous
environ['swift.clean_acl'] = swift_acl.clean_acl#防盗链功能实现
return self.app(environ, start_response)
主要的authorize
def authorize(self, req):
env = req.environ
env_identity = env.get('keystone.identity', {})
tenant_id, tenant_name = env_identity.get('tenant')
try:
part = swift_utils.split_path(req.path, 1, 4, True)
version, account, container, obj = part
except ValueError:
return webob.exc.HTTPNotFound(request=req)
user_roles = env_identity.get('roles', [])
# Give unconditional access to a user with the reseller_admin
# role.
if self.reseller_admin_role in user_roles:#如果有reseller_admin_role返回空,意味着有权限
msg = 'User %s has reseller admin authorizing'
self.logger.debug(msg % tenant_id)
req.environ['swift_owner'] = True
return
# Check if a user tries to access an account that does not match their
# token#检查是否用户没有访问的account的权限
if not self._reseller_check(account, tenant_id):
log_msg = 'tenant mismatch: %s != %s' % (account, tenant_id)
self.logger.debug(log_msg)
return self.denied_response(req)
# Check the roles the user is belonging to. If the user is
# part of the role defined in the config variable
# operator_roles (like admin) then it will be
# promoted as an admin of the account/tenant.
for role in self.operator_roles.split(','):#如果是admin,或者是swiftoperator权限,返回空
role = role.strip()
if role in user_roles:
log_msg = 'allow user with role %s as account admin' % (role)
self.logger.debug(log_msg)
req.environ['swift_owner'] = True
return
# If user is of the same name of the tenant then make owner of it.
user = env_identity.get('user', '')
if self.is_admin and user == tenant_name:
req.environ['swift_owner'] = True
return