Code

- update with possible tracker issue
[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 __repr__(self):
58         return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name,
59             self.klass, self.properties, self.check)
61     def __cmp__(self, other):
62         if self.name != other.name:
63             return cmp(self.name, other.name)
65         if self.klass != other.klass: return 1
66         if self.properties != other.properties: return 1
67         if self.check != other.check: return 1
69         # match
70         return 0
72 class Role:
73     ''' Defines a Role with the attributes
74         - name
75         - description
76         - permissions
77     '''
78     def __init__(self, name='', description='', permissions=None):
79         self.name = name.lower()
80         self.description = description
81         if permissions is None:
82             permissions = []
83         self.permissions = permissions
85     def __repr__(self):
86         return '<Role 0x%x %r,%r>'%(id(self), self.name, self.permissions)
88 class Security:
89     def __init__(self, db):
90         ''' Initialise the permission and role classes, and add in the
91             base roles (for admin user).
92         '''
93         self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
95         # permssions are mapped by name to a list of Permissions by class
96         self.permission = {}
98         # roles are mapped by name to the Role
99         self.role = {}
101         # the default Roles
102         self.addRole(name="User", description="A regular user, no privs")
103         self.addRole(name="Admin", description="An admin user, full privs")
104         self.addRole(name="Anonymous", description="An anonymous user")
106         # default permissions - Admin may do anything
107         for p in 'create edit retire view'.split():
108             p = self.addPermission(name=p.title(),
109                 description="User may %s everthing"%p)
110             self.addPermissionToRole('Admin', p)
112         # initialise the permissions and roles needed for the UIs
113         from roundup.cgi import client
114         client.initialiseSecurity(self)
115         from roundup import mailgw
116         mailgw.initialiseSecurity(self)
118     def getPermission(self, permission, classname=None, properties=None,
119             check=None):
120         ''' Find the Permission matching the name and for the class, if the
121             classname is specified.
123             Raise ValueError if there is no exact match.
124         '''
125         if not self.permission.has_key(permission):
126             raise ValueError, 'No permission "%s" defined'%permission
128         if classname:
129             try:
130                 self.db.getclass(classname)
131             except KeyError:
132                 raise ValueError, 'No class "%s" defined'%classname
134         # look through all the permissions of the given name
135         tester = Permission(permission, klass=classname, properties=properties,
136             check=check)
137         for perm in self.permission[permission]:
138             if perm == tester:
139                 return perm
140         raise ValueError, 'No permission "%s" defined for "%s"'%(permission,
141             classname)
143     def hasPermission(self, permission, userid, classname=None,
144             property=None, itemid=None):
145         '''Look through all the Roles, and hence Permissions, and
146            see if "permission" exists given the constraints of
147            classname, property and itemid.
149            If classname is specified (and only classname) then the
150            search will match if there is *any* Permission for that
151            classname, even if the Permission has additional
152            constraints.
154            If property is specified, the Permission matched must have
155            either no properties listed or the property must appear in
156            the list.
158            If itemid is specified, the Permission matched must have
159            either no check function defined or the check function,
160            when invoked, must return a True value.
162            Note that this functionality is actually implemented by the
163            Permission.test() method.
164         '''
165         roles = self.db.user.get(userid, 'roles')
166         if roles is None:
167             return 0
168         if itemid and classname is None:
169             raise ValueError, 'classname must accompany itemid'
170         for rolename in [x.lower().strip() for x in roles.split(',')]:
171             if not rolename or not self.role.has_key(rolename):
172                 continue
173             # for each of the user's Roles, check the permissions
174             for perm in self.role[rolename].permissions:
175                 # permission match?
176                 if perm.test(self.db, permission, classname, property,
177                         userid, itemid):
178                     return 1
179         return 0
181     def addPermission(self, **propspec):
182         ''' Create a new Permission with the properties defined in
183             'propspec'. See the Permission class for the possible
184             keyword args.
185         '''
186         perm = Permission(**propspec)
187         self.permission.setdefault(perm.name, []).append(perm)
188         return perm
190     def addRole(self, **propspec):
191         ''' Create a new Role with the properties defined in 'propspec'
192         '''
193         role = Role(**propspec)
194         self.role[role.name] = role
195         return role
197     def addPermissionToRole(self, rolename, permission, classname=None,
198             properties=None, check=None):
199         ''' Add the permission to the role's permission list.
201             'rolename' is the name of the role to add the permission to.
203             'permission' is either a Permission *or* a permission name
204             accompanied by 'classname' (thus in the second case a Permission
205             is obtained by passing 'permission' and 'classname' to
206             self.getPermission)
207         '''
208         if not isinstance(permission, Permission):
209             permission = self.getPermission(permission, classname,
210                 properties, check)
211         role = self.role[rolename.lower()]
212         role.permissions.append(permission)
214 # vim: set filetype=python sts=4 sw=4 et si :