From 123dfbe40b7afeee7da08beea4d7290771b14978 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Oct 2015 16:04:57 -0400 Subject: [PATCH] Retire stackforge/swiftpolicy --- .gitignore | 6 - .gitreview | 4 - README.md | 112 --- README.rst | 7 + policies/default.json | 37 - requirements | 9 - setup.py | 39 - swiftpolicy/__init__.py | 30 - swiftpolicy/enforcer.py | 201 ----- swiftpolicy/openstack/__init__.py | 1 - swiftpolicy/openstack/common/__init__.py | 1 - swiftpolicy/openstack/common/policy_parser.py | 758 ------------------ swiftpolicy/swiftpolicy.py | 271 ------- tests/__init__.py | 0 tests/policies/default.json | 36 - tests/test_swiftpolicy.py | 527 ------------ tox.ini | 8 - 17 files changed, 7 insertions(+), 2040 deletions(-) delete mode 100644 .gitignore delete mode 100644 .gitreview delete mode 100644 README.md create mode 100644 README.rst delete mode 100644 policies/default.json delete mode 100644 requirements delete mode 100644 setup.py delete mode 100644 swiftpolicy/__init__.py delete mode 100644 swiftpolicy/enforcer.py delete mode 100644 swiftpolicy/openstack/__init__.py delete mode 100644 swiftpolicy/openstack/common/__init__.py delete mode 100644 swiftpolicy/openstack/common/policy_parser.py delete mode 100644 swiftpolicy/swiftpolicy.py delete mode 100644 tests/__init__.py delete mode 100644 tests/policies/default.json delete mode 100644 tests/test_swiftpolicy.py delete mode 100644 tox.ini diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 77afc88..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.egg-info -*.pyc -.idea -build/ -dist/ -.tox diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 807d48a..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=stackforge/swiftpolicy.git diff --git a/README.md b/README.md deleted file mode 100644 index cb16e7f..0000000 --- a/README.md +++ /dev/null @@ -1,112 +0,0 @@ -SwiftPolicy Middleware ----------------------- - -The SwiftPolicy Middleware for OpenStack Swift allows to use a JSON policy file -to handle swift authorizations. - -SwiftPolicy is an adaptation of the keystoneauth middleware here: -https://github.com/openstack/swift/blob/master/swift/common/middleware/keystoneauth.py - - -Install -------- - -1) Install SwiftPolicy with ``sudo python setup.py install`` or ``sudo python - setup.py develop``. - -2) Alter your proxy-server.conf pipeline to include SwiftPolicy: - -For example, you can use SwiftPolicy in place of the keystoneauth middleware: - - Change:: - - [pipeline:main] - pipeline = catch_errors cache tempauth proxy-server - - To:: - - [pipeline:main] - pipeline = catch_errors cache swiftpolicy tempauth proxy-server - -3) Add to your proxy-server.conf the section for the SwiftPolicy WSGI filter. - -The policy file is set with the ``policy`` option :: - - [filter:swiftpolicy] - use = egg:swiftpolicy#swiftpolicy - policy = %(here)s/default.json - -This middleware comes with a default policy file in /etc/swift/default.json that maintains -compatibility with keystoneauth. - - -Policy file ------------ - -The policy file will list all possible actions on a swift proxy. -Action's syntax is: ``_`` (example: "get_container", "put_object", etc). - - ... - "get_container": "rule:allowed_for_user", - "put_container": "rule:allowed_for_user", - "delete_container": "rule:allowed_for_user", - ... - - -The policy file contains also two specific rules: "swift_owner" "reseller_request", they are defined -when swift_owner and reseller_request headers are set to true, as those two values are part -of the contract with the auth system (more details here: http://docs.openstack.org/developer/swift/overview_auth.html) - - ... - "swift_owner": "rule:swift_reseller or rule:swift_operator", - "reseller_request": "rule:swift_reseller", - ... -  -Example -------- - -* To forbid the creation of new containers: set put_container to '!': - - ... - "get_container": "rule:allowed_for_user", - "put_container": "!", - ... - -* To restrict the creation of new containers to users with the role "admin": - - ... - "get_container": "rule:allowed_for_user", - "put_container": "role:admin", - ... - -Limitations ------------ - -* swiftpolicy does not support dynamic reload of policies, and thus, the swift proxy has -to be restarted when the policy file is updated. - - -License / Copyright -------------------- - -This software is released under the MIT License. - -Copyright (c) 2014 Cloudwatt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9006052 --- /dev/null +++ b/README.rst @@ -0,0 +1,7 @@ +This project is no longer maintained. + +The contents of this repository are still available in the Git source code +management system. To see the contents of this repository before it reached +its end of life, please check out the previous commit with +"git checkout HEAD^1". + diff --git a/policies/default.json b/policies/default.json deleted file mode 100644 index ec341f2..0000000 --- a/policies/default.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "is_anonymous": "identity:None", - "is_authenticated": "not rule:is_anonymous", - - "swift_reseller": "role:reseller", - "swift_operator": "role:admin or role:Member", - - "swift_owner": "rule:swift_reseller or rule:swift_operator", - "reseller_request": "rule:swift_reseller", - - "same_tenant": "account:%(account)s", - "tenant_mismatch": "not rule:same_tenant", - "allowed_for_authenticated": "rule:swift_reseller or acl:check_cross_tenant or acl:check_is_public or (rule:same_tenant and rule:swift_operator) or (rule:same_tenant and acl:check_roles)", - "allowed_for_anonymous": "is_authoritative:True and acl:check_is_public", - "allowed_for_user": "(rule:is_authenticated and rule:allowed_for_authenticated) or rule:allowed_for_anonymous", - - "get_account": "rule:allowed_for_user", - "post_account": "rule:allowed_for_user", - "head_account": "rule:allowed_for_user", - "delete_account": "rule:swift_reseller", - "options_account": "", - - "get_container": "rule:allowed_for_user", - "put_container": "rule:allowed_for_user", - "delete_container": "rule:allowed_for_user", - "post_container": "rule:allowed_for_user", - "head_container": "rule:allowed_for_user", - "options_container": "", - - "get_object": "rule:allowed_for_user", - "put_object": "rule:allowed_for_user", - "copy_object": "rule:allowed_for_user", - "delete_object": "rule:allowed_for_user", - "head_object": "rule:allowed_for_user", - "post_object": "rule:allowed_for_user", - "options_object": "" -} \ No newline at end of file diff --git a/requirements b/requirements deleted file mode 100644 index c4339a2..0000000 --- a/requirements +++ /dev/null @@ -1,9 +0,0 @@ -dnspython>=1.9.4 -eventlet>=0.9.15 -greenlet>=0.3.1 -netifaces>=0.5,!=0.10.0,!=0.10.1 -pastedeploy>=1.3.3 -simplejson>=2.0.9 -xattr>=0.4 -git+https://github.com/openstack/swift.git@1.13.0#egg=swift-1.13.0 -six diff --git a/setup.py b/setup.py deleted file mode 100644 index 63618e0..0000000 --- a/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/python -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from setuptools import setup - -import swiftpolicy - - -setup(name='swiftpolicy', - version=swiftpolicy.version, - description='Swift authentication/authorization middleware for keystone that uses "policy" file format.', - author='CloudWatt', - author_email='ala.rezmerita@cloudwatt.com', - url='https://github.com/cloudwatt/swiftpolicy', - packages=['swiftpolicy', 'swiftpolicy.openstack', 'swiftpolicy.openstack.common'], - test_suite='tests', - data_files=[('/etc/swift', ['policies/default.json']),], - entry_points={'paste.filter_factory': - ['swiftpolicy=swiftpolicy.swiftpolicy:filter_factory']}) diff --git a/swiftpolicy/__init__.py b/swiftpolicy/__init__.py deleted file mode 100644 index 93259b2..0000000 --- a/swiftpolicy/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from swiftpolicy import filter_factory - -__all__ = [filter_factory, 'version_info', 'version'] - -#: Version information ``(major, minor, revision)``. -version_info = (1, 0, 0) -#: Version string ``'major.minor.revision'``. -version = '.'.join(map(str, version_info)) diff --git a/swiftpolicy/enforcer.py b/swiftpolicy/enforcer.py deleted file mode 100644 index ff170ab..0000000 --- a/swiftpolicy/enforcer.py +++ /dev/null @@ -1,201 +0,0 @@ -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from openstack.common import policy_parser as parser - -def get_enforcer(logger, policy_file): - parser.registry.register('logger', logger) - if policy_file: - return FileBasedEnforcer(policy_file, logger) - - -class Enforcer(object): - def __init__(self, rules=None): - self.rules = rules - - def enforce(self, rule, target, creds, do_raise=False, - exc=None, *args, **kwargs): - """Checks authorization of a rule against the target and credentials. - - :param rule: A string or BaseCheck instance specifying the rule - to evaluate. - :param target: As much information about the object being operated - on as possible, as a dictionary. - :param creds: As much information about the user performing the - action as possible, as a dictionary. - :param do_raise: Whether to raise an exception or not if check - fails. - :param exc: Class of the exception to raise if the check fails. - Any remaining arguments passed to check() (both - positional and keyword arguments) will be passed to - the exception class. If not specified, PolicyNotAuthorized - will be used. - - :return: Returns False if the policy does not allow the action and - exc is not provided; otherwise, returns a value that - evaluates to True. Note: for rules using the "case" - expression, this True value will be the specified string - from the expression. - """ - - # NOTE(flaper87): Not logging target or creds to avoid - # potential security issues. - - self.load_rules() - - # Allow the rule to be a Check tree - if isinstance(rule, parser.BaseCheck): - result = rule(target, creds, self) - elif not self.rules: - # No rules to reference means we're going to fail closed - result = False - else: - try: - # Evaluate the rule - result = self.rules[rule](target, creds, self) - except KeyError: - # If the rule doesn't exist, fail closed - result = False - - # If it is False, raise the exception if requested - if do_raise and not result: - if exc: - raise exc(*args, **kwargs) - - raise parser.PolicyNotAuthorized(rule) - - return result - - def load_rules(self, force_reload=False): - policy = self._get_policy() - rules = parser.Rules.load_json(policy) - self.rules = rules - - -class FileBasedEnforcer(Enforcer): - def __init__(self, policy_file, logger): - super(FileBasedEnforcer, self).__init__() - self.policy_file = policy_file - self.log = logger - - def _get_policy(self): - with open(self.policy_file, 'r') as policies: - policy = policies.read() - - return policy - -@parser.register("acl") -class AclCheck(parser.Check): - @staticmethod - def _authorize_cross_tenant(user_id, user_name, - tenant_id, tenant_name, acls): - """Check cross-tenant ACLs. - - Match tenant:user, tenant and user could be its id, name or '*' - - :param user_id: The user id from the identity token. - :param user_name: The user name from the identity token. - :param tenant_id: The tenant ID from the identity token. - :param tenant_name: The tenant name from the identity token. - :param acls: The given container ACL. - - :returns: matched string if tenant(name/id/*):user(name/id/*) matches - the given ACL. - None otherwise. - - """ - for tenant in [tenant_id, tenant_name, '*']: - for user in [user_id, user_name, '*']: - s = '%s:%s' % (tenant, user) - if s in acls: - return True - return False - - @staticmethod - def _check_role(roles, acls): - # Check if we have the role in the acls and allow it - for user_role in roles: - if user_role in (r.lower() for r in acls): - #log_msg = 'user %s:%s allowed in ACL: %s authorizing' - #self.logger.debug(log_msg, tenant_name, user_name, - # user_role) - return True - return False - - @staticmethod - def _authorize_unconfirmed_identity(req, obj, referrers, acls): - """" - Perform authorization for access that does not require a - confirmed identity. - - :returns: A boolean if authorization is granted or denied. None if - a determination could not be made. - """ - # Allow container sync. - if (req.environ.get('swift_sync_key') - and (req.environ['swift_sync_key'] == - req.headers.get('x-container-sync-key', None)) - and 'x-timestamp' in req.headers): - #log_msg = 'allowing proxy %s for container-sync' - #self.logger.debug(log_msg, req.remote_addr) - return True - - # Check if referrer is allowed. - from swift.common.middleware import acl as swift_acl - if swift_acl.referrer_allowed(req.referer, referrers): - if obj or '.rlistings' in acls: - #log_msg = 'authorizing %s via referer ACL' - #self.logger.debug(log_msg, req.referrer) - return True - return False - - def __call__(self, target, creds, enforcer): - """ """ - user_id = creds.get("user_id", None) - user_name = creds.get("user_name", None) - tenant_id = creds.get("tenant_id", None) - tenant_name = creds.get("tenant_name", None) - roles = creds.get("roles", None) - - acls = target["acls"] - req = target["req"] - obj = target["object"] - referrers = target["referrers"] - - if self.match == "check_cross_tenant": - res = self._authorize_cross_tenant(user_id, user_name, - tenant_id, tenant_name, - acls) - - elif self.match == "check_roles": - res = self._check_role(roles, acls) - - elif self.match == "check_is_public": - res = self._authorize_unconfirmed_identity(req, obj, - referrers, acls) - - else: - raise ValueError("{match} not allowed for rule 'acl'". - format(match=self.match)) - - enforcer.log.debug("Rule '%s' evaluated to %s" % (self.match, res)) - return res \ No newline at end of file diff --git a/swiftpolicy/openstack/__init__.py b/swiftpolicy/openstack/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/swiftpolicy/openstack/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/swiftpolicy/openstack/common/__init__.py b/swiftpolicy/openstack/common/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/swiftpolicy/openstack/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/swiftpolicy/openstack/common/policy_parser.py b/swiftpolicy/openstack/common/policy_parser.py deleted file mode 100644 index e2d8df3..0000000 --- a/swiftpolicy/openstack/common/policy_parser.py +++ /dev/null @@ -1,758 +0,0 @@ -# Copyright (c) 2012 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Common Policy Engine Implementation - -Policies can be expressed in one of two forms: A list of lists, or a -string written in the new policy language. - -In the list-of-lists representation, each check inside the innermost -list is combined as with an "and" conjunction--for that check to pass, -all the specified checks must pass. These innermost lists are then -combined as with an "or" conjunction. This is the original way of -expressing policies, but there now exists a new way: the policy -language. - -In the policy language, each check is specified the same way as in the -list-of-lists representation: a simple "a:b" pair that is matched to -the correct code to perform that check. However, conjunction -operators are available, allowing for more expressiveness in crafting -policies. - -As an example, take the following rule, expressed in the list-of-lists -representation:: - - [["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]] - -In the policy language, this becomes:: - - role:admin or (project_id:%(project_id)s and role:projectadmin) - -The policy language also has the "not" operator, allowing a richer -policy rule:: - - project_id:%(project_id)s and not role:dunce - -It is possible to perform policy checks on the following user -attributes (obtained through the token): user_id, domain_id or -project_id:: - - domain_id: - -Attributes sent along with API calls can be used by the policy engine -(on the right side of the expression), by using the following syntax:: - - :user.id - -Contextual attributes of objects identified by their IDs are loaded -from the database. They are also available to the policy engine and -can be checked through the `target` keyword:: - - :target.role.name - -All these attributes (related to users, API calls, and context) can be -checked against each other or against constants, be it literals (True, -) or strings. - -Finally, two special policy checks should be mentioned; the policy -check "@" will always accept an access, and the policy check "!" will -always reject an access. (Note that if a rule is either the empty -list ("[]") or the empty string, this is equivalent to the "@" policy -check.) Of these, the "!" policy check is probably the most useful, -as it allows particular rules to be explicitly disabled. -""" - -import abc -import ast -import gettext -import json -import logging -import re -import six -import six.moves.urllib.parse as urlparse -import six.moves.urllib.request as urlrequest - - -class Registry(object): - components = { - "rule_formatter": json, - "trans": gettext.gettext, - "trans_error": gettext.gettext, - "logger": logging.getLogger(__name__) - } - - def register(self, name, obj): - if name in self.components: - self.components[name] = obj - - def get(self, name): - return self.components.get(name, None) - -registry = Registry() - -# set global components. -rule_formatter = registry.get('rule_formatter') -_, _LE = registry.get('trans'), registry.get('trans_error') -log = registry.get('logger') - - -class PolicyNotAuthorized(Exception): - - def __init__(self, rule): - msg = _("Policy doesn't allow %s to be performed.") % rule - super(PolicyNotAuthorized, self).__init__(msg) - - -class Rules(dict): - """A store for rules. Handles the default_rule setting directly.""" - - @classmethod - def load_json(cls, data, default_rule=None): - """Allow loading of JSON rule data.""" - - # Suck in the JSON data and parse the rules - rules = dict((k, parse_rule(v)) for k, v in - rule_formatter.loads(data).items()) - - return cls(rules, default_rule) - - def __init__(self, rules=None, default_rule=None): - """Initialize the Rules store.""" - - super(Rules, self).__init__(rules or {}) - self.default_rule = default_rule - - def __missing__(self, key): - """Implements the default rule handling.""" - - if isinstance(self.default_rule, dict): - raise KeyError(key) - - # If the default rule isn't actually defined, do something - # reasonably intelligent - if not self.default_rule: - raise KeyError(key) - - if isinstance(self.default_rule, BaseCheck): - return self.default_rule - - # We need to check this or we can get infinite recursion - if self.default_rule not in self: - raise KeyError(key) - - elif isinstance(self.default_rule, six.string_types): - return self[self.default_rule] - - def __str__(self): - """Dumps a string representation of the rules.""" - - # Start by building the canonical strings for the rules - out_rules = {} - for key, value in self.items(): - # Use empty string for singleton TrueCheck instances - if isinstance(value, TrueCheck): - out_rules[key] = '' - else: - out_rules[key] = str(value) - - # Dump a pretty-printed JSON representation - return rule_formatter.dumps(out_rules, indent=4) - - -@six.add_metaclass(abc.ABCMeta) -class BaseCheck(object): - """Abstract base class for Check classes.""" - - @abc.abstractmethod - def __str__(self): - """String representation of the Check tree rooted at this node.""" - - pass - - @abc.abstractmethod - def __call__(self, target, cred, enforcer): - """Triggers if instance of the class is called. - - Performs the check. Returns False to reject the access or a - true value (not necessary True) to accept the access. - """ - - pass - - -class FalseCheck(BaseCheck): - """A policy check that always returns False (disallow).""" - - def __str__(self): - """Return a string representation of this check.""" - - return "!" - - def __call__(self, target, cred, enforcer): - """Check the policy.""" - - return False - - -class TrueCheck(BaseCheck): - """A policy check that always returns True (allow).""" - - def __str__(self): - """Return a string representation of this check.""" - - return "@" - - def __call__(self, target, cred, enforcer): - """Check the policy.""" - - return True - - -class Check(BaseCheck): - """A base class to allow for user-defined policy checks.""" - - def __init__(self, kind, match): - """Initiates Check instance. - - :param kind: The kind of the check, i.e., the field before the - ':'. - :param match: The match of the check, i.e., the field after - the ':'. - """ - - self.kind = kind - self.match = match - - def __str__(self): - """Return a string representation of this check.""" - - return "%s:%s" % (self.kind, self.match) - - -class NotCheck(BaseCheck): - """Implements the "not" logical operator. - - A policy check that inverts the result of another policy check. - """ - - def __init__(self, rule): - """Initialize the 'not' check. - - :param rule: The rule to negate. Must be a Check. - """ - - self.rule = rule - - def __str__(self): - """Return a string representation of this check.""" - - return "not %s" % self.rule - - def __call__(self, target, cred, enforcer): - """Check the policy. - - Returns the logical inverse of the wrapped check. - """ - - return not self.rule(target, cred, enforcer) - - -class AndCheck(BaseCheck): - """Implements the "and" logical operator. - - A policy check that requires that a list of other checks all return True. - """ - - def __init__(self, rules): - """Initialize the 'and' check. - - :param rules: A list of rules that will be tested. - """ - - self.rules = rules - - def __str__(self): - """Return a string representation of this check.""" - - return "(%s)" % ' and '.join(str(r) for r in self.rules) - - def __call__(self, target, cred, enforcer): - """Check the policy. - - Requires that all rules accept in order to return True. - """ - - for rule in self.rules: - if not rule(target, cred, enforcer): - return False - - return True - - def add_check(self, rule): - """Adds rule to be tested. - - Allows addition of another rule to the list of rules that will - be tested. Returns the AndCheck object for convenience. - """ - - self.rules.append(rule) - return self - - -class OrCheck(BaseCheck): - """Implements the "or" operator. - - A policy check that requires that at least one of a list of other - checks returns True. - """ - - def __init__(self, rules): - """Initialize the 'or' check. - - :param rules: A list of rules that will be tested. - """ - - self.rules = rules - - def __str__(self): - """Return a string representation of this check.""" - - return "(%s)" % ' or '.join(str(r) for r in self.rules) - - def __call__(self, target, cred, enforcer): - """Check the policy. - - Requires that at least one rule accept in order to return True. - """ - - for rule in self.rules: - if rule(target, cred, enforcer): - return True - return False - - def add_check(self, rule): - """Adds rule to be tested. - - Allows addition of another rule to the list of rules that will - be tested. Returns the OrCheck object for convenience. - """ - - self.rules.append(rule) - return self - -_checks = {} - - -def _parse_check(rule): - """Parse a single base check rule into an appropriate Check object.""" - - # Handle the special checks - if rule == '!': - return FalseCheck() - elif rule == '@': - return TrueCheck() - - try: - kind, match = rule.split(':', 1) - except Exception: - log.exception(_LE("Failed to understand rule %s") % rule) - # If the rule is invalid, we'll fail closed - return FalseCheck() - - # Find what implements the check - if kind in _checks: - return _checks[kind](kind, match) - elif None in _checks: - return _checks[None](kind, match) - else: - log.error(_LE("No handler for matches of kind %s") % kind) - return FalseCheck() - - -def _parse_list_rule(rule): - """Translates the old list-of-lists syntax into a tree of Check objects. - - Provided for backwards compatibility. - """ - - # Empty rule defaults to True - if not rule: - return TrueCheck() - - # Outer list is joined by "or"; inner list by "and" - or_list = [] - for inner_rule in rule: - # Elide empty inner lists - if not inner_rule: - continue - - # Handle bare strings - if isinstance(inner_rule, six.string_types): - inner_rule = [inner_rule] - - # Parse the inner rules into Check objects - and_list = [_parse_check(r) for r in inner_rule] - - # Append the appropriate check to the or_list - if len(and_list) == 1: - or_list.append(and_list[0]) - else: - or_list.append(AndCheck(and_list)) - - # If we have only one check, omit the "or" - if not or_list: - return FalseCheck() - elif len(or_list) == 1: - return or_list[0] - - return OrCheck(or_list) - - -# Used for tokenizing the policy language -_tokenize_re = re.compile(r'\s+') - - -def _parse_tokenize(rule): - """Tokenizer for the policy language. - - Most of the single-character tokens are specified in the - _tokenize_re; however, parentheses need to be handled specially, - because they can appear inside a check string. Thankfully, those - parentheses that appear inside a check string can never occur at - the very beginning or end ("%(variable)s" is the correct syntax). - """ - - for tok in _tokenize_re.split(rule): - # Skip empty tokens - if not tok or tok.isspace(): - continue - - # Handle leading parens on the token - clean = tok.lstrip('(') - for i in range(len(tok) - len(clean)): - yield '(', '(' - - # If it was only parentheses, continue - if not clean: - continue - else: - tok = clean - - # Handle trailing parens on the token - clean = tok.rstrip(')') - trail = len(tok) - len(clean) - - # Yield the cleaned token - lowered = clean.lower() - if lowered in ('and', 'or', 'not'): - # Special tokens - yield lowered, clean - elif clean: - # Not a special token, but not composed solely of ')' - if len(tok) >= 2 and ((tok[0], tok[-1]) in - [('"', '"'), ("'", "'")]): - # It's a quoted string - yield 'string', tok[1:-1] - else: - yield 'check', _parse_check(clean) - - # Yield the trailing parens - for i in range(trail): - yield ')', ')' - - -class ParseStateMeta(type): - """Metaclass for the ParseState class. - - Facilitates identifying reduction methods. - """ - - def __new__(mcs, name, bases, cls_dict): - """Create the class. - - Injects the 'reducers' list, a list of tuples matching token sequences - to the names of the corresponding reduction methods. - """ - - reducers = [] - - for key, value in cls_dict.items(): - if not hasattr(value, 'reducers'): - continue - for reduction in value.reducers: - reducers.append((reduction, key)) - - cls_dict['reducers'] = reducers - - return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict) - - -def reducer(*tokens): - """Decorator for reduction methods. - - Arguments are a sequence of tokens, in order, which should trigger running - this reduction method. - """ - - def decorator(func): - # Make sure we have a list of reducer sequences - if not hasattr(func, 'reducers'): - func.reducers = [] - - # Add the tokens to the list of reducer sequences - func.reducers.append(list(tokens)) - - return func - - return decorator - - -@six.add_metaclass(ParseStateMeta) -class ParseState(object): - """Implement the core of parsing the policy language. - - Uses a greedy reduction algorithm to reduce a sequence of tokens into - a single terminal, the value of which will be the root of the Check tree. - - Note: error reporting is rather lacking. The best we can get with - this parser formulation is an overall "parse failed" error. - Fortunately, the policy language is simple enough that this - shouldn't be that big a problem. - """ - - def __init__(self): - """Initialize the ParseState.""" - - self.tokens = [] - self.values = [] - - def reduce(self): - """Perform a greedy reduction of the token stream. - - If a reducer method matches, it will be executed, then the - reduce() method will be called recursively to search for any more - possible reductions. - """ - - for reduction, methname in self.reducers: - if (len(self.tokens) >= len(reduction) and - self.tokens[-len(reduction):] == reduction): - # Get the reduction method - meth = getattr(self, methname) - - # Reduce the token stream - results = meth(*self.values[-len(reduction):]) - - # Update the tokens and values - self.tokens[-len(reduction):] = [r[0] for r in results] - self.values[-len(reduction):] = [r[1] for r in results] - - # Check for any more reductions - return self.reduce() - - def shift(self, tok, value): - """Adds one more token to the state. Calls reduce().""" - - self.tokens.append(tok) - self.values.append(value) - - # Do a greedy reduce... - self.reduce() - - @property - def result(self): - """Obtain the final result of the parse. - - Raises ValueError if the parse failed to reduce to a single result. - """ - - if len(self.values) != 1: - raise ValueError("Could not parse rule") - return self.values[0] - - @reducer('(', 'check', ')') - @reducer('(', 'and_expr', ')') - @reducer('(', 'or_expr', ')') - def _wrap_check(self, _p1, check, _p2): - """Turn parenthesized expressions into a 'check' token.""" - - return [('check', check)] - - @reducer('check', 'and', 'check') - def _make_and_expr(self, check1, _and, check2): - """Create an 'and_expr'. - - Join two checks by the 'and' operator. - """ - - return [('and_expr', AndCheck([check1, check2]))] - - @reducer('and_expr', 'and', 'check') - def _extend_and_expr(self, and_expr, _and, check): - """Extend an 'and_expr' by adding one more check.""" - - return [('and_expr', and_expr.add_check(check))] - - @reducer('check', 'or', 'check') - def _make_or_expr(self, check1, _or, check2): - """Create an 'or_expr'. - - Join two checks by the 'or' operator. - """ - - return [('or_expr', OrCheck([check1, check2]))] - - @reducer('or_expr', 'or', 'check') - def _extend_or_expr(self, or_expr, _or, check): - """Extend an 'or_expr' by adding one more check.""" - - return [('or_expr', or_expr.add_check(check))] - - @reducer('not', 'check') - def _make_not_expr(self, _not, check): - """Invert the result of another check.""" - - return [('check', NotCheck(check))] - - -def _parse_text_rule(rule): - """Parses policy to the tree. - - Translates a policy written in the policy language into a tree of - Check objects. - """ - - # Empty rule means always accept - if not rule: - return TrueCheck() - - # Parse the token stream - state = ParseState() - for tok, value in _parse_tokenize(rule): - state.shift(tok, value) - - try: - return state.result - except ValueError: - # Couldn't parse the rule - log.exception(_LE("Failed to understand rule %r") % rule) - - # Fail closed - return FalseCheck() - - -def parse_rule(rule): - """Parses a policy rule into a tree of Check objects.""" - - # If the rule is a string, it's in the policy language - if isinstance(rule, six.string_types): - return _parse_text_rule(rule) - return _parse_list_rule(rule) - - -def register(name, func=None): - """Register a function or Check class as a policy check. - - :param name: Gives the name of the check type, e.g., 'rule', - 'role', etc. If name is None, a default check type - will be registered. - :param func: If given, provides the function or class to register. - If not given, returns a function taking one argument - to specify the function or class to register, - allowing use as a decorator. - """ - - # Perform the actual decoration by registering the function or - # class. Returns the function or class for compliance with the - # decorator interface. - def decorator(func): - _checks[name] = func - return func - - # If the function or class is given, do the registration - if func: - return decorator(func) - - return decorator - - -@register("rule") -class RuleCheck(Check): - def __call__(self, target, creds, enforcer): - """Recursively checks credentials based on the defined rules.""" - - try: - return enforcer.rules[self.match](target, creds, enforcer) - except KeyError: - # We don't have any matching rule; fail closed - return False - - -@register("role") -class RoleCheck(Check): - def __call__(self, target, creds, enforcer): - """Check that there is a matching role in the cred dict.""" - - return self.match.lower() in [x.lower() for x in creds['roles']] - - -@register('http') -class HttpCheck(Check): - def __call__(self, target, creds, enforcer): - """Check http: rules by calling to a remote server. - - This example implementation simply verifies that the response - is exactly 'True'. - """ - - url = ('http:' + self.match) % target - data = {'target': rule_formatter.dumps(target), - 'credentials': rule_formatter.dumps(creds)} - post_data = urlparse.urlencode(data) - f = urlrequest.urlopen(url, post_data) - return f.read() == "True" - - -@register(None) -class GenericCheck(Check): - def __call__(self, target, creds, enforcer): - """Check an individual match. - - Matches look like: - - tenant:%(tenant_id)s - role:compute:admin - True:%(user.enabled)s - 'Member':%(role.name)s - """ - - # TODO(termie): do dict inspection via dot syntax - try: - match = self.match % target - except KeyError: - # While doing GenericCheck if key not - # present in Target return false - return False - - try: - # Try to interpret self.kind as a literal - leftval = ast.literal_eval(self.kind) - except ValueError: - try: - leftval = creds[self.kind] - except KeyError: - return False - return match == six.text_type(leftval) diff --git a/swiftpolicy/swiftpolicy.py b/swiftpolicy/swiftpolicy.py deleted file mode 100644 index 6194dad..0000000 --- a/swiftpolicy/swiftpolicy.py +++ /dev/null @@ -1,271 +0,0 @@ -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -from swift.common import utils as swift_utils -from swift.common.middleware import acl as swift_acl -from swift.common.swob import HTTPNotFound, HTTPForbidden, HTTPUnauthorized -from swift.common.swob import Request -from swift.common.utils import register_swift_info -from enforcer import get_enforcer - - -class SwiftPolicy(object): - """Swift middleware to handle Keystone authorization based - openstack policy.json format - - In Swift's proxy-server.conf add this middleware to your pipeline:: - - [pipeline:main] - pipeline = catch_errors cache authtoken swiftpolicy proxy-server - - Make sure you have the authtoken middleware before the - swiftpolicy middleware. - - The authtoken middleware will take care of validating the user and - swiftpolicy will authorize access. - - The authtoken middleware is shipped directly with keystone it - does not have any other dependences than itself so you can either - install it by copying the file directly in your python path or by - installing keystone. - - If support is required for unvalidated users (as with anonymous - access) or for formpost/staticweb/tempurl middleware, authtoken will - need to be configured with ``delay_auth_decision`` set to true. See - the Keystone documentation for more detail on how to configure the - authtoken middleware. - - In proxy-server.conf you will need to have the setting account - auto creation to true:: - - [app:proxy-server] - account_autocreate = true - - And add a swift authorization filter section, such as:: - - [filter:swiftpolicy] - use = egg:swiftpolicy#swiftpolicy - policy = /path/to/policy.json - - This maps tenants to account in Swift. - - If you need to have a different reseller_prefix to be able to - mix different auth servers you can configure the option - ``reseller_prefix`` in your swiftpolicy entry like this:: - - reseller_prefix = NEWAUTH - - :param app: The next WSGI app in the pipeline - :param conf: The dict of configuration values - """ - def __init__(self, app, conf): - self.app = app - self.conf = conf - self.logger = swift_utils.get_logger(conf, log_route='swiftpolicy') - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_').strip() - if self.reseller_prefix and self.reseller_prefix[-1] != '_': - self.reseller_prefix += '_' - #self.operator_roles = conf.get('operator_roles', - # 'admin, swiftoperator').lower() - #self.reseller_admin_role = conf.get('reseller_admin_role', - # 'ResellerAdmin').lower() - #config_is_admin = conf.get('is_admin', "false").lower() - #self.is_admin = swift_utils.config_true_value(config_is_admin) - config_overrides = conf.get('allow_overrides', 't').lower() - self.allow_overrides = swift_utils.config_true_value(config_overrides) - self.policy_file = conf.get('policy', 'default.json') - - def __call__(self, environ, start_response): - identity = self._keystone_identity(environ) - - # Check if one of the middleware like tempurl or formpost have - # set the swift.authorize_override environ and want to control the - # authentication - if (self.allow_overrides and - environ.get('swift.authorize_override', False)): - msg = 'Authorizing from an overriding middleware (i.e: tempurl)' - self.logger.debug(msg) - return self.app(environ, start_response) - - if identity: - self.logger.debug('Using identity: %r', identity) - environ['keystone.identity'] = identity - environ['REMOTE_USER'] = identity.get('tenant') - environ['swift.authorize'] = self.authorize - # Check reseller_request against policy - if self.check_action('reseller_request', environ): - environ['reseller_request'] = True - else: - self.logger.debug('Authorizing as anonymous') - environ['swift.authorize'] = self.authorize - - environ['swift.clean_acl'] = swift_acl.clean_acl - - return self.app(environ, start_response) - - def _keystone_identity(self, environ): - """Extract the identity from the Keystone auth component.""" - # In next release, we would add user id in env['keystone.identity'] by - # using _integral_keystone_identity to replace current - # _keystone_identity. The purpose of keeping it in this release it for - # back compatibility. - if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed': - return - roles = [] - if 'HTTP_X_ROLES' in environ: - roles = environ['HTTP_X_ROLES'].split(',') - identity = {'user': environ.get('HTTP_X_USER_NAME'), - 'tenant': (environ.get('HTTP_X_TENANT_ID'), - environ.get('HTTP_X_TENANT_NAME')), - 'roles': roles} - return identity - - def _integral_keystone_identity(self, environ): - """Extract the identity from the Keystone auth component.""" - if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed': - return - roles = [] - if 'HTTP_X_ROLES' in environ: - roles = environ['HTTP_X_ROLES'].split(',') - identity = {'user': (environ.get('HTTP_X_USER_ID'), - environ.get('HTTP_X_USER_NAME')), - 'tenant': (environ.get('HTTP_X_TENANT_ID'), - environ.get('HTTP_X_TENANT_NAME')), - 'roles': roles} - return identity - - def _get_account_for_tenant(self, tenant_id): - return '%s%s' % (self.reseller_prefix, tenant_id) - - def get_creds(self, environ): - req = Request(environ) - try: - parts = req.split_path(1, 4, True) - _, account, _, _ = parts - except ValueError: - account = None - - env_identity = self._integral_keystone_identity(environ) - if not env_identity: - # user identity is not confirmed. (anonymous?) - creds = { - 'identity': None, - 'is_authoritative': (account and - account.startswith(self.reseller_prefix)) - } - return creds - - tenant_id, tenant_name = env_identity['tenant'] - user_id, user_name = env_identity['user'] - roles = [r.strip() for r in env_identity.get('roles', [])] - account = self._get_account_for_tenant(tenant_id) - is_admin = (tenant_name == user_name) - - creds = { - "identity": env_identity, - "roles": roles, - "account": account, - "tenant_id": tenant_id, - "tenant_name": tenant_name, - "user_id": user_id, - "user_name": user_name, - "is_admin": is_admin - } - return creds - - def get_target(self, environ): - req = Request(environ) - try: - parts = req.split_path(1, 4, True) - version, account, container, obj = parts - except ValueError: - version = account = container = obj = None - - referrers, acls = swift_acl.parse_acl(getattr(req, 'acl', None)) - target = { - "req": req, - "method": req.method.lower(), - "version": version, - "account": account, - "container": container, - "object": obj, - "acls": acls, - "referrers": referrers - } - return target - - @staticmethod - def get_action(method, parts): - version, account, container, obj = parts - action = method.lower() + "_" - if obj: - action += "object" - elif container: - action += "container" - elif account: - action += "account" - - return action - - def check_action(self, action, environ): - creds = self.get_creds(environ) - target = self.get_target(environ) - enforcer = get_enforcer(self.logger, self.policy_file) - self.logger.debug("enforce action '%s'", action) - return enforcer.enforce(action, target, creds) - - def authorize(self, req): - try: - parts = req.split_path(1, 4, True) - except ValueError: - return HTTPNotFound(request=req) - - env = req.environ - action = self.get_action(req.method, parts) - - if self.check_action(action, env): - if self.check_action("swift_owner", env): - req.environ['swift_owner'] = True - return - return self.denied_response(req) - - def denied_response(self, req): - """Deny WSGI Response. - - 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: - return HTTPUnauthorized(request=req) - - -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) - register_swift_info('swiftpolicy') - - def auth_filter(app): - return SwiftPolicy(app, conf) - return auth_filter diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/policies/default.json b/tests/policies/default.json deleted file mode 100644 index 38444f4..0000000 --- a/tests/policies/default.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "is_anonymous": "identity:None", - "is_authenticated": "not rule:is_anonymous", - "swift_reseller": "role:reseller", - "swift_operator": "role:admin or role:swiftoperator", - - "swift_owner": "rule:swift_reseller or rule:swift_operator", - - "reseller_request": "rule:swift_reseller", - "same_tenant": "account:%(account)s", - "tenant_mismatch": "not rule:same_tenant", - - "allowed_for_authenticated": "rule:swift_reseller or acl:check_cross_tenant or acl:check_is_public or (rule:same_tenant and rule:swift_operator) or (rule:same_tenant and acl:check_roles)", - "allowed_for_anonymous": "is_authoritative:True and acl:check_is_public", - - "allowed_for_user": "(rule:is_authenticated and rule:allowed_for_authenticated) or rule:allowed_for_anonymous", - - "get_account": "rule:allowed_for_user", - "post_account": "rule:allowed_for_user", - "head_account": "rule:allowed_for_user", - "delete_account": "rule:swift_reseller", - "options_account": "", - "get_container": "rule:allowed_for_user", - "put_container": "rule:allowed_for_user", - "delete_container": "rule:allowed_for_user", - "post_container": "rule:allowed_for_user", - "head_container": "rule:allowed_for_user", - "options_container": "", - "get_object": "rule:allowed_for_user", - "put_object": "rule:allowed_for_user", - "copy_object": "rule:allowed_for_user", - "delete_object": "rule:allowed_for_user", - "head_object": "rule:allowed_for_user", - "post_object": "rule:allowed_for_user", - "options_object": "" -} \ No newline at end of file diff --git a/tests/test_swiftpolicy.py b/tests/test_swiftpolicy.py deleted file mode 100644 index f6246d0..0000000 --- a/tests/test_swiftpolicy.py +++ /dev/null @@ -1,527 +0,0 @@ -# This software is released under the MIT License. -# -# Copyright (c) 2014 Cloudwatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -import logging -import time -import unittest -from collections import defaultdict - -from swiftpolicy import swiftpolicy -from swift.common.swob import Request, Response -from swift.common.http import HTTP_FORBIDDEN -from swiftpolicy.enforcer import AclCheck - - -class UnmockTimeModule(object): - """ - Even if a test mocks time.time - you can restore unmolested behavior in a - another module who imports time directly by monkey patching it's imported - reference to the module with an instance of this class - """ - - _orig_time = time.time - - def __getattribute__(self, name): - if name == 'time': - return UnmockTimeModule._orig_time - return getattr(time, name) - - -# logging.LogRecord.__init__ calls time.time -logging.time = UnmockTimeModule() - - -class FakeLogger(logging.Logger): - # a thread safe logger - - def __init__(self, *args, **kwargs): - self._clear() - self.name = 'swift.unit.fake_logger' - self.level = logging.NOTSET - if 'facility' in kwargs: - self.facility = kwargs['facility'] - self.statsd_client = None - self.thread_locals = None - - def _clear(self): - self.log_dict = defaultdict(list) - self.lines_dict = defaultdict(list) - - def _store_in(store_name): - def stub_fn(self, *args, **kwargs): - self.log_dict[store_name].append((args, kwargs)) - return stub_fn - - def _store_and_log_in(store_name): - def stub_fn(self, *args, **kwargs): - self.log_dict[store_name].append((args, kwargs)) - self._log(store_name, args[0], args[1:], **kwargs) - return stub_fn - - def get_lines_for_level(self, level): - return self.lines_dict[level] - - error = _store_and_log_in('error') - info = _store_and_log_in('info') - warning = _store_and_log_in('warning') - warn = _store_and_log_in('warning') - debug = _store_and_log_in('debug') - - def exception(self, *args, **kwargs): - self.log_dict['exception'].append((args, kwargs, - str(sys.exc_info()[1]))) - print 'FakeLogger Exception: %s' % self.log_dict - - # mock out the StatsD logging methods: - increment = _store_in('increment') - decrement = _store_in('decrement') - timing = _store_in('timing') - timing_since = _store_in('timing_since') - update_stats = _store_in('update_stats') - set_statsd_prefix = _store_in('set_statsd_prefix') - - def get_increments(self): - return [call[0][0] for call in self.log_dict['increment']] - - def get_increment_counts(self): - counts = {} - for metric in self.get_increments(): - if metric not in counts: - counts[metric] = 0 - counts[metric] += 1 - return counts - - def setFormatter(self, obj): - self.formatter = obj - - def close(self): - self._clear() - - def set_name(self, name): - # don't touch _handlers - self._name = name - - def acquire(self): - pass - - def release(self): - pass - - def createLock(self): - pass - - def emit(self, record): - pass - - def _handle(self, record): - try: - line = record.getMessage() - except TypeError: - print 'WARNING: unable to format log message %r %% %r' % ( - record.msg, record.args) - raise - self.lines_dict[record.levelno].append(line) - - def handle(self, record): - self._handle(record) - - def flush(self): - pass - - def handleError(self, record): - pass - - -class FakeApp(object): - def __init__(self, status_headers_body_iter=None): - self.calls = 0 - self.status_headers_body_iter = status_headers_body_iter - if not self.status_headers_body_iter: - self.status_headers_body_iter = iter([('404 Not Found', {}, '')]) - - def __call__(self, env, start_response): - self.calls += 1 - self.request = Request.blank('', environ=env) - if 'swift.authorize' in env: - resp = env['swift.authorize'](self.request) - if resp: - return resp(env, start_response) - status, headers, body = self.status_headers_body_iter.next() - return Response(status=status, headers=headers, - body=body)(env, start_response) - - -class SwiftAuth(unittest.TestCase): - def setUp(self): - self.test_auth = swiftpolicy.filter_factory({'policy': 'policies/default.json'})(FakeApp()) - # set in default.json - self.reseller_admin_role = "reseller" - self.test_auth.logger = FakeLogger() - - def _make_request(self, path=None, headers=None, **kwargs): - if not path: - path = '/v1/%s/c/o' % self.test_auth._get_account_for_tenant('foo') - return Request.blank(path, headers=headers, **kwargs) - - def _get_identity_headers(self, status='Confirmed', tenant_id='1', - tenant_name='acct', user='usr', role=''): - return dict(X_IDENTITY_STATUS=status, - X_TENANT_ID=tenant_id, - X_TENANT_NAME=tenant_name, - X_ROLES=role, - X_USER_NAME=user) - - def _get_successful_middleware(self): - response_iter = iter([('200 OK', {}, '')]) - return swiftpolicy.filter_factory({'policy': 'policies/default.json'})(FakeApp(response_iter)) - - def test_invalid_request_authorized(self): - role = self.reseller_admin_role - headers = self._get_identity_headers(role=role) - req = self._make_request('/', headers=headers) - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 404) - - def test_invalid_request_non_authorized(self): - req = self._make_request('/') - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 404) - - def test_confirmed_identity_is_authorized(self): - role = self.reseller_admin_role - headers = self._get_identity_headers(role=role) - req = self._make_request('/v1/AUTH_acct/c', headers) - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 200) - - def test_detect_reseller_request(self): - role = self.reseller_admin_role - headers = self._get_identity_headers(role=role) - req = self._make_request('/v1/AUTH_acct/c', headers) - req.get_response(self._get_successful_middleware()) - self.assertTrue(req.environ.get('reseller_request')) - - def test_confirmed_identity_is_not_authorized(self): - headers = self._get_identity_headers() - req = self._make_request('/v1/AUTH_acct/c', headers) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 403) - - def test_anonymous_is_authorized_for_permitted_referrer(self): - req = self._make_request(headers={'X_IDENTITY_STATUS': 'Invalid'}) - req.acl = '.r:*' - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 200) - - def test_anonymous_with_validtoken_authorized_for_permitted_referrer(self): - req = self._make_request(headers={'X_IDENTITY_STATUS': 'Confirmed'}) - req.acl = '.r:*' - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 200) - - def test_anonymous_is_not_authorized_for_unknown_reseller_prefix(self): - req = self._make_request(path='/v1/BLAH_foo/c/o', - headers={'X_IDENTITY_STATUS': 'Invalid'}) - # check user is not authorized even object is "public" - req.acl = '.r:*' - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - - def test_blank_reseller_prefix(self): - conf = {'reseller_prefix': ''} - test_auth = swiftpolicy.filter_factory(conf)(FakeApp()) - account = tenant_id = 'foo' - self.assertEqual(account, test_auth._get_account_for_tenant(tenant_id)) - - def test_reseller_prefix_added_underscore(self): - conf = {'reseller_prefix': 'AUTH'} - test_auth = swiftpolicy.filter_factory(conf)(FakeApp()) - self.assertEqual(test_auth.reseller_prefix, "AUTH_") - - def test_reseller_prefix_not_added_double_underscores(self): - conf = {'reseller_prefix': 'AUTH_'} - test_auth = swiftpolicy.filter_factory(conf)(FakeApp()) - self.assertEqual(test_auth.reseller_prefix, "AUTH_") - - def test_override_asked_for_but_not_allowed(self): - conf = {'allow_overrides': 'false', 'policy': 'policies/default.json'} - self.test_auth = swiftpolicy.filter_factory(conf)(FakeApp()) - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEquals(resp.status_int, 401) - - def test_override_asked_for_and_allowed(self): - conf = {'allow_overrides': 'true'} - self.test_auth = swiftpolicy.filter_factory(conf)(FakeApp()) - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEquals(resp.status_int, 404) - - def test_override_default_allowed(self): - req = self._make_request('/v1/AUTH_account', - environ={'swift.authorize_override': True}) - resp = req.get_response(self.test_auth) - self.assertEquals(resp.status_int, 404) - - def test_anonymous_options_allowed(self): - req = self._make_request('/v1/AUTH_account', - environ={'REQUEST_METHOD': 'OPTIONS'}) - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 200) - - def test_identified_options_allowed(self): - headers = self._get_identity_headers() - headers['REQUEST_METHOD'] = 'OPTIONS' - req = self._make_request('/v1/AUTH_account', - headers=self._get_identity_headers(), - environ={'REQUEST_METHOD': 'OPTIONS'}) - resp = req.get_response(self._get_successful_middleware()) - self.assertEqual(resp.status_int, 200) - - def test_auth_scheme(self): - req = self._make_request(path='/v1/BLAH_foo/c/o', - headers={'X_IDENTITY_STATUS': 'Invalid'}) - resp = req.get_response(self.test_auth) - self.assertEqual(resp.status_int, 401) - self.assertTrue('Www-Authenticate' in resp.headers) - - -class TestAuthorize(unittest.TestCase): - def setUp(self): - self.test_auth = swiftpolicy.filter_factory({'policy': 'policies/default.json'})(FakeApp()) - # set in default.json - self.reseller_admin_role = "reseller" - self.operator_roles = ["admin", "swiftoperator",] - self.test_auth.logger = FakeLogger() - - def _make_request(self, path, **kwargs): - return Request.blank(path, **kwargs) - - def _get_account(self, identity=None): - if not identity: - identity = self._get_identity() - return self.test_auth._get_account_for_tenant( - identity['HTTP_X_TENANT_ID']) - - def _get_identity(self, tenant_id='tenant_id', tenant_name='tenant_name', - user_id='user_id', user_name='user_name', roles=[]): - if isinstance(roles, list): - roles = ','.join(roles) - return {'HTTP_X_USER_ID': user_id, - 'HTTP_X_USER_NAME': user_name, - 'HTTP_X_TENANT_ID': tenant_id, - 'HTTP_X_TENANT_NAME': tenant_name, - 'HTTP_X_ROLES': roles, - 'HTTP_X_IDENTITY_STATUS': 'Confirmed'} - - def _check_authenticate(self, account=None, identity=None, headers=None, - exception=None, acl=None, env=None, path=None): - if not identity: - identity = self._get_identity() - if not account: - account = self._get_account(identity) - if not path: - path = '/v1/%s/c' % account - default_env = {'REMOTE_USER': identity['HTTP_X_TENANT_ID']} - default_env.update(identity) - if env: - default_env.update(env) - req = self._make_request(path, headers=headers, environ=default_env) - req.acl = acl - result = self.test_auth.authorize(req) - - # if we have requested an exception but nothing came back then - if exception and not result: - self.fail("error %s was not returned" % (str(exception))) - elif exception: - self.assertEquals(result.status_int, exception) - else: - self.assertTrue(result is None) - return req - - def test_authorize_fails_for_unauthorized_user(self): - self._check_authenticate(exception=HTTP_FORBIDDEN) - - def test_authorize_fails_for_invalid_reseller_prefix(self): - self._check_authenticate(account='BLAN_a', - exception=HTTP_FORBIDDEN) - - def test_authorize_succeeds_for_reseller_admin(self): - roles = [self.reseller_admin_role] - identity = self._get_identity(roles=roles) - req = self._check_authenticate(identity=identity) - self.assertTrue(req.environ.get('swift_owner')) - - def test_authorize_succeeds_for_insensitive_reseller_admin(self): - roles = [self.reseller_admin_role.upper()] - identity = self._get_identity(roles=roles) - req = self._check_authenticate(identity=identity) - self.assertTrue(req.environ.get('swift_owner')) - - def test_authorize_succeeds_as_owner_for_operator_role(self): - roles = self.operator_roles - identity = self._get_identity(roles=roles) - req = self._check_authenticate(identity=identity) - self.assertTrue(req.environ.get('swift_owner')) - - def test_authorize_succeeds_as_owner_for_insensitive_operator_role(self): - roles = [r.upper() for r in self.operator_roles] - identity = self._get_identity(roles=roles) - req = self._check_authenticate(identity=identity) - self.assertTrue(req.environ.get('swift_owner')) - - def test_authorize_succeeds_for_container_sync(self): - env = {'swift_sync_key': 'foo', 'REMOTE_ADDR': '127.0.0.1'} - headers = {'x-container-sync-key': 'foo', 'x-timestamp': '1'} - self._check_authenticate(env=env, headers=headers) - - def test_authorize_fails_for_invalid_referrer(self): - env = {'HTTP_REFERER': 'http://invalid.com/index.html'} - self._check_authenticate(acl='.r:example.com', env=env, - exception=HTTP_FORBIDDEN) - - def test_authorize_fails_for_referrer_without_rlistings(self): - env = {'HTTP_REFERER': 'http://example.com/index.html'} - self._check_authenticate(acl='.r:example.com', env=env, - exception=HTTP_FORBIDDEN) - - def test_authorize_succeeds_for_referrer_with_rlistings(self): - env = {'HTTP_REFERER': 'http://example.com/index.html'} - self._check_authenticate(acl='.r:example.com,.rlistings', env=env) - - def test_authorize_succeeds_for_referrer_with_obj(self): - path = '/v1/%s/c/o' % self._get_account() - env = {'HTTP_REFERER': 'http://example.com/index.html'} - self._check_authenticate(acl='.r:example.com', env=env, path=path) - - def test_authorize_succeeds_for_user_role_in_roles(self): - acl = 'allowme' - identity = self._get_identity(roles=[acl]) - self._check_authenticate(identity=identity, acl=acl) - - def test_authorize_succeeds_for_tenant_name_user_in_roles(self): - identity = self._get_identity() - user_name = identity['HTTP_X_USER_NAME'] - user_id = identity['HTTP_X_USER_ID'] - tenant_id = identity['HTTP_X_TENANT_ID'] - for user in [user_id, user_name, '*']: - acl = '%s:%s' % (tenant_id, user) - self._check_authenticate(identity=identity, acl=acl) - - def test_authorize_succeeds_for_tenant_id_user_in_roles(self): - identity = self._get_identity() - user_name = identity['HTTP_X_USER_NAME'] - user_id = identity['HTTP_X_USER_ID'] - tenant_name = identity['HTTP_X_TENANT_NAME'] - for user in [user_id, user_name, '*']: - acl = '%s:%s' % (tenant_name, user) - self._check_authenticate(identity=identity, acl=acl) - - def test_authorize_succeeds_for_wildcard_tenant_user_in_roles(self): - identity = self._get_identity() - user_name = identity['HTTP_X_USER_NAME'] - user_id = identity['HTTP_X_USER_ID'] - for user in [user_id, user_name, '*']: - acl = '*:%s' % user - self._check_authenticate(identity=identity, acl=acl) - - def test_delete_own_account_not_allowed(self): - roles = self.operator_roles - identity = self._get_identity(roles=roles) - account = self._get_account(identity) - self._check_authenticate(account=account, - identity=identity, - exception=HTTP_FORBIDDEN, - path='/v1/' + account, - env={'REQUEST_METHOD': 'DELETE'}) - - def test_delete_own_account_when_reseller_allowed(self): - roles = [self.reseller_admin_role] - identity = self._get_identity(roles=roles) - account = self._get_account(identity) - req = self._check_authenticate(account=account, - identity=identity, - path='/v1/' + account, - env={'REQUEST_METHOD': 'DELETE'}) - self.assertEqual(bool(req.environ.get('swift_owner')), True) - - -class TestAclCheckCrossTenant(unittest.TestCase): - def setUp(self): - self.cross_tenant_check = AclCheck._authorize_cross_tenant - - def test_cross_tenant_authorization_success(self): - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', - ['tenantID:userA']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', - ['tenantNAME:userA']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', ['*:userA']), - True) - - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', - ['tenantID:userID']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', - ['tenantNAME:userID']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', ['*:userID']), - True) - - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', ['tenantID:*']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', ['tenantNAME:*']), - True) - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', ['*:*']), - True) - - def test_cross_tenant_authorization_failure(self): - self.assertEqual( - self.cross_tenant_check( - 'userID', 'userA', 'tenantID', 'tenantNAME', - ['tenantXYZ:userA']), - False) - - -if __name__ == '__main__': - unittest.main() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index c6567b3..0000000 --- a/tox.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist = py27 - -[testenv] -deps = nose - -r{toxinidir}/requirements -commands=nosetests -usedevelop = True