66
77from roundup import hyperdb , support
88
9+ import logging
10+ logger = logging .getLogger ('roundup.security' )
11+
912class Permission :
1013 ''' Defines a Permission with the attributes
1114 - name
1215 - description
1316 - klass (optional)
1417 - properties (optional)
1518 - check function (optional)
19+ - props_only (optional, internal field is limit_perm_to_props_only)
1620
1721 The klass may be unset, indicating that this permission is not
1822 locked to a particular class. That means there may be multiple
@@ -24,16 +28,49 @@ class Permission:
2428 If check function is set, permission is granted only when
2529 the function returns value interpreted as boolean true.
2630 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.
2744 '''
45+
46+ limit_perm_to_props_only = False
47+
2848 def __init__ (self , name = '' , description = '' , klass = None ,
29- properties = None , check = None ):
49+ properties = None , check = None , props_only = None ):
3050 import inspect
3151 self .name = name
3252 self .description = description
3353 self .klass = klass
3454 self .properties = properties
3555 self ._properties_dict = support .TruthDict (properties )
3656 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+
3774
3875 if check is None :
3976 self .check_version = 0
@@ -51,6 +88,17 @@ def __init__(self, name='', description='', klass=None,
5188 self .check_version = 2
5289
5390 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+
54102 if permission != self .name :
55103 return 0
56104
@@ -62,13 +110,19 @@ def test(self, db, permission, classname, property, userid, itemid):
62110 if property is not None and not self ._properties_dict [property ]:
63111 return 0
64112
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 :
116+ return 0
117+
65118 # check code
66119 if itemid is not None and self .check is not None :
67120 if self .check_version == 1 :
68121 if not self .check (db , userid , itemid ):
69122 return 0
70123 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 ):
72126 return 0
73127
74128 # we have a winner
@@ -97,8 +151,9 @@ def searchable(self, classname, property):
97151
98152
99153 def __repr__ (self ):
100- return '<Permission 0x%x %r,%r,%r,%r>' % (id (self ), self .name ,
101- self .klass , self .properties , self .check )
154+ return '<Permission 0x%x %r,%r,%r,%r,%r>' % (id (self ), self .name ,
155+ self .klass , self .properties , self .check ,
156+ self .limit_perm_to_props_only )
102157
103158 def __cmp__ (self , other ):
104159 if self .name != other .name :
@@ -107,10 +162,16 @@ def __cmp__(self, other):
107162 if self .klass != other .klass : return 1
108163 if self .properties != other .properties : return 1
109164 if self .check != other .check : return 1
165+ if self .limit_perm_to_props_only != \
166+ other .limit_perm_to_props_only : return 1
110167
111168 # match
112169 return 0
113170
171+ def __getitem__ (self ,index ):
172+ return (self .name , self .klass , self .properties , self .check ,
173+ self .limit_perm_to_props_only )[index ]
174+
114175class Role :
115176 ''' Defines a Role with the attributes
116177 - name
@@ -158,7 +219,7 @@ def __init__(self, db):
158219 mailgw .initialiseSecurity (self )
159220
160221 def getPermission (self , permission , classname = None , properties = None ,
161- check = None ):
222+ check = None , props_only = None ):
162223 ''' Find the Permission matching the name and for the class, if the
163224 classname is specified.
164225
@@ -175,7 +236,8 @@ def getPermission(self, permission, classname=None, properties=None,
175236
176237 # look through all the permissions of the given name
177238 tester = Permission (permission , klass = classname , properties = properties ,
178- check = check )
239+ check = check ,
240+ props_only = props_only )
179241 for perm in self .permission [permission ]:
180242 if perm == tester :
181243 return perm
@@ -186,12 +248,19 @@ def hasPermission(self, permission, userid, classname=None,
186248 property = None , itemid = None ):
187249 '''Look through all the Roles, and hence Permissions, and
188250 see if "permission" exists given the constraints of
189- classname, property and itemid .
251+ classname, property, itemid, and props_only .
190252
191- If classname is specified (and only classname) then the
192- search will match if there is *any* Permission for that
193- classname, even if the Permission has additional
194- constraints.
253+ If classname is specified (and only classname) the
254+ search will match:
255+
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.
195264
196265 If property is specified, the Permission matched must have
197266 either no properties listed or the property must appear in
@@ -203,6 +272,7 @@ def hasPermission(self, permission, userid, classname=None,
203272
204273 Note that this functionality is actually implemented by the
205274 Permission.test() method.
275+
206276 '''
207277 if itemid and classname is None :
208278 raise ValueError , 'classname must accompany itemid'
@@ -308,8 +378,17 @@ def addRole(self, **propspec):
308378 self .role [role .name ] = role
309379 return role
310380
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+
311390 def addPermissionToRole (self , rolename , permission , classname = None ,
312- properties = None , check = None ):
391+ properties = None , check = None , props_only = None ):
313392 ''' Add the permission to the role's permission list.
314393
315394 'rolename' is the name of the role to add the permission to.
@@ -321,7 +400,7 @@ def addPermissionToRole(self, rolename, permission, classname=None,
321400 '''
322401 if not isinstance (permission , Permission ):
323402 permission = self .getPermission (permission , classname ,
324- properties , check )
403+ properties , check , props_only )
325404 role = self .role [rolename .lower ()]
326405 role .permissions .append (permission )
327406
0 commit comments