diff --git a/doc/source/development_auth.rst b/doc/source/development_auth.rst new file mode 100644 index 0000000000..fce52187b5 --- /dev/null +++ b/doc/source/development_auth.rst @@ -0,0 +1,449 @@ +=============== +Auth Middleware +=============== + +--------------------------------- +Creating Your Own Auth Middleware +--------------------------------- + +The included swift/common/middleware/auth.py is a good minimal example of how +to create 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. + +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 diff --git a/doc/source/index.rst b/doc/source/index.rst index 7e4681ca28..8760852f13 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -32,6 +32,7 @@ Development: development_guidelines development_saio + development_auth Deployment: diff --git a/swift/auth/server.py b/swift/auth/server.py index db290a72c4..39c8dd8837 100644 --- a/swift/auth/server.py +++ b/swift/auth/server.py @@ -353,8 +353,10 @@ class AuthController(object): validation = self.validate_token(token) if not validation: return HTTPNotFound() + # X-Auth-User: account:user,account,cfaccount return HTTPNoContent(headers={'X-Auth-TTL': validation[0], - 'X-Auth-User': ':'.join(validation[1:])}) + 'X-Auth-User': '%s:%s,%s,%s' % + (validation[1], validation[2], validation[1], validation[3])}) def handle_account_create(self, request): """ diff --git a/swift/common/middleware/acl.py b/swift/common/middleware/acl.py new file mode 100644 index 0000000000..bfa88b8105 --- /dev/null +++ b/swift/common/middleware/acl.py @@ -0,0 +1,76 @@ +# Copyright (c) 2010 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def clean_acl(name, value): + values = [] + for raw_value in value.lower().split(','): + raw_value = raw_value.strip() + if raw_value: + if ':' in raw_value: + first, second = (v.strip() for v in raw_value.split(':', 1)) + if not first: + raise ValueError('No value before colon in %s' % + repr(raw_value)) + if first == '.ref' and 'write' in name: + raise ValueError('Referrers not allowed in write ACLs: %s' + % repr(raw_value)) + if second: + if first == '.ref' and second[0] == '-': + second = second[1:].strip() + if not second: + raise ValueError('No value after referrer deny ' + 'designation in %s' % repr(raw_value)) + second = '-' + second + values.append('%s:%s' % (first, second)) + elif first == '.ref': + raise ValueError('No value after referrer designation in ' + '%s' % repr(raw_value)) + else: + values.append(first) + else: + values.append(raw_value) + return ','.join(values) + + +def parse_acl(acl_string): + referrers = [] + groups = [] + if acl_string: + for value in acl_string.split(','): + if value.startswith('.ref:'): + referrers.append(value[len('.ref:'):]) + else: + groups.append(value) + return referrers, groups + + +def referrer_allowed(req, referrers): + allow = False + if referrers: + parts = req.referer.split('//', 1) + if len(parts) == 2: + rhost = parts[1].split('/', 1)[0].split(':', 1)[0].lower() + else: + rhost = 'unknown' + for mhost in referrers: + if mhost[0] == '-': + mhost = mhost[1:] + if mhost == rhost or \ + (mhost[0] == '.' and rhost.endswith(mhost)): + allow = False + elif mhost == 'any' or mhost == rhost or \ + (mhost[0] == '.' and rhost.endswith(mhost)): + allow = True + return allow diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index 32800839ba..aee5e99353 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -16,268 +16,79 @@ 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 webob.exc import HTTPForbidden, HTTPUnauthorized 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 -class DevAuthorization(object): +class DevAuth(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'] = self.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) - groups = [account] - acl = self.parse_acl(getattr(req, 'acl', None)) - if acl: - referrers, accounts, users = acl - if referrers: - parts = req.referer.split('//', 1) - allow = False - if len(parts) == 2: - rhost = parts[1].split('/', 1)[0].split(':', 1)[0].lower() - else: - rhost = 'unknown' - for mhost in referrers: - if mhost[0] == '-': - mhost = mhost[1:] - if mhost == rhost or \ - (mhost[0] == '.' and rhost.endswith(mhost)): - allow = False - elif mhost == 'any' or mhost == rhost or \ - (mhost[0] == '.' and rhost.endswith(mhost)): - allow = True - if allow: - return None - groups.extend(accounts) - groups.extend(users) - try: - in_any_group(*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) - - def clean_acl(self, header_name, value): - values = [] - for raw_value in value.lower().split(','): - raw_value = raw_value.strip() - if raw_value: - if ':' in raw_value: - first, second = \ - (v.strip() for v in raw_value.split(':', 1)) - if not first: - raise ValueError('No value before colon in %s' % - repr(raw_value)) - if first == '.ref' and 'write' in header_name: - raise ValueError('Referrers not allowed in write ' - 'ACLs: %s' % repr(raw_value)) - if second: - if first == '.ref' and second[0] == '-': - second = second[1:].strip() - if not second: - raise ValueError('No value after referrer ' - 'deny designation in %s' % repr(raw_value)) - second = '-' + second - values.append('%s:%s' % (first, second)) - elif first == '.ref': - raise ValueError('No value after referrer designation ' - 'in %s' % repr(raw_value)) - else: - values.append(first) - else: - values.append(raw_value) - return ','.join(values) - - def parse_acl(self, acl_string): - if not acl_string: - return None - referrers = [] - accounts = [] - users = [] - for value in acl_string.split(','): - if value.startswith('.ref:'): - referrers.append(value[len('.ref:'):]) - elif ':' in value: - users.append(value) - else: - accounts.append(value) - return (referrers, accounts, users) - - -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: + def __call__(self, env, start_response): + user = None + token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + if token: + 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: + user = None + if not 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 // 100 != 2: + return HTTPUnauthorized()(env, start_response) + expiration = float(resp.getheader('x-auth-ttl')) + user = resp.getheader('x-auth-user') + memcache_client.set(key, (time(), expiration, user), + timeout=expiration) + env['REMOTE_USER'] = user + env['swift.authorize'] = self.authorize + env['swift.clean_acl'] = clean_acl + return self.app(env, 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) + if req.remote_user and account in req.remote_user.split(','): 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 + referrers, groups = parse_acl(getattr(req, 'acl', None)) + if referrer_allowed(req, referrers): + return None + if not req.remote_user: + return self.denied_response(req) + for user_group in req.remote_user.split(','): + if user_group in groups: + return None + return self.denied_response(req) - -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): - creds = credentials['repoze.what.userid'].split(':') - if len(creds) != 3: - return set() - rv = set([creds[0], ':'.join(creds[:2]), creds[2]]) - return rv - - 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 denied_response(self, req): + 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 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 DevAuth(app, conf) return auth_filter diff --git a/test/unit/common/middleware/test_acl.py b/test/unit/common/middleware/test_acl.py new file mode 100644 index 0000000000..69de606849 --- /dev/null +++ b/test/unit/common/middleware/test_acl.py @@ -0,0 +1,150 @@ +# Copyright (c) 2010 OpenStack, LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import with_statement +import logging +import os +import sys +import unittest +from contextlib import contextmanager + +import eventlet +from webob import Request + +from swift.common.middleware import acl + +# mocks +logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) + + +class FakeMemcache(object): + def __init__(self): + self.store = {} + + def get(self, key): + return self.store.get(key) + + def set(self, key, value, timeout=0): + self.store[key] = value + return True + + def incr(self, key, timeout=0): + self.store[key] = self.store.setdefault(key, 0) + 1 + return self.store[key] + + @contextmanager + def soft_lock(self, key, timeout=0, retries=5): + yield True + + def delete(self, key): + try: + del self.store[key] + except: + pass + return True + + +def mock_http_connect(response, headers=None, with_exc=False): + class FakeConn(object): + def __init__(self, status, headers, with_exc): + self.status = status + self.reason = 'Fake' + self.host = '1.2.3.4' + self.port = '1234' + self.with_exc = with_exc + self.headers = headers + if self.headers is None: + self.headers = {} + def getresponse(self): + if self.with_exc: + raise Exception('test') + return self + def getheader(self, header): + return self.headers[header] + def read(self, amt=None): + return '' + def close(self): + return + return lambda *args, **kwargs: FakeConn(response, headers, with_exc) + + +class Logger(object): + def __init__(self): + self.error_value = None + self.exception_value = None + def error(self, msg, *args, **kwargs): + self.error_value = (msg, args, kwargs) + def exception(self, msg, *args, **kwargs): + _, exc, _ = sys.exc_info() + self.exception_value = (msg, + '%s %s' % (exc.__class__.__name__, str(exc)), args, kwargs) +# tests + +class FakeApp(object): + def __call__(self, env, start_response): + return "OK" + +def start_response(*args): + pass + +class TestAuth(unittest.TestCase): + # I brought these over from another refactor I've been trying, but they + # need work. + + def test_clean_acl(self): + value = acl.clean_acl('header', '.ref:any') + self.assertEquals(value, '.ref:any') + value = acl.clean_acl('header', '.ref:specific.host') + self.assertEquals(value, '.ref:specific.host') + value = acl.clean_acl('header', '.ref:.ending.with') + self.assertEquals(value, '.ref:.ending.with') + value = acl.clean_acl('header', '.ref:one,.ref:two') + self.assertEquals(value, '.ref:one,.ref:two') + value = acl.clean_acl('header', '.ref:any,.ref:-specific.host') + self.assertEquals(value, '.ref:any,.ref:-specific.host') + value = acl.clean_acl('header', '.ref:any,.ref:-.ending.with') + self.assertEquals(value, '.ref:any,.ref:-.ending.with') + value = acl.clean_acl('header', '.ref:one,.ref:-two') + self.assertEquals(value, '.ref:one,.ref:-two') + value = acl.clean_acl('header', + ' .ref : one , ,, .ref:two , .ref : - three ') + self.assertEquals(value, '.ref:one,.ref:two,.ref:-three') + self.assertRaises(ValueError, acl.clean_acl, 'header', '.ref:') + self.assertRaises(ValueError, acl.clean_acl, 'header', ' .ref : ') + self.assertRaises(ValueError, acl.clean_acl, 'header', + 'user , .ref : ') + self.assertRaises(ValueError, acl.clean_acl, 'header', '.ref:-') + self.assertRaises(ValueError, acl.clean_acl, 'header', ' .ref : - ') + self.assertRaises(ValueError, acl.clean_acl, 'header', + 'user , .ref : - ') + + def test_parse_acl(self): + self.assertEquals(acl.parse_acl(None), ([], [])) + self.assertEquals(acl.parse_acl(''), ([], [])) + self.assertEquals(acl.parse_acl('.ref:ref1'), (['ref1'], [])) + self.assertEquals(acl.parse_acl('.ref:-ref1'), (['-ref1'], [])) + self.assertEquals(acl.parse_acl('account:user'), + ([], ['account:user'])) + self.assertEquals(acl.parse_acl('account'), ([], ['account'])) + self.assertEquals(acl.parse_acl('acc1,acc2:usr2,.ref:ref3,.ref:-ref4'), + (['ref3', '-ref4'], ['acc1', 'acc2:usr2'])) + self.assertEquals(acl.parse_acl( + 'acc1,acc2:usr2,.ref:ref3,acc3,acc4:usr4,.ref:ref5,.ref:-ref6'), + (['ref3', 'ref5', '-ref6'], + ['acc1', 'acc2:usr2', 'acc3', 'acc4:usr4'])) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/common/middleware/test_auth.py b/test/unit/common/middleware/test_auth.py index 08f8898d1b..d48ce39202 100644 --- a/test/unit/common/middleware/test_auth.py +++ b/test/unit/common/middleware/test_auth.py @@ -101,57 +101,7 @@ def start_response(*args): class TestAuth(unittest.TestCase): # TODO: With the auth refactor, these tests have to be refactored as well. - # I brought some over from another refactor I've been trying, but these - # also need work. - - def test_clean_acl(self): - devauth = auth.DevAuthorization(None, None) - value = devauth.clean_acl('header', '.ref:any') - self.assertEquals(value, '.ref:any') - value = devauth.clean_acl('header', '.ref:specific.host') - self.assertEquals(value, '.ref:specific.host') - value = devauth.clean_acl('header', '.ref:.ending.with') - self.assertEquals(value, '.ref:.ending.with') - value = devauth.clean_acl('header', '.ref:one,.ref:two') - self.assertEquals(value, '.ref:one,.ref:two') - value = devauth.clean_acl('header', '.ref:any,.ref:-specific.host') - self.assertEquals(value, '.ref:any,.ref:-specific.host') - value = devauth.clean_acl('header', '.ref:any,.ref:-.ending.with') - self.assertEquals(value, '.ref:any,.ref:-.ending.with') - value = devauth.clean_acl('header', '.ref:one,.ref:-two') - self.assertEquals(value, '.ref:one,.ref:-two') - value = devauth.clean_acl('header', - ' .ref : one , ,, .ref:two , .ref : - three ') - self.assertEquals(value, '.ref:one,.ref:two,.ref:-three') - self.assertRaises(ValueError, devauth.clean_acl, 'header', '.ref:') - self.assertRaises(ValueError, devauth.clean_acl, 'header', ' .ref : ') - self.assertRaises(ValueError, devauth.clean_acl, 'header', - 'user , .ref : ') - self.assertRaises(ValueError, devauth.clean_acl, 'header', '.ref:-') - self.assertRaises(ValueError, devauth.clean_acl, 'header', - ' .ref : - ') - self.assertRaises(ValueError, devauth.clean_acl, 'header', - 'user , .ref : - ') - - def test_parse_acl(self): - devauth = auth.DevAuthorization(None, None) - self.assertEquals(devauth.parse_acl(None), None) - self.assertEquals(devauth.parse_acl(''), None) - self.assertEquals(devauth.parse_acl('.ref:ref1'), - (['ref1'], [], [])) - self.assertEquals(devauth.parse_acl('.ref:-ref1'), - (['-ref1'], [], [])) - self.assertEquals(devauth.parse_acl('account:user'), - ([], [], ['account:user'])) - self.assertEquals(devauth.parse_acl('account'), - ([], ['account'], [])) - self.assertEquals( - devauth.parse_acl('acc1,acc2:usr2,.ref:ref3,.ref:-ref4'), - (['ref3', '-ref4'], ['acc1'], ['acc2:usr2'])) - self.assertEquals(devauth.parse_acl( - 'acc1,acc2:usr2,.ref:ref3,acc3,acc4:usr4,.ref:ref5,.ref:-ref6'), - (['ref3', 'ref5', '-ref6'], ['acc1', 'acc3'], - ['acc2:usr2', 'acc4:usr4'])) + pass if __name__ == '__main__':