Guest User

swift_auth.py

a guest
Feb 14th, 2012
124
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.14 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # vim: tabstop=4 shiftwidth=4 softtabstop=4
  3. #
  4. # Copyright (c) 2010-2011 OpenStack, LLC.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  15. # implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18.  
  19.  
  20. """
  21. TOKEN-BASED AUTH MIDDLEWARE FOR SWIFT
  22.  
  23. Authentication on incoming request
  24. * grab token from X-Auth-Token header
  25. * TODO: grab the memcache servers from the request env
  26. * TODOcheck for auth information in memcache
  27. * check for auth information from keystone
  28. * return if unauthorized
  29. * decorate the request for authorization in swift
  30. * forward to the swift proxy app
  31.  
  32. Authorization via callback
  33. * check the path and extract the tenant
  34. * get the auth information stored in keystone.identity during
  35. authentication
  36. * TODO: check if the user is an account admin or a reseller admin
  37. * determine what object-type to authorize (account, container, object)
  38. * use knowledge of tenant, admin status, and container acls to authorize
  39.  
  40. """
  41.  
  42. import json
  43. from urlparse import urlparse
  44. from webob.exc import HTTPUnauthorized, HTTPNotFound, HTTPExpectationFailed
  45.  
  46. from keystone.common.bufferedhttp import http_connect_raw as http_connect
  47.  
  48. from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
  49. from swift.common.utils import cache_from_env, get_logger, split_path
  50.  
  51.  
  52. PROTOCOL_NAME = "Swift Token Authentication"
  53.  
  54.  
  55. class AuthProtocol(object):
  56. """Handles authenticating and aurothrizing client calls.
  57.  
  58. Add to your pipeline in paste config like:
  59.  
  60. [pipeline:main]
  61. pipeline = catch_errors healthcheck cache keystone proxy-server
  62.  
  63. [filter:keystone]
  64. use = egg:keystone#swiftauth
  65. keystone_url = http://127.0.0.1:8080
  66. keystone_admin_token = 999888777666
  67. """
  68.  
  69. def __init__(self, app, conf):
  70. """Store valuable bits from the conf and set up logging."""
  71. self.app = app
  72. self.keystone_url = urlparse(conf.get('keystone_url'))
  73. self.admin_token = conf.get('keystone_admin_token')
  74. self.reseller_prefix = conf.get('reseller_prefix', 'AUTH')
  75. self.log = get_logger(conf, log_route='keystone')
  76. self.log.info('Keystone middleware started')
  77.  
  78. def __call__(self, env, start_response):
  79. """Authenticate the incoming request.
  80.  
  81. If authentication fails return an appropriate http status here,
  82. otherwise forward through the rest of the app.
  83. """
  84.  
  85. self.log.debug('Keystone middleware called')
  86. token = self._get_claims(env)
  87. self.log.debug('token: %s', token)
  88. if token:
  89. identity = self._validate_claims(token)
  90. if identity:
  91. self.log.debug('request authenticated: %r', identity)
  92. return self.perform_authenticated_request(identity, env,
  93. start_response)
  94. else:
  95. self.log.debug('anonymous request')
  96. return self.unauthorized_request(env, start_response)
  97. self.log.debug('no auth token in request headers')
  98. return self.perform_unidentified_request(env, start_response)
  99.  
  100. def unauthorized_request(self, env, start_response):
  101. """Clinet provided a token that wasn't acceptable, error out."""
  102. return HTTPUnauthorized()(env, start_response)
  103.  
  104. def unauthorized(self, req):
  105. """Return unauthorized given a webob Request object.
  106.  
  107. This can be stuffed into the evironment for swift.authorize or
  108. called from the authoriztion callback when authorization fails.
  109. """
  110. return HTTPUnauthorized(request=req)
  111.  
  112. def perform_authenticated_request(self, identity, env, start_response):
  113. """Client provieded a valid identity, so use it for authorization."""
  114. env['keystone.identity'] = identity
  115. env['swift.authorize'] = self.authorize
  116. env['swift.clean_acl'] = clean_acl
  117. self.log.debug('calling app: %s // %r', start_response, env)
  118. rv = self.app(env, start_response)
  119. self.log.debug('return from app: %r', rv)
  120. return rv
  121.  
  122. def perform_unidentified_request(self, env, start_response):
  123. """Withouth authentication data, use acls for access control."""
  124. env['swift.authorize'] = self.authorize_via_acl
  125. env['swift.clean_acl'] = self.authorize_via_acl
  126. return self.app(env, start_response)
  127.  
  128. def authorize(self, req):
  129. """Used when we have a valid identity from keystone."""
  130. self.log.debug('keystone middleware authorization begin')
  131. env = req.environ
  132. tenant = env.get('keystone.identity', {}).get('tenant')
  133. if not tenant:
  134. self.log.warn('identity info not present in authorize request')
  135. return HTTPExpectationFailed('Unable to locate auth claim',
  136. request=req)
  137. # TODO(todd): everyone under a tenant can do anything to that tenant.
  138. # more realistic would be role/group checking to do things
  139. # like deleting the account or creating/deleting containers
  140. # esp. when owned by other users in the same tenant.
  141. if req.path.startswith('/v1/%s_%s' % (self.reseller_prefix, tenant)):
  142. self.log.debug('AUTHORIZED OKAY')
  143. return None
  144.  
  145. self.log.debug('tenant mismatch: %r', tenant)
  146. return self.unauthorized(req)
  147.  
  148. def authorize_via_acl(self, req):
  149. """Anon request handling.
  150.  
  151. For now this only allows anon read of objects. Container and account
  152. actions are prohibited.
  153. """
  154.  
  155. self.log.debug('authorizing anonymous request')
  156. try:
  157. version, account, container, obj = split_path(req.path, 1, 4, True)
  158. except ValueError:
  159. return HTTPNotFound(request=req)
  160.  
  161. if obj:
  162. return self._authorize_anon_object(req, account, container, obj)
  163.  
  164. if container:
  165. return self._authorize_anon_container(req, account, container)
  166.  
  167. if account:
  168. return self._authorize_anon_account(req, account)
  169.  
  170. return self._authorize_anon_toplevel(req)
  171.  
  172. def _authorize_anon_object(self, req, account, container, obj):
  173. referrers, groups = parse_acl(getattr(req, 'acl', None))
  174. if referrer_allowed(req.referer, referrers):
  175. self.log.debug('anonymous request AUTHORIZED OKAY')
  176. return None
  177. return self.unauthorized(req)
  178.  
  179. def _authorize_anon_container(self, req, account, container):
  180. return self.unauthorized(req)
  181.  
  182. def _authorize_anon_account(self, req, account):
  183. return self.unauthorized(req)
  184.  
  185. def _authorize_anon_toplevel(self, req):
  186. return self.unauthorized(req)
  187.  
  188. def _get_claims(self, env):
  189. claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
  190. return claims
  191.  
  192. def _validate_claims(self, claims):
  193. """Ask keystone (as keystone admin) for information for this user."""
  194.  
  195. # TODO(todd): cache
  196.  
  197. self.log.debug('Asking keystone to validate token')
  198. headers = {"Content-type": "application/json",
  199. "Accept": "text/json",
  200. "X-Auth-Token": self.admin_token}
  201. self.log.debug('headers: %r', headers)
  202. self.log.debug('url: %s', self.keystone_url)
  203. conn = http_connect(self.keystone_url.hostname, self.keystone_url.port,
  204. 'GET', '/v2.0/tokens/%s' % claims, headers=headers)
  205. resp = conn.getresponse()
  206. data = resp.read()
  207. conn.close()
  208.  
  209. # Check http status code for the "OK" family of responses
  210. if not str(resp.status).startswith('20'):
  211. return False
  212.  
  213. identity_info = json.loads(data)
  214. roles = []
  215. role_refs = identity_info["auth"]["user"]["roleRefs"]
  216. if role_refs is not None:
  217. for role_ref in role_refs:
  218. roles.append(role_ref["roleId"])
  219.  
  220. try:
  221. tenant = identity_info['auth']['token']['tenantId']
  222. except:
  223. tenant = None
  224. if not tenant:
  225. tenant = identity_info['auth']['user']['tenantId']
  226. # TODO(Ziad): add groups back in
  227. identity = {'user': identity_info['auth']['user']['username'],
  228. 'tenant': tenant,
  229. 'roles': roles}
  230.  
  231. return identity
  232.  
  233.  
  234. def filter_factory(global_conf, **local_conf):
  235. """Returns a WSGI filter app for use with paste.deploy."""
  236. conf = global_conf.copy()
  237. conf.update(local_conf)
  238.  
  239. def auth_filter(app):
  240. return AuthProtocol(app, conf)
  241.  
  242. return auth_filter
Advertisement
Add Comment
Please, Sign In to add comment