swift/doc/source/development_auth.rst

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