ERP5 Handbook/Magic Security

Playing with security

edit

Needless to say, security is crucial in most projects. And designing a tight yet flexible permission system could be a developer's nightmare.

Fortunately, ERP5 comes with extremely powerful security machinery. Unfortunately, it does not work out of the box and need to be customized.

5A Security model

edit

ERP5's security model is based on roles. Roles are a Zope concept and are explained in the Zope documentation (Zope Book). Generally speaking, a role describes who the user is (e.g. accountant, clerk, officer), but not what he can do. That is defined by permissions (e.g. view permission, modify permission). For each document there is a role-to-permission mapping. The user with the accountant role can be allowed to access some documents, but not allowed to access others.

ERP5 extends Zope's concept and introduces the 5A Security Model, which provides a simple and consistent view of business security. 5A stands for the ERP5 roles of Author, Auditor, Assignor, Assignee and Associate. Each of these roles have a specific purpose:

Author can create new documents. He doesn't necessarily need to be able to view documents (he would probably be able to view his own documents, thanks to Zope's role of Owner).

Auditor is allowed to view documents.

Assignor can view, modify, delete and create documents, as well as assign other users to specified documents.

Assignee can work only on some documents from the module. For example, he can access only documents related to a specified region.

Associate can work on documents under certain conditions (e.g., he can view Organisations only if they are related to the Invoice documents he is responsible for).

Roles are described in: http://wiki.erp5.org/HowToDesignSecurity

How can one become a user?

edit

When a user tries to log into the ERP5 site, the security machinery tries to find a Person (in a person_module) that has the value of the reference property (called "user name" on the Details tab on Person form) set to the login name of a user. If such a Person exists, has 'Internal' role and is Validated (see chapter about workflows), and the password given by the user matches the password of this Person, then the user is allowed to log in.

