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_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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue