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:
parent
5b72249d1a
commit
4f69fbaa8b
|
@ -282,6 +282,7 @@ rather than it's built in 'tempauth'.
|
||||||
service_host = 127.0.0.1
|
service_host = 127.0.0.1
|
||||||
service_port = 8100
|
service_port = 8100
|
||||||
service_pass = dTpw
|
service_pass = dTpw
|
||||||
|
cache = swift.cache
|
||||||
|
|
||||||
[filter:cache]
|
[filter:cache]
|
||||||
use = egg:swift#memcache
|
use = egg:swift#memcache
|
||||||
|
@ -290,6 +291,12 @@ rather than it's built in 'tempauth'.
|
||||||
[filter:catch_errors]
|
[filter:catch_errors]
|
||||||
use = egg:swift#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
|
4. Restart swift
|
||||||
|
|
||||||
5. Verify that keystone is providing authentication to Swift
|
5. Verify that keystone is providing authentication to Swift
|
||||||
|
|
|
@ -105,7 +105,8 @@ from urlparse import urlparse
|
||||||
from webob.exc import HTTPUnauthorized
|
from webob.exc import HTTPUnauthorized
|
||||||
from webob.exc import Request, Response
|
from webob.exc import Request, Response
|
||||||
import keystone.tools.tracer # @UnusedImport # module runs on import
|
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
|
from keystone.common.bufferedhttp import http_connect_raw as http_connect
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,6 +171,8 @@ class AuthProtocol(object):
|
||||||
# server
|
# server
|
||||||
self.cert_file = conf.get('certfile', None)
|
self.cert_file = conf.get('certfile', None)
|
||||||
self.key_file = conf.get('keyfile', None)
|
self.key_file = conf.get('keyfile', None)
|
||||||
|
# Caching
|
||||||
|
self.cache = conf.get('cache', None)
|
||||||
|
|
||||||
def __init__(self, app, conf):
|
def __init__(self, app, conf):
|
||||||
""" Common initialization code """
|
""" Common initialization code """
|
||||||
|
@ -204,8 +207,8 @@ class AuthProtocol(object):
|
||||||
del proxy_headers[header]
|
del proxy_headers[header]
|
||||||
|
|
||||||
#Look for authentication claims
|
#Look for authentication claims
|
||||||
claims = self._get_claims(env)
|
token = self._get_claims(env)
|
||||||
if not claims:
|
if not token:
|
||||||
#No claim(s) provided
|
#No claim(s) provided
|
||||||
if self.delay_auth_decision:
|
if self.delay_auth_decision:
|
||||||
#Configured to allow downstream service to make final 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
|
#Respond to client as appropriate for this auth protocol
|
||||||
return self._reject_request(env, start_response)
|
return self._reject_request(env, start_response)
|
||||||
else:
|
else:
|
||||||
|
# Use cache if available
|
||||||
|
cached_claims = self._cache_get(env, token)
|
||||||
|
|
||||||
# this request is presenting claims. Let's validate them
|
# this request is presenting claims. Let's validate them
|
||||||
try:
|
try:
|
||||||
claims = self._verify_claims(claims)
|
claims = cached_claims or self._verify_claims(token)
|
||||||
except ValidationFailed:
|
except ValidationFailed:
|
||||||
# Keystone rejected claim
|
# Keystone rejected claim
|
||||||
if self.delay_auth_decision:
|
if self.delay_auth_decision:
|
||||||
|
@ -234,6 +240,10 @@ class AuthProtocol(object):
|
||||||
|
|
||||||
# Store authentication data
|
# Store authentication data
|
||||||
if claims:
|
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" %
|
self._decorate_request('X_AUTHORIZATION', "Proxy %s" %
|
||||||
claims['user'], env, proxy_headers)
|
claims['user'], env, proxy_headers)
|
||||||
|
|
||||||
|
@ -266,6 +276,48 @@ class AuthProtocol(object):
|
||||||
#Send request downstream
|
#Send request downstream
|
||||||
return self._forward_request(env, start_response, proxy_headers)
|
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):
|
def _get_claims(self, env):
|
||||||
"""Get claims from request"""
|
"""Get claims from request"""
|
||||||
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
|
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
|
||||||
|
@ -342,7 +394,8 @@ class AuthProtocol(object):
|
||||||
'id': tenant_id,
|
'id': tenant_id,
|
||||||
'name': tenant_name
|
'name': tenant_name
|
||||||
},
|
},
|
||||||
'roles': roles}
|
'roles': roles,
|
||||||
|
'expires': token_info['access']['token']['expires']}
|
||||||
|
|
||||||
return verified_claims
|
return verified_claims
|
||||||
|
|
||||||
|
|
|
@ -13,46 +13,41 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import json
|
from webob.exc import HTTPForbidden, HTTPNotFound, HTTPUnauthorized
|
||||||
|
|
||||||
from urllib import quote
|
from swift.common.utils import get_logger, split_path, get_remote_client
|
||||||
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.middleware.acl import clean_acl, parse_acl, referrer_allowed
|
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::
|
Add to your pipeline in proxy-server.conf, such as::
|
||||||
|
|
||||||
[pipeline:main]
|
[pipeline:main]
|
||||||
pipeline = catch_errors cache keystone proxy-server
|
pipeline = catch_errors cache tokenauth swiftauth proxy-server
|
||||||
|
|
||||||
Set account auto creation to true::
|
Set account auto creation to true::
|
||||||
|
|
||||||
[app:proxy-server]
|
[app:proxy-server]
|
||||||
account_autocreate = true
|
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
|
use = egg:keystone#swiftauth
|
||||||
keystone_url = http://keystone_url:5000/v2.0
|
|
||||||
keystone_admin_token = admin_token
|
|
||||||
keystone_swift_operator_roles = Admin, SwiftOperator
|
keystone_swift_operator_roles = Admin, SwiftOperator
|
||||||
keystone_tenant_user_admin = true
|
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.
|
This maps tenants to account in Swift.
|
||||||
|
|
||||||
The user whose able to give ACL / create Containers permissions
|
The user whose able to give ACL / create Containers permissions
|
||||||
|
@ -75,11 +70,8 @@ class AuthProtocol(object):
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.logger = get_logger(conf, log_route='keystone')
|
self.logger = get_logger(conf, log_route='keystone')
|
||||||
self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
|
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 = \
|
self.keystone_swift_operator_roles = \
|
||||||
conf.get('keystone_swift_operator_roles', 'Admin, SwiftOperator')
|
conf.get('keystone_swift_operator_roles', 'Admin, SwiftOperator')
|
||||||
self.admin_token = conf.get('keystone_admin_token')
|
|
||||||
self.keystone_tenant_user_admin = \
|
self.keystone_tenant_user_admin = \
|
||||||
conf.get('keystone_tenant_user_admin', "false").lower() in \
|
conf.get('keystone_tenant_user_admin', "false").lower() in \
|
||||||
('true', 't', '1', 'on', 'yes', 'y')
|
('true', 't', '1', 'on', 'yes', 'y')
|
||||||
|
@ -89,41 +81,7 @@ class AuthProtocol(object):
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
self.logger.debug('Initialise keystone middleware')
|
self.logger.debug('Initialise keystone middleware')
|
||||||
|
identity = self._keystone_identity(environ)
|
||||||
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))
|
|
||||||
|
|
||||||
if not identity:
|
if not identity:
|
||||||
#TODO: non authenticated access allow via refer
|
#TODO: non authenticated access allow via refer
|
||||||
|
@ -137,57 +95,23 @@ class AuthProtocol(object):
|
||||||
environ['swift.clean_acl'] = clean_acl
|
environ['swift.clean_acl'] = clean_acl
|
||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
|
|
||||||
def convert_date(self, date):
|
def _keystone_identity(self, environ):
|
||||||
""" Convert datetime to unix timestamp """
|
""" Extract the identity from the Keystone auth component """
|
||||||
return mktime(datetime.strptime(
|
if (environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed'):
|
||||||
date[:date.rfind(':')].replace('-', ''), "%Y%m%dT%H:%M",
|
return None
|
||||||
).timetuple())
|
roles = []
|
||||||
|
if ('HTTP_X_ROLES' in environ):
|
||||||
def _keystone_validate_token(self, claim):
|
roles = environ.get('HTTP_X_ROLES').split(',')
|
||||||
"""
|
identity = {'user': environ.get('HTTP_X_USER_NAME'),
|
||||||
Will take a claimed token and validate it in keystone.
|
'tenant': (environ.get('HTTP_X_TENANT_ID'),
|
||||||
"""
|
environ.get('HTTP_X_TENANT_NAME')),
|
||||||
headers = {"X-Auth-Token": self.admin_token}
|
'roles': roles}
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
return identity
|
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):
|
def authorize(self, req):
|
||||||
env = req.environ
|
env = req.environ
|
||||||
env_identity = env.get('keystone.identity', {})
|
env_identity = env.get('keystone.identity', {})
|
||||||
|
@ -198,12 +122,13 @@ class AuthProtocol(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HTTPNotFound(request=req)
|
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')
|
self.logger.debug('tenant mismatch')
|
||||||
return self.denied_response(req)
|
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', [])
|
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(','):
|
for _group in self.keystone_swift_operator_roles.split(','):
|
||||||
_group = _group.strip()
|
_group = _group.strip()
|
||||||
if _group in user_groups:
|
if _group in user_groups:
|
||||||
|
@ -240,11 +165,16 @@ class AuthProtocol(object):
|
||||||
return None
|
return None
|
||||||
return self.denied_response(req)
|
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
|
# Check if we have the group in the usergroups and allow it
|
||||||
for user_group in user_groups:
|
for user_group in user_groups:
|
||||||
if user_group in groups:
|
if user_group in groups:
|
||||||
self.logger.debug('user in group which is allowed in" \
|
self.logger.debug('user in group which is allowed in' \
|
||||||
" ACL: %s authorizing' % (user_group))
|
' ACL: %s authorizing' % (user_group))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# last but not least retun deny
|
# last but not least retun deny
|
||||||
|
@ -267,5 +197,5 @@ def filter_factory(global_conf, **local_conf):
|
||||||
conf.update(local_conf)
|
conf.update(local_conf)
|
||||||
|
|
||||||
def auth_filter(app):
|
def auth_filter(app):
|
||||||
return AuthProtocol(app, conf)
|
return SwiftAuth(app, conf)
|
||||||
return auth_filter
|
return auth_filter
|
||||||
|
|
Loading…
Reference in New Issue