489 lines
19 KiB
ReStructuredText
489 lines
19 KiB
ReStructuredText
==========================
|
|
Auth Server and Middleware
|
|
==========================
|
|
|
|
--------------------------------------------
|
|
Creating Your Own Auth Server and Middleware
|
|
--------------------------------------------
|
|
|
|
The included swift/auth/server.py and swift/common/middleware/auth.py are good
|
|
minimal examples of how to create an external auth server and proxy server auth
|
|
middleware. Also, see the `Swauth <https://launchpad.net/swauth>`_ project for
|
|
a more complete implementation. The main points are that the auth middleware
|
|
can reject requests up front, before they ever get to the Swift Proxy
|
|
application, and afterwards when the proxy issues callbacks to verify
|
|
authorization.
|
|
|
|
It's generally good to separate the authentication and authorization
|
|
procedures. Authentication verifies that a request actually comes from who it
|
|
says it does. Authorization verifies the 'who' has access to the resource(s)
|
|
the request wants.
|
|
|
|
Authentication is performed on the request before it ever gets to the Swift
|
|
Proxy application. The identity information is gleaned from the request,
|
|
validated in some way, and the validation information is added to the WSGI
|
|
environment as needed by the future authorization procedure. What exactly is
|
|
added to the WSGI environment is solely dependent on what the installed
|
|
authorization procedures need; the Swift Proxy application itself needs no
|
|
specific information, it just passes it along. Convention has
|
|
environ['REMOTE_USER'] set to the authenticated user string but often more
|
|
information is needed than just that.
|
|
|
|
The included DevAuth will set the REMOTE_USER to a comma separated list of
|
|
groups the user belongs to. The first group will be the "user's group", a group
|
|
that only the user belongs to. The second group will be the "account's group",
|
|
a group that includes all users for that auth account (different than the
|
|
storage account). The third group is optional and is the storage account
|
|
string. If the user does not have admin access to the account, the third group
|
|
will be omitted.
|
|
|
|
It is highly recommended that authentication server implementers prefix their
|
|
tokens and Swift storage accounts they create with a configurable reseller
|
|
prefix (`AUTH_` by default with the included DevAuth). This prefix will allow
|
|
deconflicting with other authentication servers that might be using the same
|
|
Swift cluster. Otherwise, the Swift cluster will have to try all the resellers
|
|
until one validates a token or all fail.
|
|
|
|
A restriction with group names is that no group name should begin with a period
|
|
'.' as that is reserved for internal Swift use (such as the .r for referrer
|
|
designations as you'll see later).
|
|
|
|
Example Authentication with DevAuth:
|
|
|
|
* Token AUTH_tkabcd is given to the DevAuth middleware in a request's
|
|
X-Auth-Token header.
|
|
* The DevAuth middleware makes a validate token AUTH_tkabcd call to the
|
|
external DevAuth server.
|
|
* The external DevAuth server validates the token AUTH_tkabcd and discovers
|
|
it matches the "tester" user within the "test" account for the storage
|
|
account "AUTH_storage_xyz".
|
|
* The external DevAuth server responds with "X-Auth-Groups:
|
|
test:tester,test,AUTH_storage_xyz"
|
|
* Now this user will have full access (via authorization procedures later)
|
|
to the AUTH_storage_xyz Swift storage account and access to containers in
|
|
other storage accounts, provided the storage account begins with the same
|
|
`AUTH_` reseller prefix and the container has an ACL specifying at least
|
|
one of those three groups returned.
|
|
|
|
Authorization is performed through callbacks by the Swift Proxy server to the
|
|
WSGI environment's swift.authorize value, if one is set. The swift.authorize
|
|
value should simply be a function that takes a webob.Request as an argument and
|
|
returns None if access is granted or returns a callable(environ,
|
|
start_response) if access is denied. This callable is a standard WSGI callable.
|
|
Generally, you should return 403 Forbidden for requests by an authenticated
|
|
user and 401 Unauthorized for an unauthenticated request. For example, here's
|
|
an authorize function that only allows GETs (in this case you'd probably return
|
|
405 Method Not Allowed, but ignore that for the moment).::
|
|
|
|
from webob import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
def authorize(req):
|
|
if req.method == 'GET':
|
|
return None
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
Adding the swift.authorize callback is often done by the authentication
|
|
middleware as authentication and authorization are often paired together. But,
|
|
you could create separate authorization middleware that simply sets the
|
|
callback before passing on the request. To continue our example above::
|
|
|
|
from webob import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
class Authorization(object):
|
|
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ['swift.authorize'] = self.authorize
|
|
return self.app(environ, start_response)
|
|
|
|
def authorize(self, req):
|
|
if req.method == 'GET':
|
|
return None
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
def auth_filter(app):
|
|
return Authorization(app, conf)
|
|
return auth_filter
|
|
|
|
The Swift Proxy server will call swift.authorize after some initial work, but
|
|
before truly trying to process the request. Positive authorization at this
|
|
point will cause the request to be fully processed immediately. A denial at
|
|
this point will immediately send the denial response for most operations.
|
|
|
|
But for some operations that might be approved with more information, the
|
|
additional information will be gathered and added to the WSGI environment and
|
|
then swift.authorize will be called once more. These are called delay_denial
|
|
requests and currently include container read requests and object read and
|
|
write requests. For these requests, the read or write access control string
|
|
(X-Container-Read and X-Container-Write) will be fetched and set as the 'acl'
|
|
attribute in the webob.Request passed to swift.authorize.
|
|
|
|
The delay_denial procedures allow skipping possibly expensive access control
|
|
string retrievals for requests that can be approved without that information,
|
|
such as administrator or account owner requests.
|
|
|
|
To further our example, we now will approve all requests that have the access
|
|
control string set to same value as the authenticated user string. Note that
|
|
you probably wouldn't do this exactly as the access control string represents a
|
|
list rather than a single user, but it'll suffice for this example::
|
|
|
|
from webob import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
class Authorization(object):
|
|
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ['swift.authorize'] = self.authorize
|
|
return self.app(environ, start_response)
|
|
|
|
def authorize(self, req):
|
|
# Allow anyone to perform GET requests
|
|
if req.method == 'GET':
|
|
return None
|
|
# Allow any request where the acl equals the authenticated user
|
|
if getattr(req, 'acl', None) == req.remote_user:
|
|
return None
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
def auth_filter(app):
|
|
return Authorization(app, conf)
|
|
return auth_filter
|
|
|
|
The access control string has a standard format included with Swift, though
|
|
this can be overridden if desired. The standard format can be parsed with
|
|
swift.common.middleware.acl.parse_acl which converts the string into two arrays
|
|
of strings: (referrers, groups). The referrers allow comparing the request's
|
|
Referer header to control access. The groups allow comparing the
|
|
request.remote_user (or other sources of group information) to control access.
|
|
Checking referrer access can be accomplished by using the
|
|
swift.common.middleware.acl.referrer_allowed function. Checking group access is
|
|
usually a simple string comparison.
|
|
|
|
Let's continue our example to use parse_acl and referrer_allowed. Now we'll
|
|
only allow GETs after a referrer check and any requests after a group check::
|
|
|
|
from swift.common.middleware.acl import parse_acl, referrer_allowed
|
|
from webob import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
class Authorization(object):
|
|
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ['swift.authorize'] = self.authorize
|
|
return self.app(environ, start_response)
|
|
|
|
def authorize(self, req):
|
|
if hasattr(req, 'acl'):
|
|
referrers, groups = parse_acl(req.acl)
|
|
if req.method == 'GET' and referrer_allowed(req, referrers):
|
|
return None
|
|
if req.remote_user and groups and req.remote_user in groups:
|
|
return None
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
def auth_filter(app):
|
|
return Authorization(app, conf)
|
|
return auth_filter
|
|
|
|
The access control strings are set with PUTs and POSTs to containers with the
|
|
X-Container-Read and X-Container-Write headers. Swift allows these strings to
|
|
be set to any value, though it's very useful to validate the strings meet the
|
|
desired format and return a useful error to the user if they don't.
|
|
|
|
To support this validation, the Swift Proxy application will call the WSGI
|
|
environment's swift.clean_acl callback whenever one of these headers is to be
|
|
written. The callback should take a header name and value as its arguments. It
|
|
should return the cleaned value to save if valid or raise a ValueError with a
|
|
reasonable error message if not.
|
|
|
|
There is an included swift.common.middleware.acl.clean_acl that validates the
|
|
standard Swift format. Let's improve our example by making use of that::
|
|
|
|
from swift.common.middleware.acl import \
|
|
clean_acl, parse_acl, referrer_allowed
|
|
from webob import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
class Authorization(object):
|
|
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ['swift.authorize'] = self.authorize
|
|
environ['swift.clean_acl'] = clean_acl
|
|
return self.app(environ, start_response)
|
|
|
|
def authorize(self, req):
|
|
if hasattr(req, 'acl'):
|
|
referrers, groups = parse_acl(req.acl)
|
|
if req.method == 'GET' and referrer_allowed(req, referrers):
|
|
return None
|
|
if req.remote_user and groups and req.remote_user in groups:
|
|
return None
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
def auth_filter(app):
|
|
return Authorization(app, conf)
|
|
return auth_filter
|
|
|
|
Now, if you want to override the format for access control strings you'll have
|
|
to provide your own clean_acl function and you'll have to do your own parsing
|
|
and authorization checking for that format. It's highly recommended you use the
|
|
standard format simply to support the widest range of external tools, but
|
|
sometimes that's less important than meeting certain ACL requirements.
|
|
|
|
|
|
----------------------------
|
|
Integrating With repoze.what
|
|
----------------------------
|
|
|
|
Here's an example of integration with repoze.what, though honestly it just does
|
|
what the default swift/common/middleware/auth.py does in a slightly different
|
|
way. I'm no repoze.what expert by any stretch; this is just included here to
|
|
hopefully give folks a start on their own code if they want to use
|
|
repoze.what::
|
|
|
|
from time import time
|
|
|
|
from eventlet.timeout import Timeout
|
|
from repoze.what.adapters import BaseSourceAdapter
|
|
from repoze.what.middleware import setup_auth
|
|
from repoze.what.predicates import in_any_group, NotAuthorizedError
|
|
from swift.common.bufferedhttp import http_connect_raw as http_connect
|
|
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
|
|
from swift.common.utils import cache_from_env, split_path
|
|
from webob.exc import HTTPForbidden, HTTPUnauthorized
|
|
|
|
|
|
class DevAuthorization(object):
|
|
|
|
def __init__(self, app, conf):
|
|
self.app = app
|
|
self.conf = conf
|
|
|
|
def __call__(self, environ, start_response):
|
|
environ['swift.authorize'] = self.authorize
|
|
environ['swift.clean_acl'] = clean_acl
|
|
return self.app(environ, start_response)
|
|
|
|
def authorize(self, req):
|
|
version, account, container, obj = split_path(req.path, 1, 4, True)
|
|
if not account:
|
|
return self.denied_response(req)
|
|
referrers, groups = parse_acl(getattr(req, 'acl', None))
|
|
if referrer_allowed(req, referrers):
|
|
return None
|
|
try:
|
|
in_any_group(account, *groups).check_authorization(req.environ)
|
|
except NotAuthorizedError:
|
|
return self.denied_response(req)
|
|
return None
|
|
|
|
def denied_response(self, req):
|
|
if req.remote_user:
|
|
return HTTPForbidden(request=req)
|
|
else:
|
|
return HTTPUnauthorized(request=req)
|
|
|
|
|
|
class DevIdentifier(object):
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
|
|
def identify(self, env):
|
|
return {'token':
|
|
env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))}
|
|
|
|
def remember(self, env, identity):
|
|
return []
|
|
|
|
def forget(self, env, identity):
|
|
return []
|
|
|
|
|
|
class DevAuthenticator(object):
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
self.auth_host = conf.get('ip', '127.0.0.1')
|
|
self.auth_port = int(conf.get('port', 11000))
|
|
self.ssl = \
|
|
conf.get('ssl', 'false').lower() in ('true', 'on', '1', 'yes')
|
|
self.timeout = int(conf.get('node_timeout', 10))
|
|
|
|
def authenticate(self, env, identity):
|
|
token = identity.get('token')
|
|
if not token:
|
|
return None
|
|
memcache_client = cache_from_env(env)
|
|
key = 'devauth/%s' % token
|
|
cached_auth_data = memcache_client.get(key)
|
|
if cached_auth_data:
|
|
start, expiration, user = cached_auth_data
|
|
if time() - start <= expiration:
|
|
return user
|
|
with Timeout(self.timeout):
|
|
conn = http_connect(self.auth_host, self.auth_port, 'GET',
|
|
'/token/%s' % token, ssl=self.ssl)
|
|
resp = conn.getresponse()
|
|
resp.read()
|
|
conn.close()
|
|
if resp.status == 204:
|
|
expiration = float(resp.getheader('x-auth-ttl'))
|
|
user = resp.getheader('x-auth-user')
|
|
memcache_client.set(key, (time(), expiration, user),
|
|
timeout=expiration)
|
|
return user
|
|
return None
|
|
|
|
|
|
class DevChallenger(object):
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
|
|
def challenge(self, env, status, app_headers, forget_headers):
|
|
def no_challenge(env, start_response):
|
|
start_response(str(status), [])
|
|
return []
|
|
return no_challenge
|
|
|
|
|
|
class DevGroupSourceAdapter(BaseSourceAdapter):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DevGroupSourceAdapter, self).__init__(*args, **kwargs)
|
|
self.sections = {}
|
|
|
|
def _get_all_sections(self):
|
|
return self.sections
|
|
|
|
def _get_section_items(self, section):
|
|
return self.sections[section]
|
|
|
|
def _find_sections(self, credentials):
|
|
return credentials['repoze.what.userid'].split(',')
|
|
|
|
def _include_items(self, section, items):
|
|
self.sections[section] |= items
|
|
|
|
def _exclude_items(self, section, items):
|
|
for item in items:
|
|
self.sections[section].remove(item)
|
|
|
|
def _item_is_included(self, section, item):
|
|
return item in self.sections[section]
|
|
|
|
def _create_section(self, section):
|
|
self.sections[section] = set()
|
|
|
|
def _edit_section(self, section, new_section):
|
|
self.sections[new_section] = self.sections[section]
|
|
del self.sections[section]
|
|
|
|
def _delete_section(self, section):
|
|
del self.sections[section]
|
|
|
|
def _section_exists(self, section):
|
|
return self.sections.has_key(section)
|
|
|
|
|
|
class DevPermissionSourceAdapter(BaseSourceAdapter):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DevPermissionSourceAdapter, self).__init__(*args, **kwargs)
|
|
self.sections = {}
|
|
|
|
def _get_all_sections(self):
|
|
return self.sections
|
|
|
|
def _get_section_items(self, section):
|
|
return self.sections[section]
|
|
|
|
def _find_sections(self, group_name):
|
|
return set([n for (n, p) in self.sections.items()
|
|
if group_name in p])
|
|
|
|
def _include_items(self, section, items):
|
|
self.sections[section] |= items
|
|
|
|
def _exclude_items(self, section, items):
|
|
for item in items:
|
|
self.sections[section].remove(item)
|
|
|
|
def _item_is_included(self, section, item):
|
|
return item in self.sections[section]
|
|
|
|
def _create_section(self, section):
|
|
self.sections[section] = set()
|
|
|
|
def _edit_section(self, section, new_section):
|
|
self.sections[new_section] = self.sections[section]
|
|
del self.sections[section]
|
|
|
|
def _delete_section(self, section):
|
|
del self.sections[section]
|
|
|
|
def _section_exists(self, section):
|
|
return self.sections.has_key(section)
|
|
|
|
|
|
def filter_factory(global_conf, **local_conf):
|
|
conf = global_conf.copy()
|
|
conf.update(local_conf)
|
|
def auth_filter(app):
|
|
return setup_auth(DevAuthorization(app, conf),
|
|
group_adapters={'all_groups': DevGroupSourceAdapter()},
|
|
permission_adapters={'all_perms': DevPermissionSourceAdapter()},
|
|
identifiers=[('devauth', DevIdentifier(conf))],
|
|
authenticators=[('devauth', DevAuthenticator(conf))],
|
|
challengers=[('devauth', DevChallenger(conf))])
|
|
return auth_filter
|