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 is not None and 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, classname, property, *rolenames):
201 """ For each of the given roles, check the permissions.
202 Property can be a transitive property.
203 """
204 perms = []
205 # pre-compute permissions
206 for rn in rolenames :
207 for perm in self.role[rn].permissions:
208 perms.append(perm)
209 # Note: break from inner loop means "found"
210 # break from outer loop means "not found"
211 cn = classname
212 prev = None
213 prop = None
214 Link = hyperdb.Link
215 Multilink = hyperdb.Multilink
216 for propname in property.split('.'):
217 if prev:
218 try:
219 cn = prop.classname
220 except AttributeError:
221 break
222 prev = propname
223 try:
224 cls = self.db.getclass(cn)
225 prop = cls.getprops()[propname]
226 except KeyError:
227 break
228 for perm in perms:
229 if perm.searchable(cn, propname):
230 break
231 else:
232 break
233 else:
234 # for Link and Multilink require search permission on label-
235 # and order-properties and on ID
236 if isinstance(prop, Multilink) or isinstance(prop, Link):
237 try:
238 cls = self.db.getclass(prop.classname)
239 except KeyError:
240 return 0
241 props = dict.fromkeys(('id', cls.labelprop(), cls.orderprop()))
242 for p in props.iterkeys():
243 for perm in perms:
244 if perm.searchable(prop.classname, p):
245 break
246 else:
247 return 0
248 return 1
249 return 0
251 def hasSearchPermission(self, userid, classname, property):
252 '''Look through all the Roles, and hence Permissions, and
253 see if "permission" exists given the constraints of
254 classname and property.
256 A search permission is granted if we find a 'View' or
257 'Search' permission for the user which does *not* include
258 a check function. If such a permission is found, the user may
259 search for the given property in the given class.
261 Note that classname *and* property are mandatory arguments.
263 Contrary to hasPermission, the search will *not* match if
264 there are additional constraints (namely a search function)
265 on a Permission found.
267 Concerning property, the Permission matched must have
268 either no properties listed or the property must appear in
269 the list.
270 '''
271 roles = [r for r in self.db.user.get_roles(userid)
272 if r and self.role.has_key(r)]
273 return self.roleHasSearchPermission (classname, property, *roles)
275 def addPermission(self, **propspec):
276 ''' Create a new Permission with the properties defined in
277 'propspec'. See the Permission class for the possible
278 keyword args.
279 '''
280 perm = Permission(**propspec)
281 self.permission.setdefault(perm.name, []).append(perm)
282 return perm
284 def addRole(self, **propspec):
285 ''' Create a new Role with the properties defined in 'propspec'
286 '''
287 role = Role(**propspec)
288 self.role[role.name] = role
289 return role
291 def addPermissionToRole(self, rolename, permission, classname=None,
292 properties=None, check=None):
293 ''' Add the permission to the role's permission list.
295 'rolename' is the name of the role to add the permission to.
297 'permission' is either a Permission *or* a permission name
298 accompanied by 'classname' (thus in the second case a Permission
299 is obtained by passing 'permission' and 'classname' to
300 self.getPermission)
301 '''
302 if not isinstance(permission, Permission):
303 permission = self.getPermission(permission, classname,
304 properties, check)
305 role = self.role[rolename.lower()]
306 role.permissions.append(permission)
308 # Convenience methods for removing non-allowed properties from a
309 # filterspec or sort/group list
311 def filterFilterspec(self, userid, classname, filterspec):
312 """ Return a filterspec that has all non-allowed properties removed.
313 """
314 return dict ([(k, v) for k, v in filterspec.iteritems()
315 if self.hasSearchPermission(userid,classname,k)])
317 def filterSortspec(self, userid, classname, sort):
318 """ Return a sort- or group-list that has all non-allowed properties
319 removed.
320 """
321 if isinstance(sort, tuple) and sort[0] in '+-':
322 sort = [sort]
323 return [(d, p) for d, p in sort
324 if self.hasSearchPermission(userid,classname,p)]
326 # vim: set filetype=python sts=4 sw=4 et si :