comparison roundup/security.py @ 8119:c12377fb4144 permission-performance

Change permission representation Now permissions are checked in different order. Permissions without a check method (which are cheap to check) are checked first. Only if no permission is found do we check permissions with check methods.
author Ralf Schlatterbeck <rsc@runtux.com>
date Fri, 18 Oct 2024 16:52:42 +0200
parents 06e6bc21b67e
children d4fa7a9c3a21
comparison
equal deleted inserted replaced
8118:c88ad93f0e77 8119:c12377fb4144
182 - permissions 182 - permissions
183 ''' 183 '''
184 def __init__(self, name='', description='', permissions=None): 184 def __init__(self, name='', description='', permissions=None):
185 self.name = name.lower() 185 self.name = name.lower()
186 self.description = description 186 self.description = description
187 # This is a dict of permission names each containing a dict of
188 # *class names*, with a special entry for non-class permissions
189 # where the key is None. In each class dict we have a dictionary
190 # with the values True and False for permission with and without
191 # a check method. These dicts each contain a list of permissions.
187 if permissions is None: 192 if permissions is None:
188 permissions = [] 193 self._permissions = {}
189 self.permissions = permissions 194 elif isinstance(permissions, list):
195 self._permissions = {}
196 for p in permissions:
197 self.addPermission(p)
198 else:
199 raise ValueError("Invalid permissions for Role: %s" % permissions)
190 200
191 def __repr__(self): 201 def __repr__(self):
192 return '<Role 0x%x %r,%r>' % (id(self), self.name, self.permissions) 202 pl = self.permission_list()
203 return '<Role 0x%x %r,%r>' % (id(self), self.name, pl)
204
205 def addPermission (self, *permissions):
206 for p in permissions:
207 pn = p.name
208 self._permissions.setdefault(pn, {})
209 cn = p.klass
210 if p.klass not in self._permissions[pn]:
211 self._permissions[pn][cn] = dict (((False, []), (True, [])))
212 self._permissions[pn][cn][bool(p.check)].append(p)
213
214 def hasPermission (self, db, perm, uid, classname, property, itemid, chk):
215 # if itemid is given a classname must, too, checked in caller
216 assert not itemid or classname
217 perms = self._permissions
218 if perm not in perms:
219 return False
220 # If we have a classname we also need to check permission with
221 # an empty classname (e.g. 'admin' has access on everything)
222 if classname is not None and None in perms[perm]:
223 for p in perms[perm][None][chk]:
224 # permission match?
225 if p.test(db, perm, classname, property, uid, itemid):
226 return True
227 if classname not in perms[perm]:
228 return False
229 for p in perms[perm][classname][chk]:
230 # permission match?
231 if p.test(db, perm, classname, property, uid, itemid):
232 return True
233
234 def permission_list (self):
235 """ Used for reporting in admin tool """
236 l = []
237 for p in self._permissions:
238 for c in self._permissions[p]:
239 for cond in (False, True):
240 l.extend (self._permissions[p][c][cond])
241 l.sort (key = lambda x: (x.klass or '', x.name))
242 return l
243
244 def searchable (self, classname, propname):
245 for perm in 'View', 'Search':
246 # Only permissions without a check method
247 if perm not in self._permissions:
248 continue
249 if classname not in self._permissions[perm]:
250 continue
251 for p in self._permissions[perm][classname][False]:
252 if p.searchable(classname, propname):
253 return True
193 254
194 255
195 class Security: 256 class Security:
196 def __init__(self, db): 257 def __init__(self, db):
197 ''' Initialise the permission and role classes, and add in the 258 ''' Initialise the permission and role classes, and add in the
198 base roles (for admin user). 259 base roles (for admin user).
199 ''' 260 '''
200 self.db = weakref.proxy(db) # use a weak ref to avoid circularity 261 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
201 262
202 # permssions are mapped by name to a list of Permissions by class 263 # Permissions are mapped by name to a list of Permissions by class
203 self.permission = {} 264 self.permission = {}
204 265
205 # roles are mapped by name to the Role 266 # roles are mapped by name to the Role
206 self.role = {} 267 self.role = {}
207 268
276 337
277 Note that this functionality is actually implemented by the 338 Note that this functionality is actually implemented by the
278 Permission.test() method. 339 Permission.test() method.
279 340
280 ''' 341 '''
342 if classname == 'status':
343 import pdb; pdb.set_trace()
281 if itemid and classname is None: 344 if itemid and classname is None:
282 raise ValueError('classname must accompany itemid') 345 raise ValueError('classname must accompany itemid')
283 for rolename in self.db.user.get_roles(userid): 346 # for each of the user's Roles, check the permissions
284 if not rolename or (rolename not in self.role): 347 # Note that checks with a check method are typically a lot more
285 continue 348 # expensive than the ones without. So we check the ones without
286 # for each of the user's Roles, check the permissions 349 # a check method first
287 for perm in self.role[rolename].permissions: 350 for has_check in False, True:
288 # permission match? 351 for rolename in self.db.user.get_roles(userid):
289 if perm.test(self.db, permission, classname, property, 352 if not rolename or (rolename not in self.role):
290 userid, itemid): 353 continue
291 return 1 354 r = self.role[rolename]
292 return 0 355 v = r.hasPermission(self.db, permission, userid, classname,
356 property, itemid, has_check)
357 if v:
358 return v
359 return False
293 360
294 def roleHasSearchPermission(self, classname, property, *rolenames): 361 def roleHasSearchPermission(self, classname, property, *rolenames):
295 """ For each of the given roles, check the permissions. 362 """ For each of the given roles, check the permissions.
296 Property can be a transitive property. 363 Property can be a transitive property.
297 """ 364 """
298 perms = [] 365 perms = []
299 # pre-compute permissions
300 for rn in rolenames:
301 for perm in self.role[rn].permissions:
302 perms.append(perm)
303 # Note: break from inner loop means "found" 366 # Note: break from inner loop means "found"
304 # break from outer loop means "not found" 367 # break from outer loop means "not found"
305 cn = classname 368 cn = classname
306 prev = None 369 prev = None
307 prop = None 370 prop = None
317 try: 380 try:
318 cls = self.db.getclass(cn) 381 cls = self.db.getclass(cn)
319 prop = cls.getprops()[propname] 382 prop = cls.getprops()[propname]
320 except KeyError: 383 except KeyError:
321 break 384 break
322 for perm in perms: 385 for rn in rolenames:
323 if perm.searchable(cn, propname): 386 if self.role[rn].searchable(cn, propname):
324 break 387 break
325 else: 388 else:
326 break 389 break
327 else: 390 else:
328 # for Link and Multilink require search permission on label- 391 # for Link and Multilink require search permission on label-
332 cls = self.db.getclass(prop.classname) 395 cls = self.db.getclass(prop.classname)
333 except KeyError: 396 except KeyError:
334 return 0 397 return 0
335 props = dict.fromkeys(('id', cls.labelprop(), cls.orderprop())) 398 props = dict.fromkeys(('id', cls.labelprop(), cls.orderprop()))
336 for p in props.keys(): 399 for p in props.keys():
337 for perm in perms: 400 for rn in rolenames:
338 if perm.searchable(prop.classname, p): 401 if self.role[rn].searchable(prop.classname, p):
339 break 402 break
340 else: 403 else:
341 return 0 404 return 0
342 return 1 405 return 1
343 return 0 406 return 0
407 ''' 470 '''
408 if not isinstance(permission, Permission): 471 if not isinstance(permission, Permission):
409 permission = self.getPermission(permission, classname, 472 permission = self.getPermission(permission, classname,
410 properties, check, props_only) 473 properties, check, props_only)
411 role = self.role[rolename.lower()] 474 role = self.role[rolename.lower()]
412 role.permissions.append(permission) 475 role.addPermission(permission)
413 476
414 # Convenience methods for removing non-allowed properties from a 477 # Convenience methods for removing non-allowed properties from a
415 # filterspec or sort/group list 478 # filterspec or sort/group list
416 479
417 def filterFilterspec(self, userid, classname, filterspec): 480 def filterFilterspec(self, userid, classname, filterspec):

Roundup Issue Tracker: http://roundup-tracker.org/