diff --git a/doc/source/misc.rst b/doc/source/misc.rst index bae61a699a..cfd188e65a 100644 --- a/doc/source/misc.rst +++ b/doc/source/misc.rst @@ -42,6 +42,15 @@ Auth :members: :show-inheritance: +.. _acls: + +ACLs +==== + +.. automodule:: swift.common.middleware.acl + :members: + :show-inheritance: + .. _wsgi: WSGI diff --git a/swift/common/middleware/acl.py b/swift/common/middleware/acl.py index bfa88b8105..bf3148a16a 100644 --- a/swift/common/middleware/acl.py +++ b/swift/common/middleware/acl.py @@ -14,37 +14,101 @@ # limitations under the License. def clean_acl(name, value): + """ + Returns a cleaned ACL header value, validating that it meets the formatting + requirements for standard Swift ACL strings. + + The ACL format is:: + + [item[,item...]] + + Each item can be a group name to give access to or a referrer designation + to grant or deny based on the HTTP Referer header. + + The referrer designation format is:: + + .ref:[-]value + + The value can be "any" to specify any referrer host is allowed access, a + specific host name like "www.example.com", or if it has a leading period + "." it is a domain name specification, like ".example.com". The leading + minus sign "-" indicates referrer hosts that should be denied access. + + Referrer access is applied in the order they are specified. For example, + .ref:.example.com,.ref:-thief.example.com would allow all hosts ending with + .example.com except for the specific host thief.example.com. + + Example valid ACLs:: + + .ref:any + .ref:any,.ref:-.thief.com + .ref:any,.ref:-.thief.com,bobs_account,sues_account:sue + bobs_account,sues_account:sue + + Example invalid ACLs:: + + .ref: + .ref:- + + Also, .ref designations aren't allowed in headers whose names include the + word 'write'. + + ACLs that are "messy" will be cleaned up. Examples: + + ====================== ====================== + Original Cleaned + ---------------------- ---------------------- + bob, sue bob,sue + bob , sue bob,sue + bob,,,sue bob,sue + .ref : any .ref:any + ====================== ====================== + + :param name: The name of the header being cleaned, such as X-Container-Read + or X-Container-Write. + :param value: The value of the header being cleaned. + :returns: The value, cleaned of extraneous formatting. + :raises ValueError: If the value does not meet the ACL formatting + requirements; the error message will indicate why. + """ values = [] for raw_value in value.lower().split(','): raw_value = raw_value.strip() if raw_value: - if ':' in raw_value: + if ':' not in raw_value: + values.append(raw_value) + else: 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: + if first != '.ref': + values.append(raw_value) + elif 'write' in name: raise ValueError('Referrers not allowed in write ACLs: %s' % repr(raw_value)) - if second: - if first == '.ref' and second[0] == '-': + elif not second: + raise ValueError('No value after referrer designation in ' + '%s' % repr(raw_value)) + else: + if 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): + """ + Parses a standard Swift ACL string into a referrers list and groups list. + + See :func:`clean_acl` for documentation of the standard Swift ACL format. + + :param acl_string: The standard Swift ACL string to parse. + :returns: A tuple of (referrers, groups) where referrers is a list of + referrer designations (without the leading .ref:) and groups is a + list of groups to allow access. + """ referrers = [] groups = [] if acl_string: @@ -56,15 +120,29 @@ def parse_acl(acl_string): return referrers, groups -def referrer_allowed(req, referrers): +def referrer_allowed(referrer, referrer_acl): + """ + Returns True if the referrer should be allowed based on the referrer_acl + list (as returned by :func:`parse_acl`). + + See :func:`clean_acl` for documentation of the standard Swift ACL format. + + :param referrer: The value of the HTTP Referer header. + :param referrer_acl: The list of referrer designations as returned by + :func:`parse_acl`. + :returns: True if the referrer should be allowed; False if not. + """ allow = False - if referrers: - parts = req.referer.split('//', 1) - if len(parts) == 2: - rhost = parts[1].split('/', 1)[0].split(':', 1)[0].lower() - else: + if referrer_acl: + if not referrer: rhost = 'unknown' - for mhost in referrers: + else: + parts = referrer.split('//', 1) + if len(parts) == 2: + rhost = parts[1].split('/', 1)[0].split(':', 1)[0].lower() + else: + rhost = 'unknown' + for mhost in referrer_acl: if mhost[0] == '-': mhost = mhost[1:] if mhost == rhost or \ diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index aee5e99353..e433c7fda0 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -24,6 +24,7 @@ from swift.common.utils import cache_from_env, split_path class DevAuth(object): + """Auth Middleware that uses the dev auth server.""" def __init__(self, app, conf): self.app = app @@ -35,6 +36,11 @@ class DevAuth(object): self.timeout = int(conf.get('node_timeout', 10)) def __call__(self, env, start_response): + """ + Accepts a standard WSGI application call, authenticating the request + and installing callback hooks for authorization and ACL header + validation. + """ user = None token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) if token: @@ -64,13 +70,17 @@ class DevAuth(object): return self.app(env, start_response) def authorize(self, req): + """ + Returns None if the request is authorized to continue or a standard + WSGI response callable if not. + """ 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 referrers, groups = parse_acl(getattr(req, 'acl', None)) - if referrer_allowed(req, referrers): + if referrer_allowed(req.referer, referrers): return None if not req.remote_user: return self.denied_response(req) @@ -80,6 +90,10 @@ class DevAuth(object): return self.denied_response(req) def denied_response(self, req): + """ + Returns a standard WSGI response callable with the status of 403 or 401 + depending on whether the REMOTE_USER is set or not. + """ if req.remote_user: return HTTPForbidden(request=req) else: @@ -87,6 +101,7 @@ class DevAuth(object): def filter_factory(global_conf, **local_conf): + """Returns a WSGI filter app for use with paste.deploy.""" conf = global_conf.copy() conf.update(local_conf) def auth_filter(app):