X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fsecurity.py;h=e3841ba298508776a4667d7affdc08066152e655;hb=80cd6f610ee95dbd3f49638ec2ebf3f16e3587c0;hp=0526b4eb8c66e3e0730d550fc42744a7229d4cce;hpb=8e00f73a02f9bb8d84d7aac739d829cfecb246e9;p=roundup.git diff --git a/roundup/security.py b/roundup/security.py index 0526b4e..e3841ba 100644 --- a/roundup/security.py +++ b/roundup/security.py @@ -1,24 +1,95 @@ +"""Handle the security declarations used in Roundup trackers. +""" +__docformat__ = 'restructuredtext' + import weakref -from roundup import hyperdb +from roundup import hyperdb, support class Permission: ''' Defines a Permission with the attributes - name - description - klass (optional) + - properties (optional) + - check function (optional) The klass may be unset, indicating that this permission is not locked to a particular class. That means there may be multiple Permissions for the same name for different classes. + + If property names are set, permission is restricted to those + properties only. + + If check function is set, permission is granted only when + the function returns value interpreted as boolean true. + The function is called with arguments db, userid, itemid. ''' - def __init__(self, name='', description='', klass=None): + def __init__(self, name='', description='', klass=None, + properties=None, check=None): self.name = name self.description = description self.klass = klass + self.properties = properties + self._properties_dict = support.TruthDict(properties) + self.check = check + + def test(self, db, permission, classname, property, userid, itemid): + if permission != self.name: + return 0 + + # are we checking the correct class + if self.klass is not None and self.klass != classname: + return 0 + + # what about property? + if property is not None and not self._properties_dict[property]: + return 0 + + # check code + if itemid is not None and self.check is not None: + if not self.check(db, userid, itemid): + return 0 + + # we have a winner + return 1 + + def searchable(self, classname, property): + """ A Permission is searchable for the given permission if it + doesn't include a check method and otherwise matches the + given parameters. + """ + if self.name not in ('View', 'Search'): + return 0 + + # are we checking the correct class + if self.klass is not None and self.klass != classname: + return 0 + + # what about property? + if not self._properties_dict[property]: + return 0 + + if self.check: + return 0 + + return 1 + def __repr__(self): - return ''%(id(self), self.name, self.klass) + return ''%(id(self), self.name, + self.klass, self.properties, self.check) + + def __cmp__(self, other): + if self.name != other.name: + return cmp(self.name, other.name) + + if self.klass != other.klass: return 1 + if self.properties != other.properties: return 1 + if self.check != other.check: return 1 + + # match + return 0 class Role: ''' Defines a Role with the attributes @@ -27,7 +98,7 @@ class Role: - permissions ''' def __init__(self, name='', description='', permissions=None): - self.name = name + self.name = name.lower() self.description = description if permissions is None: permissions = [] @@ -54,16 +125,11 @@ class Security: self.addRole(name="Admin", description="An admin user, full privs") self.addRole(name="Anonymous", description="An anonymous user") - ee = self.addPermission(name="Edit", - description="User may edit everthing") - self.addPermissionToRole('Admin', ee) - ae = self.addPermission(name="View", - description="User may access everything") - self.addPermissionToRole('Admin', ae) - reg = self.addPermission(name="Register Web", - description="User may register through the web") - reg = self.addPermission(name="Register Email", - description="User may register through the email") + # default permissions - Admin may do anything + for p in 'create edit retire view'.split(): + p = self.addPermission(name=p.title(), + description="User may %s everthing"%p) + self.addPermissionToRole('Admin', p) # initialise the permissions and roles needed for the UIs from roundup.cgi import client @@ -71,7 +137,8 @@ class Security: from roundup import mailgw mailgw.initialiseSecurity(self) - def getPermission(self, permission, classname=None): + def getPermission(self, permission, classname=None, properties=None, + check=None): ''' Find the Permission matching the name and for the class, if the classname is specified. @@ -80,64 +147,135 @@ class Security: if not self.permission.has_key(permission): raise ValueError, 'No permission "%s" defined'%permission + if classname: + try: + self.db.getclass(classname) + except KeyError: + raise ValueError, 'No class "%s" defined'%classname + # look through all the permissions of the given name + tester = Permission(permission, klass=classname, properties=properties, + check=check) for perm in self.permission[permission]: - # if we're passed a classname, the permission must match - if perm.klass is not None and perm.klass == classname: - return perm - # otherwise the permission klass must be unset - elif not perm.klass and not classname: + if perm == tester: return perm raise ValueError, 'No permission "%s" defined for "%s"'%(permission, classname) - def hasPermission(self, permission, userid, classname=None): - ''' Look through all the Roles, and hence Permissions, and see if - "permission" is there for the specified classname. + def hasPermission(self, permission, userid, classname=None, + property=None, itemid=None): + '''Look through all the Roles, and hence Permissions, and + see if "permission" exists given the constraints of + classname, property and itemid. + + If classname is specified (and only classname) then the + search will match if there is *any* Permission for that + classname, even if the Permission has additional + constraints. + + If property is specified, the Permission matched must have + either no properties listed or the property must appear in + the list. + + If itemid is specified, the Permission matched must have + either no check function defined or the check function, + when invoked, must return a True value. + + Note that this functionality is actually implemented by the + Permission.test() method. ''' - roles = self.db.user.get(userid, 'roles') - if roles is None: - return 0 - for rolename in roles.split(','): + if itemid and classname is None: + raise ValueError, 'classname must accompany itemid' + for rolename in self.db.user.get_roles(userid): if not rolename or not self.role.has_key(rolename): continue # for each of the user's Roles, check the permissions for perm in self.role[rolename].permissions: - # permission name match? - if perm.name == permission: - # permission klass match? - if perm.klass is None or perm.klass == classname: - # we have a winner - return 1 + # permission match? + if perm.test(self.db, permission, classname, property, + userid, itemid): + return 1 + return 0 + + def roleHasSearchPermission(self, classname, property, *rolenames): + """ For each of the given roles, check the permissions. + Property can be a transitive property. + """ + perms = [] + # pre-compute permissions + for rn in rolenames : + for perm in self.role[rn].permissions: + perms.append(perm) + # Note: break from inner loop means "found" + # break from outer loop means "not found" + cn = classname + prev = None + prop = None + Link = hyperdb.Link + Multilink = hyperdb.Multilink + for propname in property.split('.'): + if prev: + try: + cn = prop.classname + except AttributeError: + break + prev = propname + try: + cls = self.db.getclass(cn) + prop = cls.getprops()[propname] + except KeyError: + break + for perm in perms: + if perm.searchable(cn, propname): + break + else: + break + else: + # for Link and Multilink require search permission on label- + # and order-properties and on ID + if isinstance(prop, Multilink) or isinstance(prop, Link): + try: + cls = self.db.getclass(prop.classname) + except KeyError: + return 0 + props = dict.fromkeys(('id', cls.labelprop(), cls.orderprop())) + for p in props.iterkeys(): + for perm in perms: + if perm.searchable(prop.classname, p): + break + else: + return 0 + return 1 return 0 - def hasNodePermission(self, classname, nodeid, **propspec): - ''' Check the named properties of the given node to see if the - userid appears in them. If it does, then the user is granted - this permission check. + def hasSearchPermission(self, userid, classname, property): + '''Look through all the Roles, and hence Permissions, and + see if "permission" exists given the constraints of + classname and property. + + A search permission is granted if we find a 'View' or + 'Search' permission for the user which does *not* include + a check function. If such a permission is found, the user may + search for the given property in the given class. - 'propspec' consists of a set of properties and values that - must be present on the given node for access to be granted. + Note that classname *and* property are mandatory arguments. - If a property is a Link, the value must match the property - value. If a property is a Multilink, the value must appear - in the Multilink list. + Contrary to hasPermission, the search will *not* match if + there are additional constraints (namely a search function) + on a Permission found. + + Concerning property, the Permission matched must have + either no properties listed or the property must appear in + the list. ''' - klass = self.db.getclass(classname) - properties = klass.getprops() - for k,v in propspec.items(): - value = klass.get(nodeid, k) - if isinstance(properties[k], hyperdb.Multilink): - if v not in value: - return 0 - else: - if v != value: - return 0 - return 1 + roles = [r for r in self.db.user.get_roles(userid) + if r and self.role.has_key(r)] + return self.roleHasSearchPermission (classname, property, *roles) def addPermission(self, **propspec): ''' Create a new Permission with the properties defined in - 'propspec' + 'propspec'. See the Permission class for the possible + keyword args. ''' perm = Permission(**propspec) self.permission.setdefault(perm.name, []).append(perm) @@ -150,12 +288,39 @@ class Security: self.role[role.name] = role return role - def addPermissionToRole(self, rolename, permission): + def addPermissionToRole(self, rolename, permission, classname=None, + properties=None, check=None): ''' Add the permission to the role's permission list. 'rolename' is the name of the role to add the permission to. + + 'permission' is either a Permission *or* a permission name + accompanied by 'classname' (thus in the second case a Permission + is obtained by passing 'permission' and 'classname' to + self.getPermission) ''' - role = self.role[rolename] + if not isinstance(permission, Permission): + permission = self.getPermission(permission, classname, + properties, check) + role = self.role[rolename.lower()] role.permissions.append(permission) -# vim: set filetype=python ts=4 sw=4 et si + # Convenience methods for removing non-allowed properties from a + # filterspec or sort/group list + + def filterFilterspec(self, userid, classname, filterspec): + """ Return a filterspec that has all non-allowed properties removed. + """ + return dict ([(k, v) for k, v in filterspec.iteritems() + if self.hasSearchPermission(userid,classname,k)]) + + def filterSortspec(self, userid, classname, sort): + """ Return a sort- or group-list that has all non-allowed properties + removed. + """ + if isinstance(sort, tuple) and sort[0] in '+-': + sort = [sort] + return [(d, p) for d, p in sort + if self.hasSearchPermission(userid,classname,p)] + +# vim: set filetype=python sts=4 sw=4 et si :