Skip to content

Instantly share code, notes, and snippets.

@optilude
Created December 31, 2011 15:28
Show Gist options
  • Save optilude/1544337 to your computer and use it in GitHub Desktop.
Save optilude/1544337 to your computer and use it in GitHub Desktop.

Zope secrets

This guide will attempt to explain some of Zope's internals. It may be useful for debugging purposes, or simply to better understand how Zope works.

If you only want to know how to use the APIs and features described below, you are probably better served reading the Zope Developer's Guide.

A startup script (e.g. bin/instance) calls Zope 2's run.py in an appropriate interpreter context (i.e. one that has the necessary packages on sys.path). This invokes a subclass of ZopeStarter from Zope2.Startup:

import Zope2.Startup
starter = Zope2.Startup.get_starter()
opts = _setconfig()
starter.setConfiguration(opts.configroot)
starter.prepare()
starter.run()

There are various variants that allow different ways to supply configuration.

There are two versions of the starter, one for Unix and one for Windows. It performs a number of actions during the prepare() phase:

def prepare(self):
    self.setupInitialLogging()
    self.setupLocale()
    self.setupSecurityOptions()
    self.setupPublisher()
    # Start ZServer servers before we drop privileges so we can bind to
    # "low" ports:
    self.setupZServer()
    self.setupServers()
    # drop privileges after setting up servers
    self.dropPrivileges()
    self.setupFinalLogging()
    self.makeLockFile()
    self.makePidFile()
    self.setupInterpreter()
    self.startZope()
    self.serverListen()
    from App.config import getConfiguration
    config = getConfiguration()
    self.registerSignals()
    # emit a "ready" message in order to prevent the kinds of emails
    # to the Zope maillist in which people claim that Zope has "frozen"
    # after it has emitted ZServer messages.

    logger.info('Ready to handle requests')
    self.sendEvents()

Mostly, this is about using information from the configuration (read using ZConfig from a configuration file, or taken from the global defaults) to set various module level variables and options.

The startZope() call ends up in Zope2.App.startup.startup(), which performs a number of startup tasks:

  • Importing products (OFS.Application.import_products())

  • Creating a ZODB for the chosen storage (as set in the ZConfig configuration). This is stored variously in Globals.DB and Zope2.DB, and is configured using a dbtab (mount points) read from the configuration file. When this is done, the event zope.processlifetime.DatabaseOpened is notified.

  • Setting the ClassFactory on the ZODB instance.

  • Loading ZCML configuration from site.zcml. This in turn loads ZCML for all installed products in the Products.* namespace, and ZCML slugs. The load_zcml() call also sets up a Zope2VocabularyRegistry.

  • Creating the app object, an instance of App.ZApplication.ZApplicationWrapper that wraps a OFS.Application.Application. The purpose of the wrapper is to:

    • Create an instance of the application object at the root of the ZODB on __init__() if not there already. The name by default is Application.
    • Implement traversal over this wrapper (__bobo_traverse__) to open a ZODB connection before continuing traversal, and closing it at the end of the request.
    • Return the persistent instance of the true application root object when called.

    The wrapper is set as Zope2.bobo_application, which is used when the low- level Bobo publisher publishes the Zope2 module.

  • Initialising the application object using OFS.Application.initialize(). This defensively creates a number of items:

    def initialize(self):
        # make sure to preserve relative ordering of calls below.
        self.install_cp_and_products()
        self.install_tempfolder_and_sdc()
        self.install_session_data_manager()
        self.install_browser_id_manager()
        self.install_required_roles()
        self.install_inituser()
        self.install_errorlog()
        self.install_products()
        self.install_standards()
        self.install_virtual_hosting()
    
  • Notfiying the event zope.processlifetime.DatabaseOpenedWithRoot

  • Setting a number of ZPublisher hooks:

    Zope2.zpublisher_transactions_manager = TransactionsManager()
    Zope2.zpublisher_exception_hook = zpublisher_exception_hook
    Zope2.zpublisher_validated_hook = validated_hook
    Zope2.__bobo_before__ = noSecurityManager
    

The run() method of the ZopeStarter then runs the main startup loop (note: this is not applicable for WSGI startup using make_wsgi_app() in run.py, where the WSGI server is responsible for the event loop):

def run(self):
    # the mainloop.
    try:
        from App.config import getConfiguration
        config = getConfiguration()
        import ZServer
        import Lifetime
        Lifetime.loop()
        sys.exit(ZServer.exit_code)
    finally:
        self.shutdown()

The Lifetime module uses asyncore to poll for connected sockets until shutdown is initiated, either through a signal or explicit changing of the flag Lifetime._shutdown_phase.

Sockets are created when new connections are received on a defined server. When using the built-in ZServer (i.e. not WSGI), the default HTTP server is defined in ZServer.HTTPServer.zhttp_server, which derives from ZServer.medusa.http_server, which in turn is an asyncore.dispatcher.

Servers are created in ZopeStarter.setupServers(), which loops over the ZConfig-defined server factories and call their create() metohod. The server factories are defined in ZServer.datatypes. (The word datatypes refers to ZConfig data types.)

Note also that some of the configuration data is mutated in the prepare() method of the server instance, which is called from Zope2.startup.handlers.root_handler() during the configuration phase. These handlers are registered with a call to Zope2.startup.handlers.handleConfig() during the _setconfig() call in run.py.

During application initialisation, the method install_products() will call the method OFS.Application.install_products(). This will record products in the Control_Panel if this is enabled in zope.conf, and call the initialize() function for any product that has one with a product context that allows the product to register constructors for the Zope runtime.

install_products() loops over all product directories (configured via zope.conf and kept in Products.__path___ by Zope2.startup.handlers.root_handler()) and scans these for product directories with an __init__.py. For each, it calls OFS.Application.install_product. This will:

  • Import the product as a Python package

  • Look for an attribute misc_ at the product root, which is used to store things like icons by some products. If it is a dict, wrap it in an OFS.misc_.Misc_ object, which is just a simple, security-aware class. Then store a copy of it as an attribute on the object Application.misc_. The attribute name is the product name. This allows traversal to the misc resources.

    As an example of the use of the use of misc_, consider this dict set up in Products/CMFPlone/__init__.py:

    misc_ = {'plone_icon': ImageFile(
              os.path.join('skins', 'plone_images', 'logoIcon.png'),
              cmfplone_globals)}
    

    This can now be traversed to as /misc_/CMFPlone/plone_icon by virtue of the misc_ attribute on the application root. On the PloneSite root object, the attribute icon = 'misc_/CMFPlone/tool.gif' provides the icon this object in the ZMI.

  • Next, create a App.ProductContext.ProductContext to be used during product initialisation. This is passed a product object (see below), a handle to the application root, and the product's package.

    There are two ways to obtain the product object:

    If persistent product installation (in the Control_Panel) is enabled in zope.conf, call App.Product.initializeProduct. This will create a App.Product.Product object and save it persistently in App.Control_Panel.Products. It also reads the file version.txt from the product to determine a version number, and will change the persistent object (at Zope startup) if the version has changed. The Product object is initialised with a product name and title and is used to store basic information about the product. The Product object is then returned.

    If persistent product installation is disabled (the default), simply instantiate a FactoryDispatcher.Product object, which is a simpler, duck-typing equivalent of App.Product.Product, with the product name.

  • If the product has an initialize() method at its root, call it with the product context as an argument.

