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, db, permission, 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 permission != self.name:
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 """
203 for perm in self.role[rolename].permissions:
204 # permission match?
205 for p in 'View', 'Search':
206 if perm.searchable(self.db, p, classname, property):
207 return 1
208 return 0
210 def hasSearchPermission(self, userid, classname, property):
211 '''Look through all the Roles, and hence Permissions, and
212 see if "permission" exists given the constraints of
213 classname and property.
215 A search permission is granted if we find a 'View' or
216 'Search' permission for the user which does *not* include
217 a check function. If such a permission is found, the user may
218 search for the given property in the given class.
220 Note that classname *and* property are mandatory arguments.
222 Contrary to hasPermission, the search will *not* match if
223 there are additional constraints (namely a search function)
224 on a Permission found.
226 Concerning property, the Permission matched must have
227 either no properties listed or the property must appear in
228 the list.
229 '''
230 for rolename in self.db.user.get_roles(userid):
231 if not rolename or not self.role.has_key(rolename):
232 continue
233 # for each of the user's Roles, check the permissions
234 if self.roleHasSearchPermission (rolename, classname, property):
235 return 1
236 return 0
238 def addPermission(self, **propspec):
239 ''' Create a new Permission with the properties defined in
240 'propspec'. See the Permission class for the possible
241 keyword args.
242 '''
243 perm = Permission(**propspec)
244 self.permission.setdefault(perm.name, []).append(perm)
245 return perm
247 def addRole(self, **propspec):
248 ''' Create a new Role with the properties defined in 'propspec'
249 '''
250 role = Role(**propspec)
251 self.role[role.name] = role
252 return role
254 def addPermissionToRole(self, rolename, permission, classname=None,
255 properties=None, check=None):
256 ''' Add the permission to the role's permission list.
258 'rolename' is the name of the role to add the permission to.
260 'permission' is either a Permission *or* a permission name
261 accompanied by 'classname' (thus in the second case a Permission
262 is obtained by passing 'permission' and 'classname' to
263 self.getPermission)
264 '''
265 if not isinstance(permission, Permission):
266 permission = self.getPermission(permission, classname,
267 properties, check)
268 role = self.role[rolename.lower()]
269 role.permissions.append(permission)
271 # Convenience methods for removing non-allowed properties from a
272 # filterspec or sort/group list
274 def filterFilterspec(self, userid, classname, filterspec):
275 """ Return a filterspec that has all non-allowed properties removed.
276 """
277 return dict ([(k, v) for k, v in filterspec.iteritems()
278 if self.hasSearchPermission(userid,classname,k)])
280 def filterSortspec(self, userid, classname, sort):
281 """ Return a sort- or group-list that has all non-allowed properties
282 removed.
283 """
284 if isinstance(sort, tuple) and sort[0] in '+-':
285 sort = [sort]
286 return [(d, p) for d, p in sort
287 if self.hasSearchPermission(userid,classname,p)]
289 # vim: set filetype=python sts=4 sw=4 et si :