The schema is the core piece of a CubicWeb instance as it defines and handles the data model. It is based on entity types that are either already defined in Yams and the CubicWeb standard library; or more specific types defined in cubes. The schema for a cube is defined in a schema python module or package.
The core idea of the yams schema is not far from the classical Entity-relationship model. But while an E/R model (or logical model) traditionally has to be manually translated to a lower-level data description language (such as the SQL create table sublanguage), also often described as the physical model, no such step is required with Yams and CubicWeb.
This is because in addition to high-level, logical Yams models, one uses the RQL data manipulation language to query, insert, update and delete data. RQL abstracts as much of the underlying SQL database as a Yams schema abstracts from the physical layout. The vagaries of SQL are avoided.
As a bonus point, such abstraction make it quite comfortable to build or use different backends to which RQL queries apply.
So, as in the E/R formalism, the building blocks are entities (Entity type), relationships (Relation type, Relation definition) and attributes (handled like relation with Yams).
Let us detail a little the divergences between E/R and Yams:
Also Yams supports the notions of:
Finally Yams has a few concepts of its own:
Note
The Yams schema is available at run time through the .schema attribute of the vregistry. It’s an instance of cubicweb.schema.Schema, which extends yams.schema.Schema.
An entity type is an instance of yams.schema.EntitySchema. Each entity type has a set of attributes and relations, and some permissions which define who can add, read, update or delete entities of this type.
The following built-in types are available: String, Int, Float, Decimal, Boolean, Date, Datetime, Time, Interval, Byte and Password. They can only be used as attributes of an other entity type.
There is also a RichString kindof type:
- class yams.buildobjs.RichString(default_format='text/plain', format_constraints=None, **kwargs)¶
RichString is a convenience attribute type for attribute containing text in a format that should be specified in another attribute.
The following declaration:
class Card(EntityType): content = RichString(fulltextindexed=True, default_format='text/rest')is equivalent to:
class Card(EntityType): content_format = String(internationalizable=True, default='text/rest', constraints=[format_constraint]) content = String(fulltextindexed=True)
You can find more base entity types in Pre-defined entities in the library.
A relation type is an instance of yams.schema.RelationSchema. A relation type is simply a semantic definition of a kind of relationship that may occur in an application.
It may be referenced by zero, one or more relation definitions.
It is important to choose a good name, at least to avoid conflicts with some semantically different relation defined in other cubes (since there’s only a shared name space for these names).
A relation type holds the following properties (which are hence shared between all relation definitions of that type):
A relation definition is an instance of yams.schema.RelationDefinition. It is a complete triplet “<subject entity type> <relation type> <object entity type>”.
When creating a new instance of that class, the corresponding RelationType instance is created on the fly if necessary.
The available properties for relation definitions are enumerated here. There are several kind of properties, as some relation definitions are actually attribute definitions, and other are not.
Some properties may be completely optional, other may have a default value.
Common properties for attributes and relations:
description: an unicode string describing an attribute or a relation. By default this string will be used in the editing form of the entity, which means that it is supposed to help the end-user and should be flagged by the function _ to be properly internationalized.
constraints: a list of conditions/constraints that the relation has to satisfy (c.f. Constraints)
cardinality: a two character string specifying the cardinality of the relation. The first character defines the cardinality of the relation on the subject, and the second on the object. When a relation can have multiple subjects or objects, the cardinality applies to all, not on a one-to-one basis (so it must be consistent...). Default value is ‘**’. The possible values are inspired from regular expression syntax:
- 1: 1..1
- ?: 0..1
- +: 1..n
- *: 0..n
Attributes properties:
Properties for String attributes:
Relation properties:
By default, the available constraint types are:
from yams.constraints import BoundConstraint, TODAY
BoundConstraint('<=', TODAY())
class Node(EntityType):
latitude = Float(constraints=[IntervalBoundConstraint(-90, +90)])
RQL based constraints may take three arguments. The first one is the WHERE clause of a RQL query used by the constraint. The second argument mainvars is the Any clause of the query. By default this include S reserved for the subject of the relation and O for the object. Additional variables could be specified using mainvars. The argument expects a single string with all variable’s name separated by spaces. The last one, msg, is the error message displayed when the constraint fails. As RQLVocabularyConstraint never fails the third argument is not available.
# Check that in the same Workflow each state's name is unique. Using
# UniqueConstraint (or unique=True) here would prevent states in different
# workflows to have the same name.
# With: State S, Workflow W, String N ; S state_of W, S name N
RQLUniqueConstraint('S name N, S state_of WF, Y state_of WF, Y name N',
mainvars='Y',
msg=_('workflow already has a state of that name'))
The security model of CubicWeb is based on Access Control List. The main principles are:
For CubicWeb in particular:
Setting permissions is done with the attribute __permissions__ of entities and relation definition. The value of this attribute is a dictionary where the keys are the access types (action), and the values are the authorized groups or expressions.
For an entity type, the possible actions are read, add, update and delete.
For a relation, the possible actions are read, add, and delete.
For an attribute, the possible actions are read, and update.
For each access type, a tuple indicates the name of the authorized groups and/or one or multiple RQL expressions to satisfy to grant access. The access is provided if the user is in one of the listed groups or if one of the RQL condition is satisfied.
The default permissions for EntityType are:
__permissions__ = {
'read': ('managers', 'users', 'guests',),
'update': ('managers', 'owners',),
'delete': ('managers', 'owners'),
'add': ('managers', 'users',)
}
The default permissions for relations are:
__permissions__ = {'read': ('managers', 'users', 'guests',),
'delete': ('managers', 'users'),
'add': ('managers', 'users',)}
The default permissions for attributes are:
__permissions__ = {'read': ('managers', 'users', 'guests',),
'update': ('managers', ERQLExpression('U has_update_permission X')),}
It is also possible to use specific groups if they are defined in the precreate script of the cube (migration/precreate.py). Defining groups in postcreate script or later makes them unavailable for security purposes (in this case, an sync_schema_props_perms command has to be issued in a CubicWeb shell).
It is possible to define RQL expression to provide update permission (add, delete and update) on entity type / relation definitions. An rql expression is a piece of query (corresponds to the WHERE statement of an RQL query), and the expression will be considered as satisfied if it returns some results. They can not be used in read permission.
To use RQL expression in entity type permission:
For RQL expressions on a relation type, the principles are the same except for the following:
To define security for attributes of an entity (non-final relation), you have to use the class ERQLExpression in which X represents the entity the attribute belongs to.
It is possible to use in those expression a special relation has_<ACTION>_permission where the subject is the user (eg ‘U’) and the object is any variable representing an entity (usually ‘X’ in ERQLExpression, ‘S’ or ‘O’ in RRQLExpression), meaning that the user needs to have permission to execute the action <ACTION> on the entities represented by this variable. It’s recommanded to use this feature whenever possible since it simplify greatly complex security definition and upgrade.
class my_relation(RelationDefinition):
__permissions__ = {'read': ('managers', 'users'),
'add': ('managers', RRQLExpression('U has_update_permission S')),
'delete': ('managers', RRQLExpression('U has_update_permission S'))
}
In the above example, user will be allowed to add/delete my_relation if he has the update permission on the subject of the relation.
Note
Potentially, the use of an RQL expression to add an entity or a relation can cause problems for the user interface, because if the expression uses the entity or the relation to create, we are not able to verify the permissions before we actually added the entity (please note that this is not a problem for the RQL server at all, because the permissions checks are done after the creation). In such case, the permission check methods (CubicWebEntitySchema.check_perm and has_perm) can indicate that the user is not allowed to create this entity while it would obtain the permission. To compensate this problem, it is usually necessary in such case to use an action that reflects the schema permissions but which check properly the permissions so that it would show up only if possible.
The principles are the same but with the following restrictions:
Write permissions (e.g. ‘add’, ‘update’, ‘delete’) are checked in core hooks.
When a permission is checked slightly vary according to if it’s an entity or relation, and if the relation is an attribute relation or not). It’s important to understand that since according to when a permission is checked, values returned by rql expressions may changes, hence the permission being granted or not.
Here are the current rules:
Last but not least, remember queries issued from hooks and operation are by default ‘unsafe’, eg there are no read or write security checks.
See cubicweb.hooks.security for more details.
An entity type is defined by a Python class which inherits from yams.buildobjs.EntityType. The class definition contains the description of attributes and relations for the defined entity type. The class name corresponds to the entity type name. It is expected to be defined in the module mycube.schema.
Note on schema definition: | |
---|---|
The code in mycube.schema is not meant to be executed. The class EntityType mentioned above is different from the EntitySchema class described in the previous chapter. EntityType is a helper class to make Entity definition easier. Yams will process EntityType classes and create EntitySchema instances from these class definitions. Similar manipulation happen for relations. |
When defining a schema using python files, you may use the following shortcuts:
For example:
class Person(EntityType):
"""A person with the properties and the relations necessary for my
application"""
last_name = String(required=True, fulltextindexed=True)
first_name = String(required=True, fulltextindexed=True)
title = String(vocabulary=('Mr', 'Mrs', 'Miss'))
date_of_birth = Date()
works_for = SubjectRelation('Company', cardinality='?*')
The entity described above defines three attributes of type String, last_name, first_name and title, an attribute of type Date for the date of birth and a relation that connects a Person to another entity of type Company through the semantic works_for.
Naming convention: | |
---|---|
Entity class names must start with an uppercase letter. The common usage is to use CamelCase names. Attribute and relation names must start with a lowercase letter. The common usage is to use underscore_separated_words. Attribute and relation names starting with a single underscore are permitted, to denote a somewhat “protected” or “private” attribute. In any case, identifiers starting with “CW” or “cw” are reserved for internal use by the framework. |
The name of the Python attribute corresponds to the name of the attribute or the relation in CubicWeb application.
An attribute is defined in the schema as follows:
attr_name = attr_type(properties)
where attr_type is one of the type listed above and properties is a list of the attribute needs to satisfy (see Properties for more details).
Note: if you end up with an if in the definition of your entity, this probably means that you need two separate entities that implement the ITree interface and get the result from .children() which ever entity is concerned.
A relation is defined by a Python class heriting RelationType. The name of the class corresponds to the name of the type. The class then contains a description of the properties of this type of relation, and could as well contain a string for the subject and a string for the object. This allows to create new definition of associated relations, (so that the class can have the definition properties from the relation) for example
class locked_by(RelationType):
"""relation on all entities indicating that they are locked"""
inlined = True
cardinality = '?*'
subject = '*'
object = 'CWUser'
If provided, the subject and object attributes denote the subject and object of the various relation definitions related to the relation type. Allowed values for these attributes are:
When a relation is not inlined and not symmetrical, and it does not require specific permissions, it can be defined using a SubjectRelation attribute in the EntityType class. The first argument of SubjectRelation gives the entity type for the object of the relation.
Naming convention: | |
---|---|
Although this way of defining relations uses a Python class, the naming convention defined earlier prevails over the PEP8 conventions used in the framework: relation type class names use underscore_separated_words. | |
Historical note: | |
It has been historically possible to use ObjectRelation which defines a relation in the opposite direction. This feature is deprecated and therefore should not be used in newly written code. | |
Future deprecation note: | |
In an even more remote future, it is quite possible that the SubjectRelation shortcut will become deprecated, in favor of the RelationType declaration which offers some advantages in the context of reusable cubes. |
The entity type CWPermission from the standard library allows to build very complex and dynamic security architectures. The schema of this entity type is as follow:
class CWPermission(EntityType):
"""entity type that may be used to construct some advanced security configuration
"""
name = String(required=True, indexed=True, internationalizable=True, maxsize=100)
require_group = SubjectRelation('CWGroup', cardinality='+*',
description=_('groups to which the permission is granted'))
require_state = SubjectRelation('State',
description=_("entity's state in which the permission is applicable"))
# can be used on any entity
require_permission = ObjectRelation('**', cardinality='*1', composite='subject',
description=_("link a permission to the entity. This "
"permission should be used in the security "
"definition of the entity's type to be useful."))
Example of configuration:
class Version(EntityType):
"""a version is defining the content of a particular project's release"""
__permissions__ = {'read': ('managers', 'users', 'guests',),
'update': ('managers', 'logilab', 'owners',),
'delete': ('managers', ),
'add': ('managers', 'logilab',
ERQLExpression('X version_of PROJ, U in_group G,'
'PROJ require_permission P, P name "add_version",'
'P require_group G'),)}
class version_of(RelationType):
"""link a version to its project. A version is necessarily linked to one and only one project.
"""
__permissions__ = {'read': ('managers', 'users', 'guests',),
'delete': ('managers', ),
'add': ('managers', 'logilab',
RRQLExpression('O require_permission P, P name "add_version",'
'U in_group G, P require_group G'),)
}
inlined = True
This configuration indicates that an entity CWPermission named “add_version” can be associated to a project and provides rights to create new versions on this project to specific groups. It is important to notice that:
Also, it should be clear that to properly handle data migration, an instance’s schema is stored in the database, so the python schema file used to defined it is only read when the instance is created or upgraded.