如何想用使用 Swift的服務,都須要通過認證鑑權,例如,某用戶想上傳一個文件X,首先該用戶須要有權限進入到系統中,而後他須要有能夠上傳文件的權限,早期版本Swift有本身的實現認證鑑權的程序tempauth,在/swift/common/middlleware/下你能夠找到這個python文件,可是後期,openstack退出了本身的認證鑑權的模塊keystone,提供統一的接口,這樣服務向外暴露的只是認證鑑權的url。一般咱們使用keystone做爲 swift鑑權模塊,由於它功能強大,同時能夠支持其餘服務。 python
以前的文章中,我分析過請求的流程,一個請求->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 其實它們基本上就是一個文件。 web
值得注意的是,swift須要啓動auth_token服務,也就是keystone的代碼,因此,一般swift proxy和keystone會安裝在同一臺機器上,若是不安裝在一臺機器上,則須要在安裝swift proxy 的機器上安裝auth_token及相關的代碼。 json
我自己沒有研究 keystone這個項目,因此對一些東西也不是很熟悉,可是keystone確實是一個功能很強大的鑑權模塊,好比支持ssl,acl(防盜鏈)等,我諮詢過源碼貢獻者,因此下面的源碼分析有必定的借鑑意義。 swift
下面是源碼分析: 緩存
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 app
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 ide
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 post
咱們在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