Safe import hook
----------------

The safe import hook, when installed, allows objects to get
security-proxied at import time.  It makes good use of
module-level security definitions, by postponing the actual
check until the imported object is used. This allows defining
security constraints that can only be evaluated inside the
context where the object (class, function, etc) is needed.

  >>> import canonical.security.safe
  >>> canonical.security.safe.install("safe_prefix")

  >>> from safe_prefix import os
  >>> type(os)
  <class 'canonical.security.safe.SecuredModule'>
  >>> type(os.path)
  <class 'canonical.security.safe.SecuredModule'>
  >>> type(os.path.dirname)
  <class 'zope.security._proxy._Proxy'>

  >>> dirname = os.path.dirname
  >>> type(dirname)
  <class 'zope.security._proxy._Proxy'>

  >>> dirname("/foo/bar") # doctest: +ELLIPSIS
  Traceback (most recent call last):
  ...
  zope.security.interfaces.ForbiddenAttribute: ('dirname', <module ...>)


It's also possible to use additional elements after the prefix
during importing.

  >>> from safe_prefix.os.path import dirname

  >>> dirname("/foo/bar") # doctest: +ELLIPSIS
  Traceback (most recent call last):
  ...
  zope.security.interfaces.ForbiddenAttribute: ('dirname', <module ...>)


Even this won't change the standard module (Python internally assigns
the returned 'path' object to 'os.path' in the case above, which might
break everything if not correctly handled).

  >>> import os
  >>> type(os.path)
  <class 'module'>


Let's create a fake module to play with.

  >>> from zope.security.proxy import Proxy
  >>> import sys, types
  >>> safe_test = sys.modules["safe_test"] = types.ModuleType("safe_test")


Proxied objects shouldn't be proxied again.

  >>> safe_test.proxied = Proxy(object(), object())
  >>> from safe_prefix.safe_test import proxied
  >>> safe_test.proxied is proxied
  True


But a function should.

  >>> safe_test.hello_world = lambda: "Hello world!"
  >>> from safe_prefix.safe_test import hello_world
  >>> hello_world()
  Traceback (most recent call last):
  ...
  zope.security.interfaces.ForbiddenAttribute: ('hello_world', <module 'safe_prefix.safe_test' (SafeImporter)>)


Unless the module has a checker allowing the given name. Notice that
when the safe import above was made, a new checker was defined to the
module since it didn't have one. That's why we have to undefine it
below.

  >>> from zope.security.checker import defineChecker, undefineChecker
  >>> from zope.security.checker import CheckerPublic, Checker, NoProxy

  >>> checker = Checker(dict(hello_world=CheckerPublic))
  >>> defineChecker(safe_test, checker)

  >>> from safe_prefix.safe_test import hello_world
  >>> hello_world()
  'Hello world!'


Or the whole module is unprotected by tagging it with NoProxy.

  >>> undefineChecker(safe_test)
  >>> defineChecker(safe_test, NoProxy)

  >>> from safe_prefix.safe_test import hello_world
  >>> hello_world()
  'Hello world!'


If the object has a custom checker, it should also be respected,
in addition to the module level one.

  >>> class C(object):
  ...    public = "PUBLIC"
  ...    restricted = "RESTRICTED"

  >>> checker = Checker(dict(public=CheckerPublic))

  >>> safe_test.public = C()
  >>> safe_test.restricted = C()

  >>> undefineChecker(safe_test)
  >>> defineChecker(safe_test, checker)
  >>> defineChecker(C, checker)

  >>> from safe_prefix.safe_test import public, restricted

  >>> public.public
  'PUBLIC'

  >>> public.restricted # doctest: +ELLIPSIS
  Traceback (most recent call last):
  ...
  zope.security.interfaces.ForbiddenAttribute: ('restricted', <...C object at ...>)

  >>> restricted.public # doctest: +ELLIPSIS
  Traceback (most recent call last):
  ...
  zope.security.interfaces.ForbiddenAttribute: ('restricted', <module 'safe_prefix.safe_test' (SafeImporter)>)


vim:ts=4:sw=4:et:ft=doctest
