diff -r c25da7573ebd -r 02b52bf9f5f8 schema.py --- a/schema.py Fri Feb 12 15:18:00 2010 +0100 +++ b/schema.py Wed Mar 24 10:23:31 2010 +0100 @@ -15,14 +15,14 @@ from logilab.common.decorators import cached, clear_cache, monkeypatch from logilab.common.logging_ext import set_log_methods -from logilab.common.deprecation import deprecated +from logilab.common.deprecation import deprecated, class_moved from logilab.common.graph import get_cycles from logilab.common.compat import any from yams import BadSchemaDefinition, buildobjs as ybo -from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema -from yams.constraints import (BaseConstraint, StaticVocabularyConstraint, - FormatConstraint) +from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \ + RelationDefinitionSchema, PermissionMixIn +from yams.constraints import BaseConstraint, FormatConstraint from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader, obsolete as yobsolete, cleanup_sys_modules) @@ -31,11 +31,6 @@ import cubicweb from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized -# XXX <3.2 bw compat -from yams import schema -schema.use_py_datetime() -nodes.use_py_datetime() - PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',)) VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',)) @@ -54,6 +49,13 @@ 'constrained_by', 'cstrtype', )) +WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow', + 'WorkflowTransition', 'BaseTransition', + 'SubWorkflowExitPoint')) + +INTERNAL_TYPES = set(('CWProperty', 'CWPermission', 'CWCache', 'ExternalUri')) + + _LOGGER = getLogger('cubicweb.schemaloader') # schema entities created from serialized schema have an eid rproperty @@ -62,6 +64,31 @@ ybo.RDEF_PROPERTIES += ('eid',) +PUB_SYSTEM_ENTITY_PERMS = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), + 'delete': ('managers',), + 'update': ('managers',), + } +PUB_SYSTEM_REL_PERMS = { + 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), + 'delete': ('managers',), + } +PUB_SYSTEM_ATTR_PERMS = { + 'read': ('managers', 'users', 'guests',), + 'update': ('managers',), + } +RO_REL_PERMS = { + 'read': ('managers', 'users', 'guests',), + 'add': (), + 'delete': (), + } +RO_ATTR_PERMS = { + 'read': ('managers', 'users', 'guests',), + 'update': (), + } + # XXX same algorithm as in reorder_cubes and probably other place, # may probably extract a generic function def order_eschemas(eschemas): @@ -117,7 +144,7 @@ else: return unicode(req._(key)).lower() -__builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name) +__builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name) # rql expression utilities function ############################################ @@ -151,15 +178,15 @@ # Schema objects definition ################################################### -def ERSchema_display_name(self, req, form=''): +def ERSchema_display_name(self, req, form='', context=None): """return a internationalized string for the entity/relation type name in a given form """ - return display_name(req, self.type, form) + return display_name(req, self.type, form, context) ERSchema.display_name = ERSchema_display_name @cached -def ERSchema_get_groups(self, action): +def get_groups(self, action): """return the groups authorized to perform on entities of this type @@ -172,28 +199,13 @@ assert action in self.ACTIONS, action #assert action in self._groups, '%s %s' % (self, action) try: - return frozenset(g for g in self._groups[action] if isinstance(g, basestring)) + return frozenset(g for g in self.permissions[action] if isinstance(g, basestring)) except KeyError: return () -ERSchema.get_groups = ERSchema_get_groups - -def ERSchema_set_groups(self, action, groups): - """set the groups allowed to perform on entities of this type. Don't - change rql expressions for the same action. - - :type action: str - :param action: the name of a permission - - :type groups: list or tuple - :param groups: names of the groups granted to do the given action - """ - assert action in self.ACTIONS, action - clear_cache(self, 'ERSchema_get_groups') - self._groups[action] = tuple(groups) + self.get_rqlexprs(action) -ERSchema.set_groups = ERSchema_set_groups +PermissionMixIn.get_groups = get_groups @cached -def ERSchema_get_rqlexprs(self, action): +def get_rqlexprs(self, action): """return the rql expressions representing queries to check the user is allowed to perform on entities of this type @@ -206,27 +218,13 @@ assert action in self.ACTIONS, action #assert action in self._rqlexprs, '%s %s' % (self, action) try: - return tuple(g for g in self._groups[action] if not isinstance(g, basestring)) + return tuple(g for g in self.permissions[action] if not isinstance(g, basestring)) except KeyError: return () -ERSchema.get_rqlexprs = ERSchema_get_rqlexprs - -def ERSchema_set_rqlexprs(self, action, rqlexprs): - """set the rql expression allowing to perform on entities of this type. Don't - change groups for the same action. - - :type action: str - :param action: the name of a permission +PermissionMixIn.get_rqlexprs = get_rqlexprs - :type rqlexprs: list or tuple - :param rqlexprs: the rql expressions allowing the given action - """ - assert action in self.ACTIONS, action - clear_cache(self, 'ERSchema_get_rqlexprs') - self._groups[action] = tuple(self.get_groups(action)) + tuple(rqlexprs) -ERSchema.set_rqlexprs = ERSchema_set_rqlexprs - -def ERSchema_set_permissions(self, action, permissions): +orig_set_action_permissions = PermissionMixIn.set_action_permissions +def set_action_permissions(self, action, permissions): """set the groups and rql expressions allowing to perform on entities of this type @@ -236,22 +234,12 @@ :type permissions: tuple :param permissions: the groups and rql expressions allowing the given action """ - assert action in self.ACTIONS, action - clear_cache(self, 'ERSchema_get_rqlexprs') - clear_cache(self, 'ERSchema_get_groups') - self._groups[action] = tuple(permissions) -ERSchema.set_permissions = ERSchema_set_permissions + orig_set_action_permissions(self, action, tuple(permissions)) + clear_cache(self, 'get_rqlexprs') + clear_cache(self, 'get_groups') +PermissionMixIn.set_action_permissions = set_action_permissions -def ERSchema_has_perm(self, session, action, *args, **kwargs): - """return true if the action is granted globaly or localy""" - try: - self.check_perm(session, action, *args, **kwargs) - return True - except Unauthorized: - return False -ERSchema.has_perm = ERSchema_has_perm - -def ERSchema_has_local_role(self, action): +def has_local_role(self, action): """return true if the action *may* be granted localy (eg either rql expressions or the owners group are used in security definition) @@ -262,9 +250,73 @@ if self.get_rqlexprs(action): return True if action in ('update', 'delete'): - return self.has_group(action, 'owners') + return 'owners' in self.get_groups(action) return False -ERSchema.has_local_role = ERSchema_has_local_role +PermissionMixIn.has_local_role = has_local_role + +def may_have_permission(self, action, req): + if action != 'read' and not (self.has_local_role('read') or + self.has_perm(req, 'read')): + return False + return self.has_local_role(action) or self.has_perm(req, action) +PermissionMixIn.may_have_permission = may_have_permission + +def has_perm(self, session, action, **kwargs): + """return true if the action is granted globaly or localy""" + try: + self.check_perm(session, action, **kwargs) + return True + except Unauthorized: + return False +PermissionMixIn.has_perm = has_perm + +def check_perm(self, session, action, **kwargs): + # NB: session may be a server session or a request object check user is + # in an allowed group, if so that's enough internal sessions should + # always stop there + groups = self.get_groups(action) + if session.user.matching_groups(groups): + return + # if 'owners' in allowed groups, check if the user actually owns this + # object, if so that's enough + if 'owners' in groups and ( + kwargs.get('creating') + or ('eid' in kwargs and session.user.owns(kwargs['eid']))): + return + # else if there is some rql expressions, check them + if any(rqlexpr.check(session, **kwargs) + for rqlexpr in self.get_rqlexprs(action)): + return + raise Unauthorized(action, str(self)) +PermissionMixIn.check_perm = check_perm + + +RelationDefinitionSchema._RPROPERTIES['eid'] = None + +def rql_expression(self, expression, mainvars=None, eid=None): + """rql expression factory""" + if self.rtype.final: + return ERQLExpression(expression, mainvars, eid) + return RRQLExpression(expression, mainvars, eid) +RelationDefinitionSchema.rql_expression = rql_expression + +orig_check_permission_definitions = RelationDefinitionSchema.check_permission_definitions +def check_permission_definitions(self): + orig_check_permission_definitions(self) + schema = self.subject.schema + for action, groups in self.permissions.iteritems(): + for group_or_rqlexpr in groups: + if action == 'read' and \ + isinstance(group_or_rqlexpr, RQLExpression): + msg = "can't use rql expression for read permission of %s" + raise BadSchemaDefinition(msg % self) + if self.final and isinstance(group_or_rqlexpr, RRQLExpression): + msg = "can't use RRQLExpression on %s, use an ERQLExpression" + raise BadSchemaDefinition(msg % self) + if not self.final and isinstance(group_or_rqlexpr, ERQLExpression): + msg = "can't use ERQLExpression on %s, use a RRQLExpression" + raise BadSchemaDefinition(msg % self) +RelationDefinitionSchema.check_permission_definitions = check_permission_definitions class CubicWebEntitySchema(EntitySchema): @@ -277,13 +329,14 @@ if eid is None and edef is not None: eid = getattr(edef, 'eid', None) self.eid = eid - # take care: no _groups attribute when deep-copying - if getattr(self, '_groups', None): - for groups in self._groups.itervalues(): - for group_or_rqlexpr in groups: - if isinstance(group_or_rqlexpr, RRQLExpression): - msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)" - raise BadSchemaDefinition(msg % self.type) + + def check_permission_definitions(self): + super(CubicWebEntitySchema, self).check_permission_definitions() + for groups in self.permissions.itervalues(): + for group_or_rqlexpr in groups: + if isinstance(group_or_rqlexpr, RRQLExpression): + msg = "can't use RRQLExpression on %s, use an ERQLExpression" + raise BadSchemaDefinition(msg % self.type) def attribute_definitions(self): """return an iterator on attribute definitions @@ -326,7 +379,7 @@ if rschema.final: if rschema == 'has_text': has_has_text = True - elif self.rproperty(rschema, 'fulltextindexed'): + elif self.rdef(rschema).get('fulltextindexed'): may_need_has_text = True elif rschema.fulltext_container: if rschema.fulltext_container == 'subject': @@ -342,7 +395,8 @@ if need_has_text is None: need_has_text = may_need_has_text if need_has_text and not has_has_text and not deletion: - rdef = ybo.RelationDefinition(self.type, 'has_text', 'String') + rdef = ybo.RelationDefinition(self.type, 'has_text', 'String', + __permissions__=RO_ATTR_PERMS) self.schema.add_relation_def(rdef) elif not need_has_text and has_has_text: self.schema.del_relation_def(self.type, 'has_text', 'String') @@ -351,32 +405,12 @@ """return True if this entity type is used to build the schema""" return self.type in SCHEMA_TYPES - def check_perm(self, session, action, eid=None): - # NB: session may be a server session or a request object - user = session.user - # check user is in an allowed group, if so that's enough - # internal sessions should always stop there - if user.matching_groups(self.get_groups(action)): - return - # if 'owners' in allowed groups, check if the user actually owns this - # object, if so that's enough - if eid is not None and 'owners' in self.get_groups(action) and \ - user.owns(eid): - return - # else if there is some rql expressions, check them - if any(rqlexpr.check(session, eid) - for rqlexpr in self.get_rqlexprs(action)): - return - raise Unauthorized(action, str(self)) - def rql_expression(self, expression, mainvars=None, eid=None): """rql expression factory""" return ERQLExpression(expression, mainvars, eid) class CubicWebRelationSchema(RelationSchema): - RelationSchema._RPROPERTIES['eid'] = None - _perms_checked = False def __init__(self, schema=None, rdef=None, eid=None, **kwargs): if rdef is not None: @@ -391,80 +425,69 @@ def meta(self): return self.type in META_RTYPES - def update(self, subjschema, objschema, rdef): - super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef) - if not self._perms_checked and self._groups: - for action, groups in self._groups.iteritems(): - for group_or_rqlexpr in groups: - if action == 'read' and \ - isinstance(group_or_rqlexpr, RQLExpression): - msg = "can't use rql expression for read permission of "\ - "a relation type (%s)" - raise BadSchemaDefinition(msg % self.type) - elif self.final and isinstance(group_or_rqlexpr, RRQLExpression): - if self.schema.reading_from_database: - # we didn't have final relation earlier, so turn - # RRQLExpression into ERQLExpression now - rqlexpr = group_or_rqlexpr - newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr] - newrqlexprs.append(ERQLExpression(rqlexpr.expression, - rqlexpr.mainvars, - rqlexpr.eid)) - self.set_rqlexprs(action, newrqlexprs) - else: - msg = "can't use RRQLExpression on a final relation "\ - "type (eg attribute relation), use an ERQLExpression (%s)" - raise BadSchemaDefinition(msg % self.type) - elif not self.final and \ - isinstance(group_or_rqlexpr, ERQLExpression): - msg = "can't use ERQLExpression on a relation type, use "\ - "a RRQLExpression (%s)" - raise BadSchemaDefinition(msg % self.type) - self._perms_checked = True - - def cardinality(self, subjtype, objtype, target): - card = self.rproperty(subjtype, objtype, 'cardinality') - return (target == 'subject' and card[0]) or \ - (target == 'object' and card[1]) - def schema_relation(self): """return True if this relation type is used to build the schema""" return self.type in SCHEMA_TYPES - def physical_mode(self): - """return an appropriate mode for physical storage of this relation type: - * 'subjectinline' if every possible subject cardinalities are 1 or ? - * 'objectinline' if 'subjectinline' mode is not possible but every - possible object cardinalities are 1 or ? - * None if neither 'subjectinline' and 'objectinline' - """ - assert not self.final - return self.inlined and 'subjectinline' or None + def may_have_permission(self, action, req, eschema=None, role=None): + if eschema is not None: + for tschema in self.targets(eschema, role): + rdef = self.role_rdef(eschema, tschema, role) + if rdef.may_have_permission(action, req): + return True + else: + for rdef in self.rdefs.itervalues(): + if rdef.may_have_permission(action, req): + return True + return False - def check_perm(self, session, action, *args, **kwargs): - # NB: session may be a server session or a request object check user is - # in an allowed group, if so that's enough internal sessions should - # always stop there - if session.user.matching_groups(self.get_groups(action)): - return - # else if there is some rql expressions, check them - if any(rqlexpr.check(session, *args, **kwargs) - for rqlexpr in self.get_rqlexprs(action)): - return - raise Unauthorized(action, str(self)) + def has_perm(self, session, action, **kwargs): + """return true if the action is granted globaly or localy""" + if self.final: + assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs + assert action in ('read', 'update') + if 'eid' in kwargs: + subjtype = session.describe(kwargs['eid'])[0] + else: + subjtype = objtype = None + else: + assert not 'eid' in kwargs, kwargs + assert action in ('read', 'add', 'delete') + if 'fromeid' in kwargs: + subjtype = session.describe(kwargs['fromeid'])[0] + else: + subjtype = None + if 'toeid' in kwargs: + objtype = session.describe(kwargs['toeid'])[0] + else: + objtype = None + if objtype and subjtype: + return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs) + elif subjtype: + for tschema in self.targets(subjtype, 'subject'): + rdef = self.rdef(subjtype, tschema) + if not rdef.has_perm(session, action, **kwargs): + return False + elif objtype: + for tschema in self.targets(objtype, 'object'): + rdef = self.rdef(tschema, objtype) + if not rdef.has_perm(session, action, **kwargs): + return False + else: + for rdef in self.rdefs.itervalues(): + if not rdef.has_perm(session, action, **kwargs): + return False + return True - def rql_expression(self, expression, mainvars=None, eid=None): - """rql expression factory""" - if self.final: - return ERQLExpression(expression, mainvars, eid) - return RRQLExpression(expression, mainvars, eid) + @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)') + def cardinality(self, subjtype, objtype, target): + return self.rdef(subjtype, objtype).role_cardinality(target) class CubicWebSchema(Schema): """set of entities and relations schema defining the possible data sets used in an application - :type name: str :ivar name: name of the schema, usually the instance identifier @@ -482,13 +505,10 @@ ybo.register_base_types(self) rschema = self.add_relation_type(ybo.RelationType('eid')) rschema.final = True - rschema.set_default_groups() rschema = self.add_relation_type(ybo.RelationType('has_text')) rschema.final = True - rschema.set_default_groups() rschema = self.add_relation_type(ybo.RelationType('identity')) rschema.final = False - rschema.set_default_groups() def add_entity_type(self, edef): edef.name = edef.name.encode() @@ -498,9 +518,11 @@ if not eschema.final: # automatically add the eid relation to non final entity types rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int', - cardinality='11', uid=True) + cardinality='11', uid=True, + __permissions__=RO_ATTR_PERMS) self.add_relation_def(rdef) - rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type) + rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type, + __permissions__=RO_REL_PERMS) self.add_relation_def(rdef) self._eid_index[eschema.eid] = eschema return eschema @@ -530,13 +552,13 @@ rdef.name = rdef.name.lower() rdef.subject = bw_normalize_etype(rdef.subject) rdef.object = bw_normalize_etype(rdef.object) - if super(CubicWebSchema, self).add_relation_def(rdef): + rdefs = super(CubicWebSchema, self).add_relation_def(rdef) + if rdefs: try: - self._eid_index[rdef.eid] = (self.eschema(rdef.subject), - self.rschema(rdef.name), - self.eschema(rdef.object)) + self._eid_index[rdef.eid] = rdefs except AttributeError: pass # not a serialized schema + return rdefs def del_relation_type(self, rtype): rschema = self.rschema(rtype) @@ -545,7 +567,9 @@ def del_relation_def(self, subjtype, rtype, objtype): for k, v in self._eid_index.items(): - if v == (subjtype, rtype, objtype): + if not isinstance(v, RelationDefinitionSchema): + continue + if v.subject == subjtype and v.rtype == rtype and v.object == objtype: del self._eid_index[k] break super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype) @@ -730,6 +754,11 @@ def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.full_rql) + def __cmp__(self, other): + if hasattr(other, 'expression'): + return cmp(other.expression, self.expression) + return -1 + def __deepcopy__(self, memo): return self.__class__(self.expression, self.mainvars) def __getstate__(self): @@ -765,7 +794,7 @@ rqlst.add_selected(objvar) else: colindex = selected.index(objvar.name) - found.append((action, objvar, colindex)) + found.append((action, colindex)) # remove U eid %(u)s if U is not used in any other relation uvrefs = rqlst.defined_vars['U'].references() if len(uvrefs) == 1: @@ -787,20 +816,25 @@ session may actually be a request as well """ - if self.eid is not None: + creating = kwargs.get('creating') + if not creating and self.eid is not None: key = (self.eid, tuple(sorted(kwargs.iteritems()))) try: return session.local_perm_cache[key] except KeyError: pass rql, has_perm_defs, keyarg = self.transform_has_permission() + if creating: + # when creating an entity, consider has_*_permission satisfied + if has_perm_defs: + return True + return False if keyarg is None: # on the server side, use unsafe_execute, but this is not available # on the client side (session is actually a request) execute = getattr(session, 'unsafe_execute', session.execute) - # XXX what if 'u' in kwargs + kwargs.setdefault('u', session.user.eid) cachekey = kwargs.keys() - kwargs['u'] = session.user.eid try: rset = execute(rql, kwargs, cachekey, build_descr=True) except NotImplementedError: @@ -828,10 +862,10 @@ # check every special has_*_permission relation is satisfied get_eschema = session.vreg.schema.eschema try: - for eaction, var, col in has_perm_defs: + for eaction, col in has_perm_defs: for i in xrange(len(rset)): eschema = get_eschema(rset.description[i][col]) - eschema.check_perm(session, eaction, rset[i][col]) + eschema.check_perm(session, eaction, eid=rset[i][col]) if self.eid is not None: session.local_perm_cache[key] = True return True @@ -864,12 +898,15 @@ rql += ', U eid %(u)s' return rql - def check(self, session, eid=None): + def check(self, session, eid=None, creating=False, **kwargs): if 'X' in self.rqlst.defined_vars: if eid is None: + if creating: + return self._check(session, creating=True, **kwargs) return False - return self._check(session, x=eid) - return self._check(session) + assert creating == False + return self._check(session, x=eid, **kwargs) + return self._check(session, **kwargs) class RRQLExpression(RQLExpression): @@ -918,6 +955,11 @@ kwargs['o'] = toeid return self._check(session, **kwargs) +# in yams, default 'update' perm for attributes granted to managers and owners. +# Within cw, we want to default to users who may edit the entity holding the +# attribute. +ybo.DEFAULT_ATTRPERMS['update'] = ( + 'managers', ERQLExpression('U has_update_permission X')) # workflow extensions ######################################################### @@ -1036,13 +1078,21 @@ @monkeypatch(FormatConstraint) def vocabulary(self, entity=None, form=None): - req = None + cw = None if form is None and entity is not None: - req = entity.req + cw = entity._cw elif form is not None: - req = form.req - if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT): - return self.regular_formats + tuple(NEED_PERM_FORMATS) + cw = form._cw + if cw is not None: + if hasattr(cw, 'is_super_session'): + # cw is a server session + hasperm = cw.is_super_session or \ + not cw.vreg.config.is_hook_category_activated('integrity') or \ + cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT) + else: + hasperm = cw.user.has_permission(PERM_USE_TEMPLATE_FORMAT) + if hasperm: + return self.regular_formats + tuple(NEED_PERM_FORMATS) return self.regular_formats # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is @@ -1076,10 +1126,14 @@ # XXX deprecated -from yams.constraints import format_constraint from yams.buildobjs import RichString +from yams.constraints import StaticVocabularyConstraint + +RichString = class_moved(RichString) + +StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint) +FormatConstraint = class_moved(FormatConstraint) PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression) PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression) PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType -PyFileReader.context['format_constraint'] = format_constraint