schema.py
brancholdstable
changeset 4985 02b52bf9f5f8
parent 4759 af2e6c377c71
child 4834 b718626a0e60
equal deleted inserted replaced
4563:c25da7573ebd 4985:02b52bf9f5f8
    13 from logging import getLogger
    13 from logging import getLogger
    14 from warnings import warn
    14 from warnings import warn
    15 
    15 
    16 from logilab.common.decorators import cached, clear_cache, monkeypatch
    16 from logilab.common.decorators import cached, clear_cache, monkeypatch
    17 from logilab.common.logging_ext import set_log_methods
    17 from logilab.common.logging_ext import set_log_methods
    18 from logilab.common.deprecation import deprecated
    18 from logilab.common.deprecation import deprecated, class_moved
    19 from logilab.common.graph import get_cycles
    19 from logilab.common.graph import get_cycles
    20 from logilab.common.compat import any
    20 from logilab.common.compat import any
    21 
    21 
    22 from yams import BadSchemaDefinition, buildobjs as ybo
    22 from yams import BadSchemaDefinition, buildobjs as ybo
    23 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema
    23 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
    24 from yams.constraints import (BaseConstraint, StaticVocabularyConstraint,
    24      RelationDefinitionSchema, PermissionMixIn
    25                               FormatConstraint)
    25 from yams.constraints import BaseConstraint, FormatConstraint
    26 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
    26 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
    27                          obsolete as yobsolete, cleanup_sys_modules)
    27                          obsolete as yobsolete, cleanup_sys_modules)
    28 
    28 
    29 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    29 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    30 
    30 
    31 import cubicweb
    31 import cubicweb
    32 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
    32 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
    33 
       
    34 # XXX <3.2 bw compat
       
    35 from yams import schema
       
    36 schema.use_py_datetime()
       
    37 nodes.use_py_datetime()
       
    38 
    33 
    39 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
    34 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
    40 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
    35 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
    41 
    36 
    42 #  set of meta-relations available for every entity types
    37 #  set of meta-relations available for every entity types
    52     'CWConstraint', 'CWConstraintType', 'RQLExpression',
    47     'CWConstraint', 'CWConstraintType', 'RQLExpression',
    53     'relation_type', 'from_entity', 'to_entity',
    48     'relation_type', 'from_entity', 'to_entity',
    54     'constrained_by', 'cstrtype',
    49     'constrained_by', 'cstrtype',
    55     ))
    50     ))
    56 
    51 
       
    52 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
       
    53                       'WorkflowTransition', 'BaseTransition',
       
    54                       'SubWorkflowExitPoint'))
       
    55 
       
    56 INTERNAL_TYPES = set(('CWProperty', 'CWPermission', 'CWCache', 'ExternalUri'))
       
    57 
       
    58 
    57 _LOGGER = getLogger('cubicweb.schemaloader')
    59 _LOGGER = getLogger('cubicweb.schemaloader')
    58 
    60 
    59 # schema entities created from serialized schema have an eid rproperty
    61 # schema entities created from serialized schema have an eid rproperty
    60 ybo.ETYPE_PROPERTIES += ('eid',)
    62 ybo.ETYPE_PROPERTIES += ('eid',)
    61 ybo.RTYPE_PROPERTIES += ('eid',)
    63 ybo.RTYPE_PROPERTIES += ('eid',)
    62 ybo.RDEF_PROPERTIES += ('eid',)
    64 ybo.RDEF_PROPERTIES += ('eid',)
    63 
    65 
       
    66 
       
    67 PUB_SYSTEM_ENTITY_PERMS = {
       
    68     'read':   ('managers', 'users', 'guests',),
       
    69     'add':    ('managers',),
       
    70     'delete': ('managers',),
       
    71     'update': ('managers',),
       
    72     }
       
    73 PUB_SYSTEM_REL_PERMS = {
       
    74     'read':   ('managers', 'users', 'guests',),
       
    75     'add':    ('managers',),
       
    76     'delete': ('managers',),
       
    77     }
       
    78 PUB_SYSTEM_ATTR_PERMS = {
       
    79     'read':   ('managers', 'users', 'guests',),
       
    80     'update':    ('managers',),
       
    81     }
       
    82 RO_REL_PERMS = {
       
    83     'read':   ('managers', 'users', 'guests',),
       
    84     'add':    (),
       
    85     'delete': (),
       
    86     }
       
    87 RO_ATTR_PERMS = {
       
    88     'read':   ('managers', 'users', 'guests',),
       
    89     'update': (),
       
    90     }
    64 
    91 
    65 # XXX same algorithm as in reorder_cubes and probably other place,
    92 # XXX same algorithm as in reorder_cubes and probably other place,
    66 # may probably extract a generic function
    93 # may probably extract a generic function
    67 def order_eschemas(eschemas):
    94 def order_eschemas(eschemas):
    68     """return entity schemas ordered such that entity types which specializes an
    95     """return entity schemas ordered such that entity types which specializes an
   115     if context is not None:
   142     if context is not None:
   116         return unicode(req.pgettext(context, key)).lower()
   143         return unicode(req.pgettext(context, key)).lower()
   117     else:
   144     else:
   118         return unicode(req._(key)).lower()
   145         return unicode(req._(key)).lower()
   119 
   146 
   120 __builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name)
   147 __builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name)
   121 
   148 
   122 
   149 
   123 # rql expression utilities function ############################################
   150 # rql expression utilities function ############################################
   124 
   151 
   125 def guess_rrqlexpr_mainvars(expression):
   152 def guess_rrqlexpr_mainvars(expression):
   149     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
   176     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
   150 
   177 
   151 
   178 
   152 # Schema objects definition ###################################################
   179 # Schema objects definition ###################################################
   153 
   180 
   154 def ERSchema_display_name(self, req, form=''):
   181 def ERSchema_display_name(self, req, form='', context=None):
   155     """return a internationalized string for the entity/relation type name in
   182     """return a internationalized string for the entity/relation type name in
   156     a given form
   183     a given form
   157     """
   184     """
   158     return display_name(req, self.type, form)
   185     return display_name(req, self.type, form, context)
   159 ERSchema.display_name = ERSchema_display_name
   186 ERSchema.display_name = ERSchema_display_name
   160 
   187 
   161 @cached
   188 @cached
   162 def ERSchema_get_groups(self, action):
   189 def get_groups(self, action):
   163     """return the groups authorized to perform <action> on entities of
   190     """return the groups authorized to perform <action> on entities of
   164     this type
   191     this type
   165 
   192 
   166     :type action: str
   193     :type action: str
   167     :param action: the name of a permission
   194     :param action: the name of a permission
   170     :return: names of the groups with the given permission
   197     :return: names of the groups with the given permission
   171     """
   198     """
   172     assert action in self.ACTIONS, action
   199     assert action in self.ACTIONS, action
   173     #assert action in self._groups, '%s %s' % (self, action)
   200     #assert action in self._groups, '%s %s' % (self, action)
   174     try:
   201     try:
   175         return frozenset(g for g in self._groups[action] if isinstance(g, basestring))
   202         return frozenset(g for g in self.permissions[action] if isinstance(g, basestring))
   176     except KeyError:
   203     except KeyError:
   177         return ()
   204         return ()
   178 ERSchema.get_groups = ERSchema_get_groups
   205 PermissionMixIn.get_groups = get_groups
   179 
       
   180 def ERSchema_set_groups(self, action, groups):
       
   181     """set the groups allowed to perform <action> on entities of this type. Don't
       
   182     change rql expressions for the same action.
       
   183 
       
   184     :type action: str
       
   185     :param action: the name of a permission
       
   186 
       
   187     :type groups: list or tuple
       
   188     :param groups: names of the groups granted to do the given action
       
   189     """
       
   190     assert action in self.ACTIONS, action
       
   191     clear_cache(self, 'ERSchema_get_groups')
       
   192     self._groups[action] = tuple(groups) + self.get_rqlexprs(action)
       
   193 ERSchema.set_groups = ERSchema_set_groups
       
   194 
   206 
   195 @cached
   207 @cached
   196 def ERSchema_get_rqlexprs(self, action):
   208 def get_rqlexprs(self, action):
   197     """return the rql expressions representing queries to check the user is allowed
   209     """return the rql expressions representing queries to check the user is allowed
   198     to perform <action> on entities of this type
   210     to perform <action> on entities of this type
   199 
   211 
   200     :type action: str
   212     :type action: str
   201     :param action: the name of a permission
   213     :param action: the name of a permission
   204     :return: the rql expressions with the given permission
   216     :return: the rql expressions with the given permission
   205     """
   217     """
   206     assert action in self.ACTIONS, action
   218     assert action in self.ACTIONS, action
   207     #assert action in self._rqlexprs, '%s %s' % (self, action)
   219     #assert action in self._rqlexprs, '%s %s' % (self, action)
   208     try:
   220     try:
   209         return tuple(g for g in self._groups[action] if not isinstance(g, basestring))
   221         return tuple(g for g in self.permissions[action] if not isinstance(g, basestring))
   210     except KeyError:
   222     except KeyError:
   211         return ()
   223         return ()
   212 ERSchema.get_rqlexprs = ERSchema_get_rqlexprs
   224 PermissionMixIn.get_rqlexprs = get_rqlexprs
   213 
   225 
   214 def ERSchema_set_rqlexprs(self, action, rqlexprs):
   226 orig_set_action_permissions = PermissionMixIn.set_action_permissions
   215     """set the rql expression allowing to perform <action> on entities of this type. Don't
   227 def set_action_permissions(self, action, permissions):
   216     change groups for the same action.
   228     """set the groups and rql expressions allowing to perform <action> on
       
   229     entities of this type
   217 
   230 
   218     :type action: str
   231     :type action: str
   219     :param action: the name of a permission
   232     :param action: the name of a permission
   220 
   233 
   221     :type rqlexprs: list or tuple
       
   222     :param rqlexprs: the rql expressions allowing the given action
       
   223     """
       
   224     assert action in self.ACTIONS, action
       
   225     clear_cache(self, 'ERSchema_get_rqlexprs')
       
   226     self._groups[action] = tuple(self.get_groups(action)) + tuple(rqlexprs)
       
   227 ERSchema.set_rqlexprs = ERSchema_set_rqlexprs
       
   228 
       
   229 def ERSchema_set_permissions(self, action, permissions):
       
   230     """set the groups and rql expressions allowing to perform <action> on
       
   231     entities of this type
       
   232 
       
   233     :type action: str
       
   234     :param action: the name of a permission
       
   235 
       
   236     :type permissions: tuple
   234     :type permissions: tuple
   237     :param permissions: the groups and rql expressions allowing the given action
   235     :param permissions: the groups and rql expressions allowing the given action
   238     """
   236     """
   239     assert action in self.ACTIONS, action
   237     orig_set_action_permissions(self, action, tuple(permissions))
   240     clear_cache(self, 'ERSchema_get_rqlexprs')
   238     clear_cache(self, 'get_rqlexprs')
   241     clear_cache(self, 'ERSchema_get_groups')
   239     clear_cache(self, 'get_groups')
   242     self._groups[action] = tuple(permissions)
   240 PermissionMixIn.set_action_permissions = set_action_permissions
   243 ERSchema.set_permissions = ERSchema_set_permissions
   241 
   244 
   242 def has_local_role(self, action):
   245 def ERSchema_has_perm(self, session, action, *args, **kwargs):
       
   246     """return true if the action is granted globaly or localy"""
       
   247     try:
       
   248         self.check_perm(session, action, *args, **kwargs)
       
   249         return True
       
   250     except Unauthorized:
       
   251         return False
       
   252 ERSchema.has_perm = ERSchema_has_perm
       
   253 
       
   254 def ERSchema_has_local_role(self, action):
       
   255     """return true if the action *may* be granted localy (eg either rql
   243     """return true if the action *may* be granted localy (eg either rql
   256     expressions or the owners group are used in security definition)
   244     expressions or the owners group are used in security definition)
   257 
   245 
   258     XXX this method is only there since we don't know well how to deal with
   246     XXX this method is only there since we don't know well how to deal with
   259     'add' action checking. Also find a better name would be nice.
   247     'add' action checking. Also find a better name would be nice.
   260     """
   248     """
   261     assert action in self.ACTIONS, action
   249     assert action in self.ACTIONS, action
   262     if self.get_rqlexprs(action):
   250     if self.get_rqlexprs(action):
   263         return True
   251         return True
   264     if action in ('update', 'delete'):
   252     if action in ('update', 'delete'):
   265         return self.has_group(action, 'owners')
   253         return 'owners' in self.get_groups(action)
   266     return False
   254     return False
   267 ERSchema.has_local_role = ERSchema_has_local_role
   255 PermissionMixIn.has_local_role = has_local_role
       
   256 
       
   257 def may_have_permission(self, action, req):
       
   258     if action != 'read' and not (self.has_local_role('read') or
       
   259                                  self.has_perm(req, 'read')):
       
   260         return False
       
   261     return self.has_local_role(action) or self.has_perm(req, action)
       
   262 PermissionMixIn.may_have_permission = may_have_permission
       
   263 
       
   264 def has_perm(self, session, action, **kwargs):
       
   265     """return true if the action is granted globaly or localy"""
       
   266     try:
       
   267         self.check_perm(session, action, **kwargs)
       
   268         return True
       
   269     except Unauthorized:
       
   270         return False
       
   271 PermissionMixIn.has_perm = has_perm
       
   272 
       
   273 def check_perm(self, session, action, **kwargs):
       
   274     # NB: session may be a server session or a request object check user is
       
   275     # in an allowed group, if so that's enough internal sessions should
       
   276     # always stop there
       
   277     groups = self.get_groups(action)
       
   278     if session.user.matching_groups(groups):
       
   279         return
       
   280     # if 'owners' in allowed groups, check if the user actually owns this
       
   281     # object, if so that's enough
       
   282     if 'owners' in groups and (
       
   283           kwargs.get('creating')
       
   284           or ('eid' in kwargs and session.user.owns(kwargs['eid']))):
       
   285         return
       
   286     # else if there is some rql expressions, check them
       
   287     if any(rqlexpr.check(session, **kwargs)
       
   288            for rqlexpr in self.get_rqlexprs(action)):
       
   289         return
       
   290     raise Unauthorized(action, str(self))
       
   291 PermissionMixIn.check_perm = check_perm
       
   292 
       
   293 
       
   294 RelationDefinitionSchema._RPROPERTIES['eid'] = None
       
   295 
       
   296 def rql_expression(self, expression, mainvars=None, eid=None):
       
   297     """rql expression factory"""
       
   298     if self.rtype.final:
       
   299         return ERQLExpression(expression, mainvars, eid)
       
   300     return RRQLExpression(expression, mainvars, eid)
       
   301 RelationDefinitionSchema.rql_expression = rql_expression
       
   302 
       
   303 orig_check_permission_definitions = RelationDefinitionSchema.check_permission_definitions
       
   304 def check_permission_definitions(self):
       
   305     orig_check_permission_definitions(self)
       
   306     schema = self.subject.schema
       
   307     for action, groups in self.permissions.iteritems():
       
   308         for group_or_rqlexpr in groups:
       
   309             if action == 'read' and \
       
   310                    isinstance(group_or_rqlexpr, RQLExpression):
       
   311                 msg = "can't use rql expression for read permission of %s"
       
   312                 raise BadSchemaDefinition(msg % self)
       
   313             if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   314                 msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   315                 raise BadSchemaDefinition(msg % self)
       
   316             if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
       
   317                 msg = "can't use ERQLExpression on %s, use a RRQLExpression"
       
   318                 raise BadSchemaDefinition(msg % self)
       
   319 RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
   268 
   320 
   269 
   321 
   270 class CubicWebEntitySchema(EntitySchema):
   322 class CubicWebEntitySchema(EntitySchema):
   271     """a entity has a type, a set of subject and or object relations
   323     """a entity has a type, a set of subject and or object relations
   272     the entity schema defines the possible relations for a given type and some
   324     the entity schema defines the possible relations for a given type and some
   275     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
   327     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
   276         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
   328         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
   277         if eid is None and edef is not None:
   329         if eid is None and edef is not None:
   278             eid = getattr(edef, 'eid', None)
   330             eid = getattr(edef, 'eid', None)
   279         self.eid = eid
   331         self.eid = eid
   280         # take care: no _groups attribute when deep-copying
   332 
   281         if getattr(self, '_groups', None):
   333     def check_permission_definitions(self):
   282             for groups in self._groups.itervalues():
   334         super(CubicWebEntitySchema, self).check_permission_definitions()
   283                 for group_or_rqlexpr in groups:
   335         for groups in self.permissions.itervalues():
   284                     if isinstance(group_or_rqlexpr, RRQLExpression):
   336             for group_or_rqlexpr in groups:
   285                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
   337                 if isinstance(group_or_rqlexpr, RRQLExpression):
   286                         raise BadSchemaDefinition(msg % self.type)
   338                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   339                     raise BadSchemaDefinition(msg % self.type)
   287 
   340 
   288     def attribute_definitions(self):
   341     def attribute_definitions(self):
   289         """return an iterator on attribute definitions
   342         """return an iterator on attribute definitions
   290 
   343 
   291         attribute relations are a subset of subject relations where the
   344         attribute relations are a subset of subject relations where the
   324         need_has_text = None
   377         need_has_text = None
   325         for rschema in self.subject_relations():
   378         for rschema in self.subject_relations():
   326             if rschema.final:
   379             if rschema.final:
   327                 if rschema == 'has_text':
   380                 if rschema == 'has_text':
   328                     has_has_text = True
   381                     has_has_text = True
   329                 elif self.rproperty(rschema, 'fulltextindexed'):
   382                 elif self.rdef(rschema).get('fulltextindexed'):
   330                     may_need_has_text = True
   383                     may_need_has_text = True
   331             elif rschema.fulltext_container:
   384             elif rschema.fulltext_container:
   332                 if rschema.fulltext_container == 'subject':
   385                 if rschema.fulltext_container == 'subject':
   333                     may_need_has_text = True
   386                     may_need_has_text = True
   334                 else:
   387                 else:
   340                 else:
   393                 else:
   341                     need_has_text = False
   394                     need_has_text = False
   342         if need_has_text is None:
   395         if need_has_text is None:
   343             need_has_text = may_need_has_text
   396             need_has_text = may_need_has_text
   344         if need_has_text and not has_has_text and not deletion:
   397         if need_has_text and not has_has_text and not deletion:
   345             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String')
   398             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String',
       
   399                                           __permissions__=RO_ATTR_PERMS)
   346             self.schema.add_relation_def(rdef)
   400             self.schema.add_relation_def(rdef)
   347         elif not need_has_text and has_has_text:
   401         elif not need_has_text and has_has_text:
   348             self.schema.del_relation_def(self.type, 'has_text', 'String')
   402             self.schema.del_relation_def(self.type, 'has_text', 'String')
   349 
   403 
   350     def schema_entity(self):
   404     def schema_entity(self):
   351         """return True if this entity type is used to build the schema"""
   405         """return True if this entity type is used to build the schema"""
   352         return self.type in SCHEMA_TYPES
   406         return self.type in SCHEMA_TYPES
   353 
   407 
   354     def check_perm(self, session, action, eid=None):
       
   355         # NB: session may be a server session or a request object
       
   356         user = session.user
       
   357         # check user is in an allowed group, if so that's enough
       
   358         # internal sessions should always stop there
       
   359         if user.matching_groups(self.get_groups(action)):
       
   360             return
       
   361         # if 'owners' in allowed groups, check if the user actually owns this
       
   362         # object, if so that's enough
       
   363         if eid is not None and 'owners' in self.get_groups(action) and \
       
   364                user.owns(eid):
       
   365             return
       
   366         # else if there is some rql expressions, check them
       
   367         if any(rqlexpr.check(session, eid)
       
   368                for rqlexpr in self.get_rqlexprs(action)):
       
   369             return
       
   370         raise Unauthorized(action, str(self))
       
   371 
       
   372     def rql_expression(self, expression, mainvars=None, eid=None):
   408     def rql_expression(self, expression, mainvars=None, eid=None):
   373         """rql expression factory"""
   409         """rql expression factory"""
   374         return ERQLExpression(expression, mainvars, eid)
   410         return ERQLExpression(expression, mainvars, eid)
   375 
   411 
   376 
   412 
   377 class CubicWebRelationSchema(RelationSchema):
   413 class CubicWebRelationSchema(RelationSchema):
   378     RelationSchema._RPROPERTIES['eid'] = None
       
   379     _perms_checked = False
       
   380 
   414 
   381     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
   415     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
   382         if rdef is not None:
   416         if rdef is not None:
   383             # if this relation is inlined
   417             # if this relation is inlined
   384             self.inlined = rdef.inlined
   418             self.inlined = rdef.inlined
   389 
   423 
   390     @property
   424     @property
   391     def meta(self):
   425     def meta(self):
   392         return self.type in META_RTYPES
   426         return self.type in META_RTYPES
   393 
   427 
   394     def update(self, subjschema, objschema, rdef):
       
   395         super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
       
   396         if not self._perms_checked and self._groups:
       
   397             for action, groups in self._groups.iteritems():
       
   398                 for group_or_rqlexpr in groups:
       
   399                     if action == 'read' and \
       
   400                            isinstance(group_or_rqlexpr, RQLExpression):
       
   401                         msg = "can't use rql expression for read permission of "\
       
   402                               "a relation type (%s)"
       
   403                         raise BadSchemaDefinition(msg % self.type)
       
   404                     elif self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   405                         if self.schema.reading_from_database:
       
   406                             # we didn't have final relation earlier, so turn
       
   407                             # RRQLExpression into ERQLExpression now
       
   408                             rqlexpr = group_or_rqlexpr
       
   409                             newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr]
       
   410                             newrqlexprs.append(ERQLExpression(rqlexpr.expression,
       
   411                                                               rqlexpr.mainvars,
       
   412                                                               rqlexpr.eid))
       
   413                             self.set_rqlexprs(action, newrqlexprs)
       
   414                         else:
       
   415                             msg = "can't use RRQLExpression on a final relation "\
       
   416                                   "type (eg attribute relation), use an ERQLExpression (%s)"
       
   417                             raise BadSchemaDefinition(msg % self.type)
       
   418                     elif not self.final and \
       
   419                              isinstance(group_or_rqlexpr, ERQLExpression):
       
   420                         msg = "can't use ERQLExpression on a relation type, use "\
       
   421                               "a RRQLExpression (%s)"
       
   422                         raise BadSchemaDefinition(msg % self.type)
       
   423             self._perms_checked = True
       
   424 
       
   425     def cardinality(self, subjtype, objtype, target):
       
   426         card = self.rproperty(subjtype, objtype, 'cardinality')
       
   427         return (target == 'subject' and card[0]) or \
       
   428                (target == 'object' and card[1])
       
   429 
       
   430     def schema_relation(self):
   428     def schema_relation(self):
   431         """return True if this relation type is used to build the schema"""
   429         """return True if this relation type is used to build the schema"""
   432         return self.type in SCHEMA_TYPES
   430         return self.type in SCHEMA_TYPES
   433 
   431 
   434     def physical_mode(self):
   432     def may_have_permission(self, action, req, eschema=None, role=None):
   435         """return an appropriate mode for physical storage of this relation type:
   433         if eschema is not None:
   436         * 'subjectinline' if every possible subject cardinalities are 1 or ?
   434             for tschema in self.targets(eschema, role):
   437         * 'objectinline' if 'subjectinline' mode is not possible but every
   435                 rdef = self.role_rdef(eschema, tschema, role)
   438           possible object cardinalities are 1 or ?
   436                 if rdef.may_have_permission(action, req):
   439         * None if neither 'subjectinline' and 'objectinline'
   437                     return True
   440         """
   438         else:
   441         assert not self.final
   439             for rdef in self.rdefs.itervalues():
   442         return self.inlined and 'subjectinline' or None
   440                 if rdef.may_have_permission(action, req):
   443 
   441                     return True
   444     def check_perm(self, session, action, *args, **kwargs):
   442         return False
   445         # NB: session may be a server session or a request object check user is
   443 
   446         # in an allowed group, if so that's enough internal sessions should
   444     def has_perm(self, session, action, **kwargs):
   447         # always stop there
   445         """return true if the action is granted globaly or localy"""
   448         if session.user.matching_groups(self.get_groups(action)):
       
   449             return
       
   450         # else if there is some rql expressions, check them
       
   451         if any(rqlexpr.check(session, *args, **kwargs)
       
   452                for rqlexpr in self.get_rqlexprs(action)):
       
   453             return
       
   454         raise Unauthorized(action, str(self))
       
   455 
       
   456     def rql_expression(self, expression, mainvars=None, eid=None):
       
   457         """rql expression factory"""
       
   458         if self.final:
   446         if self.final:
   459             return ERQLExpression(expression, mainvars, eid)
   447             assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
   460         return RRQLExpression(expression, mainvars, eid)
   448             assert action in ('read', 'update')
       
   449             if 'eid' in kwargs:
       
   450                 subjtype = session.describe(kwargs['eid'])[0]
       
   451             else:
       
   452                 subjtype = objtype = None
       
   453         else:
       
   454             assert not 'eid' in kwargs, kwargs
       
   455             assert action in ('read', 'add', 'delete')
       
   456             if 'fromeid' in kwargs:
       
   457                 subjtype = session.describe(kwargs['fromeid'])[0]
       
   458             else:
       
   459                 subjtype = None
       
   460             if 'toeid' in kwargs:
       
   461                 objtype = session.describe(kwargs['toeid'])[0]
       
   462             else:
       
   463                 objtype = None
       
   464         if objtype and subjtype:
       
   465             return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs)
       
   466         elif subjtype:
       
   467             for tschema in self.targets(subjtype, 'subject'):
       
   468                 rdef = self.rdef(subjtype, tschema)
       
   469                 if not rdef.has_perm(session, action, **kwargs):
       
   470                     return False
       
   471         elif objtype:
       
   472             for tschema in self.targets(objtype, 'object'):
       
   473                 rdef = self.rdef(tschema, objtype)
       
   474                 if not rdef.has_perm(session, action, **kwargs):
       
   475                     return False
       
   476         else:
       
   477             for rdef in self.rdefs.itervalues():
       
   478                 if not rdef.has_perm(session, action, **kwargs):
       
   479                     return False
       
   480         return True
       
   481 
       
   482     @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
       
   483     def cardinality(self, subjtype, objtype, target):
       
   484         return self.rdef(subjtype, objtype).role_cardinality(target)
   461 
   485 
   462 
   486 
   463 class CubicWebSchema(Schema):
   487 class CubicWebSchema(Schema):
   464     """set of entities and relations schema defining the possible data sets
   488     """set of entities and relations schema defining the possible data sets
   465     used in an application
   489     used in an application
   466 
       
   467 
   490 
   468     :type name: str
   491     :type name: str
   469     :ivar name: name of the schema, usually the instance identifier
   492     :ivar name: name of the schema, usually the instance identifier
   470 
   493 
   471     :type base: str
   494     :type base: str
   480         self._eid_index = {}
   503         self._eid_index = {}
   481         super(CubicWebSchema, self).__init__(*args, **kwargs)
   504         super(CubicWebSchema, self).__init__(*args, **kwargs)
   482         ybo.register_base_types(self)
   505         ybo.register_base_types(self)
   483         rschema = self.add_relation_type(ybo.RelationType('eid'))
   506         rschema = self.add_relation_type(ybo.RelationType('eid'))
   484         rschema.final = True
   507         rschema.final = True
   485         rschema.set_default_groups()
       
   486         rschema = self.add_relation_type(ybo.RelationType('has_text'))
   508         rschema = self.add_relation_type(ybo.RelationType('has_text'))
   487         rschema.final = True
   509         rschema.final = True
   488         rschema.set_default_groups()
       
   489         rschema = self.add_relation_type(ybo.RelationType('identity'))
   510         rschema = self.add_relation_type(ybo.RelationType('identity'))
   490         rschema.final = False
   511         rschema.final = False
   491         rschema.set_default_groups()
       
   492 
   512 
   493     def add_entity_type(self, edef):
   513     def add_entity_type(self, edef):
   494         edef.name = edef.name.encode()
   514         edef.name = edef.name.encode()
   495         edef.name = bw_normalize_etype(edef.name)
   515         edef.name = bw_normalize_etype(edef.name)
   496         assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
   516         assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
   497         eschema = super(CubicWebSchema, self).add_entity_type(edef)
   517         eschema = super(CubicWebSchema, self).add_entity_type(edef)
   498         if not eschema.final:
   518         if not eschema.final:
   499             # automatically add the eid relation to non final entity types
   519             # automatically add the eid relation to non final entity types
   500             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
   520             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
   501                                           cardinality='11', uid=True)
   521                                           cardinality='11', uid=True,
       
   522                                           __permissions__=RO_ATTR_PERMS)
   502             self.add_relation_def(rdef)
   523             self.add_relation_def(rdef)
   503             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type)
   524             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type,
       
   525                                           __permissions__=RO_REL_PERMS)
   504             self.add_relation_def(rdef)
   526             self.add_relation_def(rdef)
   505         self._eid_index[eschema.eid] = eschema
   527         self._eid_index[eschema.eid] = eschema
   506         return eschema
   528         return eschema
   507 
   529 
   508     def add_relation_type(self, rdef):
   530     def add_relation_type(self, rdef):
   528         :param: the newly created or just completed relation schema
   550         :param: the newly created or just completed relation schema
   529         """
   551         """
   530         rdef.name = rdef.name.lower()
   552         rdef.name = rdef.name.lower()
   531         rdef.subject = bw_normalize_etype(rdef.subject)
   553         rdef.subject = bw_normalize_etype(rdef.subject)
   532         rdef.object = bw_normalize_etype(rdef.object)
   554         rdef.object = bw_normalize_etype(rdef.object)
   533         if super(CubicWebSchema, self).add_relation_def(rdef):
   555         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
       
   556         if rdefs:
   534             try:
   557             try:
   535                 self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
   558                 self._eid_index[rdef.eid] = rdefs
   536                                              self.rschema(rdef.name),
       
   537                                              self.eschema(rdef.object))
       
   538             except AttributeError:
   559             except AttributeError:
   539                 pass # not a serialized schema
   560                 pass # not a serialized schema
       
   561         return rdefs
   540 
   562 
   541     def del_relation_type(self, rtype):
   563     def del_relation_type(self, rtype):
   542         rschema = self.rschema(rtype)
   564         rschema = self.rschema(rtype)
   543         self._eid_index.pop(rschema.eid, None)
   565         self._eid_index.pop(rschema.eid, None)
   544         super(CubicWebSchema, self).del_relation_type(rtype)
   566         super(CubicWebSchema, self).del_relation_type(rtype)
   545 
   567 
   546     def del_relation_def(self, subjtype, rtype, objtype):
   568     def del_relation_def(self, subjtype, rtype, objtype):
   547         for k, v in self._eid_index.items():
   569         for k, v in self._eid_index.items():
   548             if v == (subjtype, rtype, objtype):
   570             if not isinstance(v, RelationDefinitionSchema):
       
   571                 continue
       
   572             if v.subject == subjtype and v.rtype == rtype and v.object == objtype:
   549                 del self._eid_index[k]
   573                 del self._eid_index[k]
   550                 break
   574                 break
   551         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
   575         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
   552 
   576 
   553     def del_entity_type(self, etype):
   577     def del_entity_type(self, etype):
   727 
   751 
   728     def __str__(self):
   752     def __str__(self):
   729         return self.full_rql
   753         return self.full_rql
   730     def __repr__(self):
   754     def __repr__(self):
   731         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
   755         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
       
   756 
       
   757     def __cmp__(self, other):
       
   758         if hasattr(other, 'expression'):
       
   759             return cmp(other.expression, self.expression)
       
   760         return -1
   732 
   761 
   733     def __deepcopy__(self, memo):
   762     def __deepcopy__(self, memo):
   734         return self.__class__(self.expression, self.mainvars)
   763         return self.__class__(self.expression, self.mainvars)
   735     def __getstate__(self):
   764     def __getstate__(self):
   736         return (self.expression, self.mainvars)
   765         return (self.expression, self.mainvars)
   763                 if objvar.name not in selected:
   792                 if objvar.name not in selected:
   764                     colindex = len(selected)
   793                     colindex = len(selected)
   765                     rqlst.add_selected(objvar)
   794                     rqlst.add_selected(objvar)
   766                 else:
   795                 else:
   767                     colindex = selected.index(objvar.name)
   796                     colindex = selected.index(objvar.name)
   768                 found.append((action, objvar, colindex))
   797                 found.append((action, colindex))
   769                 # remove U eid %(u)s if U is not used in any other relation
   798                 # remove U eid %(u)s if U is not used in any other relation
   770                 uvrefs = rqlst.defined_vars['U'].references()
   799                 uvrefs = rqlst.defined_vars['U'].references()
   771                 if len(uvrefs) == 1:
   800                 if len(uvrefs) == 1:
   772                     rqlst.remove_node(uvrefs[0].relation())
   801                     rqlst.remove_node(uvrefs[0].relation())
   773         if found is not None:
   802         if found is not None:
   785         """return True if the rql expression is matching the given relation
   814         """return True if the rql expression is matching the given relation
   786         between fromeid and toeid
   815         between fromeid and toeid
   787 
   816 
   788         session may actually be a request as well
   817         session may actually be a request as well
   789         """
   818         """
   790         if self.eid is not None:
   819         creating = kwargs.get('creating')
       
   820         if not creating and self.eid is not None:
   791             key = (self.eid, tuple(sorted(kwargs.iteritems())))
   821             key = (self.eid, tuple(sorted(kwargs.iteritems())))
   792             try:
   822             try:
   793                 return session.local_perm_cache[key]
   823                 return session.local_perm_cache[key]
   794             except KeyError:
   824             except KeyError:
   795                 pass
   825                 pass
   796         rql, has_perm_defs, keyarg = self.transform_has_permission()
   826         rql, has_perm_defs, keyarg = self.transform_has_permission()
       
   827         if creating:
       
   828             # when creating an entity, consider has_*_permission satisfied
       
   829             if has_perm_defs:
       
   830                 return True
       
   831             return False
   797         if keyarg is None:
   832         if keyarg is None:
   798             # on the server side, use unsafe_execute, but this is not available
   833             # on the server side, use unsafe_execute, but this is not available
   799             # on the client side (session is actually a request)
   834             # on the client side (session is actually a request)
   800             execute = getattr(session, 'unsafe_execute', session.execute)
   835             execute = getattr(session, 'unsafe_execute', session.execute)
   801             # XXX what if 'u' in kwargs
   836             kwargs.setdefault('u', session.user.eid)
   802             cachekey = kwargs.keys()
   837             cachekey = kwargs.keys()
   803             kwargs['u'] = session.user.eid
       
   804             try:
   838             try:
   805                 rset = execute(rql, kwargs, cachekey, build_descr=True)
   839                 rset = execute(rql, kwargs, cachekey, build_descr=True)
   806             except NotImplementedError:
   840             except NotImplementedError:
   807                 self.critical('cant check rql expression, unsupported rql %s', rql)
   841                 self.critical('cant check rql expression, unsupported rql %s', rql)
   808                 if self.eid is not None:
   842                 if self.eid is not None:
   826                 return True
   860                 return True
   827         elif rset:
   861         elif rset:
   828             # check every special has_*_permission relation is satisfied
   862             # check every special has_*_permission relation is satisfied
   829             get_eschema = session.vreg.schema.eschema
   863             get_eschema = session.vreg.schema.eschema
   830             try:
   864             try:
   831                 for eaction, var, col in has_perm_defs:
   865                 for eaction, col in has_perm_defs:
   832                     for i in xrange(len(rset)):
   866                     for i in xrange(len(rset)):
   833                         eschema = get_eschema(rset.description[i][col])
   867                         eschema = get_eschema(rset.description[i][col])
   834                         eschema.check_perm(session, eaction, rset[i][col])
   868                         eschema.check_perm(session, eaction, eid=rset[i][col])
   835                 if self.eid is not None:
   869                 if self.eid is not None:
   836                     session.local_perm_cache[key] = True
   870                     session.local_perm_cache[key] = True
   837                 return True
   871                 return True
   838             except Unauthorized:
   872             except Unauthorized:
   839                 pass
   873                 pass
   862             rql += ', X eid %(x)s'
   896             rql += ', X eid %(x)s'
   863         if 'U' in defined:
   897         if 'U' in defined:
   864             rql += ', U eid %(u)s'
   898             rql += ', U eid %(u)s'
   865         return rql
   899         return rql
   866 
   900 
   867     def check(self, session, eid=None):
   901     def check(self, session, eid=None, creating=False, **kwargs):
   868         if 'X' in self.rqlst.defined_vars:
   902         if 'X' in self.rqlst.defined_vars:
   869             if eid is None:
   903             if eid is None:
       
   904                 if creating:
       
   905                     return self._check(session, creating=True, **kwargs)
   870                 return False
   906                 return False
   871             return self._check(session, x=eid)
   907             assert creating == False
   872         return self._check(session)
   908             return self._check(session, x=eid, **kwargs)
       
   909         return self._check(session, **kwargs)
   873 
   910 
   874 
   911 
   875 class RRQLExpression(RQLExpression):
   912 class RRQLExpression(RQLExpression):
   876     def __init__(self, expression, mainvars=None, eid=None):
   913     def __init__(self, expression, mainvars=None, eid=None):
   877         if mainvars is None:
   914         if mainvars is None:
   916             if toeid is None:
   953             if toeid is None:
   917                 return False
   954                 return False
   918             kwargs['o'] = toeid
   955             kwargs['o'] = toeid
   919         return self._check(session, **kwargs)
   956         return self._check(session, **kwargs)
   920 
   957 
       
   958 # in yams, default 'update' perm for attributes granted to managers and owners.
       
   959 # Within cw, we want to default to users who may edit the entity holding the
       
   960 # attribute.
       
   961 ybo.DEFAULT_ATTRPERMS['update'] = (
       
   962     'managers', ERQLExpression('U has_update_permission X'))
   921 
   963 
   922 # workflow extensions #########################################################
   964 # workflow extensions #########################################################
   923 
   965 
   924 from yams.buildobjs import _add_relation as yams_add_relation
   966 from yams.buildobjs import _add_relation as yams_add_relation
   925 
   967 
  1034 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
  1076 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
  1035 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
  1077 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
  1036 
  1078 
  1037 @monkeypatch(FormatConstraint)
  1079 @monkeypatch(FormatConstraint)
  1038 def vocabulary(self, entity=None, form=None):
  1080 def vocabulary(self, entity=None, form=None):
  1039     req = None
  1081     cw = None
  1040     if form is None and entity is not None:
  1082     if form is None and entity is not None:
  1041         req = entity.req
  1083         cw = entity._cw
  1042     elif form is not None:
  1084     elif form is not None:
  1043         req = form.req
  1085         cw = form._cw
  1044     if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
  1086     if cw is not None:
  1045         return self.regular_formats + tuple(NEED_PERM_FORMATS)
  1087         if hasattr(cw, 'is_super_session'):
       
  1088             # cw is a server session
       
  1089             hasperm = cw.is_super_session or \
       
  1090                       not cw.vreg.config.is_hook_category_activated('integrity') or \
       
  1091                       cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT)
       
  1092         else:
       
  1093             hasperm = cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT)
       
  1094         if hasperm:
       
  1095             return self.regular_formats + tuple(NEED_PERM_FORMATS)
  1046     return self.regular_formats
  1096     return self.regular_formats
  1047 
  1097 
  1048 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
  1098 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
  1049 # necessary
  1099 # necessary
  1050 orig_import_erschema = PyFileReader.import_erschema
  1100 orig_import_erschema = PyFileReader.import_erschema
  1074     return orig_set_statement_type(self, bw_normalize_etype(etype))
  1124     return orig_set_statement_type(self, bw_normalize_etype(etype))
  1075 stmts.Select.set_statement_type = bw_set_statement_type
  1125 stmts.Select.set_statement_type = bw_set_statement_type
  1076 
  1126 
  1077 # XXX deprecated
  1127 # XXX deprecated
  1078 
  1128 
  1079 from yams.constraints import format_constraint
       
  1080 from yams.buildobjs import RichString
  1129 from yams.buildobjs import RichString
       
  1130 from yams.constraints import StaticVocabularyConstraint
       
  1131 
       
  1132 RichString = class_moved(RichString)
       
  1133 
       
  1134 StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
       
  1135 FormatConstraint = class_moved(FormatConstraint)
  1081 
  1136 
  1082 PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
  1137 PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
  1083 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
  1138 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
  1084 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
  1139 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
  1085 PyFileReader.context['format_constraint'] = format_constraint