Code

search permissions must allow transitive properties
[roundup.git] / roundup / security.py
1 """Handle the security declarations used in Roundup trackers.
2 """
3 __docformat__ = 'restructuredtext'
5 import weakref
7 from roundup import hyperdb, support
9 class Permission:
10     ''' Defines a Permission with the attributes
11         - name
12         - description
13         - klass (optional)
14         - properties (optional)
15         - check function (optional)
17         The klass may be unset, indicating that this permission is not
18         locked to a particular class. That means there may be multiple
19         Permissions for the same name for different classes.
21         If property names are set, permission is restricted to those
22         properties only.
24         If check function is set, permission is granted only when
25         the function returns value interpreted as boolean true.
26         The function is called with arguments db, userid, itemid.
27     '''
28     def __init__(self, name='', description='', klass=None,
29             properties=None, check=None):
30         self.name = name
31         self.description = description
32         self.klass = klass
33         self.properties = properties
34         self._properties_dict = support.TruthDict(properties)
35         self.check = check
37     def test(self, db, permission, classname, property, userid, itemid):
38         if permission != self.name:
39             return 0
41         # are we checking the correct class
42         if self.klass is not None and self.klass != classname:
43             return 0
45         # what about property?
46         if property is not None and not self._properties_dict[property]:
47             return 0
49         # check code
50         if itemid is not None and self.check is not None:
51             if not self.check(db, userid, itemid):
52                 return 0
54         # we have a winner
55         return 1
57     def searchable(self, classname, property):
58         """ A Permission is searchable for the given permission if it
59             doesn't include a check method and otherwise matches the
60             given parameters.
61         """
62         if self.name not in ('View', 'Search'):
63             return 0
65         # are we checking the correct class
66         if self.klass != classname:
67             return 0
69         # what about property?
70         if not self._properties_dict[property]:
71             return 0
73         if self.check:
74             return 0
76         return 1
79     def __repr__(self):
80         return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name,
81             self.klass, self.properties, self.check)
83     def __cmp__(self, other):
84         if self.name != other.name:
85             return cmp(self.name, other.name)
87         if self.klass != other.klass: return 1
88         if self.properties != other.properties: return 1
89         if self.check != other.check: return 1
91         # match
92         return 0
94 class Role:
95     ''' Defines a Role with the attributes
96         - name
97         - description
98         - permissions
99     '''
100     def __init__(self, name='', description='', permissions=None):
101         self.name = name.lower()
102         self.description = description
103         if permissions is None:
104             permissions = []
105         self.permissions = permissions
107     def __repr__(self):
108         return '<Role 0x%x %r,%r>'%(id(self), self.name, self.permissions)
110 class Security:
111     def __init__(self, db):
112         ''' Initialise the permission and role classes, and add in the
113             base roles (for admin user).
114         '''
115         self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
117         # permssions are mapped by name to a list of Permissions by class
118         self.permission = {}
120         # roles are mapped by name to the Role
121         self.role = {}
123         # the default Roles
124         self.addRole(name="User", description="A regular user, no privs")
125         self.addRole(name="Admin", description="An admin user, full privs")
126         self.addRole(name="Anonymous", description="An anonymous user")
128         # default permissions - Admin may do anything
129         for p in 'create edit retire view'.split():
130             p = self.addPermission(name=p.title(),
131                 description="User may %s everthing"%p)
132             self.addPermissionToRole('Admin', p)
134         # initialise the permissions and roles needed for the UIs
135         from roundup.cgi import client
136         client.initialiseSecurity(self)
137         from roundup import mailgw
138         mailgw.initialiseSecurity(self)
140     def getPermission(self, permission, classname=None, properties=None,
141             check=None):
142         ''' Find the Permission matching the name and for the class, if the
143             classname is specified.
145             Raise ValueError if there is no exact match.
146         '''
147         if not self.permission.has_key(permission):
148             raise ValueError, 'No permission "%s" defined'%permission
150         if classname:
151             try:
152                 self.db.getclass(classname)
153             except KeyError:
154                 raise ValueError, 'No class "%s" defined'%classname
156         # look through all the permissions of the given name
157         tester = Permission(permission, klass=classname, properties=properties,
158             check=check)
159         for perm in self.permission[permission]:
160             if perm == tester:
161                 return perm
162         raise ValueError, 'No permission "%s" defined for "%s"'%(permission,
163             classname)
165     def hasPermission(self, permission, userid, classname=None,
166             property=None, itemid=None):
167         '''Look through all the Roles, and hence Permissions, and
168            see if "permission" exists given the constraints of
169            classname, property and itemid.
171            If classname is specified (and only classname) then the
172            search will match if there is *any* Permission for that
173            classname, even if the Permission has additional
174            constraints.
176            If property is specified, the Permission matched must have
177            either no properties listed or the property must appear in
178            the list.
180            If itemid is specified, the Permission matched must have
181            either no check function defined or the check function,
182            when invoked, must return a True value.
184            Note that this functionality is actually implemented by the
185            Permission.test() method.
186         '''
187         if itemid and classname is None:
188             raise ValueError, 'classname must accompany itemid'
189         for rolename in self.db.user.get_roles(userid):
190             if not rolename or not self.role.has_key(rolename):
191                 continue
192             # for each of the user's Roles, check the permissions
193             for perm in self.role[rolename].permissions:
194                 # permission match?
195                 if perm.test(self.db, permission, classname, property,
196                         userid, itemid):
197                     return 1
198         return 0
200     def roleHasSearchPermission(self, rolename, classname, property):
201         """ For each of the user's Roles, check the permissions.
202             Property can be a transitive property.
203         """
204         cn = classname
205         last = None
206         # Note: break from inner loop means "found"
207         #       break from outer loop means "not found"
208         for propname in property.split('.'):
209             if last:
210                 try:
211                     cls = self.db.getclass(cn)
212                     lprop = cls.getprops()[last]
213                 except KeyError:
214                     break
215                 cn = lprop.classname
216             last = propname
217             for perm in self.role[rolename].permissions:
218                 if perm.searchable(cn, propname):
219                     break
220             else:
221                 break
222         else:
223             return 1
224         return 0
226     def hasSearchPermission(self, userid, classname, property):
227         '''Look through all the Roles, and hence Permissions, and
228            see if "permission" exists given the constraints of
229            classname and property.
231            A search permission is granted if we find a 'View' or
232            'Search' permission for the user which does *not* include
233            a check function. If such a permission is found, the user may
234            search for the given property in the given class.
236            Note that classname *and* property are mandatory arguments.
238            Contrary to hasPermission, the search will *not* match if
239            there are additional constraints (namely a search function)
240            on a Permission found.
242            Concerning property, the Permission matched must have
243            either no properties listed or the property must appear in
244            the list.
245         '''
246         for rolename in self.db.user.get_roles(userid):
247             if not rolename or not self.role.has_key(rolename):
248                 continue
249             # for each of the user's Roles, check the permissions
250             if self.roleHasSearchPermission (rolename, classname, property):
251                 return 1
252         return 0
254     def addPermission(self, **propspec):
255         ''' Create a new Permission with the properties defined in
256             'propspec'. See the Permission class for the possible
257             keyword args.
258         '''
259         perm = Permission(**propspec)
260         self.permission.setdefault(perm.name, []).append(perm)
261         return perm
263     def addRole(self, **propspec):
264         ''' Create a new Role with the properties defined in 'propspec'
265         '''
266         role = Role(**propspec)
267         self.role[role.name] = role
268         return role
270     def addPermissionToRole(self, rolename, permission, classname=None,
271             properties=None, check=None):
272         ''' Add the permission to the role's permission list.
274             'rolename' is the name of the role to add the permission to.
276             'permission' is either a Permission *or* a permission name
277             accompanied by 'classname' (thus in the second case a Permission
278             is obtained by passing 'permission' and 'classname' to
279             self.getPermission)
280         '''
281         if not isinstance(permission, Permission):
282             permission = self.getPermission(permission, classname,
283                 properties, check)
284         role = self.role[rolename.lower()]
285         role.permissions.append(permission)
287     # Convenience methods for removing non-allowed properties from a
288     # filterspec or sort/group list
290     def filterFilterspec(self, userid, classname, filterspec):
291         """ Return a filterspec that has all non-allowed properties removed.
292         """
293         return dict ([(k, v) for k, v in filterspec.iteritems()
294             if self.hasSearchPermission(userid,classname,k)])
296     def filterSortspec(self, userid, classname, sort):
297         """ Return a sort- or group-list that has all non-allowed properties
298             removed.
299         """
300         if isinstance(sort, tuple) and sort[0] in '+-':
301             sort = [sort]
302         return [(d, p) for d, p in sort
303             if self.hasSearchPermission(userid,classname,p)]
305 # vim: set filetype=python sts=4 sw=4 et si :