(Important — Starting from revision r15915, the Person doesn't need to have an 'Internal' role any longer. Instead, the Person must have at least one open Assignment.)

So to let somebody log into the ERP5 site, you must:

  • define a Person;
  • set reference property to user login name;
  • set password;
  • set Person's role to Internal (not needed in revisions later than r15915);
  • validate a person;
  • open an Assignment for that Person (from revision r15915).

Security machinery

edit
The concept
edit

Security is divided into two parts – user side security and object side security.

User side security defines security settings user has after successful logging in.

Object side security defines conditions that should be met by the user to obtain role in context of the given object.

In ERP5 almost everything is an object – modules, preferences, categories and so on. Every time the user tries to access an object, ERP5 does the following:

  • checks if the user security settings and object security settings allow to grant user a role or roles in context of this object;
  • finds permissions granted for those role(s);
  • allows or disallows access to the object.
How does it work?
edit

When a user logs to the ERP5, the security categories are calculated for him. Those security categories are analogous to a set of keys (by "key" I mean a small metal object, not the key in the sense used in cryptography!). Back to the programming, categories are just a list of strings, for example: ['MA_CS','MA_CS_WA']

Each ERP5 object has also its security categories. Those categories can be considered as keyholes. Each category has the role associated to it. For instance: [('HR','Assignor'),('SD','Assignee'),('MA_CS_WA','Auditor')]

When a user has a key that matches a given keyhole, he gets the role "hidden" behind the keyhole. In the previous examples the user gets the Auditor role for the object, as he has (among others) MA_CS_WA security category, and the object has Auditor role associated with it.

 

As each object can have different security categories, user can obtain different roles for different objects. If another object has the following category: [('HR','Assignor'),('SD','Assignee'),('MA_CS_LD','Auditor')] the user from the first example will not get any role and thus will not be able to access the object.

 

The main question now is - how are security categories calculated? Well, it depends if we are talking about user side security settings or object side security settings.

User side security settings

edit

User side security is based on portal categories (see the explanation of the category tool if you are not familiar with it). As we mentioned earlier (see " How can one become a user?" above), each logged user has the associated Person object, defined in person module (this is not true for special users like Zope, so the ERP5 security machinery will not work for them!). This Person belongs to some categories (directly off by acquisition from contained Careers or Assignments), like function, group or site.

If you take a look at the portal categories, you'll find that each category value could have a codification defined, that is, a short string identifying that value. Let's assume that category function can have (among others) value of "Manager", which has "MA" codification. Category group can have value of "Customer Service", which has "CS" codification.

If the user Person's function is "Manager" and group is "Customer Service", the security category for this user could be "MA" (codification of "Manager") and "CS" (codification of "Customer Service"), joined with an underscore: MA_CS. For another user, which has function of "Clerk" ("CL" codification) and group "Customer Service", the security category would be CL_CS.

The key question is: what categories are used for security calculations and how does it work?

The insides
edit

So let's take the screwdriver, remove the front panel of the ERP5 security machinery, and have a look at the insides.

The central point in calculating user security settings is the Python script called: ERP5Type_getSecurityCategoryMapping, which defines which scripts and which base categories would be used for security calculations. This script should return a tuple:

((e1),(e2),...,(en))

Each element of this tuple is also a tuple: (script_name, cat_list) where script is the name of the script that will be used to calculate security category, while cat_list a list of base categories that will be passed to this script as one of its parameters (rest of them will be discussed later). So if ERP5Type_getSecurityCategoryMapping returns: (('script1', ['function']),('script2',['function','group'])) then first script1 would be called with ['function'] then script2 would be called with ['function','group'].

What do those scripts do? They examine user's Person object, get values for specified categories and return them as a list of dicts. In previous example, script1 should get the function category value id, while script2 should get the function and group categories value ids. So for Person being Manager in Customer Service, script1 would probably return: [{'function':'manager'}] while script2 would return: [{'function':'manager'},{'group','customer_service'}].

One thing should be explained – why the scripts return list of dicts instead of a single dict? This is because some categories could be defined in objects contained in Person object, e.g. Assignments, and user could have several open Assignments, which results in multiple values for the given category.

What do those scripts look like? They should take the following arguments:

  • base_category_list: this was discussed earlier.
  • user_name: name (login) of a logged user,
  • object: generally not used here;
  • portal_type: the portal type of the object (not used here either).

ERP5Type_getSecurityCategoryMapping should be provided by the developer and be placed somewhere in portal skins. If there is no such script, the system behaves as if the script exists and returns the following tuple:

(('ERP5Type_getSecurityCategoryFromAssignment',
     self.getPortalAssignmentBaseCategoryList() ),)

which results in calling ERP5Type_getSecurityCategoryFromAssignment with a base_category_list parameter got from call to getPortalAssignmentBaseCategoryList(), which currently is: ('function', 'group', 'site')

The ERP5Type_getSecurityCategoryFromAssignment calculates security mappings from all open Assignments of the logged users Person object.

An (almost) real case example

edit

Let's imagine there is an energy selling company that is spanned across multiple regions. The company has Customer Service department. Regional Managers of Customer Service should be able to access connection documents of end customers from their respective regions, but shouldn't be able to access documents of end customers from other regions. The region is stored as site category.

Assume that user jacek is a Manager. He has two open assignments – for the Warsaw and the Pruszkow regions. The first of these assignments has the following settings:

function: manager
group: customer_service
site: warsaw

while the second has the following settings:

function: manager
group: customer_service
site: pruszkow

Note: Having a manager with two assignments for two different regions is probably not very common for such companies. However, I want to show that even such uncommon needs could be easily fulfilled in ERP5.

The ERP5Type_getSecurityCategoryFromAssignment will return the following list of dicts for user jacek:

[{'function': 'manager', 'group': 'customer_service', 'site': 'warsaw'},
 {'function': 'manager', 'group': 'customer_service', 'site': 'pruszkow'}]

The user bartek is a manager in the Siedlce region. He has the following assignment:

function: manager
group: customer_service
site: siedlce

so the script returns for him:

[{'function': 'manager', 'group': 'customer_service', 'site': 'siedlce'},]

But what about the string representation of the security categories I've mentioned at the beginning of this chapter? For that, a codification is used. Let's imagine that: function/manager has the codification MA

group/production has the codification CS

site/warsaw has the codification WA

site/pruszkow has the codification PRS

and

site/siedlce has the codification SL

After obtaining list of dict, ERP5 security machinery for each dict:

  • sorts keys alphabetically;
  • replaces category value id by its codification;
  • joins all codifications in one string, separated by underscores;

So for user jacek and first dict, the codification would be: MA_CS_WA while for the second: MA_CS_PRS

And jacek finally gets the following mappings: ['MA_CS_WA','MA_CS_PRS']

User bartek gets : ['MA_CS_SL']

Note: If the codification is not defined for a given category, the category's id is taken. However, it is good practice to define codifications for categories used in security.

These security settings would be then used by ERP5 to control access to objects. It will be explained in next chapter.

Object side security settings

edit

Object security settings (roles settings) are defined on Portal Type basis (if you are not familiar with Portal Types, read here). Those setting are "keyholes" that allows the keys owners to get access to specified roles.

Roles may be calculated either statically or dynamically. Unfortunately, there is no apparent difference between static and dynamic roles calculation, as both are defined in the same form. We well discuss that difference later.

To define security settings for the portal type, you should navigate to its management form and click the "Roles" tab.

<picture pending – role definition form>

In the Name field you can put a name of the setting. (it is NOT the role name). In practice, a short description is often put here. This is informational only.

In the Role field you can set a name of the role that would be granted to the user whose "key" matches this "keyhole" (that is, the user that has proper security category).

In the Description field you can set a more detailed description of the setting. This is informational only.

In the Condition field you can set a condition that should be fulfilled in order to apply the settings (it could be a Python script call, for instance).

In the Base Category field you can set space-separated list of names of base categories that would be used for role calculation.

In the Base Category Script field you can set the name of the Python script that would be called if dynamic role calculation method is used. If the static role calculation is used, the script is not called and the field value has no meaning.

In Category text area you can set a list of category values ids that would be used for role calculation. Each category value id should be put ion a separate line.

Static vs. dynamic
edit

If, for each base category name defined in Base Category field, there is a matching definition in Category text area, the calculation is static, that is, the Base Category Script is not called. If there are base category names for whom there are no matching definition in Category text area, the calculation is dynamic, that is, the script would be called and its return value would be used for the missing definitions.

How does it work?
edit

Static calculation is really simple – a mapping is created based on category values. If Base Category field contains function group and Category text area contains:

function/sale_manager
group/customer_service

then calculated mapping would be MA_CS, providing that codifications are the same as in previous examples. Any user that has MA_CS security category would be granted role for the object.

However, dynamic calculation is more difficult. If there are more entries in the Base Category field than category values in Category text area, then the script defined in Base Category Script field would be called for the missing categories.

The script gets following parameters:

  • base_category_list: a list of categories defined in Base Category field.
  • user_name: name (login) of a logged user,
  • object: object for which the role setting is calculated,
  • portal_type: the portal type of the object.

Script should return a list of dicts, just like scripts for user-side security calculation do (in fact, in some situations those can be the same scripts). Each dict contains names of categories from base_category_list and their respective value ids.

What should those scripts do? It's up to the developer.

Back to the (almost) real case example

edit

In our example we wanted to restrict access to customer connection parameters only to managers in charge of the specified region (i.e. region in which the customer lives). Connection parameters are stored in objects of portal type Connection Parameters. This object is linked to the Person object (representing the end customer) by the source relation. The Person contains Address object, which in turn has city property.

Let's navigate to Connection Parameters management form, select Role tab and fill the form:

  • In the Name field put short description, e.g. Customer Service Management People.
  • In the Role field a name of the role to be granted to the user, e.g. Assignor;
  • In the Description field put description of the setting, e.g. Customer Management People should have Assignor access to all Connection Parameters for customers living in their region.
  • Leave the Condition field empty.
  • In the Base Category field put space-separated list of names of base categories: "function group site".
  • In the Base Category Script field put the name of the Python script. Let's name the script ConnectionParameters_getSecurityCategoryFromCustomerAddress, to follow ERP5 naming convention.
  • In Category text area put a list of category values ids:
function/manager
group/customer_service

(You should put each category value id in separate line) It can be easily seen, that we have three base categories defined, but only two category value ids. So for the missing category (site) the ConnectionParameters_getSecurityCategoryFromCustomerAddress script would be used.

The script should examine the Connection Parameters object, get linked Person object, get its default address city, calculate the region from the city name (it can have city-to-region mapping defined in categories; or call external GIS system; or use crystal ball), then return the appropriate dictionary list.

This script could be similar to the following one:

if portal_type != 'Connection Parameters':
 raise RuntimeError, 'Error: Script called for invalid portal type'

customer = object.getSourceValue()
city = customer.getDefaultAddressCity()
region = context.ERP5Site_GetRegionFromCity()  #guess the region somehow
category_dict = {}

for base_category in base_category_list:
  if base_category == 'site':
    category_dict[base_category]=region

For customer living in city in Pruszkow region the script should then return: [{'site':'pruszkow'}]

The rest of the mapping would be calculated statically – function/manager maps to MA, group/customer_service maps to CS, and as site/pruszkow maps to PRS, the final mapping would be MA_CS_PRS. Any user that have such security category (that is, a Manager of Customer Service in charge of Pruszków region) would be granted Assignor role for the document.

And what about documents related to more than one customer, possibly living in different regions? No problem, this just needs a small modification. The script should examine the list of Persons related to the document and return a list of dicts – one dict for one customer. So if the document is related to two customers – one in Pruszkow region and one in Jeziorna region, the return value would be as follows: [{'site':'pruszkow'},{'site':'jeziorna'}] and finally the document gets following mappings 'MA_CS_PRS','MA_CS_JEZ'


Extending the defaults
edit

Let's assume there is a class of documents (say connection standards, portal type Connection Standard) that should be available to all managers, regardless of the region they are responsible for. How to achieve this? The first idea would be to write a Base Category Script for Connection Standard portal type so that it returns all possible mappings for all regions: MA_CS_PRS, MA_CS_WA, MA_CS_SL,... and so on. Well, if the company has twenty regions, there would be twenty different mappings. Quite a lot, huh? Worse, if the company grows and new regions appears, security settings for all documents would have to be redefined.

Better idea is to define mapping without the region code, that is MA_CS. Documents with this mapping would be accessible for all Customer Service Managers... or not?

Well, not exactly, as no manager would got MA_CS security category! So there is a need to modify the default ERP5 security machinery a little bit.

To do this, you must place the ERP5Type_getSecurityCategoryMapping script somewhere in skins. The script should look as follows:

return
(('ERP5Type_getSecurityCategoryFromAssignment', ['function','group','site']),
 ('ERP5Type_getSecurityCategoryFromAssignment', ['function','group']))

The first line looks similar to the system default (I replaced getPortalAssignmentBaseCategoryList() with explicit list of base categories), the second is new. Now, when user logs in, the ERP5Type_getSecurityCategoryFromAssignment would be called twice – once with ['function','group','site'], and once with ['function','group'] only. This second call produces MA_CS for any 'Customer Service Manager regardless of the region.

For the Connection Standard portal type we should define the following role definition (only relevant fields shown)

  • In the Role field: Assignor;
  • In the Base Category field: function group.
  • In Category text area:
function/manager
group/customer_service

This would produce mapping MA_CS, so all users with MA_CS security category would be given Assignor role.

Roles are NOT permissions

edit

Well, at this point you may think that all that should be done has been done, but if you do, you are wrong. You have not defined the roles-to-permissions mapping yet.

What-to-what mapping? - you may ask. Well, despite their names, roles don't tell anything about privileges associated with them. There is nothing magical in the words Auditor or Assignor and ERP5 does not know by itself what those roles are allowed to do.

(This is true even for a Zope role of Manager! In many systems a Manager (or whatever he is called) is a mighty sheriff which can do everything. Not in ERP5, though. It is you who decide what permissions each role have. Even a Manager would have only as much power as you would give to him. You can define Managers without any permissions at all if you like to, thought it is rather a poor idea. However, you can easily grant a Manager insufficient privileges by mistake, and in such case you will find yourself in heavy troubles during development.)

How to define permissions for roles? It can be done either statically or dynamically.

Static permissions definition
edit

This applies for objects that exists during the development time. They are mainly modules. To define permissions for such object, navigate to this object's management page and go to the Security tab. Read Zope documentation for details.

Dynamic permissions definition
edit

This is the moment workflows enter the scene. As you probably know (and if you don't, read the section about workflows), each portal type can be assigned to one or more workflows. Objects (instances) of this portal type can be therefore in some workflow states. Each workflow state defines separate roles-to-permissions mapping. Object entering specific workflow state gets role-to-permission mapping defined for this workflow state.

This allows for a very flexible privileges manipulation. For instance, a newly created Connection Parameters document should be accessible only by its creator, so the draft state defines view and modify permissions for Owner only. After the document is completed, it can be submitted. In this state it can become readable to Assignees and Auditors and readable/writable to Assignors. Then the document goes through the rest of the workflow and finally ends in an archived state, where nobody can change it, but many can read.

To enable workflow-based security, navigate to the workflow management page, click the Permissions tab and add required permissions. At least the following permissions should be managed by workflow: View and Access content information (read), Modify portal content (write) and Add portal content (create new objects).

To define role-to-permission mapping for a workflow state, navigate to the workflow state management page and click the Permissions tab. You will see only permissions managed by this workflow.


Updating security settings

edit

Security settings on objects are not updated automatically. This is developer's responsibility to update security settings after object state changes.

One way to achieve this is a separate interaction workflow, which have interactions defined for setters of properties that takes part in security calculation (in our "Almost real case example" the Person's Address city name is an example of such property – change of the city name can result in security settings change for related Connection Parameters documents, if the new city is in the different region then the old one). The interaction should fire a script that examines all related objects and recalculates security settings for them.

To recalculate security settings on object, call updateLocalRolesOnSecurityGroups() in context of that object. Then, reindex the object (this is necessary, as object permissions are stored in portal catalog -> see the explanation of catalog tool).

Debugging security

edit

<to be written>