Once old-style products are initialised, any packages outside the Products.* namespace that want to be initialised are processed. The <five:registerProduct /> ZCML directive stores a list of packages to be processed and any referenced initialize() method in the variable OFS.metaconfigure._packages_to_initialize, accessible via the function get_packages_to_initialize() in the same module. install_products() loops over this list, calling install_package() for each. This works very much like install_product(). When it is done, it calls the function OFS.metaconfigure.package_initialized() to remove the package from the list of packages to initalise.

Products can make constructors available to the Zope runtime. This is what powers the Add drop-down in the ZMI, for instance. They do so by calling registerClass() on the product context passed to the initialize() function. This takes the following arguments (from the docstring):

instance_class
The class of the object that will be created.
meta_type
A string representing kind of object being created, which appears in add lists. If not specified, then the class meta_type will be used.
permission
The permission name for the constructors. If not specified, a permission name based on the meta type will be used.
constructors

A list of constructor methods. A method can me a callable object with a __name__ attribute giving the name the method should have in the product, or the method may be a tuple consisting of a name and a callable object. The first method will be used as the initial method called when creating an object through the web (in the ZMI).

It is quite common to pass in two constructor callables: one that is a DTMLMethod or PageTemplateFile that renders an add form and one that is a method that actually creates and adds an instance. A typical example from Products.MailHost is:

manage_addMailHostForm = DTMLFile('dtml/addMailHost_form', globals())

def manage_addMailHost(self,
                       id,
                       title='',
                       smtp_host='localhost',
                       localhost='localhost',
                       smtp_port=25,
                       timeout=1.0,
                       REQUEST=None,
                      ):
    """ Add a MailHost into the system.
    """
    i = MailHost(id, title, smtp_host, smtp_port)
    self._setObject(id, i)

    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')

These are then referenced in initialize():

def initialize(context):
  context.registerClass(
      MailHost.MailHost,
      permission='Add MailHost objects',
      constructors=(MailHost.manage_addMailHostForm,
                    MailHost.manage_addMailHost),
      icon='www/MailHost_icon.gif',
  )

The form will be called with a path like /container/manage_addProduct/MailHost/manage_addMailHostForm. The <form /> on this page has a relative URL action="manage_addMailHost", which means that when the form is submitted, the manage_addMailHost() function is called. id, title and the other variables are passed as request parameters and marshalled (by mapply() - see below) into function arguments, and the REQUEST is implicitly passed (again by mapply()).

icon
The name of an image file in the package to be used for instances. The class icon attribute will be set automagically if an icon is provided.
permissions
Additional permissions to be registered. If not provided, then permissions defined in the class will be registered.
visibility
"Global" if the object is globally visible, or None.
interfaces
A list of the interfaces the object supports
container_filter
A function that is called with an ObjectManager object as the only parameter, which should return a truth value if the object is happy to be created in that container. The filter is called before showing ObjectManager's Add list, and before pasting (after object copy or cut), but not before calling an object's constructor.

The main aims of this method are to register some new permissions, store some information about the class in the variable Products.meta_types, and record some construct information in the variable _m set at the root of the product. Here is how it works:

  • If an icon and instance_class are supplied, set an icon attribute on instance_class to path like misc_/<productname>/<iconfilename>.

  • Register any permissions by calling AccessControl.Permission.registerPermissions() (described later in this guide).

  • If there is no permission provided, generate a permission name as the string "Add <meta_type>", defaulting to being granted to Manager only. Register this permission as well.

  • Grab the name of the first constructor passed in the constructors tuple. This can either be the function's __name__, or a name can be provided explicitly by passing as the first list element a tuple of (name, function).

  • Try to obtain the value of the symbol __FactoryDispatcher__ in the package root (__init__.py) if set. If not, create a class on the fly by deriving from App.FactoryDispatcher.FactoryDispatcher and set this onto the package name as __FactoryDispatcher__.

  • Set an attribute _m in the package root if it does not exist to an instance of AttrDict wrapped around the factory dispatcher. This is a bizzarre construction best described by its implementation:

    class AttrDict:
    
      def __init__(self, ob):
          self.ob = ob
    
      def __setitem__(self, name, v):
          setattr(self.ob, name, v)
    
  • If no interfaces were passed in explicitly, obtain the interfaces implemented by the instance_class, if provided.

  • Record information about the primary constructor in the tuple Products.meta_types by appending dict with keys:

    name

    The meta_type passed in or obtained from the instance_class

    action

    A path segment like manage_addProduct/<productname>/<constructorname>. More on manage_addProduct below.

    product

    The name of the product

    permission

    The add permission passed in or generated

    visibility

    Either "Global" or None as passed in to the method

    interfaces

    The list of interfaces passed in or obtained from instance_class

    instance

    The instance_class as passed in to the method

    container_filter

    The container_filter as passed in to the method

  • Next, put the initial constructor and any further constructors passed in onto the _m pseudo-dictionary (which really just means setting them as attributes on the FactoryDispatcher-subclass). The appropriate <methodname>__roles__ attribute is set to a PermissionRole describing the "add" permission as well.

  • If an icon name was passed in, construct an ImageFile to read it from the package and stash it in the OFS.misc_.misc_ class so that it can be traversed to later.

Note that previously, the approach taken was to stash factory methods onto the class OFS.ObjectManager.ObjectManager, which is the base class for most folderish types in Zope. This is still supported for backwards compatibility, by providing a legacty tuple of function objects, but is deprecated.

Products.meta_types is used in various places, most notably in OFS.ObjectManager.ObjectManager in the methods all_meta_types() and filtered_meta_types(). The former returns all of Products.meta_types (plus possibly some legacy entries in _product_meta_types on the application root object, used to support through-the-web defined products via App.ProductRegistry.ProductRegistry), applying the container_filter if available and optionally filtering by interfaces. The latter is used to power the Add widget in the ZMI by creating a <select /> box for all meta_types the user is allowed to add by checking the "add" permission of each of the items returned by all_meta_types(). The action stored in the meta_types list is then used to traverse to and invoke a constructor.

Note that subclasses of ObjectManager may sometimes override all_meta_types() to set a more restrictive list of addable types. They may also add to the list of the default implementation by setting a meta_types class or instance variable containing further entries in the same format as Products.meta_types.

