<divclass="focus"align="center">This section contains some deprecated information. Documentation is currently being rewritten and will soon be available.</div>
<p>The security model behinds gen-applications is similar to what Zope and Plone offer; simply, gen tries to simplify the way to declare and manage security within your applications. According to this model, <b>users</b> are put into <b>groups</b>; groups have some <b>roles</b>; roles are granted basic <b>permissions</b> on objects (create, read, write, delete, etc). Permissions-to-roles mappings may vary according to the <b>state</b> of objects.</p>
<h1><aname="noMorePrinciples"></a>Yes! We are done with the principles</h1>
<p>In this chapter, we will use the <spanclass="code">ZopeComponent</span> example, first introduced <ahref="gen.html">here</a> and refined <ahref="genCreatingAdvancedClasses.html">here</a>. Our company developing Zope 3 components hires some lucky managers: they understand almost nothing to Zope but they are well paid. Then, there are some project leaders, still lucky and incompetent. Finally, one or two guys are Zope/Python developers.</p>
<p>According to the principles introduced above, we will begin by creating some users. Although the standard Plone interface for managing users, groups and roles is not perfect, gen has not (yet?) re-worked it; we will then use it now. In a standard Plone site, users register themselves. We need to change this setting in order to create users ourselves. Log in to Plone as administrator, go to "Site setup" and click on "Portal settings" and configure the "password policy" this way:</p>
<palign="center"><imgsrc="img/workflow1.png"></p>
<p>Now, go to "Users and Groups Administration" (still in "Site setup") and add the following users using the button "add new user" (do not check "Send a mail with the password" and enter a dummy email):</p>
<p>Now, we need groups. Guess what? We will not create groups. Why? Because gen will generate groups automatically for you!</p>
<p>Now that we have users and groups, it is time to create roles. Guess what? We will not do it. Why? Because it is simply not needed. gen will scan your code and find every role you mention and will create them automatically at the Zope/Plone level if they do not exist yet. We will use the following roles:</p>
<p>gen will create one group for every role defined in your application; the group will be granted only the corresponding role. Note that we will probably not use the role <spanclass="code">ZDeveloper</span>. Indeed, developers work. They will probably not use a management tool. Now, let's tackle permissions. Again, it is not needed to create permissions (at least now): gen provides the following default permissions:</p>
<p>All the security ingredients are now ready (users, groups, roles and permissions): we will now see how to use them to define security on a gen-application.</p>
<h1><aname="createPermission"></a>Managing the <spanclass="code">create</span> permission</h1>
<p>Permission to <spanclass="code">create</span> objects is done at 2 levels. First, you may define a global list of roles that will, by default, be allowed to create any object of any class in your gen-application. In our company, <spanclass="code">ZLeader</span>s are responsible for creating Zope components. You declare this global list in attribute <spanclass="code">defaultCreators</span> of your <spanclass="code">appy.gen.Config</span> instance introduced while <ahref="genCreatingAdvancedClasses.html#i18n">presenting i18n</a>:</p>
<p>Why do I write <spanclass="code">+=</span> and not <spanclass="code">=</span> ? Because the <spanclass="code">defaultCreators</span> attribute is already initialised with this list of default Plone roles: <spanclass="code">['Manager', 'Owner']</span>. <spanclass="code">Manager</span> is the role granted to any Plone/Zope administrator (like the <spanclass="code">admin</span> user we have used in our examples so far); <spanclass="code">Owner</span> is a special role that is granted to the user that created a given object.</p>
<p>Defining default creator roles for every class of your application may not be subtle enough. This is why gen allows you do it per class, with static attribute <spanclass="code">creators</span>. For example, you may use this attribute on class ZopeComponent:</p>
<p>With this piece of code, <spanclass="code">Manager</span>s and <spanclass="code">ZLeader</span>s will be able to create <spanclass="code">ZopeComponent</span>s; only <spanclass="code">Manager</span>s will be able to create instances of other classes in your application (provided no specific <spanclass="code">creators</span> attribute is defined on them). Note that the <spanclass="code">creators</span> attribute infringes the classical rules of class inheritance: If you have non abstract classes <spanclass="code">A</span> and <spanclass="code">B(A)</span>, defining attribute <spanclass="code">creators</span> on <spanclass="code">A</span> will have absolutely no effect on <spanclass="code">B</span>.
<h1><aname="workflows"></a>Managing all other permissions: defining workflows</h1>
<p>For granting all other permissions (like read, write and delete, in short <spanclass="code">r, w, d</span>), we will not use the same approach as for the <spanclass="code">create</span> permission. Indeed, the permissions-to-roles mapping for a given object may depend on its <i>state</i>. For example, at some point in the object's life, we would like some users to be able to edit it; after a while (once the object has been validated by a manager, for example), we would like to prevent further modifications (at least for persons having certain roles). This is why we will use the concept of <i>workflow</I> as provided by Zope and Plone. This concept is simple: for a given gen-class, you may define several <i>states</i> (like "created", "validated", "ongoing" or whatever you want); for every state, you define a permissions-to-role mapping (while an object is in this state, what roles are granted what permissions on that object?). Finally, you need to decide what will be the <i>initial</i> state of the object and what are the valid state changes (= <i>transitions</i>).</p>
<p>Workflows are defined on a per-class basis. At present, if you don't define any workflow for one of your gen-classes, a default workflow provided by Plone will be used. As Plone is not really made for building web applications, this workflow will probably not be relevant for your class (it is a workflow for publishing web pages on a collaborative web site, with states like "under creation", "under validation" or "published"). In future gen releases, I will probably add an attribute <spanclass="code">defaultWorkflow</span> in the <spanclass="code">Config</span> instance and even provide some kind of web-application-minded default workflow (with some states like "active" and "inactive"). Hey I realize that it is useful to write documentation! It forces you to explore in a systematic way every aspect of the thing you have developed! Is it the birth of a new quality paradigm? Beuaaahhrk: I have written the word "quality".</p>
<p>So let's define a simple workflow for our class <spanclass="code">ZopeComponent</span>. Until now our class looks like this:</p>
<p>Field <spanclass="code">status</span> seems to be a kind of workflow embryo. So we will remove it and create a workflow whose states will look like values of this field:</p>
<p>21 lines of code for the workflow ! (including 3 lines of comments and several lines splitted because of this silly 80-characters-length constraint). Sorry, the states do not correspond exactly to the values of the removed status <spanclass="code">field</span>; this is because I felt myself guilty about being so ironic.</p>
<p>Like gen-classes, gen-workflows do not inherit from any base class provided by gen. Simply, static fields are instances of classes provided by gen like <spanclass="code">appy.gen.State</span> and <spanclass="code">appy.gen.Transition</span>. gen will decide if your class is a gen-class or a gen-workflow by analysing its static attributes. So please avoid creating hybrid classes mixing field definitions (<spanclass="code">String</span>, <spanclass="code">Ref</span>, etc) and workflow definitions (<spanclass="code">State</span>, <spanclass="code">Transition</span>, etc).</p>
<p>As shown in the last lines of the example, associating a gen-workflow to a gen-class is done through the <spanclass="code">workflow</span> attribute of a gen-class. The same workflow may be associated to different gen-classes. A gen-class defining no workflow inherits from a potential workflow association defined on a parent.</p>
<p>Let's analyse the workflow in itself. We begin by putting some roles in variables. It is not really necessary (this is not a role "registration" at all); I do it in order to avoid writing syntax errors within role names because it would lead to the creation of silly roles.</p>
<p>Then, we have the definitions of states. The first paramater is the permissions-to-roles mapping, that indicates, for every permission defined on the associated class, what role(s) have the permission. This parameter is a dictionary whose keys are permissions (remember that <spanclass="code">r</span>, <spanclass="code">w</span>, and <spanclass="code">d</span> correspond to read, write and delete permissions; I can use them as is because of the clause <spanclass="code">from appy.gen import *</span>) and whose values are, as suggested by the example, either a tuple/list of roles, a single role, or <spanclass="code">None</span>. For example, when the component is <spanclass="code">underDevelopment</span>, only project leaders (and administrators) may modify them; when it is in state <spanclass="code">whereIsTheClient</span>, only managers (and administrators) may edit them. As soon as a component is <spanclass="code">validated</span>, nobody may delete it: permission <spanclass="code">d</span> is granted to <spanclass="code">None</span> (=nobody). The parameter <spanclass="code">initial=True</span> indicates that the first state is the one the object gets as soon as it is created. Avoid specifying this for more than one state.</p>
<p>Definitions of transitions are based on state definitions. Indeed, when defining a transition, the first parameter is a 2-tuple <spanclass="code">(startState, endState)</span>. So a transition is simply a specified way to go to from one state to the other. Additional parameter <spanclass="code">condition</span> specifies under what circumstances the transition may be "triggered". In the example, only persons having roles <spanclass="code">Manager</span> or <spanclass="code">ZManager</span> are allowed to trigger transition <spanclass="code">validate</span>, that will change object state from <spanclass="code">created</span> to <spanclass="code">validated</span>. It is also possible to define <i>multi-transitions</i>, which are transitions having multiple 2-tuples <spanclass="code">(startState, endState)</span> (grouped in one big tuple) like transition <spanclass="code">cancel</span>. Multi-transitions may be seen as a shortcut that allows you to write several similar transitions in only one. In the example, <spanclass="code">cancel</span> transitions are used to "go backward", if a user triggered a transition by error.</p>
<p>Such a workflow is called a <i>state machine</i>. The following diagram represents the workflow defined above.</p>
<palign="center"><imgsrc="img/workflow2.png"></p>
<p>Other frameworks allow you to define your workflows this way, with tools like ArgoUML. This is the case for ArchGenXML for example. I have been a ArchGenXML user for 2 years, and this is why I decided to create a code-based approach for defining workflows in gen. Why? As <ahref="gen.html">already mentioned</a>, working with a UML model gives you an additional dependency (towards a tool and a format), prevents collaborative work, cut & paste or more powerful subtleties like multi-transitions or workflow inheritance (see below). Moreover, a model is (when you compare it with code) a much poorer way to describe things. It abstracts a lot of "details", that you are forced to add in an unnatural way (like defining permissions-to-roles mappings in UML tagged values that you can't represent on the diagram), or, worse, that you can't simply put in the model (like the actions triggered by the workflow or specific conditions that you define with Python methods, like explained hereafter). The conclusion is: when using a model approach, you are always forced to complete it with a code approach (this is what happens typically with ArchGenXML: specific actions and conditions are written in additional Python scripts. It implies spreading and duplicating information about the workflow, augmenting complexity and the maintainability effort. That said, diagrams may represent a good way to <i>communicate</i> your ideas. This is why we plan to integrate in future gen releases the possibility to generate diagrams from gen-workflows and gen-classes.</p>
<p>In order to see our workflow at work, we need to perform a last action: granting roles to our users. Because gen managed automatically groups, roles, and their links, the only action we need to perform is to put <spanclass="code">sidney</span> and <spanclass="code">ludovic</span> in the right groups.</p>
<p>Re-generate your product, restart Zope, go to "Site setup", re-install your Plone product, go to "Site setup" -> "Users and Groups Administration" and click on tab "groups". You will get this screen:</p>
<palign="center"><imgsrc="img/workflow3.png"></p>
<p>Groups "Administrators" and "Reviewers" are default Plone groups. Your gen-application has added groups "ZManager_group" and "ZLeader_group": each one has the corresponding role. Click on "ZManager_group": there is nobody in it. Click on "show all": Ludovic and Sidney appear. Check the checkbox besides Sydney and add her to the group. In a similar way, add Ludovic to group "ZLeader_group".</p>
<p>We will first walk through the application as user <spanclass="code">admin</span>, as usual. According to the workflow, <spanclass="code">admin</span>, as <spanclass="code">Manager</span>, is God: he can do everything. Besides this pleasant feeling, it will allow us to trigger all workflow transitions.</p>
<p>Because role <spanclass="code">Manager</span> may add <spanclass="code">ZopeComponent</span> instances (thanks to <spanclass="code">Config.defaultCreators</span>), on the dashboard, the "plus" icon is available in tab "Zope component". Create a new Zope component: the consult view will look like this:</p>
<palign="center"><imgsrc="img/workflow4.png"></p>
<p>Besides the component title, its state appears (here: "Created"). According to the workflow, the only possible transition to trigger from this state is <spanclass="code">validate</span>; as <spanclass="code">Manager</span> I have the right to trigger it, so the corresponding button appears on the bottom of the page. Please enter a nice comment in the field and click on button "validate": the component will go in state <spanclass="code">validated</span> as dictated by the workflow. The consult view has now evolved accordingly:</p>
<palign="center"><imgsrc="img/workflow5.png"></p>
<p>Component state is now "Validated". I have expanded the plus icon "History": all workflow actions triggered on the component appear in a table, with the (optional) comments entered by the triggering user(s). Again, according to the workflow, 2 actions may now be triggered, and I have te rights to trigger both: 2 new buttons appear... I guess you understand now how the workflow works: try now by yourself, walk through the state machine by triggering available actions and see how the history evolves.</p>
<p>The following screenshot shows how the dashboard may evolve according to permissions:</p>
<palign="center"><imgsrc="img/workflow6.png"></p>
<p>Because the workflow says that nobody may delete Zope components once they are validated, the delete icon is not available for component named "New component". By the way, you can display the workflow state in the dashboard: go to the corresponding flavour, click on tab "user interface" and, for class <spanclass="code">ZopeComponent</span>, select "workflow state" in field "Columns to display while showing query results".</p>
<p>Now, please log out (a link is available in the top-right corner, within the blue strip) and log in as <spanclass="code">ludovic</span>. Because <spanclass="code">ZLeader</span>s are among default creators, as Ludovic we may create a new Zope component. If you do so, you will then get a consult view like this one:</p>
<palign="center"><imgsrc="img/workflow7.png"></p>
<p>No workflow action is shown because Ludovic has not the right to validate the component. Reconnect now as Sidney. First of all, let's view the dashboard as it is shown to her:</p>
<palign="center"><imgsrc="img/workflow8.png"></p>
<p>Sidney is not among <spanclass="code">ZopeComponent</span><spanclass="code">creator</span>s, so the "plus" icon is not shown in the corresponding tab. Moreover, according to the workflow, she does not have the right to modify components in state "Created": the "pen" icon is not available for component "Aaaa". But if you go to the consult view for this component, Sidney will be able to <spanclass="code">validate</span> it:</p>
<palign="center"><imgsrc="img/workflow9.png"></p>
<p>We have now finished our first security tour. An important remark is that we have granted roles "globally" to groups: any user of the group has always the globally granted role, under all circumstances, on any related object in your gen-application. In our example, Ludovic and all potential other project leaders have the right to edit all <spanclass="code">created</span> components. This may not be the desired behaviour. Maybe would you prefer any project leader to be able to edit his own components but not components created by other project leaders. This is where "local roles" come into play. A local role is a role that a user or group has, but only on a given object. The default Plone role "Owner" is an example of local role: this is not a role that you grant "globally" to a user or group (like the ones shown in tab "groups" or "users" of "Site setup -> Users and Groups Administration"); this is a role that is granted on an object to the user that created it. You may of course reference local roles within gen-workflows. For example, if you want to restrict component modifications to <spanclass="code">Owner</span>s and <spanclass="code">Manager</span>s when the component is <spanclass="code">created</span>, you may modify the workflow state <spanclass="code">created</span> like this:</p>
<p>Re-generate your product and re-install it. The Plone procedure for re-installing a product updates the workflow definition but does not update the permissions-to-roles mappings defined on existing objects. In order to synchronize them with the new workflow definition, you need to go, through the ZMI, in object "portal_workflow" within your Plone site. At the bottom of the page, you will find a button "Update security settings". Click on it. This may take a long time if you have a large number of objects in your database. In future gen releases, you will be able to re-install your product directly from your tool. This specific procedure will ask you if you want to "Update workflow settings on existing objects" or not.</p>
<p>Now, log in as Ludovic. Consider the following dashboard as seen by him:</p>
<p>Components "ZC1" and "Aaaa" were created by <spanclass="code">admin</span>: Ludovic may not edit them. He can only edit the one he has created itself (= the last one in the table).</p>
<p>In future gen releases, you will be able to define and manage "custom" local roles.</p>
<h1><aname="conditionsAndActions"></a>Conditions and actions linked to transitions</h1>
<p>Until now, we have seen that, as transition <spanclass="code">condition</span>, you can specify role(s) (one, as a string, or a tuple of roles). You can also specify Python method(s) the same way, and even mix roles and Python methods. Specified Python method(s) must belong to your gen-workflow (or one of its parents, yes, we will soon talk about workflow inheritance!). With such methods, more complex conditions may be defined. Let's show it by refining our previous example. Suppose that components can be <spanclass="code">validated</span> only if a funeral date (which is not a mandatory field) has been specified. Transition <spanclass="code">validate</span> need to evolve:</p>
<p>It means that beyond having one of the roles defined in <spanclass="code">managerM</span>, method <spanclass="code">funeralOk</span> must also return <spanclass="code">True</span> (or an equivalent value) as prerequisite for triggering transition <spanclass="code">validate</span>. This kind of method takes a single parameter: the related object. In short: a transition may be triggered if the user has at least one of the roles specified <i>and</i> all specified methods return <spanclass="code">True</span>. So gen computes an <b>or</b>-operator on roles and an <b>and</b>-operator on methods.</p>
<p>One may also define action(s) (as Python method(s)) that are executed after any transition has been triggered. Let's suppose we want to reinitialise the component <spanclass="code">description</span> when we start its development. This is completely silly of course. But I like to force you doing silly things, it is a pleasant feeling. So let's update transition <spanclass="code">startDevelopment</span>:</p>
<p>We have specified a Python method in a new parameter named <spanclass="code">action</span>. Now, try to click on button "startDevelopment" and you will see the <spanclass="code">description</span> changing. As for conditions, actions need to be Python methods defined on the gen-workflow or one of its parents. Those methods take only one parameter: the related object. As already announced, the <spanclass="code">action</span> parameter may also take a list or tuple of methods instead of a single method.</p>
<h1><aname="specificFieldPermissions"></a>Specific field permissions</h1>
<p>Until now, we have considered security as an homogeneous layer encompassing a whole gen-class: when someone may read or write objects of a gen-class, she may read or write <i>any field</i> on this object. In some cases, though, we may need to be more subtle, and define specific read or write permissions on individual fields. As <ahref="genCreatingBasicClasses.html">already mentioned</a>, this can be done at the time of field definition, with boolean parameters <spanclass="code">specificReadPermission</span> and <spanclass="code">specificWritePermission</span>. For every field for which you do not declare using a specific read or write permission, the gen-class-wide read or write permission will come into play for protecting it.</p>
<p>Let's try it on our class <spanclass="code">ZopeComponent</span>. Suppose we need a specific write permission on field <spanclass="code">funeralDate</span> and a specific read permission on field <spanclass="code">responsibleBunch</span>:</p>
<p>Now, in our workflow, for every state, we need to update the permissions-to-roles mapping by specifying the roles that will be granted those 2 new permissions. But first, we need a way to designate those permissions. This is done by using classes <spanclass="code">appy.gen.ReadPermission</span> and <spanclass="code">appy.gen.WritePermission</span> like this:</p>
<p>When constructing a <spanclass="code">WritePermission</span> or <spanclass="code">ReadPermission</span> instance, you give as parameter the "path name" of the field on which the corresponding specific permission was defined. Within this "path name", you find the name of the class where the field is defined (<spanclass="code">ZopeComponent</span> in the example). If the workflow class and the field class are in the same package (like, in our case, <spanclass="code">ZopeComponentWorkflow</span> and <spanclass="code">ZopeComponent</span>), you can specify the "relative" class name of the field class (without prefixing it with the package name, ie <spanclass="code">ZopeComponent</span>). Else, you need to specify the full package name of the class (ie <spanclass="code">ZopeComponent.ZopeComponent.funeralDate</span>).</p>
<p>Now let's update every state definition by integrating those 2 permissions in the permissions-to-roles mappings:</p>
<p>Now, re-generate your product, restart Zope and re-install the product, update the security settings on <spanclass="code">portal_workflow</span> and try, as <spanclass="code">admin</span>, to edit a component that is in state <spanclass="code">created</span> and was created by Ludovic. Because <spanclass="code">Manager</span>s have the right to modify components in this state, you will be able to get the edit view. But on this view, because you do not have the specific "edit" permission on field <spanclass="code">funeralDate</span> (you are not the component <spanclass="code">Owner</span>), the field will not show up:</p>
<p>Aaaaargh! The field is visible! Impossible! How can user <spanclass="code">admin</span> bypass our security like this? This is the occasion to learn something about local roles: they <i>propagate</i> from a given object to its contained objects. Remember that Zope components, as root objects, are stored in a folder within the Plone site. This folder was created by the generated Plone product with the <spanclass="code">admin</span> user: so <spanclass="code">admin</span> has local role <spanclass="code">Owner</span> on it (and, by the way, has local role <spanclass="code">Owner</span> on the Plone site as well). It means that <spanclass="code">admin</span> will have role <spanclass="code">Owner</span> on all sub-objects of your Plone site. When you think about this, it is normal: <spanclass="code">admin</span> is God (and you are <spanclass="code">admin</span>).</p>
<p>In order to produce a working example, let's create a new user (let's call it <spanclass="code">gerard</span>) and grant him role <spanclass="code">Manager</span>. This way, we will get a <spanclass="code">Manager</span> that is not <spanclass="code">Owner</span> of all objects. Log in as <spanclass="code">gerard</span>, and go the previous edit view:</p>
<p>Yes! You do not see (so you can't edit) field <spanclass="code">funeralDate</span>. Consult views (or dashboards) will behave similarly with read permissions: fields for which the currently logged user have no read permission will be invisible. Note that if you don't have the whole-gen-class read (write) permission, and you have a read (write) permission on one of its fields, you will not be allowed to read (write) the specific field.</p>
<p>For the moment, for every state definition, you are forced to specify a permissions-to-roles mapping that includes all related permissions (class-wide and field-specific). In future gen releases, this will change. We will implement things like: if you don't specify roles for a specific read (write) field-permission, it will take the value from the corresponding read (write) class-wide permission; unspecified values may also be copied from the previous state definition, etc. This way, as usual, you will continue to be as lazy and concise as possible while writing gen-applications.</p>
<p>With gen, workflows are Python classes. This allows us to benefit from class inheritance and apply it to workflows. Our company that creates Zope components is now used to heavy technologies. They got a business revelation: some managers discovered that COBOL and Zope 3 had a lot in common on both philosophical and technical points of view. So they decided to begin creating components in COBOL. They were so excited about it that they needed to update their management software as quickly as possible. So a new class was added for registering information about COBOL components. The associated workflow was almost similar to the existing <spanclass="code">ZopeComponentWorkflow</span>; a new workflow inheriting from it was created:</p>
<p>Basically, this new workflow "removes" state <spanclass="code">whereIsTheClient</span>, creates a more optimistic end state <spanclass="code">finished</span> and performs some surgical operations on transitions for reflecting navigation to and from the new state. For defining it, we reuse the permissions-to-roles mapping that was defined on state <spanclass="code">whereIsTheClient</span>. Then, we have overridden transition <spanclass="code">validate</span> because the condition that related to field <spanclass="code">funeralDate</span> is not relevant anymore (COBOL components have no funeral date). Transition <spanclass="code">cancelDevelopment</span> was also overridden: the end state is not <spanclass="code">whereIsTheClient</span> anymore, but <spanclass="code">finished</span> instead. We also need to override transition <spanclass="code">cancel</span> for updating the tuple of <spanclass="code">(startState, endState)</span>.</p>
<p>And we are done! You may now test the result. As for classical inheritance, it is not really possible to remove elements in a child class. So state <spanclass="code">whereIsTheClient</span> is still there, but unreachable because of our operations on transitions (so it is more or less the same as a deletion). Workflow inheritance ensures reuse and conciseness: any element that does not change from <spanclass="code">ZopeComponentWorkflow</span> is kept in the child workflow; any change made in the reused part of the parent workflow will automatically impact the child workflow(s).</p>
<h1><aname="workflowsAndi18n"></a>Workflows and i18n</h1>
<p>As usual, for every workflow state and transition, i18n labels have been automatically generated (in the <spanclass="code">plone</span> domain), together with a "nice" default value. The format of those labels is defined <ahref="genCreatingAdvancedClasses.html#i18n">here</a>. There is still a small problem with the <spanclass="code">CobolComponentWorkflow</span>: the transition for finishing the work is called <spanclass="code">cancelDevelopment</span>. I am too lazy for creating another transition, so I will simply modify here the translation of this transition in the corresponding i18n file (=ZopeComponent-plone-en.po in this case):</p>
<p>Note that i18n labels are "duplicated" for every child workflow. Here, I modify label <spanclass="code">zopecomponent_<b>cobolcomponentworkflow</b>_cancelDevelopment</span> without perturbing parent label for the same transition which is <spanclass="code">zopecomponent_<b>zopecomponentworkflow</b>_cancelDevelopment</span>.</p>