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.
Contents
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 inGlobals.DB
andZope2.DB
, and is configured using adbtab
(mount points) read from the configuration file. When this is done, the eventzope.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 theProducts.*
namespace, and ZCML slugs. Theload_zcml()
call also sets up aZope2VocabularyRegistry
.Creating the
app
object, an instance ofApp.ZApplication.ZApplicationWrapper
that wraps aOFS.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 isApplication
. - 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 theZope2
module.- Create an instance of the application object at the root of the ZODB on
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 anOFS.misc_.Misc_
object, which is just a simple, security-aware class. Then store a copy of it as an attribute on the objectApplication.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 inProducts/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 themisc_
attribute on the application root. On thePloneSite
root object, the attributeicon = '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 aproduct
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 inzope.conf
, callApp.Product.initializeProduct
. This will create aApp.Product.Product
object and save it persistently inApp.Control_Panel.Products
. It also reads the fileversion.txt
from the product to determine a version number, and will change the persistent object (at Zope startup) if the version has changed. TheProduct
object is initialised with a product name and title and is used to store basic information about the product. TheProduct
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 ofApp.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
orPageTemplateFile
that renders an add form and one that is a method that actually creates and adds an instance. A typical example fromProducts.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 URLaction="manage_addMailHost"
, which means that when the form is submitted, themanage_addMailHost()
function is called.id
,title
and the other variables are passed as request parameters and marshalled (bymapply()
- see below) into function arguments, and theREQUEST
is implicitly passed (again bymapply()
).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 showingObjectManager
'sAdd
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
andinstance_class
are supplied, set anicon
attribute oninstance_class
to path likemisc_/<productname>/<iconfilename>
.Register any
permissions
by callingAccessControl.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 toManager
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 fromApp.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 ofAttrDict
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 theinstance_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 theinstance_class
action
A path segment like
manage_addProduct/<productname>/<constructorname>
. More onmanage_addProduct
below.product
The name of the product
permission
The add permission passed in or generated
visibility
Either
"Global"
orNone
as passed in to the methodinterfaces
The list of interfaces passed in or obtained from
instance_class
instance
The
instance_class
as passed in to the methodcontainer_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 theFactoryDispatcher
-subclass). The appropriate<methodname>__roles__
attribute is set to aPermissionRole
describing the "add" permission as well.If an
icon
name was passed in, construct anImageFile
to read it from the package and stash it in theOFS.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 standardFactoryDispatcher
class in the same module. - Find a persistent
App.Product.Product
if there is one, or create a simpleApp.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 variablebobo_application
(orweb_objects
)realm
, set via the module level variable__bobo_realm__
, or a global default which can be set theZConfig
configuration file.debug_mode
, a boolean set using the module level variable__bobo_debug_mode__
.err_hook
, set via the module level variablezpublisher_exception_hook
.validated_hook
, set via the module level variablezpublisher_validated_hook
.transactions_manager
, set via the module level variablezpublisher_transactions_manager
, but defaulting to theDefaultTransactionsManager
which uses thetransaction
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 valuecancel
and a keycancel_action
with a path, aRedirect
exception is raised, which will cause an HTTP 302 redirect to be raised.Set
debug_mode
andrealm
on the response, as returned byget_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 theZApplicationWrapper
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)
, wherepath
isrequest['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. Therequest
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 ofself
for methods on objects, and we pass therequest
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
interactionNotify 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
orRedirect
exceptions are re-raised.- A
ConflictError
, which indicates a write-conflict in the ZODB, is turned into aRetry
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 attributestandard_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
andaq_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 attributeREQUEST
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 theportal_factory
tool work. This method cannot easily change the traversal path, except by modifyingrequest['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 tozope.traversing.interfaces.ITraversable
with a name matching the traversal namespace (e.g.view
). Then call itstraverse()
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 anITraversable
adapter are not expected to be acquisition-wrapped. This is in contrast to objects returned by__getitem__()
or__getattr__()
or a customIPublishTraverse
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 toIPublishTraverse
, use that; or, explicitly use theDefaultPublishTraverse
implementation found inZPublisher.BaseRequest
. Then call thepublishTraverse()
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 toIPublishTraverse
.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
exceptionIf 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 anAttributeError
,KeyError
orNotFound
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 exceptionZPublisher.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 extendsAcquisition.Implicit
orAcquisition.Explicit
). - Next, try to look up a view using the same semantics as above
- Next, try
getattr()
without theaq_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)
- Attempt to look up the name as an attribute of the current object,
using
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
andIPublishTraverse
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
orNotFound
exception is raised during name resolution, return a 404 response by raising an exception. Similarly, if aForbidden
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 ofIPublishTraverse
and is implemented by theDefaultPublishTraverse
class. Again, theIBrowserPublisher
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 fallbackDefaultPublishTraverse
may be used. ThebrowserDefault()
method on theIBrowserPublisher
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 the object has a method
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 HTTPGET
andPOST
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 orindex_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 likeFile
orImage
it is theindex_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 calledold_validate()
, used if there is no user folder, but that should never happen in a modern Zope installation). This either returns a user object orNone
, 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, anUnauthorized
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 viaget_module_info()
as described above), it is called with the request and user as arguments. The standardvalidated_hook
callsnewSecurityManager()
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 variableAUTHENTICATION_PATH
.
- The roles required to access the traversed-to object are fetched by calling
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 thanNone
, no further post-traverse functions are called, and the return value is used as the return value of thetraverse()
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 withmanage_
. 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 itsapply()
method with the class as an argument, and then delete it.The
apply()
method ofClassSecurityInfo
does this:Collect any explicitly set
__ac_permissions__
tuple and turn it into internal state, as if theClassSecurityInfo
had been used to set it, so that it is not lost.For any attribute declared with
declarePublic()
ordeclarePrivate()
, set<name>__roles__
toACCESS_PUBLIC
orACCESS_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 infoapply()
call) and callAccessControl.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 classAccessControl.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 aPermissionRole
object. If a default list/tuple of roles was supplied, record this in thePermissionRole
, 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 theACCESS_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 the attribute is
- 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 theACCESS_NONE
security declaration (as used bydeclareObjectPrivate()
), immediately disallow access. - If the object's required roles is
None
, which corresponds to theACCESS_PUBLIC
security declaration (as used bydeclareObjectPublic()
), or ifAnonymous
is one of the roles (even if the user is notAnonymous
), immediately allow access. - If
Authenticated
is one of the required roles and the user is notAnonymous
, 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 toaq_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 setaccessed
.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 theaccessed
,container
,name
, andvalue
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 anaq_*
attribute other thanaq_parent
,aq_inner
oraq_explicit
, raiseUnauthorized
.Obtain the
aq_base
'd version ofcontainer
andaccessed
. If theaccessed
parent was not acquisition-wrapped, treat theaq_base
'd container as theaq_base
'daccessed
.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 inAccessControl.ZopeSecurityPolicy
. It does the following:- If the
value
has a__roles__
attribute, and it isNone
(ACCESS_PUBLIC
) or a list or tuple of roles, return them. This probably means thevalue
is a content object or similar. - If it is a
PermissionRole
object or another object with arolesForPermissionOn()
method (described above), call this with thevalue
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 aname
. Return "no roles" if not. - Attempt to find a class for the
value
'scontainer
. Ifvalue
is a method, go via theim_self
attribute to get an instance to use as thecontainer
. Then look for a<name>__roles__
attribute on the class. If this is aPermissionRole
, callrolesForPermissionOn()
as above; if it is a list, tuple or one of the sentinel values (ACCESS_PUBLIC
,ACCESS_PRIVATE
orACCESS_NONE
, return it directly.
- If the
- 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. RaiseUnauthorized
. - Attempt to get a
__roles__
value from thecontainer
. 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 theaccessed
parent is thecontainer
. If thevalue
was accessed through a more convoluted acquisition chain, say, we cannot rely solely on container assertions, so we raiseUnauthorized
. - 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 thevalue
directly from its parentcontainer
. 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, raiseUnauthorized
, otherwise allow access and return true. If the name is not found, default to allowing access. - A callable: Call it with the
name
andvalue
as arguments, and use the return value to determine whether to allow access or raiseUnauthorized
.
- An integer or boolean: if set to a truth value, allow access and return
true, otherwise raise
- If there is no
__allow_access_to_unprotected_subobjects__
, raiseUnauthorized
.
- If the
- 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 raiseUnauthorized
if access is not allowed, and continue security checking against the roles we found otherwise. In this case, we use thecontainer
(probably a content object) as thevalue
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 thecontainer
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__
isNone
(ACCESS_PUBLIC
) or containsAnonymous
, 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 raiseUnauthorized
.
- If
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!). Ifall
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 theroles
parameter contains, theacquire
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 keysname
andselected
(set to either an empty string or the stringSELECTED
). 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 thatself
here is the class object, not an instance of the class. - Ensuring that any class that has
ExtensionClass
as a__metaclass__
implicitly getExtensionClass.Base
as a base class. - Providing an
inheritedAttribute
method, which acts a lot likesuper()
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 setIDatabaseOpenedWithRoot
, notified later in the startup cycle, when the application root has been set and initalisedIProcessStarting
, 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.