Finally, let us consider the manage_addProduct method seen in the action used to traverse to a registered constructor callable (e.g. an add form) using a path such as /<container>/manage_addProduct/<productname>/<constructname>. It is set on OFS.ObjectManager.ObjectManager, and is actually an instance of App.FactoryDispatcher.ProductDispatcher. This is an implicit-acquisition capable object that implements __bobo_traverse__ as follows:

  • Attempt to obtain a __FactoryDispatcher__ attribute from the product package (using the name being traversed to), defaulting to the standard FactoryDispatcher class in the same module.
  • Find a persistent App.Product.Product if there is one, or create a simple App.FactoryDispatcher.Product wrapper if persistent product installation ahs not taken place.
  • Create an instance of the factory dispatcher on the fly, passing in the product descriptor and the parent object (i.e. the continer).
  • Return this, acquisition-wrapped in self, to allow traversal to continue.

Traversal then continues over the FactoryDispatcher. In the version of this created by registerClass(), each constructor is set as an attribute on the product-specific dispatcher, with appropriate roles, so traversal will be able to obtain the constructor callable.

There is also a fallback __getattr__() implementation in the base FactoryDispatcher class, which will inspect the _m attribute on the product package for an appropriate constructor, and is also able to obtain constructor information from a persistent Product instance (from Control_Panel if there was one.

A request is recieved either via a WSGI pipeline or the Medusa web server. It first hits handle_request() in the zhttp_handler used by zhttp_server, which consumes the request until it has enough to act on it, at which point continue_request() is called. This constructs a ZPublisher.HTTPRequest from the Medusa http_request environment and prepres a ZServerHTTPResponse, a subclass of ZPublisher's HTTPResponse.

The actual request is delegated to a threadpool. In a non-WSGI setup, this is managed by ZServer.PubCore.ZRendezvous.ZRendevous (note the typo in the module name!). This keeps track of the requests and (unfilled) responses to be processed, and passes them to an instance of a ZServer.PubCore.ZServerPublisher for handling. ZRendevous also deals with thread locking.

The ZServerPublisher will call either ZPublisher.publish_module or ZPublisher.WSGIPublisher.publish_module, depending on the deployment mode, with the request and the response. The non-WSGI version also takes a module name to publish, which is Zope2. This is a relic of the Bobo publisher, which could publish other modules with a bobo_application variable set ( recall that this variable was set in the startup phase described above).

The remainder of this section will describe the non-WSGI publisher. The WSGI publisher performs the same actions, but deals in WSGI environs and response body iterators.

There are two versions of publish_module, one with profiling and one without. publish_module_standard performs the following actions:

  • Set the default ZTK skin on the request, by adapting the request to IDefaultSkin.
  • Call publish(), which does the real publication
  • Handle errors
  • Write the response body to stdout, which is wired up to be the HTTP response stream

The more interesting function is publish(). This starts by calling get_module_info() to get the information about the published module (which, recall, is almost always going to be Zope2). The results are cached, so this will only do its work once:

(bobo_before, bobo_after, object, realm, debug_mode, err_hook,
 validated_hook, transactions_manager)= get_module_info(module_name)

The returned variables are:

  • bobo_before, set via a module level variable __bobo_before__.
  • bobo_after, set via a module level variable __bobo_after__.
  • object to publish, which defaults to the module itself, but can be set via the module-level variable bobo_application (or web_objects)
  • realm, set via the module level variable __bobo_realm__, or a global default which can be set the ZConfig configuration file.
  • debug_mode, a boolean set using the module level variable __bobo_debug_mode__.
  • err_hook, set via the module level variable zpublisher_exception_hook.
  • validated_hook, set via the module level variable zpublisher_validated_hook.
  • transactions_manager, set via the module level variable zpublisher_transactions_manager, but defaulting to the DefaultTransactionsManager which uses the transaction API to manage transactions.

The publisher then performs the following steps:

  • Notify the ZPublisher.pubevents.PubStart event.

  • Create a new zope.security interaction.

  • Call processInputs() on the request to process request parameters and the request body so that the Zope request object works as advertised.

  • If the request contains a key SUBMIT with the value cancel and a key cancel_action with a path, a Redirect exception is raised, which will cause an HTTP 302 redirect to be raised.

  • Set debug_mode and realm on the response, as returned by get_module_info().

  • If bobo_before() is set, it is called with no arguments.

  • Set the inital value for request['PARENTS'] to be the published object. This will be the ZApplicationWrapper set during the startup phase.

  • Begin a transaction using the transactions_manager.

  • Traverse to the actual object being published (e.g. a view) by calling object=request.traverse(path, validated_hook=validated_hook), where path is request['PATH_INFO']. More on traversal below.

  • Notify the ZPublisher.pubevents.PubAfterTraversal event.

  • Note the path and authenticated user in the transaction.

  • Call the object being pusblished using mapply():

    result=mapply(object, request.args, request,
                  call_object,1,
                  missing_name,
                  dont_publish_class,
                  request, bind=1)
    

    The ZPublisher.mapply.mapply() method is somewhat complicated, but in essence all it does is to call either a published method, or a published instance with a __call__() method.

    request.args can contain positional arguments supplied in an XMLRPC call, but is usually empty. The request is passed to act as a dictionary of positional arugments, which allows request parameters to be turned into method parameters to a published method.

    The other parameters are about policy - we call any object (e.g. a method or object with a __call__ method) to resolve it, but we don't publish class objects. We do allow binding of self for methods on objects, and we pass the request as context for debugging.

  • Set the result of the mapply() call as response body. As a marker, the response object itself can be returned to bypass this, i.e. if the published object set the response body itself.

  • Notify the ZPublisher.pubevents.PubBeforeCommit event.

  • Commit the transaction using the transactions_manager.

  • End the zope.security interaction

  • Notify the ZPublisher.pubevents.PubSuccess event.

  • Return the response object, which is then used by the ZServer to write to stdout.

If an exception happens during this process, the err_hook is called. This is allowed to raise a Retry exception. Regardless, the event ZPublisher.pubevents.PubBeforeAbort is notified before the transaction is aborted, and then ZPublisher.pubevents.PubFailure is raised after the zope.security interaction is ended.

If the request supports retry, it will be retried by cloning the request and calling publish recrusively. All HTTP requests support retry, but only up to a limit of retry_max_count, which by default is 3. Retry is mainly used to retry in the case of conflict errors - more on this below.

If there is no error hook installed, a simple abort is encountered, with no retry.

The default error hook is an instance of Zope2.startup.ZPublisherExceptionHook. This handles exceptions by performing the following checks:

  • SystemExit or Redirect exceptions are re-raised.
  • A ConflictError, which indicates a write-conflict in the ZODB, is turned into a Retry exception so that request can be retried.
  • Other exception are stored in the __error_log__ acquired from the published object, if possible.
  • If a view named index.html is registered with the exception type as its context, this is resolved and returned as the response.
  • If the published object or any of its acquisition parents have a method raise_standardErrorMessage(), this will be called to create an error message instead of using the view approach. This is called with a first argument of whichever object in the acquisition chain has an attribute standard_error_message, as well as the request and traceback information.

When handling an exception by returning an error message, the ZPublisherExceptionHook will call response.setStatus() with the exception type (class) as an argument. The name of the exception class is then used to look up the status code in the status_reasons dictionary in ZPublisher.HTTPResponse. Hence, raising an exception called NotFound will automatically set the response code to 404.

Traversal is the process during which the path elements of a URL are resolved to an actual object to publish (there is also path traversal, used in TAL expressions in page templates, which is similar, but implemented differently - see OFS.Traversable).

Traversal is invoked during object publication, which calls request.traverse() with the path from the request (the PATH_INFO CGI variable). This method is actually inordinately complicated, mostly because it caters for a lot of edge cases.

The basic idea is pretty simple, though: each path element represents an item to traverse to from the preceding object (its parent). Traversal can mean dict-like access (__getitem__), attribute-like access (__getattr__), or one of a number of different hooks for overiding or extending traversal. Once the final element on the path is found, the user's access to it is validated, before it is returned.

Here are the gory details:

  • First, the path is cleaned up by stripping leading and trailing slashes, explicitly disallowing access to things like REQUEST, aq_base and aq_self, and resolving . or .. elements as in filesystem paths.

  • Check if the top-level object (the application root) has a __bobo_traverse__ method (it almost certainly will - as shown above, there is a wrapper around the application root that implements this method to open and close the ZODB connection upon traversal). If so, call it to obtain a new top level object (which will be the real Zope application root in the ZODB).

  • Aquisition-wrap the top-level object in a RequestContainer. This is the fake root object that makes it possible to acquire the attribute REQUEST from any traversed-to context.

  • Record the request variable ACTUAL_URL, which is the inbound URL plus the original path. Hence, this variable provides access to the URL as the user saw it.

  • Set up (and later, pop from) the request variable TraversalRequestNameStack. This is a stack of path elements still to be processed. Traversal hooks sometimes use this to 'look ahead' at the path elements that have not been traversed to and, in some cases, modify the stack to trick traversal into going somewhere other than what the inbound path specified.

  • In a loop, traverse the traversal name stack:

    • Check if the current object (initially the application root) has a method __before_publishing_traverse__. If so, call it with the request as an argument. This hook is used by many parts of Zope, CMF and Plone to support things like content object method aliases, setting the CMF skin from the request, or making the portal_factory tool work. This method cannot easily change the traversal path, except by modifying request['TraversalRequestNameStack'].

    • If there are more elements in the path, pop the next element.

    • Append this to the variable request['URL'], which contains the traversal URL. Various traversal tricks may mean this is not quite the same as what the user sees in their address bar.

    • Attempt to traverse to the next object using the name popped from the path stack. This takes place in the traverseName() method of the request:

      • If the name starts with a + or an @, parse it as a traversal namespace. (A name starting with an @ is taken as a shorthand for writing ++view++<name>, i.e. an entry in the ++view++ traversal namespace. Other namespaces include ++skin++ and ++etc++.) If a traversal namesapce is found, attempt to look up an adapter from the current traversal object and the request to zope.traversing.interfaces.ITraversable with a name matching the traversal namespace (e.g. view). Then call its traverse() method with the name of the next entry on the traversal stack as an argument. This is expected to return an object to traverse to next. If this succeeds, acquisition-wrap the returned object in the parent object.

        Note: As this implies, objects returned from the traverse() method of an ITraversable adapter are not expected to be acquisition-wrapped. This is in contrast to objects returned by __getitem__() or __getattr__() or a custom IPublishTraverse adapter (see below), which are expected to be wrapped.

      • If there is no namespace traversal adapter, find an IPublishTraverse object in one of three places: If the current traversal object implements it directly, use that; if there is an adapter from the current object and the request to IPublishTraverse, use that; or, explicitly use the DefaultPublishTraverse implementation found in ZPublisher.BaseRequest. Then call the publishTraverse() method to find an object to traverse to and return that (without acquisition-wrapping it).

        Hint: Implementing IPublishTraverse is a common way to allow further traversal from a view, with paths like ...../@@foo/some/path, where the @@foo view either implements or is adaptable to IPublishTraverse.

        DefaultPublishTraverse is used in most cases, either directly or as a fallback from custom implementations. It uses the following semantics:

        • If the name starts with an underscore, raise a Forbidden exception

        • If the object has a __bobo_traverse__ method, call it with the request and the name of the next entry on the traversal stack as arguments. It may return either an object, or a tuple of objects. In the latter case, amend request parents list as if traversal had happened over all the elements in the tuple except the last one, and treat that as the next object.

        • If the __bobo_traverse__ call fails by raising an AttributeError, KeyError or NotFound exception, attempt to look up a view with the traversal name (which would have been given without the explicit @@ prefix). If this succeeds, set the status code to 200 (the preceding failure may have set it to 404), acquisition-wrap the view if applicable, and return it.

        • If there was no __bobo_traverse__, or if it raised the special exception ZPublisher.interfaces.UseTraversalDefault, try the following:

          • Attempt to look up the name as an attribute of the current object, using aq_base (i.e. explicitly not acquiring from parents of the current object). If this succeeds, return the attribute, which is expected to be acquisition-wrapped if applicable (i.e. the parent object extends Acquisition.Implicit or Acquisition.Explicit).
          • Next, try to look up a view using the same semantics as above
          • Next, try getattr() without the aq_base check, i.e. allowing acquired attributes.
          • Next, try __getitem__() (dict-like) access
          • If that fails, raise a KeyError to indicate the object could not be found (this is later turned into a 404 response)
        • If we now have a sub-object, check that it has a docstring. If it does not, raise a Forbidden exception.

          The requirement for a docstring is an ancident and primitive security restriction, since Zope can be used to publish all kinds of Python objects. It is mostly a nuisance these days, but note that views and custom ITraversable and IPublishTraverse traversal do not have this restriction.

        • Next, raise a Forbidden exception if traversal resolved a primitive or built-in list, tuple, set or dict - these are not directly traversable.

        • Finally, return the object

    • If a KeyError, AttributeError or NotFound exception is raised during name resolution, return a 404 response by raising an exception. Similarly, if a Forbidden exception is raised, set and return a 403 response.

    • Once the end of the path is reached, we have the most specific item mentioned in the (possibly mutated) path. However, this may choose to delegate to another object (usually a subobject) through a mechanism known as "browser default", which is similar to the way web servers often serve an index.html file by default when traversing to a folder.

      The browser publisher is described by the interface IBrowserPublisher, which is a sub-interface of IPublishTraverse and is implemented by the DefaultPublishTraverse class. Again, the IBrowserPublisher for the traversed-to object is found in one of three ways: the object may implement it itself; or it may be adaptable, with the request, to this interface; or the fallback DefaultPublishTraverse may be used. The browserDefault() method on the IBrowserPublisher is then called with the request as an argument.

      The return value from browserDefault() is a tuple of the traversed-to object (usually) and a tuple of further names to traverse to.

      The default implementation in DefaultPublishTraverse does this:

      • If the object has a method __browser_default__(), delegate to this
      • If an IDefaultViewName has been registered for the context in ZCML, look up and use this. This is deprecated, however.
      • Otherwise, return context, (), i.e. no further traversal required.
    • If a further path is returned and it has more than on element, add its elements to the TraversalRequestNameStack and continue traversal as if these elements had been part of the original path all along.

    • If there is only one element in the further path returned by browserDefault(), use this as the next entry name and continue traversal to this.

    • If no further path is used, fall back on the default method name index_html() (for HTTP GET and POST requests - there is special handling of other HTTP verbs for WebDAV that we won't go into here) and continue traversal to this.

    • If there is no index_html() method, use the traversed-to object itself as the final entry, so break out of the traversal loop. We always end up here eventually: if the browser default element or index_html method is the last item we traverse to, eventually we reach something publishable.

      This object will most likely be called (through mapply()), so we ensure the roles used in security checks are obtained from the __call__() method the traversed-to object (note: functions and methods also have a __call__!)

  • Once we have reached the end of the traversal stack (phew!), we make sure the parents list is in the right order (it is built in reverse order), even if there was a failure. Hence, request['PARENTS'] is always a useful indicator of what objects have been traversed over.

  • We then set request['PUBLISHED'] to be the published object. Note that this is usually a view or page template, though for content types like File or Image it is the index_html() method of the content object itself.

  • Next, we validate that the current user has sufficient permissions to call the published object. If not, a 403 response is returned by calling response.unauthorized().

    The authentication works as follows:

    • The roles required to access the traversed-to object are fetched by calling getRoles(), first on the application root, and, if applicable, on the __call__() method of the traversed-to object.
    • A user folder (i.e. acl_users) is obtained by looking for the special attribute __allow_groups__ on the published object or one of its parents. This attribute by user folders on their parent container when they are added.
    • The validate() method of the user folder is called (there is a fallback called old_validate(), used if there is no user folder, but that should never happen in a modern Zope installation). This either returns a user object or None, if the user is not found in this user folder, or there is a user, but the user cannot be auhtorised according to this user folder.
    • If None is returned, the search continues up the list of traversal parents until a suitable user folder is found. If no such user folder is found, an Unauthorized exception is raised, unless there are no security declarations on the context.
    • If a user with permissions is found, and the validated_hook is set (found via get_module_info() as described above), it is called with the request and user as arguments. The standard validated_hook calls newSecurityManager() with the user, which sets the security context for the duration of the request.
    • The user is then saved in the request variable AUTHENTICATED_USER. The true traversal path is saved in the request variable AUTHENTICATION_PATH.
  • Finally, if any post-traverse functions have been registered (by using the post_traverse() method of the request to register functions and optional static arguments), they are called in the order they were registered. If any post-traverse function returns a value other than None, no further post-traverse functions are called, and the return value is used as the return value of the traverse() function, discarding the actual object that was traversed to and security check.

Much of Zope security is implemented in C, for speed, but there is a Python implementation in AccessControl.ImplPython, which can be enabled by setting security-policy-implementation python in zope.conf.

Note: We will not discuss RestrictedPython, used to apply security restrictions to through-the-web python scripts and page templates, here.

The permissions required to access a given attribute are stored on classes and modules in a variable called __ac_permissions__. This contains a tuple of tuples that map a permission name to a list of attributes (e.g. methods) protected by that permission, e.g.:

__ac_permissions__=(

('View management screens',
 ['manage','manage_menu','manage_main','manage_copyright',
  'manage_tabs','manage_propertiesForm','manage_UndoForm']),
('Undo changes',       ['manage_undo_transactions']),
('Change permissions', ['manage_access']),
('Add objects',        ['manage_addObject']),
('Delete objects',     ['manage_delObjects']),
('Add properties',     ['manage_addProperty']),
('Change properties',  ['manage_editProperties']),
('Delete properties',  ['manage_delProperties']),
('Default permission', ['']),
)

The roles reuqired to access an object (e.g. a content object), are stored in a class or instance variable __roles__. This may contain a tuple or list of role names, an AccessControl.PermissionRole.PermissionRole object, or one of the following special variables:

AccessControl.SecurityInfo.ACCESS_NONE
Inaccessible from any context
AccessControl.SecurityInfo.ACCESS_PRIVATE
Accessible only from Python code
AccessControl.SecurityInfo.ACCESS_PUBLIC
Accessible from restricted Python code and publishable through the web (provided the object has a docstring)

For attributes (including methods), the roles are stored on the parent class in a variable called <name>__roles__, where <name> is the attribute name. Again, the special variables ACCESS_NONE, ACCESS_PRIVATE and ACCESS_PUBLIC can be used.

These variables are rarely set manually. Instead, declarative security info objects are used. For example:

from App.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from OFS.SimpleItem import Item

class SomeClass(Item):

  ...

  security = ClassSecurityInfo()
  security.declareObjectPublic() # like __roles__ = ACCESS_PUBLIC

  security.declareProtected('Some permission, 'someMethod')
  def someMethod(self):
    ...

  InitializeClass(SomeClass)

There is also security.declareObjectProtected(<permission>), security.declareObjectPrivate(), security.declarePrivate(<attribute>) and security.declarePublic(attribute).

Attribute security can be set in ZCML using the <class /> directive with one or more <require /> sub-directives:

<class class=".someclass.SomeClass">
  <require
    permission="some.permission"
    attributes="someMethod"
    />
</class>

This will also call InitializeClass on the given class.

Note that this uses ZTK-style permission names, not Zope 2-style permission strings. A ZTK permission is a named utility providing zope.security.interfaces.IPermission, with an id that is the short (usually dotted) name that is also the utility name, and a title that matches the Zope 2 name. New permissions can be registered using the <permission /> directive:

<permission
  id="some.permission"
  title="Some permission"
  />

Zope 2-style permission names spring into existence whenever used, which makes them susceptibly to typos (ZTK-style IPermission utilities must be explicitly registered before they can be used). Permissions are also represented by "mangled" permission names, which simply turn the arbitrary string of a permission into a valid Python identifier. For example, the permission "Access contents information" becomes _Access_contents_information_Permission. The mangling is done by the function AccessControl.Permission.pname.

Declarative security information does nothing except record information until the InitializeClass call is made. This will:

  • Loop over all attribute and assign a __name__ attribute to the value of any attribute in the class's __dict__ that has the _need__name__ marker set (this is used by through-the-web DTML and Zope Page Template objects that may not have a name until they are assigned to their parent).

  • Look for any function with the name manage() or a name starting with manage_. If this does not have a corresponding <name>__roles__ attribute, one is created with the roles ('Manager',).

  • Look for any security info object (which has an attribute __security_info__). If one is found call its apply() method with the class as an argument, and then delete it.

    The apply() method of ClassSecurityInfo does this:

    • Collect any explicitly set __ac_permissions__ tuple and turn it into internal state, as if the ClassSecurityInfo had been used to set it, so that it is not lost.

    • For any attribute declared with declarePublic() or declarePrivate(), set <name>__roles__ to ACCESS_PUBLIC or ACCESS_PRIVATE as appropriate.

    • Build an __ac_permissions__ tuple from the saved declarations of any protected attributes.

      As a special case, a call to security.declareObjectProtected(<permission>) will result in a value stored with an empty attribute name, which later translates as setting __roles__ directly on the class.

  • Find any __ac_permissions__ on the class (possibly created by the security info apply() call) and call AccessControl.Permission.registerPermissions with it as an argument. This will register the permission in a global list of known permissions with their default roles (usually ('Manager',)) held in that module under the variable _ac_permissions. The mangled permission name (see above) will also be set as a class attribute on the class AccessControl.Permission.ApplicationDefaultPermissions, which is a base class of the application root (OFS.Application.Application), hence making the mangled permission names available as (acquirable) class attributes on the application root. The value of this class variable is a tuple with the default roles for that permission.

  • For all permissions in __ac_permissions__ and for all attribute (method) names assigned to each permission, set a class attribute <name>__roles__ to a PermissionRole object. If a default list/tuple of roles was supplied, record this in the PermissionRole, otherwise default to ('Manager',).

To perform security checks, it is necessary to compare the roles a user has with the roles required for a given permission. The method to determine the roles of a permission on a given object is called rolesForPermissionOn(). It is found in AccessControl.ImplPython, though a C implementation may also be in use.

rolesForPermissionOn() can be called directly, but it should be imported from AccessControl.PermissionRole to ensure the correct implementation (C or Python) is used. Alternatively, the correct implementation can be accessed by using the rolesForPermissionOn() method of a PermissionRole object, which will supply the correct permission name and default roles.

The default rolesForPermissionOn() does the following:

  • Mangle the permission name (see above)
  • Walk from the object up the inner (containment) acquisition chain to find an object with the mangled permission name as an attribute. Then:
    • If the attribute is None, this is actually the ACCESS_PUBLIC marker. Return ('Anonymous',).
    • If the sequence of roles is a tuple, this is a signal to not acquire roles from parent objects. Stop and return any roles collected by walking the acquisition chain so far plus the roles at the current object.
    • If the sequence of roles is a list, this is a signal to acquire roles from parent objects. Hence, collect the roles at the current object and continue the walk up the acquisition chain.
    • If roles is a string, assumed to be a different mangled permission name, this is a signal to delegate to another permission. Continue acquisition from the parent, but discard any roles acquired so far.
  • If no object with the managled permission attribute is found, return the default roles. Default roles are stored in a PermissionRole object, but for other types of roles, use ('Manager',).
  • In all cases, if the global variable _embed_permission_in_roles is true, include the mangled permission name in the list of roles returned (even if an empty list). This is used as a debugging aid.

The most basic permission check can be done using:

from AccessControl import getSecurityManager
sm = getSecurityManager()
sm.checkPermission('Some permission', someObject)

This returns either 1 or None to indicate whether the current user has such a permission.

The call to getSecurityManager() returns a security manager instance for the current instance. A security manager is created using newSecurityManager() in the validated_hook at the end of traversal (hence note that it is not set during traversal itself!), which creates a new security manager with a context that is aware of the current authenticated user (Anonymous if there is none).

Again, the security manager may use a C implementation, but the default one is defined in AccessControl.ImplPython. The two most important methods on this object are checkPermission() (seen above) and validate(), which is used during traversal to validate access to an object and will throw an Unauthorized exception if not valid. Both of these delegate to a security policy, which will invariably be the ZopeSecurityPolicy also found in ImplPython (or C code) and instantiated once with a module-level call to setDefaultBehaviors().

The checkPermission() implementation in ZopeSecurityPolicy is relatively simple. It uses rolesForPermissionOn() to discover the roles on the object, and then obtains the current user from the security context (passed as a parameter to its version of checkPermission()) and calls its allowed() method with the object and its roles.

Additionally, if the security policy allows for it (it will by default), checks are made to ensure that if the "execution context" has an owner (e.g. it is a through-the-web Python script or template owned by a particular user), that the owner as well as the current user has the appropriate roles, otherwise access is disallowed. Also, if proxy roles are set (again applicable to through-the-web scripts), these are allowed to be used in lieu of the user's actual roles.

There are various user implementations that can treat allowed() differently. The most common use in Plone is the PropertiedUser from Products.PluggableAuthService (PAS), though there is also a basic implementation in AccessControl.users.BasicUser, and a class called SpecialUser in the same module that is used for emergency users and Anonymous.

The PAS version is only marginally more complex than the BasicUser implementation (e.g. it deals with roles obtained from groups a user belongs to), so we will describe the allowed() implementation from BasicUser here:

  • If the object's required roles is the special variable _what_not_even_god_should_do (you couldn't make this up), which corresponds to the ACCESS_NONE security declaration (as used by declareObjectPrivate()), immediately disallow access.
  • If the object's required roles is None, which corresponds to the ACCESS_PUBLIC security declaration (as used by declareObjectPublic()), or if Anonymous is one of the roles (even if the user is not Anonymous), immediately allow access.
  • If Authenticated is one of the required roles and the user is not Anonymous, immediately allow access unless the object does not share an acquisition parent with the user folder (this is to avoid users with the same id in different user folders trying to steal each other's access through acquisition tricks). This is referred to as the "context check" below.
  • Check if the user's global roles intersect with the roles required to access the object, and allow access if the user passes the context check.
  • Check if there are any local roles, as defined in the attribute __ac_local_roles__, granted to the user and check these against the required roles (and perform the context check). __ac_local_roles__ may be a dict or a callable that returns a dict, containing a mapping of user (or group) ids to local roles granted. The local role check is performed iteratively by walking up the acquisition chain and checking the instances of bound methods, unti the root of the acquisition chain.
  • If none of the above succeed, return None to indicate the user is not allowed to access the object.

The other type of security check is to check whether the user should be able to access a particular context. This is most commonly used during traversal, by way of the user folder's validate() method. The version in Products.PluggableAuthService.PluggableAuthService does this:

  • Get all applicable user ids from the request. Most likely, there is only one, but PAS's modular nature means it is possible more than one plugin will supply a user id.
  • Extract the following information from the published object (REQUEST['published']):
    • accessed, the object the published object was accessed through, i.e. the first traversal parent (request['PARENTS'][0]).
    • container, the physical container of the object, i.e. the inner acquisition parent. If the published object is a method, the container is also set to be the method, but stripped of any outer acquisition chains by a call to aq_inner(). If the published object does not have an inner acquisition parent, the traversal parent is used in the same way as it is used to set accessed.
    • name, the name used to access the object.
    • value, the object we are validating access to, i.e. the published object.
  • If this is the top level user folder and the user is the emergency user, return the user immediately without further authorisation.
  • Otherwise, attempt to authorise the user by creating a new security manager for this user and calling its validate() method with the accessed, container, name, and value variables.

The default security manager validate() method delegates to the equivalent method on the ZopeSecurityPolicy. This is a charming 200+ line bundle of if statements that does something like this:

  • If the name is an aq_* attribute other than aq_parent, aq_inner or aq_explicit, raise Unauthorized.

  • Obtain the aq_base'd version of container and accessed. If the accessed parent was not acquisition-wrapped, treat the aq_base'd container as the aq_base'd accessed.

  • The caller may have passed in the required roles already as an optimisation. If not, attempt to get the required roles by calling getRoles(container, name, value). The Python version of this is defined in AccessControl.ZopeSecurityPolicy. It does the following:

    • If the value has a __roles__ attribute, and it is None (ACCESS_PUBLIC) or a list or tuple of roles, return them. This probably means the value is a content object or similar.
    • If it is a PermissionRole object or another object with a rolesForPermissionOn() method (described above), call this with the value as an argument and return the results. This probably means the value is a method.
    • If there is no __roles__ attribute, check if we have a name. Return "no roles" if not.
    • Attempt to find a class for the value's container. If value is a method, go via the im_self attribute to get an instance to use as the container. Then look for a <name>__roles__ attribute on the class. If this is a PermissionRole, call rolesForPermissionOn() as above; if it is a list, tuple or one of the sentinel values (ACCESS_PUBLIC, ACCESS_PRIVATE or ACCESS_NONE, return it directly.
  • If we still have no roles, we may have a primitive or other simple object

    that is not directly security-aware. We can still try to get security information from the container:

    • If there is no container passed in, all bets are off. Raise Unauthorized.
    • Attempt to get a __roles__ value from the container. If it is acqusition-wrapped, also try to explicitly acquire __roles__. If this fails, then we may still be able to get some security assertions from the container (see below), but we only allow this if the accessed parent is the container. If the value was accessed through a more convoluted acquisition chain, say, we cannot rely solely on container assertions, so we raise Unauthorized.
    • At this point, there are two possibilities: we have some roles required to access the container, or we have no roles at all, but we accessed the value directly from its parent container. In both cases, we check container security assertions:
      • If the container is a tuple or string, and we have gotten this far, we consider access to be allowed and return true.
      • If the container `` is an object with an attribute ``__allow_access_to_unprotected_subobjects__, obtain it. __allow_access_to_unprotected_subobjects__ can be one of three things:
        • An integer or boolean: if set to a truth value, allow access and return true, otherwise raise Unauthorized.
        • A dictionary: Attempt to look up a truth value in this dictionary by using the accessed name as a key. If not found or false, raise Unauthorized, otherwise allow access and return true. If the name is not found, default to allowing access.
        • A callable: Call it with the name and value as arguments, and use the return value to determine whether to allow access or raise Unauthorized.
      • If there is no __allow_access_to_unprotected_subobjects__, raise Unauthorized.
    • If we did manage to get some roles from the container, we still check __allow_access_to_unprotected_subobjects__ as above, but only as a negative: we raise Unauthorized if access is not allowed, and continue security checking against the roles we found otherwise. In this case, we use the container (probably a content object) as the value to check.
    • At this point, we have roles, and we know the container in theory allows access to attributes (subobjects) that do not have their own security assertions. We set value to be the container so that we can check whether we are in fact allowed to access the container.
    • We can now check whether the user has the appropriate roles. This is essentially the same logic as in checkPermission() above, although stated slightly differently.
      • If __roles__ is None (ACCESS_PUBLIC) or contains Anonymous, allow access immediately.
      • If the execution context is something like a through-the-web Python script owned by a user, we raise Unauthorized if the owner does not have the given roles.
      • If the execution context has proxy roles, these are allowed to be used to validate access intead of the user's actual roles.
      • Otherwise, call user.allowed() to validate access and either return true or raise Unauthorized.

The remainder of the logic in validate() concerns the case where verbose-security is enabled in zope.conf. Various checks are made in an attempt to raise Unauthorized exceptions with meaningful descriptions about where in the validation logic access was denied.

The mapping of permissions to roles can be managed persistently at any object by setting the mangled permission attribute (see the description of rolesForPermissionOn() above) to a list of roles as an instance variable.

The most basic API to do so is the class AccessControl.Permission.Permission. This is a transient helper class initialised with a (non-mangled) permission name (i.e. the first element in an __ac_permissions__ tuple), a tuple of attributes the permission applies to (i.e. the second element in an __ac_permissions__ tuple) - referred to as the variable data - and an object where the permission is being managed.

The methods getRoles(), setRoles() and setRole() allow roles to be obtained and changed.

getRoles() will first attempt to get the mangled permission name attribute and return its value.

If it is not set, it will fall back to looping over all the listed attributes (data) and obtaining the roles from the first one found, taking into account the various ways in which __roles__ can be stored. Note that an empty string in the tuple of attributes means "check the object itself for a __roles__ attribute". If __roles__ is a list, it is returned, though if it contains the legacy role Shared, this is removed first. The sentinel None (ACCESS_PUBLIC) is turned into ['Manager', 'Anonymous']. If no roles are set, the default return value is ['Manager'], though another default can be supplied as the optional last parameter.

setRoles() will set or delete (if setting to an empty list of roles) the mangled permission name as an instance variable on the object. Next, it will ensure no other __roles__ or <name>__roles__ instance variables have been set (class variables are left alone, of course), so that the managled permission name attribute is the unambiguous statement of the permission-to- role mapping.

Note that for both getRole() and setRole(), the difference between a tuple (don't acquire roles) and a list (do acquire) is significant, and preserved.

setRole() is used to manage a single role. It takes a role name and a boolean to decide whether the role should be set or not. It simply builds the appropriate list or tuple based on the current value of getRoles() and then calls setRole().

In most cases, it is easier to use the API provided by AccessControl.rolemanager.RoleManager to manipulate roles in a particular context. This class, usually via the more specific OFS.roles.RoleManager, is a mixin to most persistent objects in Zope. It contains a number of relevant methods:

ac_inherited_permissions(all=0)
Returns a list of permissions applicable to this class, but not defined on this class directly, by walking the __bases__ of the class. (Note that this not inheritance in the persitent acquisition sense!). If all is set to a truth value, the permissions on this class are included as well. The return value is an __ac_permissions__-like tuple of tuples. For inherited permissions, the attribute list will be an empty tuple.
permission_settings(permission=None)
Returns the settings for a single or all permissions, returning a list of dicts. Used mainly by ZMI screens.
manage_role(role, permissions=[])
Uses the Permission API to grant the role to the permissions passed in, and take it away from any other permissions where the role may be set.
manage_acquiredPermissions(permissions=[])
Uses the Permission API to set the roles lists for each of the passed-in permissions to a list (acquire), and for all other permissions to a tuple (don't acquire).
manage_permission(permission, roles=[], acquire=0)
Uses the Permission API to set roles for the given permission to either a tuple or list (it does not matter what type of sequence the roles parameter contains, the acquire parameter is used), but only if the permission is known to this object.
permissionsOfRole(role)
Uses the Permission API to get the permissions of the given role. Returns a list of dicts with keys name and selected (set to either an empty string or the string SELECTED).
rolesOfPermission(permission)
The inverse of permissionsOfRole(), returning a similar data structure.
acquiredRolesAreUsedBy(permission)
Returns either CHECKED or an empty string, depending on whether the roles sequence of the given permission is a list or tuple.

The use of the strings CHECKED or SELECTED as booleans is an unfortunate side-effect of these methods being used quite literally by ZMI templates.

The list of known (valid) roles in any context is set in the attribute __ac_roles__. On the initalisation of the application root during startup, in install_required_roles() in OFS.Application.AppInitializer, this is made to include at least Owner and Authenticated. The RoleManager base class set it as a class variable to contain ('Manager', 'Owner', 'Anonymous', 'Authenticated').

In AccessControl.rolemanager.RoleManager, the method valid_roles() can be used to obtain the list of valid roles in any given context. It will also include roles from any parent objects referenced via a __parent__ attribute.

User defined roles can be set through the ZMI, or the method _addRole() in the OFS.roles.RoleManager specialisation, which simply manipulates the __ac_roles__ tuple as an instance variable. There is also _delRoles() to delete roles. The method userdefined_roles() on the base AccessControl.rolemanager.RoleManager class will return a list of all roles set as instance variables instead of class variables.

The global roles of a given user is determined by the getRoles() function on the user object (see the description of the allowed() method above). The default ZODBRoleManager plugin for PAS stores a mapping of users and roles persistently in the ZODB, though other implementations are possible, e.g. querying an LDAP repository.

Users may also have local roles, granted in a particular container and its children. These can be discovered for a given user most easily by calling the getRolesInContext() function on a user object, which takes a context object as a parameter.

These are stored in the instance variable __ac_local_roles__. This may be a dict or a callable that returns a dict, containing a mapping of user (or group) ids to local roles granted. The local role check is performed iteratively by walking up the acquisition chain and checking the instances of bound methods, unti the root of the acquisition chain.

The API to manage local role assignments in a given context is found in AccessControl.rolemanager.RoleManager, through the following methods:

get_local_roles()
Return a tuple of local roles, each represented as a tuple of user ids and a tuple of local roles for that user id. With PAS, this may also include group ids.
users_with_local_role(role)
Inspect __ac_local_roles__ to get a list of all users with the given local role.
get_local_roles_for_userid(userid)
Inspect __ac_local_roles__ to get a tuple of all local roles for the given user id.
manage_addLocalRoles(userid, roles)
Modify __ac_local_roles__ to add the given roles to the given user id. Any existing roles are kept.
manage_setLocalRoles(userid, roles)
Modify __ac_local_roles__ to add the given roles to the given user id. Any existing roles are replaced.
manage_delLocalRoles(userids)
Remove all local roles for the given user ids.

On startup, at import time of AccessControl.users, the function readUserAccessFile() is called to look for a file called accesss from the Zope instance home. If found, it reads the first line and parses it to return a tuple (name, password, domains, remote_user_mode,).

If set, the module variable emergency_user is set to an UnrestrictedUser, a special type of user where the allowed() method always returns true. If not, it is set to a NullUnrestrictedUser, which acts in reverse and disallows everything.

The user folder implementations in AccessControl and PAS make specific checks for this user during authentication and permission validation to ensure this user can always log in and has virtually any permission, with the exception of _what_not_even_god_should_do (ACCESS_NONE).

Before Python 2.2 and "new-style" classes, the ExtensionClass.ExtensionClass metaclass provided features now found in Python itself. Nowadays, it mainly provides three features:

  • Support for a class initialiser. Classes deriving form ExtensionClass.Base can define a method __class_init__(self), which is called when the class is initialised (usually at module import time). Note that self here is the class object, not an instance of the class.
  • Ensuring that any class that has ExtensionClass as a __metaclass__ implicitly get ExtensionClass.Base as a base class.
  • Providing an inheritedAttribute method, which acts a lot like super() and is hence superfluous except for in legacy code.

The base class ExtensionClass.Base provides the __of__ protocol that is used by acquisition. It is similar to the __get__ hook used in Python descriptors, except that __of__ is called when an implementor is retrieved from an instance as well as from a class. Here is an example:

>>> class C(Base):
...   pass

>>> class O(Base):
...   def __of__(*a):
...      return a

>>> o1 = O()
>>> o2 = O()
>>> C.o1 = o1
>>> c.o2 = o2
>>> c.o1 == (o1, c)
True
>>> C.o1 == o1
True

There is probably little reason to use ExtensionClass.Base in new code, though when deriving from Acquisition.Implicit or Acquisition.Explicit, it will be included as a base class of those classes.

Black magic.

Zope provides many different hooks that can be used to execute code at various times during its lifecycle. The most important ones are outlined below:

zope.processlifetime defines three events:

  • IDatabaseOpened, notified when the main ZODB has been opened, but before the root application object is set
  • IDatabaseOpenedWithRoot, notified later in the startup cycle, when the application root has been set and initalised
  • IProcessStarting, notified when the Zope startup process has completed, but before the Zope server runs (and so can listen to requests)

The list App.ZApplication.connection_open_hooks can be used to hold functions that are called with a ZODB connection as their sole argument just after traversal over the ZApplicationWrapper as it opens a ZODB connection for the request.

The ZODB transaction provides two methods to register hooks - addBeforeCommitHook() and addAfterCommitHook(). These can be passed functions and a (static) set of arguments and will be called just before, and just after, a transaction is committed. The hook function must take at least one argument, a boolean indicating whether the transaction succeeded.

Use transaction.get() to get hold of the transaction object. See transaction.interfaces.ITransaction for more details.

Request-scoped items may be held from garbage collection using request._hold(). If applicable, the item held can implement __del__(), which will be called when the request is destroyed.

The event zope.publisher.events.EndRequestEvent is triggered at the end of an event, just before any held items are cleared.

The publisher notifies a number of events, which can be used to hook into various stages of the publication process. These are all defined in the module ZPublisher.pubevents.

When an exception is raised, a view registered for the exception type as context (and a generic request) named index.html will be rendered as an error message, if it exists.

If an object has a method __bobo_traverse__(self, request, name), this will be used during traversal in lieu of attribute or item access. It is expected to return the next item to traverse to given the path segment name. A more modern approach is to register an adapter to IPublishTraverse - see above.

The method __before_publishing_traverse__(self, object, request) can be implemented to be notified when traversal first finds an object. Implemented on a class, the self and object parameters will be the same.

See also the SiteAccess package, which implements a through-the-web manageable, generic multi-hook to let any callable be invoked before access through an "AccessRule".

The event zope.traversing.interfaces.IBeforeTraverseEvent is notified when traversing over something that is a local component site, e.g. the Plone site root.

The __browser_default__ method can be implemented to specify a "default page" (akin to an index.html in a folder). A more modern way to do this is to register an adapter to IBrowserPublisher - see above.

An adapter to ITraversable can be used to implement namespace traversal (.../++<namespace>++name/...). See above for further details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment