diff options
author | Jason Madden <jason+github@nextthought.com> | 2017-09-15 08:45:18 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-15 08:45:18 -0500 |
commit | f7f3a1dfb1063933d4cfa36a68c3f9c904487cba (patch) | |
tree | 7fdfb4c6f0fbd0fc6a9435f3b4c0ab34ca421ee6 | |
parent | c192803b8e92255aea5c45349fdbd478b173224f (diff) | |
parent | e42da5125f7537b0ae7445672b0915d7f388837f (diff) | |
download | zope-security-f7f3a1dfb1063933d4cfa36a68c3f9c904487cba.tar.gz |
Merge pull request #43 from zopefoundation/docs
General documentation cleanup/update, and document issubclass issue
29 files changed, 1353 insertions, 968 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index af61fab..1c748dc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,11 @@ include *.rst include *.txt +include *.py +include .coveragerc +include appveyor.yml +include .travis.yml +include tox.ini +include buildout.cfg recursive-include docs * recursive-include src * @@ -8,3 +14,5 @@ global-exclude *.dll global-exclude *.pyc global-exclude *.pyo global-exclude *.so + +prune docs/_build diff --git a/docs/api/adapter.rst b/docs/api/adapter.rst index bf651f2..d290b47 100644 --- a/docs/api/adapter.rst +++ b/docs/api/adapter.rst @@ -1,6 +1,5 @@ -:mod:`zope.security.adapter` -============================ +======================= + zope.security.adapter +======================= .. automodule:: zope.security.adapter - :members: - :member-order: bysource diff --git a/docs/api/checker.rst b/docs/api/checker.rst index 419fe5e..7c62d5c 100644 --- a/docs/api/checker.rst +++ b/docs/api/checker.rst @@ -1,17 +1,25 @@ -:mod:`zope.security.checker` -============================ +======================= + zope.security.checker +======================= + +.. currentmodule:: zope.security.checker + + +Module API Documentation +======================== .. automodule:: zope.security.checker - :members: - :member-order: bysource +API Doctests +============ + Protections for Modules ----------------------- -The :func:`zope.secuirty.checker.moduleChecker` API can be used to -determine whether a module has been protected: Initially, there's no checker -defined for the module: +The :func:`moduleChecker` API can be used to determine whether a +module has been protected: Initially, there's no checker defined for +the module: .. doctest:: @@ -20,7 +28,9 @@ defined for the module: >>> moduleChecker(test_zcml_functest) is None True -We can add a checker using :func:`zope.security.metaconfigure.protectModule`: +We can add a checker using +:func:`zope.security.metaconfigure.protectModule` (although this is +more commonly done using ZCML): .. doctest:: @@ -202,26 +212,26 @@ Protections for standard objects ... return 'ForbiddenAttribute: %s' % e.args[0] Rocks -##### +~~~~~ -Rocks are immuatle, non-callable objects without interesting methods. They +Rocks are immutable, non-callable objects without interesting methods. They *don't* get proxied. .. doctest:: - >>> type(ProxyFactory( object() )) is object + >>> type(ProxyFactory(object())) is object True - >>> type(ProxyFactory( 1 )) is int + >>> type(ProxyFactory(1)) is int True - >>> type(ProxyFactory( 1.0 )) is float + >>> type(ProxyFactory(1.0)) is float True - >>> type(ProxyFactory( 1j )) is complex + >>> type(ProxyFactory(1j)) is complex True - >>> type(ProxyFactory( None )) is type(None) + >>> type(ProxyFactory(None)) is type(None) True - >>> type(ProxyFactory( 'xxx' )) is str + >>> type(ProxyFactory('xxx')) is str True - >>> type(ProxyFactory( True )) is type(True) + >>> type(ProxyFactory(True)) is type(True) True Datetime-reltatd instances are rocks, too: @@ -249,7 +259,7 @@ Datetime-reltatd instances are rocks, too: dicts -##### +~~~~~ We can do everything we expect to be able to do with proxied dicts. @@ -301,7 +311,7 @@ not checking that under python > 2): True lists -##### +~~~~~ We can do everything we expect to be able to do with proxied lists. @@ -357,7 +367,7 @@ Always available: True tuples -###### +~~~~~~ We can do everything we expect to be able to do with proxied tuples. @@ -404,7 +414,7 @@ Always available: True sets -#### +~~~~ we can do everything we expect to be able to do with proxied sets. @@ -576,7 +586,7 @@ with any kind of proxy. frozensets -########## +~~~~~~~~~~ we can do everything we expect to be able to do with proxied frozensets. @@ -754,7 +764,7 @@ with any kind of proxy. True iterators -######### +~~~~~~~~~ .. doctest:: @@ -823,7 +833,7 @@ We shouldn't be able to iterate if we don't have an assertion: New-style classes -################# +~~~~~~~~~~~~~~~~~ .. doctest:: @@ -859,7 +869,7 @@ Always available: True New-style Instances -################### +~~~~~~~~~~~~~~~~~~~ .. doctest:: @@ -891,7 +901,7 @@ Always available: Classic Classes -############### +~~~~~~~~~~~~~~~ .. doctest:: @@ -925,7 +935,7 @@ Always available: True Classic Instances -################# +~~~~~~~~~~~~~~~~~ .. doctest:: @@ -955,7 +965,7 @@ Always available: True Interfaces and declarations -########################### +~~~~~~~~~~~~~~~~~~~~~~~~~~~ We can still use interfaces though proxies: @@ -989,7 +999,7 @@ We can still use interfaces though proxies: abstract Base Classes -##################### +~~~~~~~~~~~~~~~~~~~~~ We work with the ABCMeta meta class: diff --git a/docs/api/decorator.rst b/docs/api/decorator.rst index 59a02bd..bc38e65 100644 --- a/docs/api/decorator.rst +++ b/docs/api/decorator.rst @@ -1,14 +1,19 @@ -:mod:`zope.security.decorator` -=============================== +========================= + zope.security.decorator +========================= + +.. automodule:: zope.security.decorator + +API Examples +============ + +.. currentmodule:: zope.security.decorator .. testsetup:: from zope.component.testing import setUp setUp() -.. autoclass:: zope.security.decorator.DecoratedSecurityCheckerDescriptor - :members: - :member-order: bysource To illustrate, we'll create a class that will be proxied: @@ -27,7 +32,7 @@ and a class to proxy it that uses a decorated security checker: ... b = 'b' ... __Security_checker__ = DecoratedSecurityCheckerDescriptor() -Next we'll create and register a checker for `Foo`: +Next we'll create and register a checker for ``Foo``: .. doctest:: @@ -35,15 +40,15 @@ Next we'll create and register a checker for `Foo`: >>> fooChecker = NamesChecker(['a']) >>> defineChecker(Foo, fooChecker) -along with a checker for `Wrapper`: +along with a checker for ``Wrapper``: .. doctest:: >>> wrapperChecker = NamesChecker(['b']) >>> defineChecker(Wrapper, wrapperChecker) -Using `selectChecker()`, we can confirm that a `Foo` object uses - `fooChecker`: +Using :func:`zope.security.checker.selectChecker`, we can confirm that + a ``Foo`` object uses ``fooChecker``: .. doctest:: @@ -59,7 +64,7 @@ Using `selectChecker()`, we can confirm that a `Foo` object uses ... e ForbiddenAttribute('b', <...Foo object ...>) -and that a `Wrapper` object uses `wrappeChecker`: +and that a ``Wrapper`` object uses ``wrappeChecker``: .. doctest:: @@ -91,7 +96,7 @@ because both objects have checkers, we get a combined checker: >>> checker.check(wrapper, 'b') The decorator checker will work even with security proxied objects. To -illustrate, we'll proxify `foo`: +illustrate, we'll proxify ``foo``: .. doctest:: @@ -105,7 +110,7 @@ illustrate, we'll proxify `foo`: ... e ForbiddenAttribute('b', <...Foo object ...>) -when we wrap the secured `foo`: +when we wrap the secured ``foo``: .. doctest:: @@ -123,9 +128,9 @@ we still get a combined checker: The decorator checker has three other scenarios: - - the wrapper has a checker but the proxied object doesn't - - the proxied object has a checker but the wrapper doesn't - - neither the wrapper nor the proxied object have checkers +- the wrapper has a checker but the proxied object doesn't +- the proxied object has a checker but the wrapper doesn't +- neither the wrapper nor the proxied object have checkers When the wrapper has a checker but the proxied object doesn't: @@ -187,25 +192,12 @@ the decorator doesn't have a checker: ... AttributeError: 'Foo' has no attribute '__Security_checker__' - __Security_checker__ cannot be None, otherwise Checker.proxy blows - up: +``__Security_checker__`` cannot be None, otherwise Checker.proxy blows +up: >>> checker.proxy(wrapper) is wrapper True - -.. autoclass:: zope.security.decorator.SecurityCheckerDecoratorBase - :members: - :member-order: bysource - - -.. autoclass:: zope.security.decorator.DecoratorBase - :members: - :member-order: bysource - - - - .. testcleanup:: from zope.component.testing import tearDown diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index cf4e757..5a3bc01 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -1,98 +1,5 @@ -:mod:`zope.security.interfaces` -=============================== +========================== + zope.security.interfaces +========================== .. automodule:: zope.security.interfaces - -Exceptions -########## - - .. autointerface:: IUnauthorized - :members: - :member-order: bysource - - .. autoexception:: Unauthorized - - .. autointerface:: IForbidden - :members: - :member-order: bysource - - .. autoexception:: Forbidden - - .. autointerface:: IForbiddenAttribute - :members: - :member-order: bysource - - .. autoexception:: ForbiddenAttribute - -Utilities -########## - - .. autointerface:: ISecurityManagement - :members: - :member-order: bysource - - .. autointerface:: ISecurityChecking - :members: - :member-order: bysource - - .. autointerface:: ISecurityProxyFactory - :members: - :member-order: bysource - - .. autointerface:: IChecker - :members: - :member-order: bysource - - .. autointerface:: INameBasedChecker - :members: - :member-order: bysource - - .. autointerface:: ISecurityPolicy - :members: - :member-order: bysource - -Principals -########## - - .. autointerface:: IInteraction - :members: - :member-order: bysource - - .. autointerface:: IParticipation - :members: - :member-order: bysource - - .. autoexception:: NoInteraction - - .. autointerface:: IInteractionManagement - :members: - :member-order: bysource - - .. autointerface:: IPrincipal - :members: - :member-order: bysource - - .. autointerface:: IGroupAwarePrincipal - :members: - :member-order: bysource - - .. autointerface:: IGroupClosureAwarePrincipal - :members: - :member-order: bysource - - .. autointerface:: IGroup - :members: - :member-order: bysource - - .. autointerface:: IMemberGetterGroup - :members: - :member-order: bysource - - .. autointerface:: IMemberAwareGroup - :members: - :member-order: bysource - - .. autointerface:: IPermission - :members: - :member-order: bysource - diff --git a/docs/api/management.rst b/docs/api/management.rst index 2c4086e..ed6d1f7 100644 --- a/docs/api/management.rst +++ b/docs/api/management.rst @@ -1,6 +1,5 @@ -:mod:`zope.security.management` -=============================== +========================== + zope.security.management +========================== .. automodule:: zope.security.management - :members: - :member-order: bysource diff --git a/docs/api/metaconfigure.rst b/docs/api/metaconfigure.rst new file mode 100644 index 0000000..a4459b8 --- /dev/null +++ b/docs/api/metaconfigure.rst @@ -0,0 +1,5 @@ +============================= + zope.security.metaconfigure +============================= + +.. automodule:: zope.security.metaconfigure diff --git a/docs/api/permission.rst b/docs/api/permission.rst index 268f8f6..107e495 100644 --- a/docs/api/permission.rst +++ b/docs/api/permission.rst @@ -1,5 +1,8 @@ -:mod:`zope.security.permission` -=============================== +========================== + zope.security.permission +========================== + +.. currentmodule:: zope.security.permission .. testsetup:: @@ -26,7 +29,7 @@ >>> checkPermission(None, 'y') Traceback (most recent call last): ... - ValueError: ('Undefined permission id', 'y') + ValueError: ('Undefined permission ID', 'y') The :data:`zope.security.checker.CheckerPublic` permission always exists: @@ -105,7 +108,7 @@ The non-public permissions 'x' and 'y' are string values: >>> print(vocab.getTermByToken('y').value) y -However, the public permission value is CheckerPublic: +However, the public permission value is :data:`~.CheckerPublic`: .. doctest:: diff --git a/docs/api/protectclass.rst b/docs/api/protectclass.rst index fdc266b..d5cd12d 100644 --- a/docs/api/protectclass.rst +++ b/docs/api/protectclass.rst @@ -1,6 +1,5 @@ -:mod:`zope.security.protectclass` -================================= +============================ + zope.security.protectclass +============================ .. automodule:: zope.security.protectclass - :members: - :member-order: bysource diff --git a/docs/api/proxy.rst b/docs/api/proxy.rst index 665912a..0acb043 100644 --- a/docs/api/proxy.rst +++ b/docs/api/proxy.rst @@ -1,14 +1,25 @@ -:mod:`zope.security.proxy` -=============================== +===================== + zope.security.proxy +===================== + +.. currentmodule:: zope.security.proxy + +An introduction to proxies and their uses can be found in :doc:`../proxy`. + +.. seealso:: :ref:`proxy-known-issues` .. testsetup:: from zope.component.testing import setUp setUp() -.. autofunction:: zope.security.proxy.getTestProxyItems +.. autofunction:: getChecker + +.. autofunction:: removeSecurityProxy + +.. autofunction:: getTestProxyItems -.. autofunction:: zope.security.proxy.isinstance +.. autofunction:: isinstance .. doctest:: @@ -35,7 +46,9 @@ >>> isinstance(ProxyFactory(c), C1) True +.. autoclass:: Proxy +.. autoclass:: ProxyPy .. testcleanup:: diff --git a/docs/api/simplepolicies.rst b/docs/api/simplepolicies.rst index 8073f9d..37cdf17 100644 --- a/docs/api/simplepolicies.rst +++ b/docs/api/simplepolicies.rst @@ -1,6 +1,5 @@ -:mod:`zope.security.simplepolicies` -=================================== +============================== + zope.security.simplepolicies +============================== .. automodule:: zope.security.simplepolicies - :members: - :member-order: bysource diff --git a/docs/api/testing.rst b/docs/api/testing.rst index a9c6a00..20ac9e3 100644 --- a/docs/api/testing.rst +++ b/docs/api/testing.rst @@ -1,6 +1,5 @@ -:mod:`zope.security.testing` -============================ +======================= + zope.security.testing +======================= .. automodule:: zope.security.testing - :members: - :member-order: bysource diff --git a/docs/api/zcml.rst b/docs/api/zcml.rst index a23075a..fc62966 100644 --- a/docs/api/zcml.rst +++ b/docs/api/zcml.rst @@ -1,40 +1,16 @@ -:mod:`zope.security.zcml` -=============================== +==================== + zope.security.zcml +==================== -Configuring security via ZCML ------------------------------ - -:mod:`zope.security` provides a ZCML file that configures some utilities and -a couple of permissions: - -.. doctest:: +.. currentmodule:: zope.security.zcml - >>> from zope.component import getGlobalSiteManager - >>> from zope.configuration.xmlconfig import XMLConfig - >>> from zope.component.testing import setUp - >>> import zope.security - >>> setUp() # clear global component registry - >>> XMLConfig('permissions.zcml', zope.security)() +Most users will not directly need to access the contents of this +module; they will probably just :ref:`configure via ZCML <via-zcml>`. - >>> len(list(getGlobalSiteManager().registeredUtilities())) - 7 +API Reference +============= -Clear the current state: - -.. doctest:: - - >>> from zope.component.testing import setUp, tearDown - >>> tearDown() - >>> setUp() - - >>> XMLConfig('configure.zcml', zope.security)() - - >>> len(list(getGlobalSiteManager().registeredUtilities())) - 10 - -.. autoclass:: zope.security.zcml.Permission - :members: - :member-order: bysource +.. autoclass:: Permission Let's look at an example: @@ -77,20 +53,47 @@ Now let's see whether validation works alright >>> field._validate('zope.Public') -.. autointerface:: zope.security.zcml.ISecurityPolicyDirective - :members: - :member-order: bysource +.. autointerface:: ISecurityPolicyDirective + +.. autofunction:: securityPolicy + +.. autointerface:: IPermissionDirective -.. autofunction:: zope.security.zcml.securityPolicy +.. autofunction:: permission -.. autointerface:: zope.security.zcml.IPermissionDirective - :members: - :member-order: bysource +.. autointerface:: IRedefinePermission -.. autofunction:: zope.security.zcml.permission +.. autofunction:: redefinePermission -.. autointerface:: zope.security.zcml.IRedefinePermission - :members: - :member-order: bysource +.. _via-zcml: -.. autofunction:: zope.security.zcml.redefinePermission +Configuring security via ZCML +============================= + +:mod:`zope.security` provides a ZCML file that configures some +utilities and a couple of standard permissions: + +.. doctest:: + + >>> from zope.component import getGlobalSiteManager + >>> from zope.configuration.xmlconfig import XMLConfig + >>> from zope.component.testing import setUp + >>> import zope.security + >>> setUp() # clear global component registry + >>> XMLConfig('permissions.zcml', zope.security)() + + >>> len(list(getGlobalSiteManager().registeredUtilities())) + 7 + +Clear the current state: + +.. doctest:: + + >>> from zope.component.testing import setUp, tearDown + >>> tearDown() + >>> setUp() + + >>> XMLConfig('configure.zcml', zope.security)() + + >>> len(list(getGlobalSiteManager().registeredUtilities())) + 10 diff --git a/docs/conf.py b/docs/conf.py index caabf00..31497bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -263,6 +263,7 @@ intersphinx_mapping = { 'https://zopeinterface.readthedocs.io/en/latest': None, 'https://zopeproxy.readthedocs.io/en/latest': None, 'https://zopeschema.readthedocs.io/en/latest': None, + 'https://zopelocation.readthedocs.io/en/latest': None, } extlinks = {'issue': ('https://github.com/zopefoundation/zope.datetime/issues/%s', diff --git a/docs/example.rst b/docs/example.rst new file mode 100644 index 0000000..af5f6de --- /dev/null +++ b/docs/example.rst @@ -0,0 +1,227 @@ +========= + Example +========= + +As an example we take a look at constructing a multi-agent distributed system, +and then adding a security layer using the Zope security model onto it. + +Scenario +======== + +Our agent simulation consists of autonomous agents that live in various agent +homes/sandboxes and perform actions that access services available at their +current home. Agents carry around authentication tokens which signify their +level of access within any given home. Additionally agents attempt to migrate +from home to home randomly. + +The agent simulation was constructed separately from any security aspects. +Now we want to define and integrate a security model into the simulation. The +full code for the simulation and the security model is available separately; +we present only relevant code snippets here for illustration as we go through +the implementation process. + +For the agent simulation we want to add a security model such that we group +agents into two authentication groups, "norse legends", including the +principals thor, odin, and loki, and "greek men", including prometheus, +archimedes, and thucydides. + +We associate permissions with access to services and homes. We differentiate +the homes such that certain authentication groups only have access to services +or the home itself based on the local settings of the home in which they +reside. + +We define the homes/sandboxes + +- origin - all agents start here, and have access to all + services here. + +- valhalla - only agents in the authentication group 'norse + legend' can reside here. + +- jail - all agents can come here, but only 'norse legend's + can leave or access services. + + +Process +======= + +Loosely we define a process for implementing this security model + +- mapping permissions onto actions + +- mapping authentication tokens onto permissions + +- implementing checkers and security policies that use our + authentication tokens and permissions. + +- binding checkers to our simulation classes + +- inserting the hooks into the original simulation code to add + proxy wrappers to automatically check security. + +- inserting hooks into the original simulation to register the + agents as the active principal in an interaction. + + +Defining a Permission Model +=========================== + +We define the following permissions:: + + NotAllowed = 'Not Allowed' + Public = Checker.CheckerPublic + TransportAgent = 'Transport Agent' + AccessServices = 'Access Services' + AccessAgents = 'Access Agents' + AccessTimeService = 'Access Time Services' + AccessAgentService = 'Access Agent Service' + AccessHomeService = 'Access Home Service' + +and create a dictionary database mapping homes to authentication groups which +are linked to associated permissions. + + +Defining and Binding Checkers +============================= + +:class:`Checkers <zope.security.checker.Checker>` are the foundational +unit for the security framework. They define what attributes can be +accessed or set on a given instance. They can be used implicitly via +Proxy objects, to guard all attribute access automatically or +explicitly to check a given access for an operation. + +Checker construction expects two functions or dictionaries, one is +used to map attribute names to permissions for attribute access and +another to do the same for setting attributes. + +We use the following checker factory function:: + + def PermissionMapChecker(permissions_map={}, + setattr_permission_func=NoSetAttr): + res = {} + for k,v in permissions_map.items(): + for iv in v: + res[iv]=k + return checker.Checker(res.get, setattr_permission_func) + + time_service_checker = PermissionMapChecker( + # permission : [methods] + {'AccessTimeService':['getTime']} + ) + +with the NoSetAttr function defined as a lambda which always return the +permission ``NotAllowed``. + +To bind the checkers to the simulation classes we :func:`register +<zope.security.checker.defineChecker>` our checkers with the security +model's global checker registry:: + + import sandbox_simulation + from zope.security.checker import defineChecker + defineChecker(sandbox_simulation.TimeService, time_service_checker) + + +Defining a Security Policy +========================== + +We implement our security policy such that it checks the current agent's +authentication token against the given permission in the home of the object +being accessed. (We extend a simple policy provided by the framework +that will track participations for us):: + + from zope.security.simplepolicies import ParanoidSecurityPolicy + + @provider(ISecurityPolicy) + @implementer(IInteraction) + class SimulationSecurityPolicy(ParanoidSecurityPolicy): + + def checkPermission(self, permission, object): + + home = object.getHome() + db = getattr(SimulationSecurityDatabase, home.getId(), None) + + if db is None: + return False + + allowed = db.get('any', ()) + if permission in allowed or ALL in allowed: + return True + + if not self.participations: + return False + for participation in self.participations: + token = participation.principal.getAuthenticationToken() + allowed = db.get(token, ()) + if permission not in allowed: + return False + + return True + +Since an interaction can have more than one principal, we check that *all* of +them are given the necessary permission. This is not really necessary since +we only create interactions with a single active principal. + +There is some additional code present to allow for shortcuts in defining the +permission database when defining permissions for all auth groups and all +permissions. + + +Integration +=========== + +At this point we have implemented our security model, and we need to integrate +it with our simulation model. We do so in three separate steps. + +First we make it such that agents only access homes that are wrapped in a +security proxy. By doing this all access to homes and services (proxies have +proxied return values for their methods) is implicitly guarded by our security +policy. + +The second step is that we want to associate the active agent with the +security context so the security policy will know which agent's authentication +token to validate against. + +The third step is to set our security policy as the default policy for the +Zope security framework. It is possible to create custom security policies at +a finer grained than global, but such is left as an exercise for the reader. + + +Interaction Access +================== + +The :mod:`*default* implementation <zope.security.management>` of the +interaction management interfaces defines interactions on a per thread +basis with a function for an accessor. This model is not appropriate +for all systems, as it restricts one to a single active interaction +per thread at any given moment. Reimplementing the interaction access +methods though is easily doable and is noted here for completeness. + + +Perspectives +============ + +It's important to keep in mind that there is a lot more that is possible using +the security framework than what's been presented here. All of the +interactions are interface based, such that if you need to re-implement the +semantics to suite your application a new implementation of the interface will +be sufficient. Additional possibilities range from restricted interpreters +and dynamic loading of untrusted code to non Zope web application security +systems. Insert imagination here ;-). + + +Zope Perspective +================ + +A Zope3 programmer will never commonly need to interact with the low level +security framework. Zope3 defines a second security package over top the low +level framework and authentication sources and checkers are handled via zcml +registration. Still those developing Zope3 will hopefully find this useful as +an introduction into the underpinnings of the security framework. + + +Authors +======= + +- Kapil Thangavelu <hazmat at objectrealms.net> +- Guido Wesdorp <guido at infrae.com> +- Marius Gedminas <marius at pov.lt> diff --git a/docs/hacking.rst b/docs/hacking.rst index c91f54f..825eb2f 100644 --- a/docs/hacking.rst +++ b/docs/hacking.rst @@ -1,9 +1,10 @@ -Hacking on :mod:`zope.security` -=============================== +================================= + Hacking on :mod:`zope.security` +================================= Getting the Code -################ +================ The main repository for :mod:`zope.security` is in the Zope Foundation Github repository: @@ -35,7 +36,7 @@ You can branch the trunk from there using Bazaar: Working in a ``virtualenv`` -########################### +=========================== Installing ---------- @@ -54,65 +55,55 @@ environment: .. code-block:: sh - $ /tmp/hack-zope.security/bin/python setup.py develop + $ /tmp/hack-zope.security/bin/pip install -e .[test] Running the tests ----------------- -Then, you canrun the tests using the build-in ``setuptools`` testrunner: +Then, you can run the tests using the zope.testrunner (or a test +runner of your choice): .. code-block:: sh - $ /tmp/hack-zope.security/bin/python setup.py test - ................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... - ---------------------------------------------------------------------- - Ran 643 tests in 0.000s - - OK - -If you have the :mod:`nose` package installed in the virtualenv, you can -use its testrunner too: - -.. code-block:: sh - - $ /tmp/hack-zope.security/bin/easy_install nose - ... - $ /tmp/hack-zope.security/bin/nosetests - ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... - ---------------------------------------------------------------------- - Ran 655 tests in 0.000s + $ /tmp/hack-zope.security/bin/zope-testrunner --test-path=src + Running zope.testrunner.layer.UnitTests tests: + Set up zope.testrunner.layer.UnitTests in 0.000 seconds. + Running: - OK + Ran 742 tests with 0 failures, 0 errors, 36 skipped in 0.253 seconds. + Tearing down left over layers: + Tear down zope.testrunner.layer.UnitTests in 0.000 seconds. -If you have the :mod:`coverage` pacakge installed in the virtualenv, +If you have the :mod:`coverage` package installed in the virtualenv, you can see how well the tests cover the code: .. code-block:: sh - $ /tmp/hack-zope.security/bin/easy_install nose coverage + $ /tmp/hack-zope.security/bin/pip install coverage + ... + $ coverage run -m zope.testrunner --test-path=src ... - $ /tmp/hack-zope.security/bin/nosetests --with coverage - ............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... + $ coverage report Name Stmts Miss Cover Missing --------------------------------------------------------------- - zope/security.py 4 0 100% - zope/security/_compat.py 9 0 100% - zope/security/_definitions.py 11 0 100% - zope/security/adapter.py 45 0 100% - zope/security/checker.py 333 0 100% - zope/security/decorator.py 33 0 100% - zope/security/i18n.py 4 0 100% - zope/security/interfaces.py 65 0 100% - zope/security/management.py 62 0 100% - zope/security/metaconfigure.py 108 0 100% - zope/security/metadirectives.py 38 0 100% - zope/security/permission.py 46 0 100% - zope/security/protectclass.py 39 0 100% + zope/security.py 4 0 100% + zope/security/_compat.py 9 0 100% + zope/security/_definitions.py 11 0 100% + zope/security/adapter.py 45 0 100% + zope/security/checker.py 333 0 100% + zope/security/decorator.py 33 0 100% + zope/security/i18n.py 4 0 100% + zope/security/interfaces.py 65 0 100% + zope/security/management.py 62 0 100% + zope/security/metaconfigure.py 108 0 100% + zope/security/metadirectives.py 38 0 100% + zope/security/permission.py 46 0 100% + zope/security/protectclass.py 39 0 100% zope/security/proxy.py 164 19 88% 55, 86, 97, 119-121, 127-129, 143-144, 153-154, 158-159, 163-164, 298, 330 - zope/security/simplepolicies.py 32 0 100% - zope/security/zcml.py 43 0 100% + zope/security/simplepolicies.py 32 0 100% + zope/security/zcml.py 43 0 100% --------------------------------------------------------------- - TOTAL 1036 19 98% + TOTAL 1036 19 98% ---------------------------------------------------------------------- Ran 655 tests in 0.000s @@ -128,7 +119,7 @@ tests, you can build the docs: .. code-block:: sh - $ /tmp/hack-zope.security/bin/easy_install Sphinx + $ /tmp/hack-zope.security/bin/pip install -e .[docs] ... $ cd docs $ PATH=/tmp/hack-zope.security/bin:$PATH make html @@ -211,7 +202,7 @@ You can also test the code snippets in the documentation: Using :mod:`zc.buildout` -######################## +======================== Setting up the buildout ----------------------- @@ -245,7 +236,7 @@ You can now run the tests: Using :mod:`tox` -################ +================ Running Tests on Multiple Python Versions ----------------------------------------- @@ -255,38 +246,34 @@ tool designed to run tests against multiple Python versions. It creates a ``virtualenv`` for each configured version, installs the current package and configured dependencies into each ``virtualenv``, and then runs the configured commands. - + :mod:`zope.security` configures the following :mod:`tox` environments via its ``tox.ini`` file: -- The ``py26``, ``py27``, ``py33``, ``py34``, and ``pypy`` environments +- The ``py27``, ``py34``, ``py35``, ``pypy``, etc, environments builds a ``virtualenv`` with the appropriate interpreter, - installs :mod:`zope.security` and dependencies, and runs the tests - via ``python setup.py test -q``. + installs :mod:`zope.security` and dependencies, and runs the tests. - The ``py27-pure`` and ``py33-pure`` environments build a ``virtualenv`` with the appropriate interpreter, installs :mod:`zope.security` and dependencies **without compiling C extensions**, and runs the tests via ``python setup.py test -q``. -- The ``coverage`` environment builds a ``virtualenv`` with ``python2.6``, - installs :mod:`zope.security`, installs - :mod:`nose` and :mod:`coverage`, and runs ``nosetests`` with statement - coverage. +- The ``coverage`` environment builds a ``virtualenv``, runs all the + tests under :mod:`coverage`, and prints a report to stdout. -- The ``docs`` environment builds a virtualenv with ``python2.6``, installs - :mod:`zope.security`, installs ``Sphinx`` and - dependencies, and then builds the docs and exercises the doctest snippets. +- The ``docs`` environment builds a virtualenv and then builds the + docs and exercises the doctest snippets. -This example requires that you have a working ``python2.6`` on your path, +This example requires that you have a working ``python2.7`` on your path, as well as installing ``tox``: .. code-block:: sh - $ tox -e py26 + $ tox -e py27 GLOB sdist-make: .../zope.security/setup.py - py26 sdist-reinst: .../zope.security/.tox/dist/zope.security-4.0.2dev.zip - py26 runtests: commands[0] + py27 sdist-reinst: .../zope.security/.tox/dist/zope.security-4.0.2dev.zip + py27 runtests: commands[0] ................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... ---------------------------------------------------------------------- Ran 643 tests in 0.000s @@ -328,7 +315,7 @@ including building the docs and testing their snippets: Contributing to :mod:`zope.security` -#################################### +==================================== Submitting a Bug Report ----------------------- @@ -356,14 +343,3 @@ in your fork, and push it. You can then submit a pull request from your branch: https://github.com/zopefoundation/zope.security/pulls - -If you branched the code from Launchpad using Bazaar, you have another -option: you can "push" your branch to Launchpad: - -.. code-block:: sh - - $ bzr push lp:~jrandom/zope.security/cool_feature - -After pushing your branch, you can link it to a bug report on Github, -or request that the maintainers merge your branch using the Launchpad -"merge request" feature. diff --git a/docs/index.rst b/docs/index.rst index f6418b6..eb3cbf5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,20 @@ -:mod:`zope.security` Documentation -================================== +============================= + zope.security Documentation +============================= Narrative Documentation ------------------------ +======================= .. toctree:: :maxdepth: 2 narr + example + proxy hacking API Reference -------------- +============= .. toctree:: :maxdepth: 2 @@ -26,13 +29,14 @@ API Reference api/proxy api/simplepolicies api/testing + api/metaconfigure api/zcml -Indices and tables -================== +==================== + Indices and tables +==================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/narr.rst b/docs/narr.rst index 4f19fe4..d746fc3 100644 --- a/docs/narr.rst +++ b/docs/narr.rst @@ -1,8 +1,8 @@ -Overview -======== +=========================== + Overview and Introduction +=========================== -Introduction ------------- +.. currentmodule:: zope.security.interfaces The Security framework provides a generic mechanism to implement security policies on Python objects. This introduction provides a tutorial of the @@ -10,22 +10,34 @@ framework explaining concepts, design, and going through sample usage from the perspective of a Python programmer using the framework outside of Zope. Definitions ------------ +=========== Principal -~~~~~~~~~ +--------- -A generalization of a concept of a user. +A generalization of a concept of a :class:`user <IPrincipal>`. Further +specializations include :class:`groups of users <IGroup>` and +principals that :class:`know what groups they belong to +<IGroupAwarePrincipal>`. All of these principals may interact with the system. Permission -~~~~~~~~~~ +---------- A kind of access, i.e. permission to READ vs. permission to WRITE. -Fundamentally the whole security framework is organized around checking -permissions on objects. +Fundamentally the whole security framework is organized around +checking permissions on objects. Permissions are represented (and +checked) as strings, with the exception of :data:`a constant +<zope.security.checker.CheckerPublic>` that has the special meaning of +"public", i.e., no checking needs to be done. + +There are :class:`permission objects <IPermission>` that can be +registered as zope.component utilities for validation, introspection, +and producing :func:`lists of available permissions +<zope.security.permission.PermissionVocabulary>` to help users assign +them to objects. Purpose -------- +======= The security framework's primary purpose is to guard and check access to Python objects. It does this by providing mechanisms for explicit and @@ -44,10 +56,10 @@ it can be substituted with one which doesn't care about principals or interactions at all. Framework Components --------------------- +==================== Low Level Components -~~~~~~~~~~~~~~~~~~~~ +-------------------- These components provide the infrastructure for guarding attribute access and providing hooks into the higher level security framework. @@ -55,9 +67,10 @@ providing hooks into the higher level security framework. Checkers ~~~~~~~~ -A checker is associated with an object kind, and provides the hooks that map -attribute checks onto permissions deferring to the security manager (which in -turn defers to the policy) to perform the check. +A :class:`checker <IChecker>` is associated +with an object kind, and provides the hooks that map attribute checks +onto permissions deferring to the security manager (which in turn +defers to the policy) to perform the check. Additionally, checkers provide for creating proxies of objects associated with the checker. @@ -68,491 +81,41 @@ grant access based on attribute names. Proxies ~~~~~~~ -Wrappers around Python objects that implicitly guard access to their wrapped -contents by delegating to their associated checker. Proxies are also viral in -nature, in that values returned by proxies are also proxied. +:class:`Wrappers around Python objects <zope.security.proxy.Proxy>` +that implicitly guard access to their wrapped contents by delegating +to their associated checker. Proxies are also viral in nature, in that +values returned by proxies are also proxied. High Level Components ---------------------- +===================== Security Management -~~~~~~~~~~~~~~~~~~~ - -Provides accessors for setting up interactions and the global security policy. +------------------- -Interaction -~~~~~~~~~~~ +Provides accessors for :class:`setting up interactions +<IInteractionManagement>` and the :class:`global security policy +<ISecurityManagement>`. -Stores transient information on the list of participations. - -Participation -~~~~~~~~~~~~~ +:class:`Interaction <zope.security.interfaces.IInteraction>` +------------------------------------------------------------ -Stores information about a principal participating in the interaction. +An :class:`interaction <IInteraction>` represents zero or more +principals manipulating or viewing (interacting with) the system. -Security Policy -~~~~~~~~~~~~~~~ - -Provides a single method that accepts the object, the permission, and the -interaction of the access being checked and is used to implement the +Interactions also provide :func:`a single method +<IInteraction.checkPermission>` that accepts the object and the +permission of the access being checked and is used to implement the application logic for the security framework. -Narrative (agent sandbox) -------------------------- - -As an example we take a look at constructing a multi-agent distributed system, -and then adding a security layer using the Zope security model onto it. - -Scenario -~~~~~~~~ - -Our agent simulation consists of autonomous agents that live in various agent -homes/sandboxes and perform actions that access services available at their -current home. Agents carry around authentication tokens which signify their -level of access within any given home. Additionally agents attempt to migrate -from home to home randomly. - -The agent simulation was constructed separately from any security aspects. -Now we want to define and integrate a security model into the simulation. The -full code for the simulation and the security model is available separately; -we present only relevant code snippets here for illustration as we go through -the implementation process. - -For the agent simulation we want to add a security model such that we group -agents into two authentication groups, "norse legends", including the -principals thor, odin, and loki, and "greek men", including prometheus, -archimedes, and thucydides. - -We associate permissions with access to services and homes. We differentiate -the homes such that certain authentication groups only have access to services -or the home itself based on the local settings of the home in which they -reside. - -We define the homes/sandboxes - - - origin - all agents start here, and have access to all - services here. - - - valhalla - only agents in the authentication group 'norse - legend' can reside here. - - - jail - all agents can come here, but only 'norse legend's - can leave or access services. - - -Process -~~~~~~~ - -Loosely we define a process for implementing this security model - - - mapping permissions onto actions - - - mapping authentication tokens onto permissions - - - implementing checkers and security policies that use our - authentication tokens and permissions. - - - binding checkers to our simulation classes - - - inserting the hooks into the original simulation code to add - proxy wrappers to automatically check security. - - - inserting hooks into the original simulation to register the - agents as the active principal in an interaction. - - -Defining a Permission Model -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We define the following permissions:: - - NotAllowed = 'Not Allowed' - Public = Checker.CheckerPublic - TransportAgent = 'Transport Agent' - AccessServices = 'Access Services' - AccessAgents = 'Access Agents' - AccessTimeService = 'Access Time Services' - AccessAgentService = 'Access Agent Service' - AccessHomeService = 'Access Home Service' - -and create a dictionary database mapping homes to authentication groups which -are linked to associated permissions. - - -Defining and Binding Checkers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Checkers are the foundational unit for the security framework. They define -what attributes can be accessed or set on a given instance. They can be used -implicitly via Proxy objects, to guard all attribute access automatically or -explicitly to check a given access for an operation. - -Checker construction expects two functions or dictionaries, one is used to map -attribute names to permissions for attribute access and another to do the same -for setting attributes. - -We use the following checker factory function:: - - def PermissionMapChecker(permissions_map={}, - setattr_permission_func=NoSetAttr): - res = {} - for k,v in permissions_map.items(): - for iv in v: - res[iv]=k - return checker.Checker(res.get, setattr_permission_func) - - time_service_checker = PermissionMapChecker( - # permission : [methods] - {'AccessTimeService':['getTime']} - ) - -with the NoSetAttr function defined as a lambda which always return the -permission `NotAllowed`. - -To bind the checkers to the simulation classes we register our checkers with -the security model's global checker registry:: - - import sandbox_simulation - from zope.security.checker import defineChecker - defineChecker(sandbox_simulation.TimeService, time_service_checker) - - -Defining a Security Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We implement our security policy such that it checks the current agent's -authentication token against the given permission in the home of the object -being accessed:: - - @implementer(ISecurityPolicy) - class SimulationSecurityPolicy: - - createInteraction = staticmethod(simpleinteraction.createInteraction) - - def checkPermission(self, permission, object, interaction): - - home = object.getHome() - db = getattr(SimulationSecurityDatabase, home.getId(), None) - - if db is None: - return False - - allowed = db.get('any', ()) - if permission in allowed or ALL in allowed: - return True - - if interaction is None: - return False - if not interaction.participations: - return False - for participation in interaction.participations: - token = participation.principal.getAuthenticationToken() - allowed = db.get(token, ()) - if permission not in allowed: - return False - - return True - -There are no specific requirements for the interaction class, so we can just -use `zope.security.simpleinteraction.Interaction`. - -Since an interaction can have more than one principal, we check that *all* of -them are given the necessary permission. This is not really necessary since -we only create interactions with a single active principal. - -There is some additional code present to allow for shortcuts in defining the -permission database when defining permissions for all auth groups and all -permissions. - - -Integration -~~~~~~~~~~~ - -At this point we have implemented our security model, and we need to integrate -it with our simulation model. We do so in three separate steps. - -First we make it such that agents only access homes that are wrapped in a -security proxy. By doing this all access to homes and services (proxies have -proxied return values for their methods) is implicitly guarded by our security -policy. - -The second step is that we want to associate the active agent with the -security context so the security policy will know which agent's authentication -token to validate against. - -The third step is to set our security policy as the default policy for the -Zope security framework. It is possible to create custom security policies at -a finer grained than global, but such is left as an exercise for the reader. - - -Interaction Access -~~~~~~~~~~~~~~~~~~ - -The *default* implementation of the interaction management interfaces defines -interactions on a per thread basis with a function for an accessor. This -model is not appropriate for all systems, as it restricts one to a single -active interaction per thread at any given moment. Reimplementing the -interaction access methods though is easily doable and is noted here for -completeness. - - -Perspectives -~~~~~~~~~~~~ - -It's important to keep in mind that there is a lot more that is possible using -the security framework than what's been presented here. All of the -interactions are interface based, such that if you need to re-implement the -semantics to suite your application a new implementation of the interface will -be sufficient. Additional possibilities range from restricted interpreters -and dynamic loading of untrusted code to non Zope web application security -systems. Insert imagination here ;-). - - -Zope Perspective -~~~~~~~~~~~~~~~~ - -A Zope3 programmer will never commonly need to interact with the low level -security framework. Zope3 defines a second security package over top the low -level framework and authentication sources and checkers are handled via zcml -registration. Still those developing Zope3 will hopefully find this useful as -an introduction into the underpinnings of the security framework. - - -Code -~~~~ - -The complete code for this example is available. - -- sandbox.py - the agent framework - -- sandbox_security.py - the security implementation and binding to the agent - framework. - - -Authors -~~~~~~~ - -- Kapil Thangavelu <hazmat at objectrealms.net> -- Guido Wesdorp <guido at infrae.com> -- Marius Gedminas <marius at pov.lt> - - -Untrusted interpreters -====================== - -Untrusted programs are executed by untrusted interpreters. Untrusted -interpreters make use of security proxies to prevent un-mediated -access to assets. An untrusted interpreter defines an environment for -running untrusted programs. All objects within the environment are -either: - -- "safe" objects created internally by the environment or created in - the course of executing the untrusted program, or - -- "basic" objects - -- security-proxied non-basic objects - -The environment includes proxied functions for accessing objects -outside of the environment. These proxied functions provide the only -way to access information outside the environment. Because these -functions are proxied, as described below, any access to objects -outside the environment is mediated by the target security functions. - -Safe objects are objects whose operations, except for attribute -retrieval, and methods access only information stored within the -objects or passed as arguments. Safe objects contained within the -interpreter environment can contain only information that is already -in the environment or computed directly from information that is -included in the environment. For this reason, safe objects created -within the environment cannot be used to directly access information -outside the environment. - -Safe objects have some attributes that could (very) indirectly be used -to access assets. For this reason, an untrusted interpreter always -proxies the results of attribute accesses on a safe objects. - -Basic objects are safe objects that are used to represent elemental -data values such as strings and numbers. Basic objects require a -lower level of protection than non-basic objects, as will be described -detail in a later section. - -Security proxies mediate all object operations. Any operation -access is checked to see whether a subject is authorized to perform -the operation. All operation results other than basic objects are, in -turn, security proxied. Security proxies will be described in greater -detail in a later section. Any operation on a security proxy that -results in a non-basic object is also security proxied. - -All external resources needed to perform an operation are security -proxied. - -Let's consider the trusted interpreter for evaluating URLs. In -operation 1 of the example, the interpreter uses a proxied method for -getting the system root object. Because the method is proxied, the -result of calling the method and the operation is also proxied. - -The interpreter has a function for traversing objects. This function -is proxied. When traversing an object, the function is passed an -object and a name. In operation 2, the function is passed the result -of operation 1, which is the proxied root object and the name 'A'. We -may traverse an object by invoking an operation on it. For example, -we may use an operation to get a sub-object. Because any operation on a -proxied object returns a proxied object or a basic object, the result -is either a proxied object or a basic object. Traversal may also look -up a component. For example, in operation 1, we might look up a -presentation component named "A" for the root object. In this case, -the external object is not proxied, but, when it is returned from the -traversal function, it is proxied (unless it is a a basic object) -because the traversal function is proxied, and the result of calling a -proxied function is proxied (unless the result is a basic object). -Operation 3 proceeds in the same way. - -When we get to operation 4, we use a function for computing the -default presentation of the result of operation 3. As with traversal, -the result of getting the default presentation is either a proxied -object or a basic object because the function for getting the default -presentation is proxied. - -When we get to the last operation, we have either a proxied object or a -basic object. If the result of operation 4 is a basic object, we -simply convert it to a string and return it as the result page. If -the result of operation 4 is a non-basic object, we invoke a render -operation on it and return the result as a string. - -Note that an untrusted interpreter may or may not provide protection -against excessive resource usage. Different interpreters will provide -different levels of service with respect to limitations on resource -usage. - -If an untrusted interpreter performs an attribute access, the trusted -interpreter must proxy the result unless the result is a basic object. - -In summary, an untrusted interpreter assures that any access to assets -is mediated through security proxies by creating an environment to run -untrusted code and making sure that: - -- The only way to access anything from outside of the environment is - to call functions that are proxied in the environment. - -- Results of any attribute access in the environment are proxied - unless the results are basic objects. - -Security proxies ----------------- - -Security proxies are objects that wrap and mediate access to objects. - -The Python programming language used by Zope defines a set of specific -named low-level operations. In addition to operations, Python objects -can have attributes, used to represent data and methods. Attributes -are accessed using a dot notation. Applications can, and usually do, -define methods to provide extended object behaviors. Methods are -accessed as attributes through the low-level operation named -"__getattribute__". The Python code:: - - a.b() - -invokes 2 operations: - - 1. Use the low-level `__getattribute__` operation with the name "b". - - 2. Use the low-level '__call__' operation on the result of the first - operation. - -For all operations except the `__getattribute__` and -`__setattribute__` operations, security proxies have a permission -value defined by the permission-declaration subsystem. Two special -permission values indicate that access is either forbidden (never -allowed) or public (always allowed). For all other permission values, -the authorization subsystem is used to decide whether the subject has -the permission for the proxied object. If the subject has the -permission, then access to the operation is allowed. Otherwise, access -is denied. - -For getting or setting attributes, a proxy has permissions for getting -and a permission for setting attribute values for a given attribute -name. As described above, these permissions may be one of the two -special permission values indicating forbidden or public access, or -another permission value that must be checked with the authorization -system. - -For all objects, Zope defines the following operations to be always public: - - comparison - "__lt__", "__le__", "__eq__", "__gt__", "__ge__", "__ne__" - - hash - "__hash__" - - boolean value - "__nonzero__" - - class introspection - "__class__" - - interface introspection - "__providedBy__", "__implements__" - - adaptation - "__conform__" - - low-level string representation - "__repr__" - -The result of an operation on a proxied object is a security proxy -unless the result is a basic value. - -Basic objects +Participation ------------- -Basic objects are safe immutable objects that contain only immutable -subobjects. Examples of basic objects include: - -- Strings, - -- Integers (long and normal), - -- Floating-point objects, - -- Date-time objects, - -- Boolean objects (True and False), and +Stores information about a single principal :class:`participating +<zope.security.interfaces.IParticipation>` in the :class:`interaction +<zope.security.interfaces.IInteraction>`. -- The special (nil) object, None. - -Basic objects are safe, so, as described earlier, operations on basic -objects, other than attribute access, use only information contained -within the objects or information passed to them. For this reason, -basic objects cannot be used to access information outside of the -untrusted interpreter environment. - -The decision not to proxy basic objects is largely an optimization. -It allows low-level safe computation to be performed without -unnecessary overhead, - -Note that a basic object could contain sensitive information, but such -a basic object would need to be obtained by making a call on a proxied -object. Therefore, the access to the basic object in the first place -is mediated by the security functions. - -Rationale for mutable safe objects ----------------------------------- - -Some safe objects are not basic. For these objects, we proxy the -objects if they originate from outside of the environment. We do this -for two reasons: - -1. Non-basic objects from outside the environment need to be proxied - to prevent unauthorized access to information. - -2. We need to prevent un-mediated change of information from outside of - the environment. +Security Policy +--------------- -We don't proxy safe objects created within the environment. This is -safe to do because such safe objects can contain and provide access to -information already in the environment. Sometimes the interpreter or -the interpreted program needs to be able to create simple data -containers to hold information computed in the course of the program -execution. Several safe container types are provided for this -purpose. +A :class:`security policy <ISecurityPolicy>` is used to create the +interaction that will ultimately be responsible for security checking. diff --git a/docs/proxy.rst b/docs/proxy.rst new file mode 100644 index 0000000..2b01ee0 --- /dev/null +++ b/docs/proxy.rst @@ -0,0 +1,403 @@ +============================================= + Untrusted Interpreters and Security Proxies +============================================= + +Untrusted programs are executed by untrusted interpreters. Untrusted +interpreters make use of security proxies to prevent un-mediated +access to assets. An untrusted interpreter defines an environment for +running untrusted programs. All objects within the environment are +either: + +- "safe" objects created internally by the environment or created in + the course of executing the untrusted program, or + +- "basic" objects + +- security-proxied non-basic objects + +The environment includes proxied functions for accessing objects +outside of the environment. These proxied functions provide the only +way to access information outside the environment. Because these +functions are proxied, as described below, any access to objects +outside the environment is mediated by the target security functions. + +Safe objects are objects whose operations, except for attribute +retrieval, and methods access only information stored within the +objects or passed as arguments. Safe objects contained within the +interpreter environment can contain only information that is already +in the environment or computed directly from information that is +included in the environment. For this reason, safe objects created +within the environment cannot be used to directly access information +outside the environment. + +Safe objects have some attributes that could (very) indirectly be used +to access assets. For this reason, an untrusted interpreter always +proxies the results of attribute accesses on a safe objects. + +Basic objects are safe objects that are used to represent elemental +data values such as strings and numbers. Basic objects require a +lower level of protection than non-basic objects, as will be described +detail in a later section. + +Security proxies mediate all object operations. Any operation +access is checked to see whether a subject is authorized to perform +the operation. All operation results other than basic objects are, in +turn, security proxied. Security proxies will be described in greater +detail in a later section. Any operation on a security proxy that +results in a non-basic object is also security proxied. + +All external resources needed to perform an operation are security +proxied. + +Let's consider the trusted interpreter for evaluating URLs. In +operation 1 of the example, the interpreter uses a proxied method for +getting the system root object. Because the method is proxied, the +result of calling the method and the operation is also proxied. + +The interpreter has a function for traversing objects. This function +is proxied. When traversing an object, the function is passed an +object and a name. In operation 2, the function is passed the result +of operation 1, which is the proxied root object and the name 'A'. We +may traverse an object by invoking an operation on it. For example, +we may use an operation to get a sub-object. Because any operation on a +proxied object returns a proxied object or a basic object, the result +is either a proxied object or a basic object. Traversal may also look +up a component. For example, in operation 1, we might look up a +presentation component named "A" for the root object. In this case, +the external object is not proxied, but, when it is returned from the +traversal function, it is proxied (unless it is a a basic object) +because the traversal function is proxied, and the result of calling a +proxied function is proxied (unless the result is a basic object). +Operation 3 proceeds in the same way. + +When we get to operation 4, we use a function for computing the +default presentation of the result of operation 3. As with traversal, +the result of getting the default presentation is either a proxied +object or a basic object because the function for getting the default +presentation is proxied. + +When we get to the last operation, we have either a proxied object or a +basic object. If the result of operation 4 is a basic object, we +simply convert it to a string and return it as the result page. If +the result of operation 4 is a non-basic object, we invoke a render +operation on it and return the result as a string. + +Note that an untrusted interpreter may or may not provide protection +against excessive resource usage. Different interpreters will provide +different levels of service with respect to limitations on resource +usage. + +If an untrusted interpreter performs an attribute access, the trusted +interpreter must proxy the result unless the result is a basic object. + +In summary, an untrusted interpreter assures that any access to assets +is mediated through security proxies by creating an environment to run +untrusted code and making sure that: + +- The only way to access anything from outside of the environment is + to call functions that are proxied in the environment. + +- Results of any attribute access in the environment are proxied + unless the results are basic objects. + +Security proxies +================ + +Security proxies are objects that wrap and mediate access to objects. + +The Python programming language used by Zope defines a set of specific +named low-level operations. In addition to operations, Python objects +can have attributes, used to represent data and methods. Attributes +are accessed using a dot notation. Applications can, and usually do, +define methods to provide extended object behaviors. Methods are +accessed as attributes through the low-level operation named +"__getattribute__". The Python code:: + + a.b() + +invokes 2 operations: + + 1. Use the low-level ``__getattribute__`` operation with the name "b". + + 2. Use the low-level ``__call__`` operation on the result of the first + operation. + +For all operations except the ``__getattribute__`` and +``__setattribute__`` operations, security proxies have a permission +value defined by the permission-declaration subsystem. Two special +permission values indicate that access is either forbidden (never +allowed) or public (always allowed). For all other permission values, +the authorization subsystem is used to decide whether the subject has +the permission for the proxied object. If the subject has the +permission, then access to the operation is allowed. Otherwise, access +is denied. + +For getting or setting attributes, a proxy has permissions for getting +and a permission for setting attribute values for a given attribute +name. As described above, these permissions may be one of the two +special permission values indicating forbidden or public access, or +another permission value that must be checked with the authorization +system. + +For all objects, Zope defines the following operations to be always public: + + comparison + "__lt__", "__le__", "__eq__", "__gt__", "__ge__", "__ne__" + + hash + "__hash__" + + boolean value + "__nonzero__" + + class introspection + "__class__" + + interface introspection + "__providedBy__", "__implements__" + + adaptation + "__conform__" + + low-level string representation + "__repr__" + +The result of an operation on a proxied object is a security proxy +unless the result is a basic value. + +Basic objects +============= + +Basic objects are safe immutable objects that contain only immutable +subobjects. Examples of basic objects include: + +- Strings, + +- Integers (long and normal), + +- Floating-point objects, + +- Date-time objects, + +- Boolean objects (True and False), and + +- The special (nil) object, None. + +Basic objects are safe, so, as described earlier, operations on basic +objects, other than attribute access, use only information contained +within the objects or information passed to them. For this reason, +basic objects cannot be used to access information outside of the +untrusted interpreter environment. + +The decision not to proxy basic objects is largely an optimization. +It allows low-level safe computation to be performed without +unnecessary overhead, + +Note that a basic object could contain sensitive information, but such +a basic object would need to be obtained by making a call on a proxied +object. Therefore, the access to the basic object in the first place +is mediated by the security functions. + +Rationale for mutable safe objects +================================== + +Some safe objects are not basic. For these objects, we proxy the +objects if they originate from outside of the environment. We do this +for two reasons: + +1. Non-basic objects from outside the environment need to be proxied + to prevent unauthorized access to information. + +2. We need to prevent un-mediated change of information from outside of + the environment. + +We don't proxy safe objects created within the environment. This is +safe to do because such safe objects can contain and provide access to +information already in the environment. Sometimes the interpreter or +the interpreted program needs to be able to create simple data +containers to hold information computed in the course of the program +execution. Several safe container types are provided for this +purpose. + +.. _proxy-known-issues: + +Known Issues With Proxies +========================= + +Security proxies (proxies in general) are not perfect in Python. There +are some things that they cannot transparently proxy. + +.. _isinstance-and-proxies: + +isinstance and proxies +---------------------- + +A proxied object cannot proxy its type (although it does proxy its ``__class__``): + +.. doctest:: + + >>> from zope.security.proxy import ProxyFactory + >>> class Object(object): + ... pass + >>> target = Object() + >>> target.__class__ + <class 'Object'> + >>> type(target) + <class 'Object'> + >>> proxy = ProxyFactory(target, None) + >>> proxy.__class__ + <class 'Object'> + >>> type(proxy) + <... 'zope.security...Proxy...'> + +This means that the builtin :func:`isinstance` may return unexpected +results: + +.. doctest:: + + >>> isinstance(target, Object) + True + >>> isinstance(proxy, Object) + False + +There are two workarounds. The safest is to use +:func:`zope.security.proxy.isinstance`, which takes specifically this +into account (in modules that will be dealing with a number of +proxies, it is common to simply place ``from zope.security.proxy +import isinstance`` at the top of the file to override the builtin +:func:`isinstance`; we won't show that here for clarity): + +.. doctest:: + + >>> import zope.security.proxy + >>> zope.security.proxy.isinstance(target, Object) + True + >>> zope.security.proxy.isinstance(proxy, Object) + True + +Alternatively, you can manually remove the security proxy (or indeed, +all proxies) with :func:`zope.security.proxy.removeSecurityProxy` or +:func:`zope.proxy.removeAllProxies`, respectively, before calling +:func:`isinstance`: + +.. doctest:: + + >>> from zope.security.proxy import removeSecurityProxy + >>> isinstance(removeSecurityProxy(target), Object) + True + >>> isinstance(removeSecurityProxy(proxy), Object) + True + + +issubclass and proxies +---------------------- + +Security proxies will proxy the return value of ``__class__``: it will +be a proxy around the real class of the proxied value. This causes +failures with ``issubclass``: + +.. doctest:: + + >>> from zope.security.proxy import ProxyFactory + >>> class Object(object): + ... pass + >>> target = Object() + >>> target.__class__ is Object + True + >>> proxy = ProxyFactory(target, None) + >>> proxy.__class__ + <class 'Object'> + >>> proxy.__class__ is Object + False + >>> issubclass(proxy.__class__, Object) + Traceback (most recent call last): + ... + TypeError: issubclass() arg 1 must be a class + +Although the above is a contrived example, using :class:`abstract base +classes <abc.ABCMeta>` can cause it to arise quite +unexpectedly: + +.. doctest:: + + >>> from collections import Mapping + >>> from abc import ABCMeta + >>> isinstance(Mapping, ABCMeta) + True + >>> isinstance(proxy, Mapping) + Traceback (most recent call last): + ... + TypeError: issubclass() arg 1 must be a class + + +In this case, the workarounds described :ref:`above <isinstance-and-proxies>` also work: + +.. doctest:: + + >>> zope.security.proxy.isinstance(proxy, Mapping) + False + >>> isinstance(removeSecurityProxy(proxy), Mapping) + False + +.. We need to clean up the caching that ABC does on +.. pure-python platforms to make sure that we get where +.. we expect to be when we construct LogRecord; otherwise +.. the ProxyMetaclass may be in the negative cache, bypassing +.. the issubclass() calls we expect + +.. doctest:: + :hide: + + >>> ABCMeta._abc_invalidation_counter += 1 + +logging +~~~~~~~ + +Starting with `Python 2.7.7 <https://bugs.python.org/issue21172>`_, +the :class:`logging.LogRecord` makes exactly the above ``isinstance`` +call: + +.. doctest:: + + >>> from logging import LogRecord + >>> LogRecord("name", 1, "/path/to/file", 1, + ... "The message %s", (proxy,), None) + Traceback (most recent call last): + ... + TypeError: issubclass() arg 1 must be a class + +`Possible workarounds include <https://github.com/zopefoundation/zope.security/issues/26>`_: + +- Carefully removing security proxies of objects before passing them + to the logging system. +- Monkey-patching the logging system to use + :func:`zope.security.proxy.isinstance` which does this + automatically:: + + import zope.security.proxy + import logging + logging.isinstance = zope.security.proxy.isinstance +- Using :func:`logging.setLogRecordfactory` to set a custom + ``LogRecord`` subclass that unwraps any security proxies before they + are given to the super class. Note that this is only available on + Python 3. On Python 2, it might be possible to achieve a similar + result with a custom :func:`logger class <logging.setLoggerClass>`: + +.. doctest:: + + >>> from zope.security.proxy import removeSecurityProxy + >>> class UnwrappingLogRecord(LogRecord): + ... def __init__(self, name, level, pathname, lineno, + ... msg, args, exc_info, *largs, **kwargs): + ... args = [removeSecurityProxy(x) for x in args] + ... LogRecord.__init__(self, name, level, pathname, + ... lineno, msg, args, exc_info, *largs, **kwargs) + ... def __repr__(self): + ... return '<UnwrappingLogRecord>' + >>> UnwrappingLogRecord("name", 1, "/path/to/file", 1, + ... "The message %s", (proxy,), None) + <UnwrappingLogRecord> + + +Each specific application will have to determine what solution is +correct for its security model. diff --git a/src/zope/security/adapter.py b/src/zope/security/adapter.py index f92ac34..661e49d 100644 --- a/src/zope/security/adapter.py +++ b/src/zope/security/adapter.py @@ -19,12 +19,13 @@ from zope.security.proxy import removeSecurityProxy from zope.location import ILocation, LocationProxy def assertLocation(adapter, parent): - """Assert locatable adapters. + """ + Assert locatable adapters. - This function asserts that the adapter get location-proxied if - it doesn't provide ILocation itself. Further more the returned - locatable adapter get its parent set if its __parent__ attribute - is currently None. + This function asserts that the adapter get location-proxied if it + doesn't provide :class:`zope.location.interfaces.ILocation` + itself. Furthermore, the returned locatable adapter get its parent + set if its ``__parent__`` attribute is currently None. """ # handle none-locatable adapters (A) if not ILocation.providedBy(adapter): @@ -42,7 +43,8 @@ def assertLocation(adapter, parent): class LocatingTrustedAdapterFactory(object): - """Adapt an adapter factory to provide trusted and (locatable) adapters. + """ + Adapt an adapter factory to provide trusted and (locatable) adapters. Trusted adapters always adapt unproxied objects. If asked to adapt any proxied objects, it will unproxy them and then @@ -50,10 +52,12 @@ class LocatingTrustedAdapterFactory(object): security-proxied before (N). Further locating trusted adapters provide a location for protected - adapters only (S). If such a protected adapter itself does not provide - ILocation it is wrapped within a location proxy and it parent will - be set. If the adapter does provide ILocation and it's __parent__ is None, - we set the __parent__ to the adapter's context: + adapters only (S). If such a protected adapter itself does not + provide ILocation it is wrapped within a location proxy and it + parent will be set. If the adapter does provide + :class:`zope.location.interfaces.ILocation` and its + ``__parent__`` is None, we set the ``__parent__`` to the adapter's + context. """ def __init__(self, factory): self.factory = factory @@ -83,15 +87,17 @@ class LocatingTrustedAdapterFactory(object): class TrustedAdapterFactory(LocatingTrustedAdapterFactory): - """Adapt an adapter factory to provide trusted adapters. + """ + Adapt an adapter factory to provide trusted adapters. Trusted adapters always adapt unproxied objects. If asked to adapt any proxied objects, it will unproxy them and then security-proxy the resulting adapter unless the objects where not security-proxied before. - If the adapter does provide ILocation and it's __parent__ is None, - we set the __parent__ to the adapter's context. + If the adapter does provide + :class:`zope.location.interfaces.ILocation` and its ``__parent__`` + is None, we set the ``__parent__`` to the adapter's context. """ # do not location-proxy the adapter @@ -100,17 +106,20 @@ class TrustedAdapterFactory(LocatingTrustedAdapterFactory): class LocatingUntrustedAdapterFactory(object): - """Adapt an adapter factory to provide locatable untrusted adapters + """ + Adapt an adapter factory to provide locatable untrusted adapters Untrusted adapters always adapt proxied objects. If any permission - other than zope.Public is required, untrusted adapters need a location - in order that the local authentication mechanism can be inovked - correctly. - - If the adapter does not provide ILocation, we location proxy it and - set the parent. If the adapter does provide ILocation and - it's __parent__ is None, we set the __parent__ to the adapter's - context only: + other than :const:`zope.Public + <zope.security.interfaces.PUBLIC_PERMISSION_NAME>` is required, + untrusted adapters need a location in order that the local + authentication mechanism can be invoked correctly. + + If the adapter does not provide + :class:`zope.location.interfaces.ILocation`, we location proxy it + and set the parent. If the adapter does provide ``ILocation`` and + its ``__parent__`` is None, we set the ``__parent__`` to the + adapter's context only. """ def __init__(self, factory): diff --git a/src/zope/security/checker.py b/src/zope/security/checker.py index 7de50d5..79975d4 100644 --- a/src/zope/security/checker.py +++ b/src/zope/security/checker.py @@ -11,17 +11,49 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Security Checkers +""" +Security Checkers. + +This module contains the primary implementations of +:class:`zope.security.interfaces.IChecker` (:class:`Checker`, +:class:`MultiChecker`, :func:`NamesChecker`) and +:class:`zope.security.interfaces.IProxyFactory` (:func:`ProxyFactory`). + +It also defines helpers for permission checking (:func:`canAccess`, +:func:`canWrite`) and getting checkers +(:func:`getCheckerForInstancesOf`, :func:`selectChecker`). + +This module is accelerated with a C implementation on CPython by +default. If the environment variable ``PURE_PYTHON`` is set (to any +value) before this module is imported, the C extensions will be +bypassed and the reference Python implementations will be used. This +can be helpful for debugging and tracing. -You can set the environment variable ZOPE_WATCH_CHECKERS to get additional -security checker debugging output on the standard error. +Debugging Permissions Problems +============================== -Setting ZOPE_WATCH_CHECKERS to 1 will display messages about unauthorized or +You can set the environment variable ``ZOPE_WATCH_CHECKERS`` before +this module is imported to get additional security checker debugging +output on the standard error. + +Setting ``ZOPE_WATCH_CHECKERS`` to 1 will display messages about unauthorized or forbidden attribute access. Setting it to a larger number will also display messages about granted attribute access. -Note that the ZOPE_WATCH_CHECKERS mechanism will eventually be +Note that the ``ZOPE_WATCH_CHECKERS`` mechanism may eventually be replaced with a more general security auditing mechanism. + +.. seealso:: :class:`CheckerLoggingMixin`, :class:`WatchingChecker`, :class:`WatchingCombinedChecker` + +API +=== + +.. py:data:: CheckerPublic + + The special constant that indicates that no permission + checking needs to be done. + +.. autofunction:: selectChecker """ import abc import os @@ -156,15 +188,22 @@ def canAccess(obj, name): @implementer(INameBasedChecker) class CheckerPy(object): + """ + The Python reference implementation of + :class:`zope.security.interfaces.INameBasedChecker`. + + Ordinarily there will be no reason to ever explicitly use this class; + instead use the class assigned to :class:`Checker`. + """ def __init__(self, get_permissions, set_permissions=None): """Create a checker A dictionary must be provided for computing permissions for names. The dictionary get will be called with attribute names - and must return a permission id, None, or the special marker, - CheckerPublic. If None is returned, then access to the name is - forbidden. If CheckerPublic is returned, then access will be + and must return a permission ID, None, or the special marker, + :const:`CheckerPublic`. If None is returned, then access to the name is + forbidden. If :const:`CheckerPublic` is returned, then access will be granted without checking a permission. An optional setattr dictionary may be provided for checking @@ -295,12 +334,14 @@ class Global(object): return "%s(%s,%s)" % (self.__class__.__name__, self.__name__, self.__module__) -# Marker for public attributes + CheckerPublic = Global('CheckerPublic') CP_HACK_XXX = CheckerPublic # Now we wrap it in a security proxy so that it retains its # identity when it needs to be security proxied. +# XXX: This means that we can't directly document it with +# sphinx because issubclass() will fail. d = {} CheckerPublic = Proxy(CheckerPublic, Checker(d)) # XXX uses CheckerPy d['__reduce__'] = CheckerPublic @@ -316,7 +357,7 @@ def NamesChecker(names=(), permission_id=CheckerPublic, **__kw__): A sequence of names is given as the first argument. If a second argument, permission_id, is given, it is the permission required - to access the names. Additional names and permission ids can be + to access the names. Additional names and permission IDs can be supplied as keyword arguments. """ @@ -330,10 +371,15 @@ def NamesChecker(names=(), permission_id=CheckerPublic, **__kw__): return Checker(data) def InterfaceChecker(interface, permission_id=CheckerPublic, **__kw__): + """ + Create a :func:`NamesChecker` for all the names defined in the *interface* + (a subclass of :class:`zope.interface.Interface`). + """ return NamesChecker(interface.names(all=True), permission_id, **__kw__) def MultiChecker(specs): - """Create a checker from a sequence of specifications + """ + Create a checker from a sequence of specifications A specification is: @@ -341,12 +387,12 @@ def MultiChecker(specs): o a sequence of names or an interface - o a permission id + o a permission ID All the names in the sequence of names or the interface are protected by the permission. - - A dictionoid (having an items method), with items that are + - A dictionary (having an items method), with items that are name/permission-id pairs. """ data = {} @@ -409,8 +455,8 @@ DEFINABLE_TYPES = CLASS_TYPES + (types.ModuleType,) def defineChecker(type_, checker): """Define a checker for a given type of object - The checker can be a Checker, or a function that, when called with - an object, returns a Checker. + The checker can be a :class:`Checker`, or a function that, when called with + an object, returns a :class:`Checker`. """ if not isinstance(type_, DEFINABLE_TYPES): raise TypeError( @@ -463,16 +509,23 @@ class CombinedChecker(Checker): The following table describes the result of a combined checker in detail. - checker1 checker2 CombinedChecker(checker1, checker2) - ------------------ ------------------ ----------------------------------- - ok anything ok (checker2 is never called) - Unauthorized ok ok - Unauthorized Unauthorized Unauthorized - Unauthorized ForbiddenAttribute Unauthorized - ForbiddenAttribute ok ok - ForbiddenAttribute Unauthorized Unauthorized - ForbiddenAttribute ForbiddenAttribute ForbiddenAttribute - ------------------ ------------------ ----------------------------------- + +--------------------+--------------------+-------------------------------------+ + | checker1 | checker2 | CombinedChecker(checker1, checker2) | + +====================+====================+=====================================+ + | ok | anything | ok (checker 2 never called) | + +--------------------+--------------------+-------------------------------------+ + | Unathorized | ok | ok | + +--------------------+--------------------+-------------------------------------+ + | Unauthorized | Unauthorized | Unauthorized | + +--------------------+--------------------+-------------------------------------+ + | Unauthorized | ForbiddenAttribute | Unauthorized | + +--------------------+--------------------+-------------------------------------+ + | ForbiddenAttribute | ok | ok | + +--------------------+--------------------+-------------------------------------+ + | ForbiddenAttribute | Unauthorized | Unauthorized | + +--------------------+--------------------+-------------------------------------+ + | ForbiddenAttribute | ForbiddenAttribute | ForbiddenAttribute | + +--------------------+--------------------+-------------------------------------+ """ def __init__(self, checker1, checker2): @@ -509,15 +562,18 @@ class CombinedChecker(Checker): raise unauthorized_exception class CheckerLoggingMixin(object): - """Debugging mixin for checkers. + """ + Debugging mixin for checkers. Prints verbose debugging information about every performed check to - sys.stderr. + :data:`sys.stderr`. - If verbosity is set to 1, only displays Unauthorized and Forbidden messages. - If verbosity is set to a larger number, displays all messages. """ + #: If set to 1 (the default), only displays ``Unauthorized`` and + #: ``Forbidden`` messages. If verbosity is set to a larger number, + #: displays all messages. Normally this is controlled via the environment + #: variable ``ZOPE_WATCH_CHECKERS``. verbosity = 1 _file = sys.stderr @@ -586,8 +642,18 @@ class CheckerLoggingMixin(object): # We have to be careful with the order of inheritance # here. See https://github.com/zopefoundation/zope.security/issues/8 class WatchingChecker(CheckerLoggingMixin, Checker): + """ + A checker that will perform verbose logging. This will be set + as the default when ``ZOPE_WATCH_CHECKERS`` is set when this + module is imported. + """ verbosity = WATCH_CHECKERS class WatchingCombinedChecker(CombinedChecker, WatchingChecker): + """ + A checker that will perform verbose logging. This will be set + as the default when ``ZOPE_WATCH_CHECKERS`` is set when this + module is imported. + """ verbosity = WATCH_CHECKERS if WATCH_CHECKERS: # pragma: no cover @@ -606,6 +672,13 @@ def _instanceChecker(inst): return _checkers.get(inst.__class__, _defaultChecker) def moduleChecker(module): + """ + Return the :class:`zope.security.interfaces.IChecker` defined for the + *module*, if any. + + .. seealso:: :func:`zope.security.metaconfigure.protectModule` + To define module protections. + """ return _checkers.get(module) diff --git a/src/zope/security/interfaces.py b/src/zope/security/interfaces.py index 81addc2..4c62bdc 100644 --- a/src/zope/security/interfaces.py +++ b/src/zope/security/interfaces.py @@ -11,7 +11,43 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Interfaces for security machinery. +""" +Interfaces for security machinery. + +These can be categorized into a few different groups of related objects. + +* Exceptions + + - :class:`IUnauthorized` + - :class:`IForbidden` + - :class:`IForbiddenAttribute` + - :class:`NoInteraction` + +* Utilities + + - :class:`ISecurityManagement` + - :class:`ISecurityChecking` + - :class:`ISecurityProxyFactory` + - :class:`IChecker` + - :class:`INameBasedChecker` + - :class:`ISecurityPolicy` + +* Principals + + - :class:`IInteraction` + - :class:`IParticipation` + - :class:`IInteractionManagement` + - :class:`IPrincipal` + - :class:`IGroupAwarePrincipal` + - :class:`IGroupClosureAwarePrincipal` + - :class:`IGroup` + - :class:`IMemberGetterGroup` + - :class:`IMemberAwareGroup` + - :class:`IPermission` + +Anywhere that an API is documented as accepting a permission, it +means the name of the permission, or the special object +:class:`zope.security.checker.CheckerPublic`. """ from zope.interface import Interface, Attribute, implementer @@ -26,31 +62,57 @@ from zope.security.i18n import ZopeMessageFactory as _ PUBLIC_PERMISSION_NAME = 'zope.Public' class IUnauthorized(IException): - pass + """ + The action is not authorized. + + Implemented in :class:`Unauthorized`. + """ @implementer(IUnauthorized) class Unauthorized(Exception): - """Some user wasn't allowed to access a resource""" + """ + Some user wasn't allowed to access a resource. + + Default implementation of :class:`IUnauthorized`. + """ class IForbidden(IException): - pass + """ + A resource cannot be accessed under any circumstances + + Implemented in :class:`Forbidden`. + """ @implementer(IForbidden) class Forbidden(Exception): - """A resource cannot be accessed under any circumstances + """ + A resource cannot be accessed under any circumstances + + Default implementation if :class:`IForbidden`. """ class IForbiddenAttribute(IForbidden, IAttributeError): - pass + """ + An attribute is unavailable because it is forbidden (private). + + Implemented in :class:`ForbiddenAttribute`. + """ @implementer(IForbiddenAttribute) class ForbiddenAttribute(Forbidden, AttributeError): - """An attribute is unavailable because it is forbidden (private) + """ + An attribute is unavailable because it is forbidden (private). + + Default implementation of :class:`IForbiddenAttribute`. """ class ISecurityManagement(Interface): - """Public security management API.""" + """ + Public security management API. + + This is implemented by :mod:`zope.security.management`. + """ def getSecurityPolicy(): """Get the system default security policy.""" @@ -64,28 +126,36 @@ class ISecurityManagement(Interface): class ISecurityChecking(Interface): - """Public security API.""" + """ + Public security API. + """ def checkPermission(permission, object, interaction=None): - """Return whether security policy allows permission on object. - - 'permission' is permission name. - - 'object' is the object being accessed according to the permission. + """ + Return whether security policy allows permission on object. - 'interaction' is an interaction, providing access to information - such as authenticated principals. If it is None, the current - interaction is used. + :param str permission: The permission name. + :param object: The object being accessed according to the permission. + :keyword interaction: An :class:`IInteraction`, providing access to information + such as authenticated principals. If it is None, the current + interaction is used. """ class ISecurityProxyFactory(Interface): + """ + A factory for creating security-proxied objects. + + See :class:`zope.security.checker.ProxyFactory` for the + default implementation. + """ def __call__(object, checker=None): - """Create a security proxy + """ + Create a security proxy - If a checker is given, then use it, otherwise, try to figure - out a checker. + If a checker (:class:`IChecker`) is given, then use it, + otherwise, try to figure out a checker. If the object is already a security proxy, then it will be returned. @@ -93,93 +163,131 @@ class ISecurityProxyFactory(Interface): class IChecker(Interface): - """Security-proxy plugin objects that implement low-level checks + """ + Security-proxy plugin objects that implement low-level checks. The checker is responsible for creating proxies for - operation return values, via the proxy method. + operation return values, via the ``proxy`` method. - There are check_getattr() and check_setattr() methods for checking - getattr and setattr, and a check() method for all other operations. + There are :meth:`check_getattr` and :meth:`check_setattr` methods + for checking getattr and setattr, and a :meth:`check` method for all + other operations. - The check methods may raise errors. They return no value. + The check methods will raise errors if access is not allowed. + They return no value. - Example (for __getitem__): + Example (for ``__getitem__``):: - checker.check(ob, \"__getitem__\") + checker.check(ob, "__getitem__") return checker.proxy(ob[key]) + + .. seealso:: :mod:`zope.security.checker` """ def check_getattr(ob, name): - """Check whether attribute access is allowed. - - May raise Unauthorized or Forbidden. Returns no value. + """ + Check whether attribute access is allowed. - If a checker implements __setitem__, then __setitem__ will be - called rather than check_getattr to check whether an attribute - access is allowed. This is a hack that allows significantly + If a checker implements ``__setitem__``, then ``__setitem__`` + will be called rather than ``check`` to ascertain whether an + operation is allowed. This is a hack that allows significantly greater performance due to the fact that low-level operator access is much faster than method access. + + :raises: :class:`Unauthorized` + :raises: :class:`Forbidden` + :return: Nothing """ def check_setattr(ob, name): - """Check whether attribute assignment is allowed. + """ + Check whether attribute assignment is allowed. - May raise Unauthorized or Forbidden. Returns no value. + If a checker implements ``__setitem__``, then ``__setitem__`` + will be called rather than ``check`` to ascertain whether an + operation is allowed. This is a hack that allows significantly + greater performance due to the fact that low-level operator + access is much faster than method access. + + :raises: :class:`Unauthorized` + :raises: :class:`Forbidden` + :return: Nothing """ def check(ob, operation): - """Check whether operation is allowed. + """ + Check whether *operation* is allowed. The operation name is the Python special method name, e.g. "__getitem__". May raise Unauthorized or Forbidden. Returns no value. - If a checker implements __setitem__, then __setitem__ will be - called rather than check to check whether an operation is - allowed. This is a hack that allows significantly greater - performance due to the fact that low-level operator access is - much faster than method access. + If a checker implements ``__setitem__``, then ``__setitem__`` + will be called rather than ``check`` to ascertain whether an + operation is allowed. This is a hack that allows significantly + greater performance due to the fact that low-level operator + access is much faster than method access. + + :raises: :class:`Unauthorized` + :raises: :class:`Forbidden` + :return: Nothing """ def proxy(value): - """Return a security proxy for the value. + """ + Return a security proxy for the *value*. - If a checker implements __getitem__, then __getitem__ will be - called rather than proxy to proxy the value. This is a hack - that allows significantly greater performance due to the fact - that low-level operator access is much faster than method - access. + If a checker implements ``__getitem__``, then ``__getitem__`` + will be called rather than ``proxy`` to proxy the value. This + is a hack that allows significantly greater performance due to + the fact that low-level operator access is much faster than + method access. """ class INameBasedChecker(IChecker): - """Security checker that uses permissions to check attribute access.""" + """ + Security checker that uses permissions to check attribute + access. + """ def permission_id(name): - """Return the permission used to check attribute access on name. + """ + Return the permission used to check attribute access on *name*. - This permission is used by both check and check_getattr. + This permission is used by both :meth:`check` and :meth:`check_getattr`. """ def setattr_permission_id(name): - """Return the permission used to check attribute assignment on name. + """ + Return the permission used to check attribute assignment on *name*. - This permission is used by check_setattr. + This permission is used by :meth:`check_setattr`. """ class ISecurityPolicy(Interface): + """ + A factory to get :class:`IInteraction` objects. + + .. seealso:: :mod:`zope.security.simplepolicies` + For default implementations. + """ def __call__(participation=None): - """Creates a new interaction for a given request. + """ + Creates and returns a new :class:`IInteraction` for a given + request. - If participation is not None, it is added to the new interaction. + If *participation* is not None, it is added to the new interaction. """ class IInteraction(Interface): - """A representation of an interaction between some actors and the system. + """ + A representation of an interaction between some actors and the + system. """ participations = Attribute("""An iterable of participations.""") @@ -193,16 +301,20 @@ class IInteraction(Interface): def checkPermission(permission, object): """Return whether security context allows permission on object. - Arguments: - permission -- A permission name - object -- The object being accessed according to the permission + :param str permission: A permission name + :param object: The object being accessed according to the permission + :return: Whether the access is allowed or not. + :rtype: bool """ class IParticipation(Interface): + """ + A single participant in an interaction. + """ interaction = Attribute("The interaction") - principal = Attribute("The authenticated principal") + principal = Attribute("The authenticated :class:`IPrincipal`") class NoInteraction(Exception): @@ -210,50 +322,58 @@ class NoInteraction(Exception): """ class IInteractionManagement(Interface): - """Interaction management API. + """ + Interaction management API. Every thread has at most one active interaction at a time. + + .. seealso:: :mod:`zope.security.management` + That module provides the default implementation. """ def newInteraction(participation=None): - """Start a new interaction. + """ + Start a new interaction. - If participation is not None, it is added to the new interaction. + If *participation* is not None, it is added to the new interaction. Raises an error if the calling thread already has an interaction. """ def queryInteraction(): - """Return the current interaction. + """ + Return the current interaction. Return None if there is no interaction. """ def getInteraction(): - """Return the current interaction. + """ + Return the current interaction. - Raise NoInteraction if there isn't a current interaction. + :raise NoInteraction: if there isn't a current interaction. """ def endInteraction(): - """End the current interaction. + """ + End the current interaction. Does nothing if there is no interaction. """ class IPrincipal(Interface): - """Principals are security artifacts that execute actions in a security - environment. + """ + Principals are security artifacts that execute actions in a + security environment. - The most common examples of principals include user and group objects. + The most common examples of principals include user and group + objects. - It is likely that IPrincipal objects will have associated views - used to list principals in management interfaces. For example, a - system in which other meta-data are provided for principals might - extend IPrincipal and register a view for the extended interface - that displays the extended information. We'll probably want to - define a standard view name (e.g. 'inline_summary') for this - purpose. + It is likely that ``IPrincipal`` objects will have associated + views used to list principals in management interfaces. For + example, a system in which other meta-data are provided for + principals might extend ``IPrincipal`` and register a view for the + extended interface that displays the extended information. """ id = TextLine( @@ -275,33 +395,47 @@ class IPrincipal(Interface): class IGroupAwarePrincipal(IPrincipal): - """Group aware principal interface - Extends IPrincipal to contain group information. + """ + Group aware principal interface. + + Extends ``IPrincipal`` to contain direct group information. """ groups = Attribute( - 'An iterable of groups to which the principal directly belongs') + 'An iterable of :class:`IGroup` objects to which the principal directly belongs') class IGroupClosureAwarePrincipal(IGroupAwarePrincipal): + """ + A group-aware principal that can recursively flatten the membership + of groups to return all the groups. + """ allGroups = Attribute( "An iterable of the full closure of the principal's groups.") class IGroup(IPrincipal): - """Group of principals + """ + Group of principals """ class IMemberGetterGroup(IGroup): - """a group that can get its members""" + """ + A group that can get its members. + """ def getMembers(): - """return an iterable of the members of the group""" + """Return an iterable of the members of the group""" class IMemberAwareGroup(IMemberGetterGroup): - """a group that can both set and get its members.""" + """ + A group that can both set and get its members. + """ def setMembers(value): - """set members of group to the principal ids in the iterable value""" + """ + Set members of group to the principal IDs in the iterable + *value*. + """ class IPermission(Interface): """A permission object.""" diff --git a/src/zope/security/management.py b/src/zope/security/management.py index 7a77f1a..0c037d0 100644 --- a/src/zope/security/management.py +++ b/src/zope/security/management.py @@ -11,7 +11,11 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Default 'ISecurityManagement' and 'IInteractionManagement' implementation +""" +Default :class:`zope.security.interfaces.ISecurityManagement` and +:class:`zope.security.interfaces.IInteractionManagement` implementation. + +Note that this module itself provides those interfaces. """ from zope.interface import moduleProvides @@ -58,6 +62,7 @@ def setSecurityPolicy(aSecurityPolicy): # def queryInteraction(): + """Return a current interaction, if there is one.""" return getattr(thread_local, 'interaction', None) def getInteraction(): @@ -70,7 +75,10 @@ def getInteraction(): class ExistingInteraction(ValueError, AssertionError, #BBB ): - pass + """ + The exception that :func:`newInteraction` will raise if called + during an existing interaction. + """ def newInteraction(*participations): """Start a new interaction.""" @@ -109,16 +117,16 @@ def restoreInteraction(): def checkPermission(permission, object, interaction=None): """Return whether security policy allows permission on object. - 'permission' is a permission name. - - 'object' is the object being accessed according to the permission. - - 'interaction' is an interaction, providing access to information - such as authenticated principals. If it is None, the current - interaction is used. - - checkPermission is guaranteed to return True if permission is - CheckerPublic or None. + :param str permission: A permission name. + :param object: The object being accessed according to the permission. + :param interaction: An interaction, providing access to information + such as authenticated principals. If it is None, the current + interaction is used. + :return: A boolean value. ``checkPermission`` is guaranteed to + return ``True`` if *permission* is + :data:`zope.security.checker.CheckerPublic` or ``None``. + :raise NoInteraction: If there is no current interaction and no + interaction argument was given. """ if permission is CheckerPublic or permission is None: return True diff --git a/src/zope/security/metadirectives.py b/src/zope/security/metadirectives.py index bd1f793..c0830d6 100644 --- a/src/zope/security/metadirectives.py +++ b/src/zope/security/metadirectives.py @@ -192,4 +192,4 @@ class IRequire(Interface): permission = Permission( title=u"Permission ID", - description=u"The id of the permission to require.") + description=u"The ID of the permission to require.") diff --git a/src/zope/security/permission.py b/src/zope/security/permission.py index 9854e3d..b54d285 100644 --- a/src/zope/security/permission.py +++ b/src/zope/security/permission.py @@ -31,6 +31,9 @@ from zope.security.interfaces import PUBLIC_PERMISSION_NAME as zope_Public @implementer(IPermission) class Permission(object): + """ + Default implementation of :class:`zope.security.interfaces.IPermission`. + """ def __init__(self, id, title="", description=""): self.id = id @@ -38,22 +41,26 @@ class Permission(object): self.description = description def checkPermission(context, permission_id): - """Check whether a given permission exists in the provided context. + """ + Check whether a given permission object exists in the provided + context as a utility. """ if permission_id is CheckerPublic: return if not queryUtility(IPermission, permission_id, context=context): - raise ValueError("Undefined permission id", permission_id) + raise ValueError("Undefined permission ID", permission_id) def allPermissions(context=None): - """Get the ids of all defined permissions + """ + Get the IDs of all defined permission object utilities. """ for name, _permission in getUtilitiesFor(IPermission, context): if name != zope_Public: yield name def PermissionsVocabulary(context=None): - """A vocabulary of permission IDs. + """ + A vocabulary of permission IDs. Term values are permissions, while term tokens are permission IDs. """ @@ -65,13 +72,18 @@ def PermissionsVocabulary(context=None): directlyProvides(PermissionsVocabulary, IVocabularyFactory) def PermissionIdsVocabulary(context=None): - """A vocabulary of permission IDs. + """ + A vocabulary of permission IDs. - Term values are the permission ID strings except for 'zope.Public', which - is the global permission CheckerPublic. + Term values are the permission ID strings except for + :data:`zope.Public + <zope.security.interfaces.PUBLIC_PERMISSION_NAME>`, which is the + global permission :data:`zope.security.checker.CheckerPublic`. - Term titles are the permission ID strings except for 'zope.Public', which - is shortened to 'Public'. + Term titles are the permission ID strings except for + :data:`zope.Public + <zope.security.interfaces.PUBLIC_PERMISSION_NAME>`, which is + shortened to 'Public'. Terms are sorted by title except for 'Public', which always appears as the first term. diff --git a/src/zope/security/proxy.py b/src/zope/security/proxy.py index 8f6d7c3..10f0353 100644 --- a/src/zope/security/proxy.py +++ b/src/zope/security/proxy.py @@ -11,7 +11,10 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Helper functions for Proxies. +""" +Helper functions for proxies. + +.. seealso:: :ref:`proxy-known-issues` """ import functools import sys @@ -60,6 +63,16 @@ def _fmt_address(obj): class ProxyPy(PyProxyBase): + """ + The pure-Python reference implementation of a security proxy. + + This should normally not be created directly, instead use the + :func:`~.ProxyFactory`. + + You can choose to use this implementation instead of the C implementation + by default by setting the ``PURE_PYTHON`` environment variable before + :mod:`zope.security` is imported. + """ __slots__ = ('_wrapped', '_checker') def __new__(cls, value, checker): @@ -394,7 +407,7 @@ def getTestProxyItems(proxy): def isinstance(object, cls): - """Test whether an object is an instance of a type. + """Test whether an *object* is an instance of a type. This works even if the object is security proxied. """ diff --git a/src/zope/security/simplepolicies.py b/src/zope/security/simplepolicies.py index b2e8980..d1617ce 100644 --- a/src/zope/security/simplepolicies.py +++ b/src/zope/security/simplepolicies.py @@ -11,7 +11,13 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Simple 'ISecurityPolicy' implementations. +""" +Simple :class:`zope.security.interfaces.ISecurityPolicy` implementations. + +As a reminder, ``ISecurityPolicy`` objects are factories for producing +:class:`zope.security.interfaces.IInteraction` objects. That means +that the classes themselves are implementations of +``ISecurityPolicy``. """ import zope.interface @@ -25,7 +31,8 @@ from zope.security._definitions import system_user @zope.interface.provider(ISecurityPolicy) class ParanoidSecurityPolicy(object): """ - Prohibit all access by any non-system principal, unless the item is public. + Prohibit all access by any non-system principal, unless the item + is :data:`public <zope.security.checker.CheckerPublic>`. This means that if there are no participations (and hence no principals), then access is allowed. diff --git a/src/zope/security/testing.py b/src/zope/security/testing.py index aae9b53..bd4b889 100644 --- a/src/zope/security/testing.py +++ b/src/zope/security/testing.py @@ -11,7 +11,8 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Testing support code +""" +Testing support code. This module provides some helper/stub objects for setting up interactions. """ @@ -38,6 +39,9 @@ output_checker = renormalizing.RENormalizing(rules) @interface.implementer(interfaces.IPrincipal) class Principal(object): + """ + A trivial implementation of :class:`zope.security.interfaces.IPrincipal`. + """ def __init__(self, id, title=None, description='', groups=None): self.id = id @@ -50,14 +54,19 @@ class Principal(object): @interface.implementer(interfaces.IParticipation) class Participation(object): - + """ + A trivial implementation of :class:`zope.security.interfaces.IParticipation`. + """ def __init__(self, principal): self.principal = principal self.interaction = None def addCheckerPublic(): - """Add the CheckerPublic permission as 'zope.Public'""" + """ + Add the CheckerPublic permission as :data:`zope.Public + <zope.security.interfaces.PUBLIC_PERMISSION_NAME>`. + """ perm = Permission( PUBLIC_PERMISSION_NAME, @@ -74,6 +83,12 @@ def addCheckerPublic(): return perm def create_interaction(principal_id, **kw): + """ + Create a new interaction for the given principal ID, make it the + :func:`current interaction + <zope.security.management.newInteraction>`, and return the + :class:`Principal` object. + """ principal = Principal(principal_id, **kw) participation = Participation(principal) zope.security.management.newInteraction(participation) @@ -82,6 +97,10 @@ def create_interaction(principal_id, **kw): @contextlib.contextmanager def interaction(principal_id, **kw): + """ + A context manager for running an interaction for the given + principal ID. + """ if zope.security.management.queryInteraction(): # There already is an interaction. Great. Leave it alone. yield diff --git a/src/zope/security/zcml.py b/src/zope/security/zcml.py index 777a4fd..78ef5cc 100644 --- a/src/zope/security/zcml.py +++ b/src/zope/security/zcml.py @@ -75,8 +75,8 @@ class IPermissionDirective(Interface): """Define a new security object.""" id = Id( - title=u"Id", - description=u"Id as which this object will be known and used.", + title=u"ID", + description=u"ID as which this object will be known and used.", required=True) title = MessageID( @@ -101,12 +101,12 @@ class IRedefinePermission(Interface): from_ = Permission( title=u"Original permission", - description=u"Original permission id to redefine.", + description=u"Original permission ID to redefine.", required=True) to = Permission( title=u"Substituted permission", - description=u"Substituted permission id.", + description=u"Substituted permission ID.", required=True) def redefinePermission(_context, from_, to): |