Skip to content

Commit 9f70adb

Browse files
committed
Optimize filtering of search results
Now the Permission class constructor takes an optional argument 'filter'. Now if we do not find a permission on the whole class *and* all permission objects on the current class with a check method also have a filter method we can improve search performance by filtering in the database (instead of in python).
1 parent f9bcf5d commit 9f70adb

File tree

2 files changed

+83
-5
lines changed

2 files changed

+83
-5
lines changed

roundup/cgi/templating.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3421,7 +3421,8 @@ def base_javascript(self):
34213421
def batch(self, permission='View'):
34223422
""" Return a batch object for results from the "current search"
34233423
"""
3424-
check = self._client.db.security.hasPermission
3424+
sec = self._client.db.security
3425+
check = sec.hasPermission
34253426
userid = self._client.userid
34263427
if not check('Web Access', userid):
34273428
return Batch(self.client, [], self.pagesize, self.startwith,
@@ -3454,11 +3455,34 @@ def batch(self, permission='View'):
34543455

34553456
# filter for visibility
34563457
item_ids = klass.filter(matches, filterspec, sort, group)
3457-
if check(permission, userid, self.classname, only_no_check = True):
3458+
cn = self.classname
3459+
if check(permission, userid, cn, only_no_check = True):
34583460
allowed = item_ids
34593461
else:
3460-
allowed = [id for id in item_ids
3461-
if check(permission, userid, self.classname, itemid=id)]
3462+
# Note that is_filterable returns True if no permissions are
3463+
# found. This makes it fail early (with an empty allowed list)
3464+
# instead of running through all ids with an empty
3465+
# permission list.
3466+
if sec.is_filterable(permission, userid, cn):
3467+
new_ids = set(item_ids)
3468+
confirmed = set()
3469+
for perm in sec.filter_iter(permission, userid, cn):
3470+
fargs = perm.filter(self._client.db, userid, klass)
3471+
for farg in fargs:
3472+
farg.update(sort=sort, group=group, retired=False)
3473+
result = klass.filter(list(new_ids), **farg)
3474+
new_ids.difference_update(result)
3475+
confirmed.update(result)
3476+
# all allowed?
3477+
if not new_ids:
3478+
break
3479+
# all allowed?
3480+
if not new_ids:
3481+
break
3482+
allowed = list(confirmed)
3483+
else:
3484+
allowed = [id for id in item_ids
3485+
if check(permission, userid, cn, itemid=id)]
34623486

34633487
# return the batch object, using IDs only
34643488
return Batch(self.client, allowed, self.pagesize, self.startwith,

roundup/security.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ class Permission:
1818
- properties (optional)
1919
- check function (optional)
2020
- props_only (optional, internal field is limit_perm_to_props_only)
21+
- filter function (optional) returns filter arguments for
22+
determining which records are visible by the user. The filter
23+
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+
the normal way would be to call the permissions for each item
26+
found, the filter call performs this on the database for all
27+
nodes.
28+
Signature of the filter function:
29+
filter(db, userid, klass)
30+
The filter must return a list of dictionaries with filter parameters.
31+
Note that sort and group parameters of the filter call should
32+
not be set by filter method (they will be overwritten) and the
33+
parameter search_matches must not be set.
34+
An empty list returned means no access for this filter method.
2135
2236
The klass may be unset, indicating that this permission is not
2337
locked to a particular class. That means there may be multiple
@@ -47,14 +61,15 @@ class Permission:
4761
limit_perm_to_props_only = False
4862

4963
def __init__(self, name='', description='', klass=None,
50-
properties=None, check=None, props_only=None):
64+
properties=None, check=None, props_only=None, filter=None):
5165
from roundup.anypy import findargspec
5266
self.name = name
5367
self.description = description
5468
self.klass = klass
5569
self.properties = properties
5670
self._properties_dict = support.TruthDict(properties)
5771
self.check = check
72+
self.filter = filter
5873
if properties is not None:
5974
# Set to None unless properties are defined.
6075
# This means that:
@@ -211,6 +226,21 @@ def addPermission (self, *permissions):
211226
self._permissions[pn][cn] = dict (((False, []), (True, [])))
212227
self._permissions[pn][cn][bool(p.check)].append(p)
213228

229+
def filter_iter (self, permission, classname):
230+
""" Loop over all permissions for the current role on the class
231+
with a check method (and props_only False).
232+
"""
233+
if permission not in self._permissions:
234+
return
235+
for c in (None, classname):
236+
if c not in self._permissions[permission]:
237+
continue
238+
perms = self._permissions[permission][c][True]
239+
for p in perms:
240+
if p.limit_perm_to_props_only and p.properties:
241+
continue
242+
yield p
243+
214244
def hasPermission (self, db, perm, uid, classname, property, itemid, chk):
215245
# if itemid is given a classname must, too, checked in caller
216246
assert not itemid or classname
@@ -283,6 +313,17 @@ def __init__(self, db):
283313
from roundup import mailgw
284314
mailgw.initialiseSecurity(self)
285315

316+
def filter_iter(self, permission, userid, classname):
317+
""" Loop over all permissions for the current user on the class
318+
with a check method (and props_only False).
319+
"""
320+
for rolename in self.db.user.get_roles(userid):
321+
if not rolename or (rolename not in self.role):
322+
continue
323+
r = self.role[rolename]
324+
for perm in r.filter_iter(permission, classname):
325+
yield perm
326+
286327
def getPermission(self, permission, classname=None, properties=None,
287328
check=None, props_only=None):
288329
''' Find the Permission matching the name and for the class, if the
@@ -359,6 +400,19 @@ def hasPermission(self, permission, userid, classname=None,
359400
return v
360401
return False
361402

403+
def is_filterable(self, permission, userid, classname):
404+
""" Check if all permissions for the current user on the class
405+
with a check method (and props_only False) also have a
406+
filter method. We only consider permissions with props_only
407+
set to False. Note that this will return True if there are
408+
no permissions with a check method found, the performed
409+
checks later will find no matching records.
410+
"""
411+
for perm in self.filter_iter (permission, userid, classname):
412+
if not perm.filter:
413+
return False
414+
return True
415+
362416
def roleHasSearchPermission(self, classname, property, *rolenames):
363417
""" For each of the given roles, check the permissions.
364418
Property can be a transitive property.

0 commit comments

Comments
 (0)