Mercurial > p > roundup > code
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): |
