Author: jure Date: Mon Mar 18 09:46:02 2013 New Revision: 1457686 URL: http://svn.apache.org/r1457686 Log: #438, implement and enforce product permission policy, patch t438_r1456016_product_perms.diff applied (from Olemis)
Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/perm.py Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/perm.py Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py?rev=1457686&r1=1457685&r2=1457686&view=diff ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py (original) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py Mon Mar 18 09:46:02 2013 @@ -353,7 +353,12 @@ class MultiProductSystem(Component): def get_permission_actions(self): acts = ['PRODUCT_CREATE', 'PRODUCT_DELETE', 'PRODUCT_MODIFY', 'PRODUCT_VIEW'] - return acts + [('PRODUCT_ADMIN', acts)] + [('ROADMAP_ADMIN', acts)] + if not isinstance(self.env, ProductEnvironment): + return acts + [('PRODUCT_ADMIN', acts)] + [('ROADMAP_ADMIN', acts)] + else: + # In product context PRODUCT_ADMIN will be provided by product env + # to ensure it will always be handy + return acts # ITicketFieldProvider methods def get_select_fields(self): Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py?rev=1457686&r1=1457685&r2=1457686&view=diff ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py (original) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py Mon Mar 18 09:46:02 2013 @@ -22,12 +22,13 @@ __all__ = 'Configuration', 'Section' import os.path -from trac.config import Configuration, ConfigurationError, Option, Section, \ - _use_default +from trac.config import Configuration, ConfigurationError, Option, \ + OrderedExtensionsOption, Section, _use_default from trac.resource import ResourceNotFound from trac.util.text import to_unicode from multiproduct.model import ProductSetting +from multiproduct.perm import MultiproductPermissionPolicy class Configuration(Configuration): """Product-aware settings repository equivalent to instances of @@ -315,3 +316,21 @@ class Section(Section): path = os.path.join(env.path, 'conf', path) return os.path.normcase(os.path.realpath(path)) +#-------------------- +# Option override classes +#-------------------- + +class ProductPermissionPolicyOption(OrderedExtensionsOption): + """Prepend an instance of `multiproduct.perm.MultiproductPermissionPolicy` + """ + def __get__(self, instance, owner): + # FIXME: Better handling of recursive imports + from multiproduct.env import ProductEnvironment + + if instance is None: + return self + components = OrderedExtensionsOption.__get__(self, instance, owner) + env = getattr(instance, 'env', None) + return [MultiproductPermissionPolicy(env)] + components \ + if isinstance(env, ProductEnvironment) \ + else components Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py?rev=1457686&r1=1457685&r2=1457686&view=diff ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py (original) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py Mon Mar 18 09:46:02 2013 @@ -16,16 +16,17 @@ # under the License. """Bloodhound product environment and related APIs""" -from multiproduct.hooks import MultiProductEnvironmentFactory import os.path from urlparse import urlsplit from sqlite3 import OperationalError from trac.config import BoolOption, ConfigSection, Option -from trac.core import Component, ComponentManager, ComponentMeta, \ - ExtensionPoint, implements, Interface -from trac.db.api import TransactionContextManager, QueryContextManager, DatabaseManager +from trac.core import Component, ComponentManager, ExtensionPoint, implements, \ + ComponentMeta +from trac.db.api import TransactionContextManager, QueryContextManager, \ + DatabaseManager +from trac.perm import IPermissionRequestor, PermissionSystem from trac.util import get_pkginfo, lazy from trac.util.compat import sha1 from trac.util.text import to_unicode, unicode_quote @@ -35,7 +36,8 @@ from trac.web.href import Href from multiproduct.api import MultiProductSystem, ISupportMultiProductEnvironment from multiproduct.cache import lru_cache, default_keymap from multiproduct.config import Configuration -from multiproduct.dbcursor import ProductEnvContextManager, BloodhoundConnectionWrapper, BloodhoundIterableCursor +from multiproduct.dbcursor import BloodhoundConnectionWrapper, BloodhoundIterableCursor, \ + ProductEnvContextManager from multiproduct.model import Product import trac.env @@ -359,7 +361,7 @@ class ProductEnvironment(Component, Comp del product_env_keymap - implements(trac.env.ISystemInfoProvider) + implements(trac.env.ISystemInfoProvider, IPermissionRequestor) setup_participants = ExtensionPoint(trac.env.IEnvironmentSetupParticipant) multi_product_support_components = ExtensionPoint(ISupportMultiProductEnvironment) @@ -559,6 +561,25 @@ class ProductEnvironment(Component, Comp os.makedirs(folder) return folder + # IPermissionRequestor methods + def get_permission_actions(self): + """Implement the product-specific `PRODUCT_ADMIN` meta permission. + """ + actions = set() + permsys = PermissionSystem(self) + for requestor in permsys.requestors: + if requestor is not self and requestor is not permsys: + for action in requestor.get_permission_actions() or []: + if isinstance(action, tuple): + actions.add(action[0]) + else: + actions.add(action) + # PermissionSystem's method was not invoked + actions.add('EMAIL_VIEW') + # FIXME: should not be needed, JIC better double check + actions.discard('TRAC_ADMIN') + return [('PRODUCT_ADMIN', list(actions))] + # ISystemInfoProvider methods # Same as parent environment's . Avoid duplicated code @@ -884,3 +905,8 @@ class ProductEnvironment(Component, Comp lookup_product_env = ProductEnvironment.lookup_env resolve_product_href = ProductEnvironment.resolve_href + +# Override product-specific options +from multiproduct.config import ProductPermissionPolicyOption +PermissionSystem.policies.__class__ = ProductPermissionPolicyOption + Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/perm.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/perm.py?rev=1457686&view=auto ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/perm.py (added) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/perm.py Mon Mar 18 09:46:02 2013 @@ -0,0 +1,49 @@ + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +"""Permission components for Bloodhound product environments""" + +__all__ = 'ProductPermissionPolicy', + +from trac.core import Component, implements +from trac.perm import IPermissionPolicy, PermissionSystem + +class MultiproductPermissionPolicy(Component): + """Apply product policy in product environments to deal with TRAC_ADMIN, + PRODUCT_ADMIN and alike. + """ + implements(IPermissionPolicy) + + # IPermissionPolicy methods + def check_permission(self, action, username, resource, perm): + # FIXME: Better handling of recursive imports + from multiproduct.env import ProductEnvironment + + if isinstance(self.env, ProductEnvironment): + if action == 'TRAC_ADMIN': + # Always lookup TRAC_ADMIN permission in global scope + permsys = PermissionSystem(self.env.parent) + return bool(permsys.check_permission(action, username, + resource, perm)) + elif username == self.env.product.owner: + # Product owner granted with PRODUCT_ADMIN permission ootb + permsys = PermissionSystem(self.env) + # FIXME: would `action != 'TRAC_ADMIN'` be enough ? + return True if action in permsys.get_actions() and \ + action != 'TRAC_ADMIN' \ + else None Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py?rev=1457686&r1=1457685&r2=1457686&view=diff ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py (original) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/setup.py Mon Mar 18 09:46:02 2013 @@ -30,6 +30,7 @@ setup( package_data = {'multiproduct' : ['templates/*.html',]}, entry_points = {'trac.plugins': [ 'multiproduct.model = multiproduct.model', + 'multiproduct.perm = multiproduct.perm', 'multiproduct.product_admin = multiproduct.product_admin', 'multiproduct.ticket.query = multiproduct.ticket.query', 'multiproduct.ticket.web_ui = multiproduct.ticket.web_ui', Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/perm.py URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/perm.py?rev=1457686&r1=1457685&r2=1457686&view=diff ============================================================================== --- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/perm.py (original) +++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/perm.py Mon Mar 18 09:46:02 2013 @@ -21,10 +21,14 @@ import unittest +from trac.admin.api import AdminCommandError from trac import perm -from trac.tests.perm import DefaultPermissionStoreTestCase +from trac.tests.perm import DefaultPermissionStoreTestCase,\ + PermissionSystemTestCase, PermissionCacheTestCase,\ + PermissionPolicyTestCase, TestPermissionPolicy, TestPermissionRequestor from multiproduct.env import ProductEnvironment +from multiproduct.perm import MultiproductPermissionPolicy from tests.env import MultiproductTestCase @@ -74,9 +78,178 @@ class ProductDefaultPermissionStoreTestC self.assertEquals(['MILESTONE_VIEW', 'TICKET_CREATE'], sorted(store1.get_user_permissions('john'))) +class ProductPermissionSystemTestCase(PermissionSystemTestCase, + MultiproductTestCase): + @property + def env(self): + env = getattr(self, '_env', None) + if env is None: + self.global_env = self._setup_test_env(enable=[ + perm.PermissionSystem, + perm.DefaultPermissionStore, + TestPermissionRequestor]) + self._upgrade_mp(self.global_env) + self._setup_test_log(self.global_env) + self._load_product_from_data(self.global_env, self.default_product) + self._env = env = ProductEnvironment( + self.global_env, self.default_product) + return env + + @env.setter + def env(self, value): + pass + + def test_all_permissions(self): + # PRODUCT_ADMIN meta-permission in product context + self.assertEqual({'EMAIL_VIEW': True, 'TRAC_ADMIN': True, + 'TEST_CREATE': True, 'TEST_DELETE': True, + 'TEST_MODIFY': True, 'TEST_ADMIN': True, + 'PRODUCT_ADMIN' : True}, + self.perm.get_user_permissions()) + + def test_expand_actions_iter_7467(self): + # Check that expand_actions works with iterators (#7467) + # PRODUCT_ADMIN meta-permission in product context + perms = set(['EMAIL_VIEW', 'TRAC_ADMIN', 'TEST_DELETE', 'TEST_MODIFY', + 'TEST_CREATE', 'TEST_ADMIN', 'PRODUCT_ADMIN']) + self.assertEqual(perms, self.perm.expand_actions(['TRAC_ADMIN'])) + self.assertEqual(perms, self.perm.expand_actions(iter(['TRAC_ADMIN']))) + + +class ProductPermissionCacheTestCase(PermissionCacheTestCase, + MultiproductTestCase): + @property + def env(self): + env = getattr(self, '_env', None) + if env is None: + self.global_env = self._setup_test_env(enable=[ + perm.DefaultPermissionStore, + perm.DefaultPermissionPolicy, + TestPermissionRequestor]) + self._upgrade_mp(self.global_env) + self._setup_test_log(self.global_env) + self._load_product_from_data(self.global_env, self.default_product) + self._env = env = ProductEnvironment( + self.global_env, self.default_product) + return env + + @env.setter + def env(self, value): + pass + + +class ProductPermissionPolicyTestCase(PermissionPolicyTestCase, + MultiproductTestCase): + @property + def env(self): + env = getattr(self, '_env', None) + if env is None: + self.global_env = self._setup_test_env(enable=[ + perm.DefaultPermissionStore, + perm.DefaultPermissionPolicy, + perm.PermissionSystem, + TestPermissionPolicy, + TestPermissionRequestor, + MultiproductPermissionPolicy]) + self._upgrade_mp(self.global_env) + self._setup_test_log(self.global_env) + self._load_product_from_data(self.global_env, self.default_product) + self._env = env = ProductEnvironment( + self.global_env, self.default_product) + return env + + @env.setter + def env(self, value): + pass + + def setUp(self): + super(ProductPermissionPolicyTestCase, self).setUp() + + self.global_env.config.set('trac', 'permission_policies', + 'DefaultPermissionPolicy') + self.permsys = perm.PermissionSystem(self.env) + self.global_perm_admin = perm.PermissionAdmin(self.global_env) + self.product_perm_admin = perm.PermissionAdmin(self.env) + + def tearDown(self): + self.global_env.reset_db() + self.global_env = self.env = None + + def test_prepend_mp_policy(self): + self.assertEqual([MultiproductPermissionPolicy(self.env), self.policy], + self.permsys.policies) + + def test_policy_chaining(self): + self.env.config.set('trac', 'permission_policies', + 'TestPermissionPolicy,DefaultPermissionPolicy') + self.policy.grant('testuser', ['TEST_MODIFY']) + system = perm.PermissionSystem(self.env) + system.grant_permission('testuser', 'TEST_ADMIN') + + self.assertEqual(list(system.policies), + [MultiproductPermissionPolicy(self.env), + self.policy, + perm.DefaultPermissionPolicy(self.env)]) + self.assertEqual('TEST_MODIFY' in self.perm, True) + self.assertEqual('TEST_ADMIN' in self.perm, True) + self.assertEqual(self.policy.results, + {('testuser', 'TEST_MODIFY'): True, + ('testuser', 'TEST_ADMIN'): None}) + + def test_product_trac_admin_success(self): + """TRAC_ADMIN in global env also valid in product env + """ + self.global_perm_admin._do_add('testuser', 'TRAC_ADMIN') + self.assertTrue(self.perm.has_permission('TRAC_ADMIN')) + + def test_product_trac_admin_fail_local(self): + """TRAC_ADMIN granted in product env will be ignored + """ + try: + # Not needed but added just in case , also for readability + self.global_perm_admin._do_remove('testuser', 'TRAC_ADMIN') + except AdminCommandError: + pass + + # Setting TRAC_ADMIN permission in product scope is in vain + # since it controls access to critical actions affecting the whole site + # This will protect the system against malicious actors + # and / or failures leading to the addition of TRAC_ADMIN permission + # in product perm store in spite of obtaining unrighteous super powers. + # On the other hand this also means that PRODUCT_ADMIN(s) are + # able to set user permissions at will without jeopardizing system + # integrity and stability. + self.product_perm_admin._do_add('testuser', 'TRAC_ADMIN') + self.assertFalse(self.perm.has_permission('TRAC_ADMIN')) + + def test_product_owner_perm(self): + """Product owner automatically granted with PRODUCT_ADMIN + """ + self.assertIs(self.env.product.owner, None) + self.assertFalse(self.perm.has_permission('PRODUCT_ADMIN')) + + self.env.product.owner = 'testuser' + # FIXME: update really needed ? + self.env.product.update() + try: + # Not needed but added just in case , also for readability + self.global_perm_admin._do_remove('testuser', 'TRAC_ADMIN') + except AdminCommandError: + pass + self.perm._cache.clear() + + self.assertTrue(self.perm.has_permission('PRODUCT_ADMIN')) + self.assertFalse(self.perm.has_permission('TRAC_ADMIN')) + def test_suite(): - return unittest.makeSuite(ProductDefaultPermissionStoreTestCase,'test') + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ProductDefaultPermissionStoreTestCase, + 'test')) + suite.addTest(unittest.makeSuite(ProductPermissionSystemTestCase, 'test')) + suite.addTest(unittest.makeSuite(ProductPermissionCacheTestCase, 'test')) + suite.addTest(unittest.makeSuite(ProductPermissionPolicyTestCase, 'test')) + return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite')