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 :