Implements blueprint keystone-swift-acls.

This basically refactors the swift_auth.py (thanks, Chmouel!) and
token_auth.py so that auth_token.py does purely token authentication,
and swift_auth.py does purely swift authorization and relies on
auth_token.py for validating the token.  This is to avoid duplicate
validate-token logics at different service-layer middleware.

The memcache functionality in Swift used for caching tokens is also
refactored to the token_auth.py, so that other services that may
provide a memcache implementation may benefit from this as well.

The swift_auth.py is also enhanced to include support for explicit
user access in Swift ACL, similar to tempauth.

Change-Id: Ifc0287898658e8d576dfeaafefdc5340cd321577
This commit is contained in:
Liem Nguyen 2011-11-29 10:49:52 -08:00
parent 5b72249d1a
commit 4f69fbaa8b
3 changed files with 107 additions and 117 deletions

View File

@ -282,6 +282,7 @@ rather than it's built in 'tempauth'.
service_host = 127.0.0.1
service_port = 8100
service_pass = dTpw
cache = swift.cache
[filter:cache]
use = egg:swift#memcache
@ -290,6 +291,12 @@ rather than it's built in 'tempauth'.
[filter:catch_errors]
use = egg:swift#catch_errors
Note that the optional "cache" property in the keystone filter allows any
service (not just Swift) to register its memcache client in the WSGI
environment. If such a cache exists, Keystone middleware will utilize it
to store validated token information, which could result in better overall
performance.
4. Restart swift
5. Verify that keystone is providing authentication to Swift

View File

@ -105,7 +105,8 @@ from urlparse import urlparse
from webob.exc import HTTPUnauthorized
from webob.exc import Request, Response
import keystone.tools.tracer # @UnusedImport # module runs on import
from time import time, mktime
from datetime import datetime
from keystone.common.bufferedhttp import http_connect_raw as http_connect
@ -170,6 +171,8 @@ class AuthProtocol(object):
# server
self.cert_file = conf.get('certfile', None)
self.key_file = conf.get('keyfile', None)
# Caching
self.cache = conf.get('cache', None)
def __init__(self, app, conf):
""" Common initialization code """
@ -204,8 +207,8 @@ class AuthProtocol(object):
del proxy_headers[header]
#Look for authentication claims
claims = self._get_claims(env)
if not claims:
token = self._get_claims(env)
if not token:
#No claim(s) provided
if self.delay_auth_decision:
#Configured to allow downstream service to make final decision.
@ -216,9 +219,12 @@ class AuthProtocol(object):
#Respond to client as appropriate for this auth protocol
return self._reject_request(env, start_response)
else:
# Use cache if available
cached_claims = self._cache_get(env, token)
# this request is presenting claims. Let's validate them
try:
claims = self._verify_claims(claims)
claims = cached_claims or self._verify_claims(token)
except ValidationFailed:
# Keystone rejected claim
if self.delay_auth_decision:
@ -234,6 +240,10 @@ class AuthProtocol(object):
# Store authentication data
if claims:
# Cache it if there is a cache available
if (self.cache and not cached_claims):
self._cache_put(env, token, claims)
self._decorate_request('X_AUTHORIZATION', "Proxy %s" %
claims['user'], env, proxy_headers)
@ -266,6 +276,48 @@ class AuthProtocol(object):
#Send request downstream
return self._forward_request(env, start_response, proxy_headers)
def _convert_date(self, date):
""" Convert datetime to unix timestamp for caching """
return mktime(datetime.strptime(
date[:date.rfind(':')].replace('-', ''), "%Y%m%dT%H:%M",
).timetuple())
def _protect_claims(self, token, claims):
""" encrypt or mac claims if necessary """
return claims
def _unprotect_claims(self, token, pclaims):
""" decrypt or demac claims if necessary """
return pclaims
def _cache_put(self, env, token, claims):
""" Put a claim into the cache """
memCache = self._cache(env)
if (memCache and claims):
key = 'tokens/%s' % (token)
expires = self._convert_date(claims['expires'])
claims = self._protect_claims(token, claims)
memCache.set(key, (expires, claims), timeout=expires - time())
def _cache_get(self, env, token):
""" Return a claim from cache, also validate expiration """
memCache = self._cache(env)
if (memCache):
key = 'tokens/%s' % (token)
cached_claims = memCache.get(key)
if (cached_claims):
expires, claims = cached_claims
if expires > time():
claims = self._unprotect_claims(token, claims)
return claims
return None
def _cache(self, env):
""" Return a cache to use for token caching, or none """
if (self.cache != None):
return env.get(self.cache, None)
return None
def _get_claims(self, env):
"""Get claims from request"""
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
@ -342,7 +394,8 @@ class AuthProtocol(object):
'id': tenant_id,
'name': tenant_name
},
'roles': roles}
'roles': roles,
'expires': token_info['access']['token']['expires']}
return verified_claims

