cubicweb/schema.py
changeset 11417 5e5e224239c3
parent 11416 9c2fbb872e91
child 11446 ca90cd9b112b
equal deleted inserted replaced
11416:9c2fbb872e91 11417:5e5e224239c3
    30 from six.moves import range
    30 from six.moves import range
    31 
    31 
    32 from logilab.common import tempattr
    32 from logilab.common import tempattr
    33 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
    33 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
    34 from logilab.common.logging_ext import set_log_methods
    34 from logilab.common.logging_ext import set_log_methods
    35 from logilab.common.deprecation import deprecated, class_moved, moved
    35 from logilab.common.deprecation import deprecated
    36 from logilab.common.textutils import splitstrip
    36 from logilab.common.textutils import splitstrip
    37 from logilab.common.graph import get_cycles
    37 from logilab.common.graph import get_cycles
    38 
    38 
    39 import yams
    39 import yams
    40 from yams import BadSchemaDefinition, buildobjs as ybo
    40 from yams import BadSchemaDefinition, buildobjs as ybo
    41 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
    41 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
    42      RelationDefinitionSchema, PermissionMixIn, role_name
    42     RelationDefinitionSchema, PermissionMixIn, role_name
    43 from yams.constraints import (BaseConstraint, FormatConstraint, BoundaryConstraint,
    43 from yams.constraints import (BaseConstraint, FormatConstraint,
    44                               IntervalBoundConstraint, StaticVocabularyConstraint,
       
    45                               cstr_json_dumps, cstr_json_loads)
    44                               cstr_json_dumps, cstr_json_loads)
    46 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
    45 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
    47                          cleanup_sys_modules, fill_schema_from_namespace)
    46                          cleanup_sys_modules, fill_schema_from_namespace)
    48 
    47 
    49 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    48 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    66 
    65 
    67 # set of meta-relations available for every entity types
    66 # set of meta-relations available for every entity types
    68 META_RTYPES = set((
    67 META_RTYPES = set((
    69     'owned_by', 'created_by', 'is', 'is_instance_of', 'identity',
    68     'owned_by', 'created_by', 'is', 'is_instance_of', 'identity',
    70     'eid', 'creation_date', 'cw_source', 'modification_date', 'has_text', 'cwuri',
    69     'eid', 'creation_date', 'cw_source', 'modification_date', 'has_text', 'cwuri',
    71     ))
    70 ))
    72 WORKFLOW_RTYPES = set(('custom_workflow', 'in_state', 'wf_info_for'))
    71 WORKFLOW_RTYPES = set(('custom_workflow', 'in_state', 'wf_info_for'))
    73 WORKFLOW_DEF_RTYPES = set(('workflow_of', 'state_of', 'transition_of',
    72 WORKFLOW_DEF_RTYPES = set(('workflow_of', 'state_of', 'transition_of',
    74                            'initial_state', 'default_workflow',
    73                            'initial_state', 'default_workflow',
    75                            'allowed_transition', 'destination_state',
    74                            'allowed_transition', 'destination_state',
    76                            'from_state', 'to_state', 'condition',
    75                            'from_state', 'to_state', 'condition',
    96     'relation_type', 'from_entity', 'to_entity',
    95     'relation_type', 'from_entity', 'to_entity',
    97     'constrained_by', 'cstrtype',
    96     'constrained_by', 'cstrtype',
    98     'constraint_of', 'relations',
    97     'constraint_of', 'relations',
    99     'read_permission', 'add_permission',
    98     'read_permission', 'add_permission',
   100     'delete_permission', 'update_permission',
    99     'delete_permission', 'update_permission',
   101     ))
   100 ))
   102 
   101 
   103 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
   102 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
   104                       'WorkflowTransition', 'BaseTransition',
   103                       'WorkflowTransition', 'BaseTransition',
   105                       'SubWorkflowExitPoint'))
   104                       'SubWorkflowExitPoint'))
   106 
   105 
   114 _LOGGER = getLogger('cubicweb.schemaloader')
   113 _LOGGER = getLogger('cubicweb.schemaloader')
   115 
   114 
   116 # entity and relation schema created from serialized schema have an eid
   115 # entity and relation schema created from serialized schema have an eid
   117 ybo.ETYPE_PROPERTIES += ('eid',)
   116 ybo.ETYPE_PROPERTIES += ('eid',)
   118 ybo.RTYPE_PROPERTIES += ('eid',)
   117 ybo.RTYPE_PROPERTIES += ('eid',)
       
   118 
   119 
   119 
   120 def build_schema_from_namespace(items):
   120 def build_schema_from_namespace(items):
   121     schema = CubicWebSchema('noname')
   121     schema = CubicWebSchema('noname')
   122     fill_schema_from_namespace(schema, items, register_base_types=False)
   122     fill_schema_from_namespace(schema, items, register_base_types=False)
   123     return schema
   123     return schema
       
   124 
   124 
   125 
   125 # Bases for manipulating RQL in schema #########################################
   126 # Bases for manipulating RQL in schema #########################################
   126 
   127 
   127 def guess_rrqlexpr_mainvars(expression):
   128 def guess_rrqlexpr_mainvars(expression):
   128     defined = set(split_expression(expression))
   129     defined = set(split_expression(expression))
   136     if not mainvars:
   137     if not mainvars:
   137         raise BadSchemaDefinition('unable to guess selection variables in %r'
   138         raise BadSchemaDefinition('unable to guess selection variables in %r'
   138                                   % expression)
   139                                   % expression)
   139     return mainvars
   140     return mainvars
   140 
   141 
       
   142 
   141 def split_expression(rqlstring):
   143 def split_expression(rqlstring):
   142     for expr in rqlstring.split(','):
   144     for expr in rqlstring.split(','):
   143         for noparen1 in expr.split('('):
   145         for noparen1 in expr.split('('):
   144             for noparen2 in noparen1.split(')'):
   146             for noparen2 in noparen1.split(')'):
   145                 for word in noparen2.split():
   147                 for word in noparen2.split():
   146                     yield word
   148                     yield word
   147 
   149 
       
   150 
   148 def normalize_expression(rqlstring):
   151 def normalize_expression(rqlstring):
   149     """normalize an rql expression to ease schema synchronization (avoid
   152     """normalize an rql expression to ease schema synchronization (avoid
   150     suppressing and reinserting an expression if only a space has been
   153     suppressing and reinserting an expression if only a space has been
   151     added/removed for instance)
   154     added/removed for instance)
   152     """
   155     """
   161     single selected node, etc.), raise BadSchemaDefinition if not
   164     single selected node, etc.), raise BadSchemaDefinition if not
   162     """
   165     """
   163     if len(formula_rqlst.children) != 1:
   166     if len(formula_rqlst.children) != 1:
   164         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   167         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   165                                   'can not use UNION in formula %(form)r' %
   168                                   'can not use UNION in formula %(form)r' %
   166                                   {'attr' : rdef.rtype,
   169                                   {'attr': rdef.rtype,
   167                                    'etype' : rdef.subject.type,
   170                                    'etype': rdef.subject.type,
   168                                    'form' : rdef.formula})
   171                                    'form': rdef.formula})
   169     select = formula_rqlst.children[0]
   172     select = formula_rqlst.children[0]
   170     if len(select.selection) != 1:
   173     if len(select.selection) != 1:
   171         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   174         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   172                                   'can only select one term in formula %(form)r' %
   175                                   'can only select one term in formula %(form)r' %
   173                                   {'attr' : rdef.rtype,
   176                                   {'attr': rdef.rtype,
   174                                    'etype' : rdef.subject.type,
   177                                    'etype': rdef.subject.type,
   175                                    'form' : rdef.formula})
   178                                    'form': rdef.formula})
   176     term = select.selection[0]
   179     term = select.selection[0]
   177     types = set(term.get_type(sol) for sol in select.solutions)
   180     types = set(term.get_type(sol) for sol in select.solutions)
   178     if len(types) != 1:
   181     if len(types) != 1:
   179         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   182         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   180                                   'multiple possible types (%(types)s) for formula %(form)r' %
   183                                   'multiple possible types (%(types)s) for formula %(form)r' %
   181                                   {'attr' : rdef.rtype,
   184                                   {'attr': rdef.rtype,
   182                                    'etype' : rdef.subject.type,
   185                                    'etype': rdef.subject.type,
   183                                    'types' : list(types),
   186                                    'types': list(types),
   184                                    'form' : rdef.formula})
   187                                    'form': rdef.formula})
   185     computed_type = types.pop()
   188     computed_type = types.pop()
   186     expected_type = rdef.object.type
   189     expected_type = rdef.object.type
   187     if computed_type != expected_type:
   190     if computed_type != expected_type:
   188         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   191         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
   189                                   'computed attribute type (%(comp_type)s) mismatch with '
   192                                   'computed attribute type (%(comp_type)s) mismatch with '
   190                                   'specified type (%(attr_type)s)' %
   193                                   'specified type (%(attr_type)s)' %
   191                                   {'attr' : rdef.rtype,
   194                                   {'attr': rdef.rtype,
   192                                    'etype' : rdef.subject.type,
   195                                    'etype': rdef.subject.type,
   193                                    'comp_type' : computed_type,
   196                                    'comp_type': computed_type,
   194                                    'attr_type' : expected_type})
   197                                    'attr_type': expected_type})
   195 
   198 
   196 
   199 
   197 class RQLExpression(object):
   200 class RQLExpression(object):
   198     """Base class for RQL expression used in schema (constraints and
   201     """Base class for RQL expression used in schema (constraints and
   199     permissions)
   202     permissions)
   200     """
   203     """
   201     # these are overridden by set_log_methods below
   204     # these are overridden by set_log_methods below
   202     # only defining here to prevent pylint from complaining
   205     # only defining here to prevent pylint from complaining
   203     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
   206     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
   204     # to be defined in concrete classes
   207     # to be defined in concrete classes
   205     predefined_variables = None
   208     predefined_variables = None
   206 
   209 
   207     # Internal cache for parsed expressions
   210     # Internal cache for parsed expressions
   208     _rql_cache = {}
   211     _rql_cache = {}
   220         :type mainvars: sequence of RQL variables' names. Can be provided as a
   223         :type mainvars: sequence of RQL variables' names. Can be provided as a
   221                         comma separated string.
   224                         comma separated string.
   222         :param mainvars: names of the variables being selected.
   225         :param mainvars: names of the variables being selected.
   223 
   226 
   224         """
   227         """
   225         self.eid = eid # eid of the entity representing this rql expression
   228         self.eid = eid  # eid of the entity representing this rql expression
   226         assert mainvars, 'bad mainvars %s' % mainvars
   229         assert mainvars, 'bad mainvars %s' % mainvars
   227         if isinstance(mainvars, string_types):
   230         if isinstance(mainvars, string_types):
   228             mainvars = set(splitstrip(mainvars))
   231             mainvars = set(splitstrip(mainvars))
   229         elif not isinstance(mainvars, set):
   232         elif not isinstance(mainvars, set):
   230             mainvars = set(mainvars)
   233             mainvars = set(mainvars)
   266     def __hash__(self):
   269     def __hash__(self):
   267         return hash(self.expression)
   270         return hash(self.expression)
   268 
   271 
   269     def __deepcopy__(self, memo):
   272     def __deepcopy__(self, memo):
   270         return self.__class__(self.expression, self.mainvars)
   273         return self.__class__(self.expression, self.mainvars)
       
   274 
   271     def __getstate__(self):
   275     def __getstate__(self):
   272         return (self.expression, self.mainvars)
   276         return (self.expression, self.mainvars)
       
   277 
   273     def __setstate__(self, state):
   278     def __setstate__(self, state):
   274         self.__init__(*state)
   279         self.__init__(*state)
   275 
   280 
   276     @cachedproperty
   281     @cachedproperty
   277     def rqlst(self):
   282     def rqlst(self):
   278         # Don't use _cached_parse here because the rqlst is modified
   283         # Don't use _cached_parse here because the rqlst is modified
   279         select = parse(self.minimal_rql, print_errors=False).children[0]
   284         select = parse(self.minimal_rql, print_errors=False).children[0]
   280         defined = set(split_expression(self.expression))
   285         defined = set(split_expression(self.expression))
   281         for varname in self.predefined_variables:
   286         for varname in self.predefined_variables:
   282             if varname in defined:
   287             if varname in defined:
   283                 select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute')
   288                 select.add_eid_restriction(select.get_variable(varname), varname.lower(),
       
   289                                            'Substitute')
   284         return select
   290         return select
   285 
   291 
   286     # permission rql expression specific stuff #################################
   292     # permission rql expression specific stuff #################################
   287 
   293 
   288     @cached
   294     @cached
   296                     continue
   302                     continue
   297                 try:
   303                 try:
   298                     prefix, action, suffix = rel.r_type.split('_')
   304                     prefix, action, suffix = rel.r_type.split('_')
   299                 except ValueError:
   305                 except ValueError:
   300                     continue
   306                     continue
   301                 if prefix != 'has' or suffix != 'permission' or \
   307                 if (prefix != 'has' or suffix != 'permission' or
   302                        not action in ('add', 'delete', 'update', 'read'):
   308                         action not in ('add', 'delete', 'update', 'read')):
   303                     continue
   309                     continue
   304                 if found is None:
   310                 if found is None:
   305                     found = []
   311                     found = []
   306                     rqlst.save_state()
   312                     rqlst.save_state()
   307                 assert rel.children[0].name == 'U'
   313                 assert rel.children[0].name == 'U'
   397     def minimal_rql(self):
   403     def minimal_rql(self):
   398         return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
   404         return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
   399                                     self.expression)
   405                                     self.expression)
   400 
   406 
   401 
   407 
   402 
       
   403 # rql expressions for use in permission definition #############################
   408 # rql expressions for use in permission definition #############################
   404 
   409 
   405 class ERQLExpression(RQLExpression):
   410 class ERQLExpression(RQLExpression):
   406     predefined_variables = 'XU'
   411     predefined_variables = 'XU'
   407 
   412 
   412         if 'X' in self.snippet_rqlst.defined_vars:
   417         if 'X' in self.snippet_rqlst.defined_vars:
   413             if eid is None:
   418             if eid is None:
   414                 if creating:
   419                 if creating:
   415                     return self._check(_cw, creating=True, **kwargs)
   420                     return self._check(_cw, creating=True, **kwargs)
   416                 return False
   421                 return False
   417             assert creating == False
   422             assert not creating
   418             return self._check(_cw, x=eid, **kwargs)
   423             return self._check(_cw, x=eid, **kwargs)
   419         return self._check(_cw, **kwargs)
   424         return self._check(_cw, **kwargs)
   420 
   425 
   421 
   426 
   422 class CubicWebRelationDefinitionSchema(RelationDefinitionSchema):
   427 class CubicWebRelationDefinitionSchema(RelationDefinitionSchema):
   432             return ERQLExpression(expression, mainvars, eid)
   437             return ERQLExpression(expression, mainvars, eid)
   433         return RRQLExpression(expression, mainvars, eid)
   438         return RRQLExpression(expression, mainvars, eid)
   434 
   439 
   435     def check_permission_definitions(self):
   440     def check_permission_definitions(self):
   436         super(CubicWebRelationDefinitionSchema, self).check_permission_definitions()
   441         super(CubicWebRelationDefinitionSchema, self).check_permission_definitions()
   437         schema = self.subject.schema
       
   438         for action, groups in self.permissions.items():
   442         for action, groups in self.permissions.items():
   439             for group_or_rqlexpr in groups:
   443             for group_or_rqlexpr in groups:
   440                 if action == 'read' and \
   444                 if action == 'read' and isinstance(group_or_rqlexpr, RQLExpression):
   441                        isinstance(group_or_rqlexpr, RQLExpression):
       
   442                     msg = "can't use rql expression for read permission of %s"
   445                     msg = "can't use rql expression for read permission of %s"
   443                     raise BadSchemaDefinition(msg % self)
   446                     raise BadSchemaDefinition(msg % self)
   444                 if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
   447                 if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
   445                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
   448                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
   446                     raise BadSchemaDefinition(msg % self)
   449                     raise BadSchemaDefinition(msg % self)
   447                 if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
   450                 if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
   448                     msg = "can't use ERQLExpression on %s, use a RRQLExpression"
   451                     msg = "can't use ERQLExpression on %s, use a RRQLExpression"
   449                     raise BadSchemaDefinition(msg % self)
   452                     raise BadSchemaDefinition(msg % self)
       
   453 
   450 
   454 
   451 def vargraph(rqlst):
   455 def vargraph(rqlst):
   452     """ builds an adjacency graph of variables from the rql syntax tree, e.g:
   456     """ builds an adjacency graph of variables from the rql syntax tree, e.g:
   453     Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF
   457     Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF
   454     => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']}
   458     => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']}
   461         except AttributeError:
   465         except AttributeError:
   462             pass
   466             pass
   463         else:
   467         else:
   464             vargraph.setdefault(lhsvarname, []).append(rhsvarname)
   468             vargraph.setdefault(lhsvarname, []).append(rhsvarname)
   465             vargraph.setdefault(rhsvarname, []).append(lhsvarname)
   469             vargraph.setdefault(rhsvarname, []).append(lhsvarname)
   466             #vargraph[(lhsvarname, rhsvarname)] = relation.r_type
       
   467     return vargraph
   470     return vargraph
   468 
   471 
   469 
   472 
   470 class GeneratedConstraint(object):
   473 class GeneratedConstraint(object):
   471     def __init__(self, rqlst, mainvars):
   474     def __init__(self, rqlst, mainvars):
   510 if 'delete' in yams.DEFAULT_COMPUTED_RELPERMS:
   513 if 'delete' in yams.DEFAULT_COMPUTED_RELPERMS:
   511     del yams.DEFAULT_COMPUTED_RELPERMS['delete']
   514     del yams.DEFAULT_COMPUTED_RELPERMS['delete']
   512 
   515 
   513 
   516 
   514 PUB_SYSTEM_ENTITY_PERMS = {
   517 PUB_SYSTEM_ENTITY_PERMS = {
   515     'read':   ('managers', 'users', 'guests',),
   518     'read': ('managers', 'users', 'guests',),
   516     'add':    ('managers',),
   519     'add': ('managers',),
   517     'delete': ('managers',),
   520     'delete': ('managers',),
   518     'update': ('managers',),
   521     'update': ('managers',),
   519     }
   522 }
   520 PUB_SYSTEM_REL_PERMS = {
   523 PUB_SYSTEM_REL_PERMS = {
   521     'read':   ('managers', 'users', 'guests',),
   524     'read': ('managers', 'users', 'guests',),
   522     'add':    ('managers',),
   525     'add': ('managers',),
   523     'delete': ('managers',),
   526     'delete': ('managers',),
   524     }
   527 }
   525 PUB_SYSTEM_ATTR_PERMS = {
   528 PUB_SYSTEM_ATTR_PERMS = {
   526     'read':   ('managers', 'users', 'guests',),
   529     'read': ('managers', 'users', 'guests',),
   527     'add': ('managers',),
   530     'add': ('managers',),
   528     'update': ('managers',),
   531     'update': ('managers',),
   529     }
   532 }
   530 RO_REL_PERMS = {
   533 RO_REL_PERMS = {
   531     'read':   ('managers', 'users', 'guests',),
   534     'read': ('managers', 'users', 'guests',),
   532     'add':    (),
   535     'add': (),
   533     'delete': (),
   536     'delete': (),
   534     }
   537 }
   535 RO_ATTR_PERMS = {
   538 RO_ATTR_PERMS = {
   536     'read':   ('managers', 'users', 'guests',),
   539     'read': ('managers', 'users', 'guests',),
   537     'add': ybo.DEFAULT_ATTRPERMS['add'],
   540     'add': ybo.DEFAULT_ATTRPERMS['add'],
   538     'update': (),
   541     'update': (),
   539     }
   542 }
       
   543 
   540 
   544 
   541 # XXX same algorithm as in reorder_cubes and probably other place,
   545 # XXX same algorithm as in reorder_cubes and probably other place,
   542 # may probably extract a generic function
   546 # may probably extract a generic function
   543 def order_eschemas(eschemas):
   547 def order_eschemas(eschemas):
   544     """return entity schemas ordered such that entity types which specializes an
   548     """return entity schemas ordered such that entity types which specializes an
   567                         deps.remove(eschema)
   571                         deps.remove(eschema)
   568                     except KeyError:
   572                     except KeyError:
   569                         continue
   573                         continue
   570     return eschemas
   574     return eschemas
   571 
   575 
       
   576 
   572 def bw_normalize_etype(etype):
   577 def bw_normalize_etype(etype):
   573     if etype in ETYPE_NAME_MAP:
   578     if etype in ETYPE_NAME_MAP:
   574         msg = '%s has been renamed to %s, please update your code' % (
   579         msg = '%s has been renamed to %s, please update your code' % (
   575             etype, ETYPE_NAME_MAP[etype])
   580             etype, ETYPE_NAME_MAP[etype])
   576         warn(msg, DeprecationWarning, stacklevel=4)
   581         warn(msg, DeprecationWarning, stacklevel=4)
   577         etype = ETYPE_NAME_MAP[etype]
   582         etype = ETYPE_NAME_MAP[etype]
   578     return etype
   583     return etype
       
   584 
   579 
   585 
   580 def display_name(req, key, form='', context=None):
   586 def display_name(req, key, form='', context=None):
   581     """return a internationalized string for the key (schema entity or relation
   587     """return a internationalized string for the key (schema entity or relation
   582     name) in a given form
   588     name) in a given form
   583     """
   589     """
   600     a given form
   606     a given form
   601     """
   607     """
   602     return display_name(req, self.type, form, context)
   608     return display_name(req, self.type, form, context)
   603 ERSchema.display_name = ERSchema_display_name
   609 ERSchema.display_name = ERSchema_display_name
   604 
   610 
       
   611 
   605 @cached
   612 @cached
   606 def get_groups(self, action):
   613 def get_groups(self, action):
   607     """return the groups authorized to perform <action> on entities of
   614     """return the groups authorized to perform <action> on entities of
   608     this type
   615     this type
   609 
   616 
   612 
   619 
   613     :rtype: tuple
   620     :rtype: tuple
   614     :return: names of the groups with the given permission
   621     :return: names of the groups with the given permission
   615     """
   622     """
   616     assert action in self.ACTIONS, action
   623     assert action in self.ACTIONS, action
   617     #assert action in self._groups, '%s %s' % (self, action)
       
   618     try:
   624     try:
   619         return frozenset(g for g in self.permissions[action] if isinstance(g, string_types))
   625         return frozenset(g for g in self.permissions[action] if isinstance(g, string_types))
   620     except KeyError:
   626     except KeyError:
   621         return ()
   627         return ()
   622 PermissionMixIn.get_groups = get_groups
   628 PermissionMixIn.get_groups = get_groups
   623 
   629 
       
   630 
   624 @cached
   631 @cached
   625 def get_rqlexprs(self, action):
   632 def get_rqlexprs(self, action):
   626     """return the rql expressions representing queries to check the user is allowed
   633     """return the rql expressions representing queries to check the user is allowed
   627     to perform <action> on entities of this type
   634     to perform <action> on entities of this type
   628 
   635 
   631 
   638 
   632     :rtype: tuple
   639     :rtype: tuple
   633     :return: the rql expressions with the given permission
   640     :return: the rql expressions with the given permission
   634     """
   641     """
   635     assert action in self.ACTIONS, action
   642     assert action in self.ACTIONS, action
   636     #assert action in self._rqlexprs, '%s %s' % (self, action)
       
   637     try:
   643     try:
   638         return tuple(g for g in self.permissions[action] if not isinstance(g, string_types))
   644         return tuple(g for g in self.permissions[action] if not isinstance(g, string_types))
   639     except KeyError:
   645     except KeyError:
   640         return ()
   646         return ()
   641 PermissionMixIn.get_rqlexprs = get_rqlexprs
   647 PermissionMixIn.get_rqlexprs = get_rqlexprs
   642 
   648 
   643 orig_set_action_permissions = PermissionMixIn.set_action_permissions
   649 
   644 def set_action_permissions(self, action, permissions):
   650 def set_action_permissions(self, action, permissions):
   645     """set the groups and rql expressions allowing to perform <action> on
   651     """set the groups and rql expressions allowing to perform <action> on
   646     entities of this type
   652     entities of this type
   647 
   653 
   648     :type action: str
   654     :type action: str
   652     :param permissions: the groups and rql expressions allowing the given action
   658     :param permissions: the groups and rql expressions allowing the given action
   653     """
   659     """
   654     orig_set_action_permissions(self, action, tuple(permissions))
   660     orig_set_action_permissions(self, action, tuple(permissions))
   655     clear_cache(self, 'get_rqlexprs')
   661     clear_cache(self, 'get_rqlexprs')
   656     clear_cache(self, 'get_groups')
   662     clear_cache(self, 'get_groups')
       
   663 orig_set_action_permissions = PermissionMixIn.set_action_permissions
   657 PermissionMixIn.set_action_permissions = set_action_permissions
   664 PermissionMixIn.set_action_permissions = set_action_permissions
       
   665 
   658 
   666 
   659 def has_local_role(self, action):
   667 def has_local_role(self, action):
   660     """return true if the action *may* be granted locally (i.e. either rql
   668     """return true if the action *may* be granted locally (i.e. either rql
   661     expressions or the owners group are used in security definition)
   669     expressions or the owners group are used in security definition)
   662 
   670 
   669     if action in ('update', 'delete'):
   677     if action in ('update', 'delete'):
   670         return 'owners' in self.get_groups(action)
   678         return 'owners' in self.get_groups(action)
   671     return False
   679     return False
   672 PermissionMixIn.has_local_role = has_local_role
   680 PermissionMixIn.has_local_role = has_local_role
   673 
   681 
       
   682 
   674 def may_have_permission(self, action, req):
   683 def may_have_permission(self, action, req):
   675     if action != 'read' and not (self.has_local_role('read') or
   684     if action != 'read' and not (self.has_local_role('read') or
   676                                  self.has_perm(req, 'read')):
   685                                  self.has_perm(req, 'read')):
   677         return False
   686         return False
   678     return self.has_local_role(action) or self.has_perm(req, action)
   687     return self.has_local_role(action) or self.has_perm(req, action)
   679 PermissionMixIn.may_have_permission = may_have_permission
   688 PermissionMixIn.may_have_permission = may_have_permission
       
   689 
   680 
   690 
   681 def has_perm(self, _cw, action, **kwargs):
   691 def has_perm(self, _cw, action, **kwargs):
   682     """return true if the action is granted globally or locally"""
   692     """return true if the action is granted globally or locally"""
   683     try:
   693     try:
   684         self.check_perm(_cw, action, **kwargs)
   694         self.check_perm(_cw, action, **kwargs)
   711     # object, if so that's enough
   721     # object, if so that's enough
   712     #
   722     #
   713     # NB: give _cw to user.owns since user is not be bound to a transaction on
   723     # NB: give _cw to user.owns since user is not be bound to a transaction on
   714     # the repository side
   724     # the repository side
   715     if 'owners' in groups and (
   725     if 'owners' in groups and (
   716           kwargs.get('creating')
   726             kwargs.get('creating')
   717           or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
   727             or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
   718         if DBG:
   728         if DBG:
   719             print('check_perm: %r %r: user is owner or creation time' %
   729             print('check_perm: %r %r: user is owner or creation time' %
   720                   (action, _self_str))
   730                   (action, _self_str))
   721         return
   731         return
   722     # else if there is some rql expressions, check them
   732     # else if there is some rql expressions, check them
   871         elif not need_has_text and has_has_text:
   881         elif not need_has_text and has_has_text:
   872             # use rschema.del_relation_def and not schema.del_relation_def to
   882             # use rschema.del_relation_def and not schema.del_relation_def to
   873             # avoid deleting the relation type accidentally...
   883             # avoid deleting the relation type accidentally...
   874             self.schema['has_text'].del_relation_def(self, self.schema['String'])
   884             self.schema['has_text'].del_relation_def(self, self.schema['String'])
   875 
   885 
   876     def schema_entity(self): # XXX @property for consistency with meta
   886     def schema_entity(self):  # XXX @property for consistency with meta
   877         """return True if this entity type is used to build the schema"""
   887         """return True if this entity type is used to build the schema"""
   878         return self.type in SCHEMA_TYPES
   888         return self.type in SCHEMA_TYPES
   879 
   889 
   880     def rql_expression(self, expression, mainvars=None, eid=None):
   890     def rql_expression(self, expression, mainvars=None, eid=None):
   881         """rql expression factory"""
   891         """rql expression factory"""
   909 
   919 
   910     @property
   920     @property
   911     def meta(self):
   921     def meta(self):
   912         return self.type in META_RTYPES
   922         return self.type in META_RTYPES
   913 
   923 
   914     def schema_relation(self): # XXX @property for consistency with meta
   924     def schema_relation(self):  # XXX @property for consistency with meta
   915         """return True if this relation type is used to build the schema"""
   925         """return True if this relation type is used to build the schema"""
   916         return self.type in SCHEMA_TYPES
   926         return self.type in SCHEMA_TYPES
   917 
   927 
   918     def may_have_permission(self, action, req, eschema=None, role=None):
   928     def may_have_permission(self, action, req, eschema=None, role=None):
   919         if eschema is not None:
   929         if eschema is not None:
   935             if 'eid' in kwargs:
   945             if 'eid' in kwargs:
   936                 subjtype = _cw.entity_metas(kwargs['eid'])['type']
   946                 subjtype = _cw.entity_metas(kwargs['eid'])['type']
   937             else:
   947             else:
   938                 subjtype = objtype = None
   948                 subjtype = objtype = None
   939         else:
   949         else:
   940             assert not 'eid' in kwargs, kwargs
   950             assert 'eid' not in kwargs, kwargs
   941             assert action in ('read', 'add', 'delete')
   951             assert action in ('read', 'add', 'delete')
   942             if 'fromeid' in kwargs:
   952             if 'fromeid' in kwargs:
   943                 subjtype = _cw.entity_metas(kwargs['fromeid'])['type']
   953                 subjtype = _cw.entity_metas(kwargs['fromeid'])['type']
   944             elif 'frometype' in kwargs:
   954             elif 'frometype' in kwargs:
   945                 subjtype = kwargs.pop('frometype')
   955                 subjtype = kwargs.pop('frometype')
   999         rschema.final = True
  1009         rschema.final = True
  1000         rschema = self.add_relation_type(ybo.RelationType('identity'))
  1010         rschema = self.add_relation_type(ybo.RelationType('identity'))
  1001         rschema.final = False
  1011         rschema.final = False
  1002 
  1012 
  1003     etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[A-Za-z0-9]*$'
  1013     etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[A-Za-z0-9]*$'
       
  1014 
  1004     def add_entity_type(self, edef):
  1015     def add_entity_type(self, edef):
  1005         edef.name = str(edef.name)
  1016         edef.name = str(edef.name)
  1006         edef.name = bw_normalize_etype(edef.name)
  1017         edef.name = bw_normalize_etype(edef.name)
  1007         if not re.match(self.etype_name_re, edef.name):
  1018         if not re.match(self.etype_name_re, edef.name):
  1008             raise BadSchemaDefinition(
  1019             raise BadSchemaDefinition(
  1054         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
  1065         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
  1055         if rdefs:
  1066         if rdefs:
  1056             try:
  1067             try:
  1057                 self._eid_index[rdef.eid] = rdefs
  1068                 self._eid_index[rdef.eid] = rdefs
  1058             except AttributeError:
  1069             except AttributeError:
  1059                 pass # not a serialized schema
  1070                 pass  # not a serialized schema
  1060         return rdefs
  1071         return rdefs
  1061 
  1072 
  1062     def del_relation_type(self, rtype):
  1073     def del_relation_type(self, rtype):
  1063         rschema = self.rschema(rtype)
  1074         rschema = self.rschema(rtype)
  1064         self._eid_index.pop(rschema.eid, None)
  1075         self._eid_index.pop(rschema.eid, None)
  1110             rqlst = parse(rdef.formula)
  1121             rqlst = parse(rdef.formula)
  1111             select = rqlst.children[0]
  1122             select = rqlst.children[0]
  1112             select.add_type_restriction(select.defined_vars['X'], str(rdef.subject))
  1123             select.add_type_restriction(select.defined_vars['X'], str(rdef.subject))
  1113             analyzer.visit(select)
  1124             analyzer.visit(select)
  1114             _check_valid_formula(rdef, rqlst)
  1125             _check_valid_formula(rdef, rqlst)
  1115             rdef.formula_select = select # avoid later recomputation
  1126             rdef.formula_select = select  # avoid later recomputation
  1116 
       
  1117 
  1127 
  1118     def finalize_computed_relations(self):
  1128     def finalize_computed_relations(self):
  1119         """Build relation definitions for computed relations
  1129         """Build relation definitions for computed relations
  1120 
  1130 
  1121         The subject and object types are infered using rql analyzer.
  1131         The subject and object types are infered using rql analyzer.
  1207     """
  1217     """
  1208 
  1218 
  1209     def repo_check(self, session, eidfrom, rtype, eidto):
  1219     def repo_check(self, session, eidfrom, rtype, eidto):
  1210         """raise ValidationError if the relation doesn't satisfy the constraint
  1220         """raise ValidationError if the relation doesn't satisfy the constraint
  1211         """
  1221         """
  1212         pass # this is a vocabulary constraint, not enforced
  1222         pass  # this is a vocabulary constraint, not enforced
  1213 
  1223 
  1214 
  1224 
  1215 class RepoEnforcedRQLConstraintMixIn(object):
  1225 class RepoEnforcedRQLConstraintMixIn(object):
  1216 
  1226 
  1217     def __init__(self, expression, mainvars=None, msg=None):
  1227     def __init__(self, expression, mainvars=None, msg=None):
  1302 
  1312 
  1303 # workflow extensions #########################################################
  1313 # workflow extensions #########################################################
  1304 
  1314 
  1305 from yams.buildobjs import _add_relation as yams_add_relation
  1315 from yams.buildobjs import _add_relation as yams_add_relation
  1306 
  1316 
       
  1317 
  1307 class workflowable_definition(ybo.metadefinition):
  1318 class workflowable_definition(ybo.metadefinition):
  1308     """extends default EntityType's metaclass to add workflow relations
  1319     """extends default EntityType's metaclass to add workflow relations
  1309     (i.e. in_state, wf_info_for and custom_workflow). This is the default
  1320     (i.e. in_state, wf_info_for and custom_workflow). This is the default
  1310     metaclass for WorkflowableEntityType.
  1321     metaclass for WorkflowableEntityType.
  1311     """
  1322     """
  1350 # schema loading ##############################################################
  1361 # schema loading ##############################################################
  1351 
  1362 
  1352 CONSTRAINTS['RQLConstraint'] = RQLConstraint
  1363 CONSTRAINTS['RQLConstraint'] = RQLConstraint
  1353 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
  1364 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
  1354 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
  1365 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
  1355 CONSTRAINTS.pop('MultipleStaticVocabularyConstraint', None) # don't want this in cw yams schema
  1366 # don't want MultipleStaticVocabularyConstraint in cw yams schema
       
  1367 CONSTRAINTS.pop('MultipleStaticVocabularyConstraint', None)
  1356 PyFileReader.context.update(CONSTRAINTS)
  1368 PyFileReader.context.update(CONSTRAINTS)
  1357 
  1369 
  1358 
  1370 
  1359 class BootstrapSchemaLoader(SchemaLoader):
  1371 class BootstrapSchemaLoader(SchemaLoader):
  1360     """cubicweb specific schema loader, loading only schema necessary to read
  1372     """cubicweb specific schema loader, loading only schema necessary to read
  1371 
  1383 
  1372     def _load_definition_files(self, cubes=None):
  1384     def _load_definition_files(self, cubes=None):
  1373         # bootstraping, ignore cubes
  1385         # bootstraping, ignore cubes
  1374         filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
  1386         filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
  1375         self.info('loading %s', filepath)
  1387         self.info('loading %s', filepath)
  1376         with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here
  1388         with tempattr(ybo, 'PACKAGE', 'cubicweb'):  # though we don't care here
  1377             self.handle_file(filepath)
  1389             self.handle_file(filepath)
  1378 
  1390 
  1379     def unhandled_file(self, filepath):
  1391     def unhandled_file(self, filepath):
  1380         """called when a file without handler associated has been found"""
  1392         """called when a file without handler associated has been found"""
  1381         self.warning('ignoring file %r', filepath)
  1393         self.warning('ignoring file %r', filepath)
  1382 
  1394 
  1383     # these are overridden by set_log_methods below
  1395     # these are overridden by set_log_methods below
  1384     # only defining here to prevent pylint from complaining
  1396     # only defining here to prevent pylint from complaining
  1385     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
  1397     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
       
  1398 
  1386 
  1399 
  1387 class CubicWebSchemaLoader(BootstrapSchemaLoader):
  1400 class CubicWebSchemaLoader(BootstrapSchemaLoader):
  1388     """cubicweb specific schema loader, automatically adding metadata to the
  1401     """cubicweb specific schema loader, automatically adding metadata to the
  1389     instance's schema
  1402     instance's schema
  1390     """
  1403     """
  1421                 with tempattr(ybo, 'PACKAGE', basename(cube)):
  1434                 with tempattr(ybo, 'PACKAGE', basename(cube)):
  1422                     self.handle_file(filepath)
  1435                     self.handle_file(filepath)
  1423 
  1436 
  1424     # these are overridden by set_log_methods below
  1437     # these are overridden by set_log_methods below
  1425     # only defining here to prevent pylint from complaining
  1438     # only defining here to prevent pylint from complaining
  1426     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
  1439     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
  1427 
  1440 
  1428 
  1441 
  1429 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
  1442 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
  1430 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
  1443 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
  1431 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
  1444 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
  1432 
  1445 
  1433 # _() is just there to add messages to the catalog, don't care about actual
  1446 # _() is just there to add messages to the catalog, don't care about actual
  1434 # translation
  1447 # translation
  1435 MAY_USE_TEMPLATE_FORMAT = set(('managers',))
  1448 MAY_USE_TEMPLATE_FORMAT = set(('managers',))
  1436 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
  1449 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
       
  1450 
  1437 
  1451 
  1438 @monkeypatch(FormatConstraint)
  1452 @monkeypatch(FormatConstraint)
  1439 def vocabulary(self, entity=None, form=None):
  1453 def vocabulary(self, entity=None, form=None):
  1440     cw = None
  1454     cw = None
  1441     if form is None and entity is not None:
  1455     if form is None and entity is not None:
  1442         cw = entity._cw
  1456         cw = entity._cw
  1443     elif form is not None:
  1457     elif form is not None:
  1444         cw = form._cw
  1458         cw = form._cw
  1445     if cw is not None:
  1459     if cw is not None:
  1446         if hasattr(cw, 'write_security'): # test it's a session and not a request
  1460         if hasattr(cw, 'write_security'):  # test it's a session and not a request
  1447             # cw is a server session
  1461             # cw is a server session
  1448             hasperm = not cw.write_security or \
  1462             hasperm = (not cw.write_security or
  1449                       not cw.is_hook_category_activated('integrity') or \
  1463                        not cw.is_hook_category_activated('integrity') or
  1450                       cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT)
  1464                        cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT))
  1451         else:
  1465         else:
  1452             hasperm = cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT)
  1466             hasperm = cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT)
  1453         if hasperm:
  1467         if hasperm:
  1454             return self.regular_formats + tuple(NEED_PERM_FORMATS)
  1468             return self.regular_formats + tuple(NEED_PERM_FORMATS)
  1455     return self.regular_formats
  1469     return self.regular_formats
  1456 
  1470 
  1457 # XXX itou for some Statement methods
  1471 # XXX itou for some Statement methods
  1458 from rql import stmts
  1472 from rql import stmts
  1459 orig_get_etype = stmts.ScopeNode.get_etype
  1473 
       
  1474 
  1460 def bw_get_etype(self, name):
  1475 def bw_get_etype(self, name):
  1461     return orig_get_etype(self, bw_normalize_etype(name))
  1476     return orig_get_etype(self, bw_normalize_etype(name))
       
  1477 orig_get_etype = stmts.ScopeNode.get_etype
  1462 stmts.ScopeNode.get_etype = bw_get_etype
  1478 stmts.ScopeNode.get_etype = bw_get_etype
  1463 
  1479 
  1464 orig_add_main_variable_delete = stmts.Delete.add_main_variable
  1480 
  1465 def bw_add_main_variable_delete(self, etype, vref):
  1481 def bw_add_main_variable_delete(self, etype, vref):
  1466     return orig_add_main_variable_delete(self, bw_normalize_etype(etype), vref)
  1482     return orig_add_main_variable_delete(self, bw_normalize_etype(etype), vref)
       
  1483 orig_add_main_variable_delete = stmts.Delete.add_main_variable
  1467 stmts.Delete.add_main_variable = bw_add_main_variable_delete
  1484 stmts.Delete.add_main_variable = bw_add_main_variable_delete
  1468 
  1485 
  1469 orig_add_main_variable_insert = stmts.Insert.add_main_variable
  1486 
  1470 def bw_add_main_variable_insert(self, etype, vref):
  1487 def bw_add_main_variable_insert(self, etype, vref):
  1471     return orig_add_main_variable_insert(self, bw_normalize_etype(etype), vref)
  1488     return orig_add_main_variable_insert(self, bw_normalize_etype(etype), vref)
       
  1489 orig_add_main_variable_insert = stmts.Insert.add_main_variable
  1472 stmts.Insert.add_main_variable = bw_add_main_variable_insert
  1490 stmts.Insert.add_main_variable = bw_add_main_variable_insert
  1473 
  1491 
  1474 orig_set_statement_type = stmts.Select.set_statement_type
  1492 
  1475 def bw_set_statement_type(self, etype):
  1493 def bw_set_statement_type(self, etype):
  1476     return orig_set_statement_type(self, bw_normalize_etype(etype))
  1494     return orig_set_statement_type(self, bw_normalize_etype(etype))
       
  1495 orig_set_statement_type = stmts.Select.set_statement_type
  1477 stmts.Select.set_statement_type = bw_set_statement_type
  1496 stmts.Select.set_statement_type = bw_set_statement_type