comparison roundup/security.py @ 8472:224ccb8b49ca

refactor: change some classes to use __slots__ Speed up access to and reduce size of some low level classes. A few classes in security.py, rest.py are heavily used. But for all, it prevents adding random properties to lower level classes that people shouldn't be mucking with. While doing this I found some test cases accessing an invalid property name and this change caused the cases to crash. admin.py: Use new method Role.props_dict() and Permission.props_dict() where original code just referenced __dict__ when printing Role/Permission. mlink_expr.py: Add slots to multiple classes. Classes Binary and Unary set real properties/attributes. Classes that inherit from them (Equals, Empty, Not, Or, And) define empty slots tuple to eliminate need for __dict__. Class Expression also gets a slot. rate_limit.py: RateLimit and Gcra classes get slots. A couple of pep8 fixes: sort imports, remove trailing spaces on a line, remove unused noqa comment. rest.py: Add slots to class SimulateFieldStorageFromJson and FsValue classes. The memory savings from this could be useful as well as speedier access to the attributes. security.py: Add slots to Permission class. To prevent conflict between slot limit_perm_to_props_only and the class variable of the same name, rename the class variable to limit_perm_to_props_only_default. Also define method props_dict() to allow other code to get a dict to iterate over when checking permissions. Add slots to class Role along with props_dict() method. Add slots to class Security. Also have to add explicit __dict__ slot to support test override of the hasPermission() method. Add props_dict() method, currently unused, but added for symmetry. support.py: TruthDict and PrioList gets slots. test/test_cgi.py: Fix incorrect setting of permission property. Was setting permissions. So testing may not have been doing what we thought it was. Multiple places found with this typo. Remove setting of permissions in some places where it should have no effect on the test and looks like it was just copypasta. test/test_xmlrpc.py Remove setting of permissions in some places where it should have no effect on the test and looks like it was just copypasta.
author John Rouillard <rouilj@ieee.org>
date Mon, 03 Nov 2025 00:13:04 -0500
parents 43899d99fc4d
children 19152fd94fcf
comparison
equal deleted inserted replaced
8471:8e72dc7b7f2f 8472:224ccb8b49ca
16 - description 16 - description
17 - klass (optional) 17 - klass (optional)
18 - properties (optional) 18 - properties (optional)
19 - check function (optional) 19 - check function (optional)
20 - props_only (optional, internal field is limit_perm_to_props_only) 20 - props_only (optional, internal field is limit_perm_to_props_only)
21 default value taken from Permission.limit_perm_to_props_only_default.
21 - filter function (optional) returns filter arguments for 22 - filter function (optional) returns filter arguments for
22 determining which records are visible by the user. The filter 23 determining which records are visible by the user. The filter
23 function comes into play when determining if a set of nodes 24 function comes into play when determining if a set of nodes
24 found via a filter call of a class can be seen by the user -- 25 found via a filter call of a class can be seen by the user --
25 the normal way would be to call the permissions for each item 26 the normal way would be to call the permissions for each item
56 db.security.set_props_only_default() 57 db.security.set_props_only_default()
57 58
58 with a True or False value. 59 with a True or False value.
59 ''' 60 '''
60 61
61 limit_perm_to_props_only = False 62 __slots__ = (
63 "_properties_dict",
64 "check",
65 "check_version",
66 "description",
67 "filter",
68 "klass",
69 "limit_perm_to_props_only",
70 "name",
71 "properties")
72
73 limit_perm_to_props_only_default = False
62 74
63 def __init__(self, name='', description='', klass=None, 75 def __init__(self, name='', description='', klass=None,
64 properties=None, check=None, props_only=None, filter=None): 76 properties=None, check=None, props_only=None, filter=None):
65 from roundup.anypy import findargspec 77 from roundup.anypy import findargspec
66 self.name = name 78 self.name = name
78 # b=Property(name="Edit", klass="issue", check=dummy, 90 # b=Property(name="Edit", klass="issue", check=dummy,
79 # props_only=False) 91 # props_only=False)
80 # a == b will be true. 92 # a == b will be true.
81 if props_only is None: 93 if props_only is None:
82 self.limit_perm_to_props_only = \ 94 self.limit_perm_to_props_only = \
83 Permission.limit_perm_to_props_only 95 Permission.limit_perm_to_props_only_default
84 else: 96 else:
85 # see note on use of bool() in set_props_only_default() 97 # see note on use of bool() in set_props_only_default()
86 self.limit_perm_to_props_only = bool(props_only) 98 self.limit_perm_to_props_only = bool(props_only)
87 else: 99 else:
88 self.limit_perm_to_props_only = None 100 self.limit_perm_to_props_only = None
121 return True 133 return True
122 return False 134 return False
123 135
124 return check 136 return check
125 137
138 def props_dict(self):
139 return {name: getattr(self, name) for name in self.__slots__}
140
126 def test(self, db, permission, classname, property, userid, itemid): 141 def test(self, db, permission, classname, property, userid, itemid):
127 ''' Test permissions 5 args: 142 ''' Test permissions 5 args:
128 permission - string like Edit, Register etc. Required, no wildcard. 143 permission - string like Edit, Register etc. Required, no wildcard.
129 classname - string like issue, msg etc. Can be None to match any 144 classname - string like issue, msg etc. Can be None to match any
130 class. 145 class.
220 ''' Defines a Role with the attributes 235 ''' Defines a Role with the attributes
221 - name 236 - name
222 - description 237 - description
223 - permissions 238 - permissions
224 ''' 239 '''
240
241 __slots__ = ("_permissions", "description", "name")
242
225 def __init__(self, name='', description='', permissions=None): 243 def __init__(self, name='', description='', permissions=None):
226 self.name = name.lower() 244 self.name = name.lower()
227 self.description = description 245 self.description = description
228 # This is a dict of permission names each containing a dict of 246 # This is a dict of permission names each containing a dict of
229 # *class names*, with a special entry for non-class permissions 247 # *class names*, with a special entry for non-class permissions
298 for c in self._permissions[p]: 316 for c in self._permissions[p]:
299 for cond in (False, True): 317 for cond in (False, True):
300 perm_list.extend(self._permissions[p][c][cond]) 318 perm_list.extend(self._permissions[p][c][cond])
301 perm_list.sort(key=lambda x: (x.klass or '', x.name)) 319 perm_list.sort(key=lambda x: (x.klass or '', x.name))
302 return perm_list 320 return perm_list
321
322 def props_dict(self):
323 return {name: getattr(self, name) for name in self.__slots__}
303 324
304 def searchable(self, classname, propname): 325 def searchable(self, classname, propname):
305 for perm_name in 'View', 'Search': 326 for perm_name in 'View', 'Search':
306 # Only permissions without a check method 327 # Only permissions without a check method
307 if perm_name not in self._permissions: 328 if perm_name not in self._permissions:
319 return True 340 return True
320 return False 341 return False
321 342
322 343
323 class Security: 344 class Security:
345
346 # __dict__ is needed to allow mocking of db.security.hasPermission
347 # in test/test_templating.py. Define slots for properties used in
348 # production to increase speed.
349 __slots__ = ("__dict__", "db", "permission", "role")
350
324 def __init__(self, db): 351 def __init__(self, db):
325 ''' Initialise the permission and role classes, and add in the 352 ''' Initialise the permission and role classes, and add in the
326 base roles (for admin user). 353 base roles (for admin user).
327 ''' 354 '''
328 self.db = weakref.proxy(db) # use a weak ref to avoid circularity 355 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
449 """ 476 """
450 for perm in self.filter_iter(permission, userid, classname): 477 for perm in self.filter_iter(permission, userid, classname):
451 if not perm.filter: 478 if not perm.filter:
452 return False 479 return False
453 return True 480 return True
481
482 def props_dict(self):
483 return {name: getattr(self, name) for name in self.__slots__}
454 484
455 def roleHasSearchPermission(self, classname, property, *rolenames): 485 def roleHasSearchPermission(self, classname, property, *rolenames):
456 """ For each of the given roles, check the permissions. 486 """ For each of the given roles, check the permissions.
457 Property can be a transitive property. 487 Property can be a transitive property.
458 """ 488 """
542 if props_only is not None: 572 if props_only is not None:
543 # NOTE: only valid values are True and False because these 573 # NOTE: only valid values are True and False because these
544 # will be compared as part of tuple == tuple and 574 # will be compared as part of tuple == tuple and
545 # (3,) == (True,) is False even though 3 is a True value 575 # (3,) == (True,) is False even though 3 is a True value
546 # in a boolean context. So use bool() to coerce value. 576 # in a boolean context. So use bool() to coerce value.
547 Permission.limit_perm_to_props_only = \ 577 Permission.limit_perm_to_props_only_default = \
548 bool(props_only) 578 bool(props_only)
549 579
550 def get_props_only_default(self): 580 def get_props_only_default(self):
551 return Permission.limit_perm_to_props_only 581 return Permission.limit_perm_to_props_only_default
552 582
553 def addPermissionToRole(self, rolename, permission, classname=None, 583 def addPermissionToRole(self, rolename, permission, classname=None,
554 properties=None, check=None, props_only=None): 584 properties=None, check=None, props_only=None):
555 ''' Add the permission to the role's permission list. 585 ''' Add the permission to the role's permission list.
556 586

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