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

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