From 968196ae38522912944a6ba9222479b7c799c839 Mon Sep 17 00:00:00 2001 From: Jan-Wijbrand Kolman Date: Mon, 30 Apr 2012 14:00:19 +0000 Subject: [PATCH] move the Role component from grok to grokcore.security too --- setup.py | 28 +++---- src/grokcore/security/__init__.py | 1 + src/grokcore/security/components.py | 16 ++++ src/grokcore/security/directive.py | 10 +-- src/grokcore/security/interfaces.py | 1 + src/grokcore/security/meta.py | 58 +++++++++++++- .../security/tests/permissions/directive.py | 2 +- src/grokcore/security/tests/role/__init__.py | 0 .../security/tests/role/missing_role_name.py | 15 ++++ .../security/tests/role/permissions.py | 35 +++++++++ src/grokcore/security/tests/role/role_i18n.py | 78 +++++++++++++++++++ src/grokcore/security/tests/test_all.py | 3 +- 12 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 src/grokcore/security/tests/role/__init__.py create mode 100644 src/grokcore/security/tests/role/missing_role_name.py create mode 100644 src/grokcore/security/tests/role/permissions.py create mode 100644 src/grokcore/security/tests/role/role_i18n.py diff --git a/setup.py b/setup.py index d515aba..f3981c0 100644 --- a/setup.py +++ b/setup.py @@ -25,24 +25,26 @@ def read(*rnames): description='Grok-like configuration for Zope security components', long_description=long_description, license='ZPL', - classifiers=['Intended Audience :: Developers', - 'License :: OSI Approved :: Zope Public License', - 'Programming Language :: Python', - 'Framework :: Zope3', - ], - + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Zope Public License', + 'Programming Language :: Python', + 'Framework :: Zope3', + ], packages=find_packages('src'), package_dir = {'': 'src'}, namespace_packages=['grokcore'], include_package_data=True, zip_safe=False, - install_requires=['setuptools', - 'grokcore.component >= 2.1', - 'martian >= 0.13', - 'zope.component', - 'zope.interface', - 'zope.security', - ], + install_requires=[ + 'setuptools', + 'grokcore.component >= 2.1', + 'martian >= 0.13', + 'zope.component', + 'zope.interface', + 'zope.security', + 'zope.securitypolicy', + ], tests_require=tests_require, extras_require={'test': tests_require}, ) diff --git a/src/grokcore/security/__init__.py b/src/grokcore/security/__init__.py index 52f39a7..a362e44 100644 --- a/src/grokcore/security/__init__.py +++ b/src/grokcore/security/__init__.py @@ -16,6 +16,7 @@ from grokcore.component import * from grokcore.security.components import Permission, Public +from grokcore.security.components import Role from grokcore.security.directive import require, permissions # Import this module so that it's available as soon as you import the diff --git a/src/grokcore/security/components.py b/src/grokcore/security/components.py index 141956b..6df485b 100644 --- a/src/grokcore/security/components.py +++ b/src/grokcore/security/components.py @@ -19,3 +19,19 @@ class Permission(Permission): pass Public = 'zope.Public' + +from zope.securitypolicy.role import Role as securitypolicy_Role + +class Role(securitypolicy_Role): + """Base class for roles in Grok applications. + + A role is a description of a class of users that gives them a + machine-readable name, a human-readable title, and a set of + permissions which users belong to that role should possess:: + + class Editor(grok.Role): + grok.name('news.Editor') + grok.title('Editor') + grok.permissions('news.EditArticle', 'news.PublishArticle') + + """ \ No newline at end of file diff --git a/src/grokcore/security/directive.py b/src/grokcore/security/directive.py index 5cf31f6..343880f 100644 --- a/src/grokcore/security/directive.py +++ b/src/grokcore/security/directive.py @@ -28,9 +28,10 @@ def get(self, directive, component, default): if (permissions is default) or not permissions: return default if len(permissions) > 1: - raise GrokError('grok.require was called multiple times in ' - '%r. It may only be set once for a class.' - % component, component) + raise GrokError( + 'grok.require was called multiple times in ' + '%r. It may only be set once for a class.' + % component, component) return permissions[0] def pop(self, locals_, directive): @@ -88,8 +89,7 @@ def validate(self, *values): if martian.util.not_unicode_or_ascii(value): raise GrokImportError( "You can only pass unicode values, ASCII values, or " - "subclasses of grokcore.security.Permission to the '%s'" - " directive." + "subclasses of grok.Permission to the '%s' directive." % self.name) def factory(self, *values): diff --git a/src/grokcore/security/interfaces.py b/src/grokcore/security/interfaces.py index 1b75c18..44caf48 100644 --- a/src/grokcore/security/interfaces.py +++ b/src/grokcore/security/interfaces.py @@ -18,6 +18,7 @@ class IBaseClasses(Interface): Permission = Attribute("Base class for permissions.") + Role = Attribute("Base class for roles.") class IDirectives(Interface): diff --git a/src/grokcore/security/meta.py b/src/grokcore/security/meta.py index ec1b7d6..ccfb68b 100644 --- a/src/grokcore/security/meta.py +++ b/src/grokcore/security/meta.py @@ -15,9 +15,12 @@ import martian import grokcore.component +import grokcore.component.util import grokcore.security -from martian.error import GrokError + from zope.security.interfaces import IPermission +from martian.error import GrokError + def default_fallback_to_name(factory, module, name, **data): return name @@ -47,3 +50,56 @@ def execute(self, factory, config, name, title, description, **kw): order=-1 # need to do this early in the process ) return True + + +from zope.i18nmessageid import Message +from zope.securitypolicy.rolepermission import rolePermissionManager +from zope.securitypolicy.interfaces import IRole + +from grokcore.security.directive import permissions +from grokcore.security.components import Role + + +class RoleGrokker(martian.ClassGrokker): + """Grokker for components subclassed from `grok.Role`. + + Each role is registered as a global utility providing the service + `IRole` under its own particular name, and then granted every + permission named in its `grok.permission()` directive. + + """ + martian.component(Role) + martian.priority(martian.priority.bind().get(PermissionGrokker()) - 1) + martian.directive(grokcore.component.name) + martian.directive( + grokcore.component.title, get_default=default_fallback_to_name) + martian.directive(grokcore.component.description) + martian.directive(permissions) + + def execute(self, factory, config, name, title, description, + permissions, **kw): + if not name: + raise GrokError( + "A role needs to have a dotted name for its id. Use " + "grok.name to specify one.", factory) + # We can safely convert to unicode, since the directives makes sure + # it is either unicode already or ASCII. + if not isinstance(title, Message): + title = unicode(title) + if not isinstance(description, Message): + description = unicode(description) + role = factory(unicode(name), title, description) + + config.action( + discriminator=('utility', IRole, name), + callable=grokcore.component.util.provideUtility, + args=(role, IRole, name), + ) + + for permission in permissions: + config.action( + discriminator=('grantPermissionToRole', permission, name), + callable=rolePermissionManager.grantPermissionToRole, + args=(permission, name), + ) + return True \ No newline at end of file diff --git a/src/grokcore/security/tests/permissions/directive.py b/src/grokcore/security/tests/permissions/directive.py index 8ee7b8a..4fd5a85 100644 --- a/src/grokcore/security/tests/permissions/directive.py +++ b/src/grokcore/security/tests/permissions/directive.py @@ -8,6 +8,6 @@ Traceback (most recent call last): ... GrokImportError: You can only pass unicode values, ASCII values, or - subclasses of grokcore.security.Permission to the 'permissions' directive. + subclasses of grok.Permission to the 'permissions' directive. """ diff --git a/src/grokcore/security/tests/role/__init__.py b/src/grokcore/security/tests/role/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/grokcore/security/tests/role/missing_role_name.py b/src/grokcore/security/tests/role/missing_role_name.py new file mode 100644 index 0000000..86afa58 --- /dev/null +++ b/src/grokcore/security/tests/role/missing_role_name.py @@ -0,0 +1,15 @@ +""" +A role has to have a name to be defined. + + >>> grokcore.security.testing.grok(__name__) + Traceback (most recent call last): + GrokError: A role needs to have a dotted name for its id. + Use grok.name to specify one. +""" + +import zope.interface +import grokcore.security +import grokcore.security.testing + +class MissingName(grokcore.security.Role): + pass diff --git a/src/grokcore/security/tests/role/permissions.py b/src/grokcore/security/tests/role/permissions.py new file mode 100644 index 0000000..b33fac3 --- /dev/null +++ b/src/grokcore/security/tests/role/permissions.py @@ -0,0 +1,35 @@ +""" +A Role component optionally defines what permission it comprises. + +The grok.permissions() directive is used to specify the set of permissions +that are aggregated in the particular Role. The permissions can be referenced +by "name" or by class. + + >>> grokcore.security.testing.grok(__name__) +""" + +import grokcore.component as grok +import grokcore.security.testing +from grokcore.security import Role +from grokcore.security import Permission, permissions +import zope.interface + +class FirstPermission(Permission): + grok.name('first permission') + +class SecondPermission(Permission): + grok.name('second permission') + +class RoleComprisingTwoPermissionsByName(Role): + grok.name('ByName') + permissions( + 'first permission', + 'second permission' + ) + +class RoleComprisingTwoPermissionsByClass(Role): + grok.name('ByClass') + permissions( + FirstPermission, + SecondPermission + ) diff --git a/src/grokcore/security/tests/role/role_i18n.py b/src/grokcore/security/tests/role/role_i18n.py new file mode 100644 index 0000000..0e2a521 --- /dev/null +++ b/src/grokcore/security/tests/role/role_i18n.py @@ -0,0 +1,78 @@ +""" +A Role component have a title and description, that can be internationalized. + +Let's grok this package and check we still have a Message object for the +internationalized title and description of the defined roles. + + >>> grokcore.security.testing.grok(__name__) + >>> from zope.securitypolicy.interfaces import IRole + >>> from zope.component import getUtility + >>> from zope.i18nmessageid import Message + +A grok.Role without any internationalization. +The id, title and description should be unicode:: + + >>> role = getUtility(IRole, name="RoleWithoutI18n") + >>> role.id + u'RoleWithoutI18n' + >>> role.title + u'RoleWithoutI18n' + >>> role.description + u'My role without i18n' + >>> + >>> isinstance(role.id, Message) + False + >>> isinstance(role.title, Message) + False + >>> isinstance(role.description, Message) + False + +A grok.Role registered with the name and description directives only, both +internationalized. +The id is taken from the name directive and should not be a Message object. +The title is taken from the name directive because the title directive +is not used. +:: + + >>> role = getUtility(IRole, name="RoleWithI18n") + >>> isinstance(role.id, Message) + False + >>> isinstance(role.title, Message) + True + >>> isinstance(role.description, Message) + True + +A grok.Role registered with name, title and description directives:: + + >>> role = getUtility(IRole, name="RoleWithI18nTitle") + >>> isinstance(role.id, Message) + False + >>> isinstance(role.title, Message) + True + >>> isinstance(role.description, Message) + True +""" + +import grokcore.component as grok +import grokcore.security + +from grokcore.security import Role +from zope.i18nmessageid import MessageFactory + +_ = MessageFactory("testi18n") + + +class RoleWithoutI18n(Role): + grok.name('RoleWithoutI18n') + grok.description('My role without i18n') + + +class RoleWithI18n(Role): + grok.name(_('RoleWithI18n')) + grok.description(_(u'My role with i18n')) + + +class RoleWithI18nTitle(Role): + grok.name('RoleWithI18nTitle') + grok.title(_('RoleWithI18n')) + grok.description(_(u'My role with i18n')) diff --git a/src/grokcore/security/tests/test_all.py b/src/grokcore/security/tests/test_all.py index 2b41744..b7c79cd 100644 --- a/src/grokcore/security/tests/test_all.py +++ b/src/grokcore/security/tests/test_all.py @@ -38,8 +38,9 @@ def suiteFromPackage(name): def test_suite(): suite = unittest.TestSuite() for name in [ - 'security', 'permissions', + 'role', + 'security', ]: suite.addTest(suiteFromPackage(name)) return suite