View File

@ -13,46 +13,41 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from webob.exc import HTTPForbidden, HTTPNotFound, HTTPUnauthorized
from urllib import quote
from urlparse import urlparse
from webob.exc import HTTPForbidden, HTTPNotFound, \
HTTPUnauthorized, HTTPBadRequest
from webob import Request
from swift.common.utils import cache_from_env, get_logger, split_path, \
get_remote_client
from swift.common.utils import get_logger, split_path, get_remote_client
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.bufferedhttp import http_connect_raw as http_connect
from time import time, mktime
from datetime import datetime
class AuthProtocol(object):
class SwiftAuth(object):
"""
Keystone to Swift authentication and authorization system.
Keystone to Swift authorization system.
Add to your pipeline in proxy-server.conf, such as::
[pipeline:main]
pipeline = catch_errors cache keystone proxy-server
pipeline = catch_errors cache tokenauth swiftauth proxy-server
Set account auto creation to true::
[app:proxy-server]
account_autocreate = true
And add a keystone filter section, such as::
And add a swift authorization filter section, such as::
[filter:keystone]
[filter:swiftauth]
use = egg:keystone#swiftauth
keystone_url = http://keystone_url:5000/v2.0
keystone_admin_token = admin_token
keystone_swift_operator_roles = Admin, SwiftOperator
keystone_tenant_user_admin = true
If Swift memcache is to be used for caching tokens, add the additional
property in the tokenauth filter:
[filter:tokenauth]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
...
cache = swift.cache
This maps tenants to account in Swift.
The user whose able to give ACL / create Containers permissions
@ -75,11 +70,8 @@ class AuthProtocol(object):
self.conf = conf
self.logger = get_logger(conf, log_route='keystone')
self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
#TODO: Error out if no url
self.keystone_url = urlparse(conf.get('keystone_url'))
self.keystone_swift_operator_roles = \
conf.get('keystone_swift_operator_roles', 'Admin, SwiftOperator')
self.admin_token = conf.get('keystone_admin_token')
self.keystone_tenant_user_admin = \
conf.get('keystone_tenant_user_admin', "false").lower() in \
('true', 't', '1', 'on', 'yes', 'y')
@ -89,41 +81,7 @@ class AuthProtocol(object):
def __call__(self, environ, start_response):
self.logger.debug('Initialise keystone middleware')
req = Request(environ)
token = environ.get('HTTP_X_AUTH_TOKEN',
environ.get('HTTP_X_STORAGE_TOKEN'))
if not token:
self.logger.debug('No token: exiting')
environ['swift.authorize'] = self.denied_response
return self.app(environ, start_response)
self.logger.debug('Got token: %s' % (token))
identity = None
memcache_client = cache_from_env(environ)
memcache_key = 'tokens/%s' % (token)
candidate_cache = memcache_client.get(memcache_key)
if candidate_cache:
expires, _identity = candidate_cache
if expires > time():
self.logger.debug('getting identity info from memcache')
identity = _identity
if not identity:
self.logger.debug("No memcache, requesting it from keystone")
identity = self._keystone_validate_token(token)
if identity and memcache_client:
expires = identity['expires']
memcache_client.set(memcache_key,
(expires, identity),
timeout=expires - time())
ts = str(datetime.fromtimestamp(expires))
self.logger.debug('setting memcache expiration to %s' % ts)
else: # if we didn't get identity it means there was an error.
return HTTPBadRequest(request=req)
self.logger.debug("Using identity: %r" % (identity))
identity = self._keystone_identity(environ)
if not identity:
#TODO: non authenticated access allow via refer
@ -137,57 +95,23 @@ class AuthProtocol(object):
environ['swift.clean_acl'] = clean_acl
return self.app(environ, start_response)
def convert_date(self, date):
""" Convert datetime to unix timestamp """
return mktime(datetime.strptime(
date[:date.rfind(':')].replace('-', ''), "%Y%m%dT%H:%M",
).timetuple())
def _keystone_validate_token(self, claim):
"""
Will take a claimed token and validate it in keystone.
"""
headers = {"X-Auth-Token": self.admin_token}
conn = http_connect(self.keystone_url.hostname,
self.keystone_url.port, 'GET',
'%s/tokens/%s' % \
(self.keystone_url.path,
quote(claim)),
headers=headers,
ssl=(self.keystone_url.scheme == 'https'))
resp = conn.getresponse()
data = resp.read()
conn.close()
self.logger.debug("Keystone came back with: status:%d, data:%s" % \
(resp.status, data))
if not str(resp.status).startswith('20'):
#TODO: Make the self.keystone_url more meaningfull
raise Exception('Error: Keystone : %s Returned: %d' % \
(self.keystone_url, resp.status))
identity_info = json.loads(data)
try:
tenant = (identity_info['access']['token']['tenant']['id'],
identity_info['access']['token']['tenant']['name'])
expires = self.convert_date(
identity_info['access']['token']['expires'])
user = 'username' in identity_info['access']['user'] and \
identity_info['access']['user']['username'] or \
identity_info['access']['user']['name']
roles = [x['name'] for x in \
identity_info['access']['user']['roles']]
except (KeyError, IndexError):
raise
identity = {'user': user,
'tenant': tenant,
'roles': roles,
'expires': expires,
}
def _keystone_identity(self, environ):
""" Extract the identity from the Keystone auth component """
if (environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed'):
return None
roles = []
if ('HTTP_X_ROLES' in environ):
roles = environ.get('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 _reseller_check(self, account, tenant_id):
""" Check reseller prefix """
return account == '%s_%s' % (self.reseller_prefix, tenant_id)
def authorize(self, req):
env = req.environ
env_identity = env.get('keystone.identity', {})
@ -198,12 +122,13 @@ class AuthProtocol(object):
except ValueError:
return HTTPNotFound(request=req)
if account != '%s_%s' % (self.reseller_prefix, tenant[0]):
if not self._reseller_check(account, tenant[0]):
self.logger.debug('tenant mismatch')
return self.denied_response(req)
# If user is in the swift operator group then make the owner of it.
user_groups = env_identity.get('roles', [])
# If user is in the swift operator group then make the owner of it.
for _group in self.keystone_swift_operator_roles.split(','):
_group = _group.strip()
if _group in user_groups:
@ -240,11 +165,16 @@ class AuthProtocol(object):
return None
return self.denied_response(req)
# Allow ACL at individual user level (tenant:user format)
if '%s:%s' % (tenant[0], user) in groups:
self.logger.debug('user explicitly allowed in ACL authorizing')
return None
# Check if we have the group in the usergroups and allow it
for user_group in user_groups:
if user_group in groups:
self.logger.debug('user in group which is allowed in" \
" ACL: %s authorizing' % (user_group))
self.logger.debug('user in group which is allowed in' \
' ACL: %s authorizing' % (user_group))
return None
# last but not least retun deny
@ -267,5 +197,5 @@ def filter_factory(global_conf, **local_conf):
conf.update(local_conf)
def auth_filter(app):
return AuthProtocol(app, conf)
return SwiftAuth(app, conf)
return auth_filter