Skip to content

Commit 00c34fe

Browse files
author
Unknown
committed
Implement props_only feature for permissions.
1 parent 85de49f commit 00c34fe

File tree

5 files changed

+318
-36
lines changed

5 files changed

+318
-36
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ Features:
176176
ctx['permission'] the name of the permission (e.g. View, Edit)
177177
At some future date the older 3 argument style check command will
178178
be deprecated. See ``upgrading.txt`` for details.
179+
- New property for permissions added to simplify the model. See
180+
``customizing.txt`` and search for props_only and
181+
set_props_only_default in the section 'Adding a new Permission'.
182+
(John Rouillard)
179183

180184
Fixed:
181185

doc/customizing.txt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,7 @@ When adding a new Permission, you will need to:
12801280
1. add it to your tracker's ``schema.py`` so it is created, using
12811281
``security.addPermission``, for example::
12821282

1283-
self.security.addPermission(name="View", klass='frozzle',
1283+
db.security.addPermission(name="View", klass='frozzle',
12841284
description="User is allowed to access frozzles")
12851285

12861286
will set up a new "View" permission on the Class "frozzle".
@@ -1290,12 +1290,36 @@ When adding a new Permission, you will need to:
12901290
4. add it to the appropriate xxxPermission methods on in your tracker
12911291
interfaces module
12921292

1293-
The ``addPermission`` method takes a couple of optional parameters:
1293+
The ``addPermission`` method takes a few optional parameters:
12941294

12951295
**properties**
12961296
A sequence of property names that are the only properties to apply the
12971297
new Permission to (eg. ``... klass='user', properties=('name',
12981298
'email') ...``)
1299+
**props_only**
1300+
A boolean value (set to false by default) that is a new feature
1301+
in roundup 1.6.
1302+
A permission defined using
1303+
``properties=('list', 'of', 'property', 'names')``
1304+
is used to determine access for things other than just those
1305+
properties. For example a check for View permission on the issue
1306+
class or an issue item can use any View permission for the issue
1307+
class even if that permission has a property list. This can be
1308+
confusing and surprising as you would think that a permission
1309+
including properties would be used only for determining the
1310+
access permission for those properties.
1311+
1312+
Setting ``props_only=True`` will make the permission valid only for
1313+
those properties.
1314+
1315+
If you use a lot of permissions with property checks, it can be
1316+
difficult to change all of them. Calling the function:
1317+
1318+
db.security.set_props_only_default(True)
1319+
1320+
at the top of ``schema.py`` will make every permission creation
1321+
behave as though props_only was set to True. It is expected that the
1322+
default of True will become the default in a future Roundup release.
12991323
**check**
13001324
A function to be executed which returns boolean determining whether
13011325
the Permission is allowed. If it returns True, the permission is

doc/upgrading.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,25 @@ parameter. This is expected to be the preferred form in the future.
396396
You do not need to use the ``ctx`` parameter in the function if you do
397397
not need it.
398398

399+
Changes to property permissions
400+
-------------------------------
401+
402+
If you create a permission:
403+
404+
db.security.addPermission(name='View', klass='user',
405+
properties=['theme'], check=own_record,
406+
description="User is allowed to view their own theme")
407+
408+
that combines checks and properties, the permission also matches a
409+
permission check for the View permission on the user class. So this
410+
also allows the user to see their user record. It is unexpected that
411+
checking for access without a property would match this permission.
412+
413+
This release adds support for making a permission like above only be
414+
used during property permission tests. See ``customizing.txt`` and
415+
search for props_only and set_props_only_default in the section
416+
'Adding a new Permission'
417+
399418
Improve query editing
400419
---------------------
401420

roundup/security.py

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66

77
from roundup import hyperdb, support
88

9+
import logging
10+
logger = logging.getLogger('roundup.security')
11+
912
class 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+
114175
class 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

Comments
 (0)