Commit | Line | Data |
---|---|---|
c638d827 CR |
1 | """Handle the security declarations used in Roundup trackers. |
2 | """ | |
3 | __docformat__ = 'restructuredtext' | |
4 | ||
5 | import weakref | |
6 | ||
7 | from roundup import hyperdb, support | |
8 | ||
9 | class Permission: | |
10 | ''' Defines a Permission with the attributes | |
11 | - name | |
12 | - description | |
13 | - klass (optional) | |
14 | - properties (optional) | |
15 | - check function (optional) | |
16 | ||
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. | |
20 | ||
21 | If property names are set, permission is restricted to those | |
22 | properties only. | |
23 | ||
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 | |
36 | ||
37 | def test(self, db, permission, classname, property, userid, itemid): | |
38 | if permission != self.name: | |
39 | return 0 | |
40 | ||
41 | # are we checking the correct class | |
42 | if self.klass is not None and self.klass != classname: | |
43 | return 0 | |
44 | ||
45 | # what about property? | |
46 | if property is not None and not self._properties_dict[property]: | |
47 | return 0 | |
48 | ||
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 | |
53 | ||
54 | # we have a winner | |
55 | return 1 | |
56 | ||
57 | def __repr__(self): | |
58 | return '<Permission 0x%x %r,%r,%r,%r>'%(id(self), self.name, | |
59 | self.klass, self.properties, self.check) | |
60 | ||
61 | def __cmp__(self, other): | |
62 | if self.name != other.name: | |
63 | return cmp(self.name, other.name) | |
64 | ||
65 | if self.klass != other.klass: return 1 | |
66 | if self.properties != other.properties: return 1 | |
67 | if self.check != other.check: return 1 | |
68 | ||
69 | # match | |
70 | return 0 | |
71 | ||
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 | |
84 | ||
85 | def __repr__(self): | |
86 | return '<Role 0x%x %r,%r>'%(id(self), self.name, self.permissions) | |
87 | ||
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 | |
94 | ||
95 | # permssions are mapped by name to a list of Permissions by class | |
96 | self.permission = {} | |
97 | ||
98 | # roles are mapped by name to the Role | |
99 | self.role = {} | |
100 | ||
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") | |
105 | ||
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) | |
111 | ||
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) | |
117 | ||
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. | |
122 | ||
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 | |
127 | ||
128 | if classname: | |
129 | try: | |
130 | self.db.getclass(classname) | |
131 | except KeyError: | |
132 | raise ValueError, 'No class "%s" defined'%classname | |
133 | ||
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) | |
142 | ||
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. | |
148 | ||
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. | |
153 | ||
154 | If property is specified, the Permission matched must have | |
155 | either no properties listed or the property must appear in | |
156 | the list. | |
157 | ||
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. | |
161 | ||
162 | Note that this functionality is actually implemented by the | |
163 | Permission.test() method. | |
164 | ''' | |
165 | if itemid and classname is None: | |
166 | raise ValueError, 'classname must accompany itemid' | |
167 | for rolename in self.db.user.get_roles(userid): | |
168 | if not rolename or not self.role.has_key(rolename): | |
169 | continue | |
170 | # for each of the user's Roles, check the permissions | |
171 | for perm in self.role[rolename].permissions: | |
172 | # permission match? | |
173 | if perm.test(self.db, permission, classname, property, | |
174 | userid, itemid): | |
175 | return 1 | |
176 | return 0 | |
177 | ||
178 | def addPermission(self, **propspec): | |
179 | ''' Create a new Permission with the properties defined in | |
180 | 'propspec'. See the Permission class for the possible | |
181 | keyword args. | |
182 | ''' | |
183 | perm = Permission(**propspec) | |
184 | self.permission.setdefault(perm.name, []).append(perm) | |
185 | return perm | |
186 | ||
187 | def addRole(self, **propspec): | |
188 | ''' Create a new Role with the properties defined in 'propspec' | |
189 | ''' | |
190 | role = Role(**propspec) | |
191 | self.role[role.name] = role | |
192 | return role | |
193 | ||
194 | def addPermissionToRole(self, rolename, permission, classname=None, | |
195 | properties=None, check=None): | |
196 | ''' Add the permission to the role's permission list. | |
197 | ||
198 | 'rolename' is the name of the role to add the permission to. | |
199 | ||
200 | 'permission' is either a Permission *or* a permission name | |
201 | accompanied by 'classname' (thus in the second case a Permission | |
202 | is obtained by passing 'permission' and 'classname' to | |
203 | self.getPermission) | |
204 | ''' | |
205 | if not isinstance(permission, Permission): | |
206 | permission = self.getPermission(permission, classname, | |
207 | properties, check) | |
208 | role = self.role[rolename.lower()] | |
209 | role.permissions.append(permission) | |
210 | ||
211 | # vim: set filetype=python sts=4 sw=4 et si : |