Code

9bfb0ecdd773777011f7ee4e7634a9e51745526a
[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, 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 :