Mercurial > p > roundup > code
comparison roundup/security.py @ 5196:e0732fd6a6c7
Implement props_only feature for permissions.
| author | rouilj@uland |
|---|---|
| date | Sat, 18 Mar 2017 10:44:10 -0400 |
| parents | 36630a062fb5 |
| children | 1f72b73d7770 |
comparison
equal
deleted
inserted
replaced
| 5195:270003714e5f | 5196:e0732fd6a6c7 |
|---|---|
| 3 __docformat__ = 'restructuredtext' | 3 __docformat__ = 'restructuredtext' |
| 4 | 4 |
| 5 import weakref | 5 import weakref |
| 6 | 6 |
| 7 from roundup import hyperdb, support | 7 from roundup import hyperdb, support |
| 8 | |
| 9 import logging | |
| 10 logger = logging.getLogger('roundup.security') | |
| 8 | 11 |
| 9 class Permission: | 12 class Permission: |
| 10 ''' Defines a Permission with the attributes | 13 ''' Defines a Permission with the attributes |
| 11 - name | 14 - name |
| 12 - description | 15 - description |
| 13 - klass (optional) | 16 - klass (optional) |
| 14 - properties (optional) | 17 - properties (optional) |
| 15 - check function (optional) | 18 - check function (optional) |
| 19 - props_only (optional, internal field is limit_perm_to_props_only) | |
| 16 | 20 |
| 17 The klass may be unset, indicating that this permission is not | 21 The klass may be unset, indicating that this permission is not |
| 18 locked to a particular class. That means there may be multiple | 22 locked to a particular class. That means there may be multiple |
| 19 Permissions for the same name for different classes. | 23 Permissions for the same name for different classes. |
| 20 | 24 |
| 22 properties only. | 26 properties only. |
| 23 | 27 |
| 24 If check function is set, permission is granted only when | 28 If check function is set, permission is granted only when |
| 25 the function returns value interpreted as boolean true. | 29 the function returns value interpreted as boolean true. |
| 26 The function is called with arguments db, userid, itemid. | 30 The function is called with arguments db, userid, itemid. |
| 31 | |
| 32 When the system checks klass permission rather than the klass | |
| 33 property permission (i.e. properties=None and item=None), it | |
| 34 will apply any permission that matches on permission name and | |
| 35 class. If the permission has a check function, the check | |
| 36 function will be run. By making the permission valid only for | |
| 37 properties using props_only=True the permission will be | |
| 38 skipped. You can set the default value for props_only for all | |
| 39 properties by calling: | |
| 40 | |
| 41 db.security.set_props_only_default() | |
| 42 | |
| 43 with a True or False value. | |
| 27 ''' | 44 ''' |
| 45 | |
| 46 limit_perm_to_props_only=False | |
| 47 | |
| 28 def __init__(self, name='', description='', klass=None, | 48 def __init__(self, name='', description='', klass=None, |
| 29 properties=None, check=None): | 49 properties=None, check=None, props_only=None): |
| 30 import inspect | 50 import inspect |
| 31 self.name = name | 51 self.name = name |
| 32 self.description = description | 52 self.description = description |
| 33 self.klass = klass | 53 self.klass = klass |
| 34 self.properties = properties | 54 self.properties = properties |
| 35 self._properties_dict = support.TruthDict(properties) | 55 self._properties_dict = support.TruthDict(properties) |
| 36 self.check = check | 56 self.check = check |
| 57 if properties is not None: | |
| 58 # Set to None unless properties are defined. | |
| 59 # This means that: | |
| 60 # a=Property(name="Edit", klass="issue", check=dummy, | |
| 61 # props_only=True) | |
| 62 # b=Property(name="Edit", klass="issue", check=dummy, | |
| 63 # props_only=False) | |
| 64 # a == b will be true. | |
| 65 if props_only is None: | |
| 66 self.limit_perm_to_props_only = \ | |
| 67 Permission.limit_perm_to_props_only | |
| 68 else: | |
| 69 # see note on use of bool() in set_props_only_default() | |
| 70 self.limit_perm_to_props_only = bool(props_only) | |
| 71 else: | |
| 72 self.limit_perm_to_props_only = None | |
| 73 | |
| 37 | 74 |
| 38 if check is None: | 75 if check is None: |
| 39 self.check_version = 0 | 76 self.check_version = 0 |
| 40 else: | 77 else: |
| 41 args=inspect.getargspec(check) | 78 args=inspect.getargspec(check) |
| 49 else: | 86 else: |
| 50 # function definition is function(db, userid, itemid, **other) | 87 # function definition is function(db, userid, itemid, **other) |
| 51 self.check_version = 2 | 88 self.check_version = 2 |
| 52 | 89 |
| 53 def test(self, db, permission, classname, property, userid, itemid): | 90 def test(self, db, permission, classname, property, userid, itemid): |
| 91 ''' Test permissions 5 args: | |
| 92 permission - string like Edit, Register etc. Required, no wildcard. | |
| 93 classname - string like issue, msg etc. Can be None to match any | |
| 94 class. | |
| 95 property - array of strings that are property names. Optional. | |
| 96 if None this is an item or klass access check. | |
| 97 userid - number that is id for user. | |
| 98 itemid - id for classname. e.g. 3 in issue3. If missing this is | |
| 99 a class access check, otherwies it's a object access check. | |
| 100 ''' | |
| 101 | |
| 54 if permission != self.name: | 102 if permission != self.name: |
| 55 return 0 | 103 return 0 |
| 56 | 104 |
| 57 # are we checking the correct class | 105 # are we checking the correct class |
| 58 if self.klass is not None and self.klass != classname: | 106 if self.klass is not None and self.klass != classname: |
| 59 return 0 | 107 return 0 |
| 60 | 108 |
| 61 # what about property? | 109 # what about property? |
| 62 if property is not None and not self._properties_dict[property]: | 110 if property is not None and not self._properties_dict[property]: |
| 111 return 0 | |
| 112 | |
| 113 # is this a props_only permission and permissions are set | |
| 114 if property is None and self.properties is not None and \ | |
| 115 self.limit_perm_to_props_only: | |
| 63 return 0 | 116 return 0 |
| 64 | 117 |
| 65 # check code | 118 # check code |
| 66 if itemid is not None and self.check is not None: | 119 if itemid is not None and self.check is not None: |
| 67 if self.check_version == 1: | 120 if self.check_version == 1: |
| 68 if not self.check(db, userid, itemid): | 121 if not self.check(db, userid, itemid): |
| 69 return 0 | 122 return 0 |
| 70 elif self.check_version == 2: | 123 elif self.check_version == 2: |
| 71 if not self.check(db, userid, itemid, property=property, permission=permission, classname=classname): | 124 if not self.check(db, userid, itemid, property=property, \ |
| 125 permission=permission, classname=classname): | |
| 72 return 0 | 126 return 0 |
| 73 | 127 |
| 74 # we have a winner | 128 # we have a winner |
| 75 return 1 | 129 return 1 |
| 76 | 130 |
| 95 | 149 |
| 96 return 1 | 150 return 1 |
| 97 | 151 |
| 98 | 152 |
| 99 def __repr__(self): | 153 def __repr__(self): |
| 100 return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name, | 154 return '<Permission 0x%x %r,%r,%r,%r,%r>'%(id(self), self.name, |
| 101 self.klass, self.properties, self.check) | 155 self.klass, self.properties, self.check, |
| 156 self.limit_perm_to_props_only) | |
| 102 | 157 |
| 103 def __cmp__(self, other): | 158 def __cmp__(self, other): |
| 104 if self.name != other.name: | 159 if self.name != other.name: |
| 105 return cmp(self.name, other.name) | 160 return cmp(self.name, other.name) |
| 106 | 161 |
| 107 if self.klass != other.klass: return 1 | 162 if self.klass != other.klass: return 1 |
| 108 if self.properties != other.properties: return 1 | 163 if self.properties != other.properties: return 1 |
| 109 if self.check != other.check: return 1 | 164 if self.check != other.check: return 1 |
| 165 if self.limit_perm_to_props_only != \ | |
| 166 other.limit_perm_to_props_only: return 1 | |
| 110 | 167 |
| 111 # match | 168 # match |
| 112 return 0 | 169 return 0 |
| 170 | |
| 171 def __getitem__(self,index): | |
| 172 return (self.name, self.klass, self.properties, self.check, | |
| 173 self.limit_perm_to_props_only)[index] | |
| 113 | 174 |
| 114 class Role: | 175 class Role: |
| 115 ''' Defines a Role with the attributes | 176 ''' Defines a Role with the attributes |
| 116 - name | 177 - name |
| 117 - description | 178 - description |
| 156 client.initialiseSecurity(self) | 217 client.initialiseSecurity(self) |
| 157 from roundup import mailgw | 218 from roundup import mailgw |
| 158 mailgw.initialiseSecurity(self) | 219 mailgw.initialiseSecurity(self) |
| 159 | 220 |
| 160 def getPermission(self, permission, classname=None, properties=None, | 221 def getPermission(self, permission, classname=None, properties=None, |
| 161 check=None): | 222 check=None, props_only=None): |
| 162 ''' Find the Permission matching the name and for the class, if the | 223 ''' Find the Permission matching the name and for the class, if the |
| 163 classname is specified. | 224 classname is specified. |
| 164 | 225 |
| 165 Raise ValueError if there is no exact match. | 226 Raise ValueError if there is no exact match. |
| 166 ''' | 227 ''' |
| 173 except KeyError: | 234 except KeyError: |
| 174 raise ValueError, 'No class "%s" defined'%classname | 235 raise ValueError, 'No class "%s" defined'%classname |
| 175 | 236 |
| 176 # look through all the permissions of the given name | 237 # look through all the permissions of the given name |
| 177 tester = Permission(permission, klass=classname, properties=properties, | 238 tester = Permission(permission, klass=classname, properties=properties, |
| 178 check=check) | 239 check=check, |
| 240 props_only=props_only) | |
| 179 for perm in self.permission[permission]: | 241 for perm in self.permission[permission]: |
| 180 if perm == tester: | 242 if perm == tester: |
| 181 return perm | 243 return perm |
| 182 raise ValueError, 'No permission "%s" defined for "%s"'%(permission, | 244 raise ValueError, 'No permission "%s" defined for "%s"'%(permission, |
| 183 classname) | 245 classname) |
| 184 | 246 |
| 185 def hasPermission(self, permission, userid, classname=None, | 247 def hasPermission(self, permission, userid, classname=None, |
| 186 property=None, itemid=None): | 248 property=None, itemid=None): |
| 187 '''Look through all the Roles, and hence Permissions, and | 249 '''Look through all the Roles, and hence Permissions, and |
| 188 see if "permission" exists given the constraints of | 250 see if "permission" exists given the constraints of |
| 189 classname, property and itemid. | 251 classname, property, itemid, and props_only. |
| 190 | 252 |
| 191 If classname is specified (and only classname) then the | 253 If classname is specified (and only classname) the |
| 192 search will match if there is *any* Permission for that | 254 search will match: |
| 193 classname, even if the Permission has additional | 255 |
| 194 constraints. | 256 if there is *any* Permission for that classname, and |
| 257 that Permission was not created with props_only = True | |
| 258 | |
| 259 *NOTE* the Permission will match even if there are | |
| 260 additional constraints like a check or properties and | |
| 261 props_only is False. This can be unexpected. Using | |
| 262 props_only = True or setting the default value to True can | |
| 263 help prevent surprises. | |
| 195 | 264 |
| 196 If property is specified, the Permission matched must have | 265 If property is specified, the Permission matched must have |
| 197 either no properties listed or the property must appear in | 266 either no properties listed or the property must appear in |
| 198 the list. | 267 the list. |
| 199 | 268 |
| 201 either no check function defined or the check function, | 270 either no check function defined or the check function, |
| 202 when invoked, must return a True value. | 271 when invoked, must return a True value. |
| 203 | 272 |
| 204 Note that this functionality is actually implemented by the | 273 Note that this functionality is actually implemented by the |
| 205 Permission.test() method. | 274 Permission.test() method. |
| 275 | |
| 206 ''' | 276 ''' |
| 207 if itemid and classname is None: | 277 if itemid and classname is None: |
| 208 raise ValueError, 'classname must accompany itemid' | 278 raise ValueError, 'classname must accompany itemid' |
| 209 for rolename in self.db.user.get_roles(userid): | 279 for rolename in self.db.user.get_roles(userid): |
| 210 if not rolename or (rolename not in self.role): | 280 if not rolename or (rolename not in self.role): |
| 306 ''' | 376 ''' |
| 307 role = Role(**propspec) | 377 role = Role(**propspec) |
| 308 self.role[role.name] = role | 378 self.role[role.name] = role |
| 309 return role | 379 return role |
| 310 | 380 |
| 381 def set_props_only_default(self, props_only=None): | |
| 382 if props_only is not None: | |
| 383 # NOTE: only valid values are True and False because these | |
| 384 # will be compared as part of tuple == tuple and | |
| 385 # (3,) == (True,) is False even though 3 is a True value | |
| 386 # in a boolean context. So use bool() to coerce value. | |
| 387 Permission.limit_perm_to_props_only = \ | |
| 388 bool(props_only) | |
| 389 | |
| 311 def addPermissionToRole(self, rolename, permission, classname=None, | 390 def addPermissionToRole(self, rolename, permission, classname=None, |
| 312 properties=None, check=None): | 391 properties=None, check=None, props_only=None): |
| 313 ''' Add the permission to the role's permission list. | 392 ''' Add the permission to the role's permission list. |
| 314 | 393 |
| 315 'rolename' is the name of the role to add the permission to. | 394 'rolename' is the name of the role to add the permission to. |
| 316 | 395 |
| 317 'permission' is either a Permission *or* a permission name | 396 'permission' is either a Permission *or* a permission name |
| 319 is obtained by passing 'permission' and 'classname' to | 398 is obtained by passing 'permission' and 'classname' to |
| 320 self.getPermission) | 399 self.getPermission) |
| 321 ''' | 400 ''' |
| 322 if not isinstance(permission, Permission): | 401 if not isinstance(permission, Permission): |
| 323 permission = self.getPermission(permission, classname, | 402 permission = self.getPermission(permission, classname, |
| 324 properties, check) | 403 properties, check, props_only) |
| 325 role = self.role[rolename.lower()] | 404 role = self.role[rolename.lower()] |
| 326 role.permissions.append(permission) | 405 role.permissions.append(permission) |
| 327 | 406 |
| 328 # Convenience methods for removing non-allowed properties from a | 407 # Convenience methods for removing non-allowed properties from a |
| 329 # filterspec or sort/group list | 408 # filterspec or sort/group list |
