Commit 7ff890ef authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

reldefsecurity branch :

* follow yams default branch api changes
* now consider permissions on relation definitions, not relation types.

This is still experimental.

--HG--
branch : reldefsecurity
parent 1169d3154be6
......@@ -92,7 +92,7 @@ class TestEnvironment(object):
schema = self.vreg.schema
# else we may run into problems since email address are ususally share in app tests
# XXX should not be necessary anymore
schema.rschema('primary_email').set_rproperty('CWUser', 'EmailAddress', 'composite', False)
schema.rschema('primary_email').rdef('CWUser', 'EmailAddress').composite = False
self.deletable_entities = unprotected_entities(schema)
def restore_database(self):
......
......@@ -34,7 +34,7 @@ _marker = object()
def greater_card(rschema, subjtypes, objtypes, index):
for subjtype in subjtypes:
for objtype in objtypes:
card = rschema.rproperty(subjtype, objtype, 'cardinality')[index]
card = rschema.rdef(subjtype, objtype).cardinality[index]
if card in '+*':
return card
return '1'
......@@ -243,7 +243,8 @@ class Entity(AppObject, dict):
cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
attr, cls.id)
continue
if not user.matching_groups(rschema.get_groups('read')):
rdef = eschema.rdef(attr)
if not user.matching_groups(rdef.get_groups('read')):
continue
var = varmaker.next()
selection.append(var)
......@@ -252,7 +253,7 @@ class Entity(AppObject, dict):
if not rschema.final:
# XXX this does not handle several destination types
desttype = rschema.objects(eschema.type)[0]
card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
card = rdef.cardinality[0]
if card not in '?1':
cls.warning('bad relation %s specified in fetch attrs for %s',
attr, cls)
......@@ -362,10 +363,10 @@ class Entity(AppObject, dict):
self.req.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
def check_perm(self, action):
self.e_schema.check_perm(self.req, action, self.eid)
self.e_schema.check_perm(self.req, action, eid=self.eid)
def has_perm(self, action):
return self.e_schema.has_perm(self.req, action, self.eid)
return self.e_schema.has_perm(self.req, action, eid=self.eid)
def view(self, vid, __registry='views', **kwargs):
"""shortcut to apply a view on this entity"""
......@@ -443,11 +444,11 @@ class Entity(AppObject, dict):
return u''
if attrtype is None:
attrtype = self.e_schema.destination(attr)
props = self.e_schema.rproperties(attr)
props = self.e_schema.rdef(attr)
if attrtype == 'String':
# internalinalized *and* formatted string such as schema
# description...
if props.get('internationalizable'):
if props.internationalizable:
value = self.req._(value)
attrformat = self.attr_metadata(attr, 'format')
if attrformat:
......@@ -495,11 +496,12 @@ class Entity(AppObject, dict):
if rschema.type in self.skip_copy_for:
continue
# skip composite relation
if self.e_schema.subjrproperty(rschema, 'composite'):
rdef = self.e_schema.rdef(rschema)
if rdef.composite:
continue
# skip relation with card in ?1 else we either change the copied
# object (inlined relation) or inserting some inconsistency
if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1':
if rdef.cardinality[1] in '?1':
continue
rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
rschema.type, rschema.type)
......@@ -509,14 +511,15 @@ class Entity(AppObject, dict):
if rschema.meta:
continue
# skip already defined relations
if getattr(self, 'reverse_%s' % rschema.type):
if self.related(rschema.type, 'object'):
continue
rdef = self.e_schema.rdef(rschema, 'object')
# skip composite relation
if self.e_schema.objrproperty(rschema, 'composite'):
if rdef.composite:
continue
# skip relation with card in ?1 else we either change the copied
# object (inlined relation) or inserting some inconsistency
if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1':
if rdef.cardinality[0] in '?1':
continue
rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
rschema.type, rschema.type)
......@@ -537,15 +540,16 @@ class Entity(AppObject, dict):
for rschema in self.e_schema.subject_relations():
if rschema.final:
continue
if len(rschema.objects(self.e_schema)) > 1:
targets = rschema.objects(self.e_schema)
if len(targets) > 1:
# ambigous relations, the querier doesn't handle
# outer join correctly in this case
continue
if rschema.inlined:
rdef = rschema.rdef(self.e_schema, targets[0])
matching_groups = self.req.user.matching_groups
if matching_groups(rschema.get_groups('read')) and \
all(matching_groups(es.get_groups('read'))
for es in rschema.objects(self.e_schema)):
if matching_groups(rdef.get_groups('read')) and \
all(matching_groups(e.get_groups('read')) for e in targets):
yield rschema, 'subject'
def to_complete_attributes(self, skip_bytes=True):
......@@ -557,7 +561,8 @@ class Entity(AppObject, dict):
if attr == 'eid':
continue
# password retreival is blocked at the repository server level
if not self.req.user.matching_groups(rschema.get_groups('read')) \
rdef = rschema.rdef(self.e_schema, attrschema)
if not self.req.user.matching_groups(rdef.get_groups('read')) \
or attrschema.type == 'Password':
self[attr] = None
continue
......@@ -593,24 +598,21 @@ class Entity(AppObject, dict):
if self.relation_cached(rtype, role):
continue
var = varmaker.next()
targettype = rschema.targets(self.e_schema, role)[0]
rdef = rschema.role_rdef(self.e_schema, targettype, role)
card = rdef.role_cardinality(role)
assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
role, card)
if role == 'subject':
targettype = rschema.objects(self.e_schema)[0]
card = rschema.rproperty(self.e_schema, targettype,
'cardinality')[0]
if card == '1':
rql.append('%s %s %s' % (V, rtype, var))
else: # '?"
else:
rql.append('%s %s %s?' % (V, rtype, var))
else:
targettype = rschema.subjects(self.e_schema)[1]
card = rschema.rproperty(self.e_schema, targettype,
'cardinality')[1]
if card == '1':
rql.append('%s %s %s' % (var, rtype, V))
else: # '?"
else:
rql.append('%s? %s %s' % (var, rtype, V))
assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
role, card)
selected.append(((rtype, role), var))
if selected:
# select V, we need it as the left most selected variable
......@@ -756,16 +758,16 @@ class Entity(AppObject, dict):
restriction = []
args = {}
securitycheck_args = {}
insertsecurity = (rtype.has_local_role('add') and not
rtype.has_perm(self.req, 'add', **securitycheck_args))
constraints = rtype.rproperty(subjtype, objtype, 'constraints')
rdef = rtype.role_rdef(self.e_schema, targettype, role)
insertsecurity = (rdef.has_local_role('add') and not
rdef.has_perm(self.req, 'add', **securitycheck_args))
if vocabconstraints:
# RQLConstraint is a subclass for RQLVocabularyConstraint, so they
# will be included as well
restriction += [cstr.restriction for cstr in constraints
restriction += [cstr.restriction for cstr in rdef.constraints
if isinstance(cstr, RQLVocabularyConstraint)]
else:
restriction += [cstr.restriction for cstr in constraints
restriction += [cstr.restriction for cstr in rdef.constraints
if isinstance(cstr, RQLConstraint)]
etypecls = self.vreg['etypes'].etype_class(targettype)
rql = etypecls.fetch_rql(self.req.user, restriction,
......@@ -775,7 +777,7 @@ class Entity(AppObject, dict):
before, after = rql.split(' WHERE ', 1)
rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
if insertsecurity:
rqlexprs = rtype.get_rqlexprs('add')
rqlexprs = rdef.get_rqlexprs('add')
rewriter = RQLRewriter(self.req)
rqlst = self.req.vreg.parse(self.req, rql, args)
if not self.has_eid():
......@@ -827,12 +829,10 @@ class Entity(AppObject, dict):
related = tuple(rset.entities(col))
rschema = self.schema.rschema(rtype)
if role == 'subject':
rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
'cardinality')[1]
rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
target = 'object'
else:
rcard = rschema.rproperty(related[0].e_schema, self.e_schema,
'cardinality')[0]
rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
target = 'subject'
if rcard in '?1':
for rentity in related:
......
......@@ -402,12 +402,12 @@ class RQLRewriter(object):
orel = self.varinfo['lhs_rels'][sniprel.r_type]
cardindex = 0
ttypes_func = rschema.objects
rprop = rschema.rproperty
rdef = rschema.rdef
else: # target == 'subject':
orel = self.varinfo['rhs_rels'][sniprel.r_type]
cardindex = 1
ttypes_func = rschema.subjects
rprop = lambda x, y, z: rschema.rproperty(y, x, z)
rdef = lambda x, y: rschema.rdef(y, x)
except KeyError, ex:
# may be raised by self.varinfo['xhs_rels'][sniprel.r_type]
return None
......@@ -419,7 +419,7 @@ class RQLRewriter(object):
# variable from the original query
for etype in self.varinfo['stinfo']['possibletypes']:
for ttype in ttypes_func(etype):
if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
if rdef(etype, ttype).cardinality[cardindex] in '+*':
return None
return orel
......
......@@ -403,25 +403,22 @@ class ResultSet(object):
select = rqlst
# take care, due to outer join support, we may find None
# values for non final relation
for i, attr, x in attr_desc_iterator(select, col):
for i, attr, role in attr_desc_iterator(select, col):
outerselidx = rqlst.subquery_selection_index(select, i)
if outerselidx is None:
continue
if x == 'subject':
if role == 'subject':
rschema = eschema.subjrels[attr]
if rschema.final:
entity[attr] = rowvalues[outerselidx]
continue
tetype = rschema.objects(etype)[0]
card = rschema.rproperty(etype, tetype, 'cardinality')[0]
else:
rschema = eschema.objrels[attr]
tetype = rschema.subjects(etype)[0]
card = rschema.rproperty(tetype, etype, 'cardinality')[1]
rdef = eschema.rdef(attr, role)
# only keep value if it can't be multivalued
if card in '1?':
if rdef.role_cardinality(role) in '1?':
if rowvalues[outerselidx] is None:
if x == 'subject':
if role == 'subject':
rql = 'Any Y WHERE X %s Y, X eid %s'
else:
rql = 'Any Y WHERE Y %s X, X eid %s'
......@@ -429,7 +426,7 @@ class ResultSet(object):
req.decorate_rset(rrset)
else:
rrset = self._build_entity(row, outerselidx).as_rset()
entity.set_related_cache(attr, x, rrset)
entity.set_related_cache(attr, role, rrset)
return entity
@cached
......
......@@ -20,7 +20,8 @@ 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.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
RelationDefinitionSchema, PermissionMixIn
from yams.constraints import (BaseConstraint, StaticVocabularyConstraint,
FormatConstraint)
from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
......@@ -127,7 +128,7 @@ def ERSchema_display_name(self, req, form=''):
ERSchema.display_name = ERSchema_display_name
@cached
def ERSchema_get_groups(self, action):
def get_groups(self, action):
"""return the groups authorized to perform <action> on entities of
this type
......@@ -140,28 +141,13 @@ def ERSchema_get_groups(self, action):
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 <action> 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 <action> on entities of this type
......@@ -174,27 +160,13 @@ def ERSchema_get_rqlexprs(self, action):
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 <action> on entities of this type. Don't
change groups for the same action.
:type action: str
:param action: the name of a permission
: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
PermissionMixIn.get_rqlexprs = get_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 <action> on
entities of this type
......@@ -204,22 +176,12 @@ def ERSchema_set_permissions(self, action, permissions):
: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)
......@@ -230,9 +192,83 @@ def ERSchema_has_local_role(self, action):
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 '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)
elif self.final and isinstance(group_or_rqlexpr, RRQLExpression):
if 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 %s, use an ERQLExpression"
raise BadSchemaDefinition(msg % self)
elif 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
def system_etypes(schema):
......@@ -256,8 +292,8 @@ class CubicWebEntitySchema(EntitySchema):
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():
if getattr(self, 'permissions', None):
for groups in self.permissions.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)"
......@@ -304,7 +340,7 @@ class CubicWebEntitySchema(EntitySchema):
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':
......@@ -329,32 +365,12 @@ class CubicWebEntitySchema(EntitySchema):
"""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:
......@@ -369,73 +385,52 @@ class CubicWebRelationSchema(RelationSchema):
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 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 may_have_permission(self, action, req, eschema=None, role=None):
if eschema is not None:
for tschema in rschema.targets(eschema, role):
rdef = rschema.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 rql_expression(self, expression, mainvars=None, eid=None):
"""rql expression factory"""
if self.final:
return ERQLExpression(expression, mainvars, eid)
return RRQLExpression(expression, mainvars, eid)
def has_perm(self, session, action, **kwargs):
"""return true if the action is granted globaly or localy"""
if 'fromeid' in kwargs:
subjtype = session.describe(kwargs['fromeid'])
else:
subjtype = None
if 'toeid' in kwargs:
objtype = session.describe(kwargs['toeid'])
else:
objtype = Nono
if objtype and subjtype: