495 lines
19 KiB
ReStructuredText
495 lines
19 KiB
ReStructuredText
==========================
|
|
Auth Server and Middleware
|
|
==========================
|
|
|
|
--------------------------------------------
|
|
Creating Your Own Auth Server and Middleware
|
|
--------------------------------------------
|
|
|
|
The included swift/common/middleware/tempauth.py is a good example of how to
|
|
create an auth subsystem with proxy server auth middleware. 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 TempAuth 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 TempAuth). This prefix will avoid
|
|
conflicts 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 TempAuth:
|
|
|
|
* Token AUTH_tkabcd is given to the TempAuth middleware in a request's
|
|
X-Auth-Token header.
|
|
* The TempAuth middleware validates the token AUTH_tkabcd and discovers
|
|
it matches the "tester" user within the "test" account for the storage
|
|
account "AUTH_storage_xyz".
|
|
* The TempAuth middleware sets the REMOTE_USER to
|
|
"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.
|
|
|
|
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 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 swift.common.swob 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 swift.common.swob 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 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 swift.common.swob 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 swift.common.swob 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 that 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 swift.common.swob 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 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 swift.common.swob 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.auth_prefix = conf.get('prefix', '/')
|
|
self.timeout = float(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',
|
|
'%stoken/%s' % (self.auth_prefix, 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),
|
|
time=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
|
|
|
|
-----------------------
|
|
Allowing CORS with Auth
|
|
-----------------------
|
|
|
|
Cross Origin Resource Sharing (CORS) require that the auth system allow the
|
|
OPTIONS method to pass through without a token. The preflight request will
|
|
make an OPTIONS call against the object or container and will not work if
|
|
the auth system stops it.
|
|
See TempAuth for an example of how OPTIONS requests are handled.
|