Retire stackforge/swiftpolicy
This commit is contained in:
parent
5e17cff750
commit
123dfbe40b
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,6 +0,0 @@
|
|||||||
*.egg-info
|
|
||||||
*.pyc
|
|
||||||
.idea
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.tox
|
|
@ -1,4 +0,0 @@
|
|||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=stackforge/swiftpolicy.git
|
|
112
README.md
112
README.md
@ -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: ``<http verb>_<swift entity>`` (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.
|
|
7
README.rst
Normal file
7
README.rst
Normal file
@ -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".
|
||||||
|
|
@ -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": ""
|
|
||||||
}
|
|
@ -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
|
|
39
setup.py
39
setup.py
@ -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']})
|
|
@ -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))
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -1 +0,0 @@
|
|||||||
|
|
@ -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:<some_value>
|
|
||||||
|
|
||||||
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::
|
|
||||||
|
|
||||||
<some_value>: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::
|
|
||||||
|
|
||||||
<some_value>: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,
|
|
||||||
<a_number>) 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)
|
|
@ -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
|
|
@ -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": ""
|
|
||||||
}
|
|
@ -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()
|
|
Loading…
x
Reference in New Issue
Block a user