VC: Fix error on clone page for legacy-ID events
[cds-indico.git] / indico / MaKaC / accessControl.py
blob24e482131e22eb1309c91a162b2dfd5ad3633d19
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
18 import itertools
19 from persistent import Persistent
20 from functools import wraps
22 from indico.core import signals
23 from MaKaC.common import info
24 from indico.modules.users.legacy import AvatarUserWrapper
25 from indico.modules.groups.legacy import GroupWrapper
26 from MaKaC.common.contextManager import ContextManager
28 def isFullyAccess(level):
29 def wrap(func):
30 @wraps(func)
31 def decorator(*args):
32 # if protected and checking for fully public OR
33 # if not protected and checking for full private
34 if (args[0].isProtected() and level == - 1) or (not args[0].isProtected() and level == 1):
35 return False
36 for child in args[0].getNonInheritingChildren():
37 if child.getAccessController().getAccessProtectionLevel() != level:
38 return False
39 return True
40 return decorator
41 return wrap
43 def getChildren(level):
44 def wrap(func):
45 @wraps(func)
46 def decorator(*args):
47 return [child for child in args[0].getNonInheritingChildren()
48 if child.getAccessController().getAccessProtectionLevel() != level]
49 return decorator
50 return wrap
53 class AccessController(Persistent):
54 """This class keeps access control information both for accessing and
55 modifying which can be related to any conference object. The fact that
56 we have a separated class which handles this allows us to reuse the code
57 and have a common policy.
58 Objects of this class provide 2 list of users (one contains the users
59 who can access the related object and another one the users which can
60 modify it) along with methods for managing them (granting or revoking
61 privileges).
62 Conference objects can delegate the access control to one of this
63 objects so they don't need to implement again the AC mechanism.
64 Parameters:
65 __accessProtection -- (int) Flag which indicates whether the resource
66 the current access controller is related to is protected (on) or
67 not (off).
68 managers -- (PList) List of recognised users or groups (Principal)
69 allowed to modify the related resource
70 allowed -- (PList) List of recognised users or groups (Principal)
71 allowed to access the related resource
72 submitters -- (PList) List of recognised chairpersons/speakers allowed to manage event materials
73 """
75 def __init__( self, owner ):
76 self._accessProtection = 0
77 self._fatherProtection = 0
78 self._hideFromUnauthorizedUsers = 0
79 self.managers = []
80 self.managersEmail = []
81 self.allowed = []
82 self.allowedEmail = []
83 self.requiredDomains = []
84 self.accessKey = ""
85 self.owner = owner
86 self.contactInfo = ""
87 self.submitters = []
88 self.nonInheritingChildren = set()
90 def getOwner(self):
91 return self.owner
93 def setOwner(self, owner):
94 self.owner = owner
96 def unlinkAvatars(self, role):
97 for prin in itertools.chain(self.getSubmitterList(), self.managers, self.allowed):
98 if isinstance(prin, AvatarUserWrapper):
99 prin.unlinkTo(self.owner, role)
101 def _getAccessProtection( self ):
102 try:
103 return self._accessProtection
104 except:
105 self._accessProtection = 0
106 return 0
108 def getAccessProtectionLevel( self ):
109 return self._getAccessProtection()
111 def _getFatherProtection( self ):
112 try:
113 from MaKaC.conference import Conference
114 if isinstance(self.getOwner(), Conference):
115 ownerList = self.getOwner().getOwnerList()
116 else:
117 ownerList = [self.getOwner().getOwner()]
118 for o in ownerList:
119 if o is not None and o.isProtected():
120 return 1
121 return 0
122 except Exception:
123 self._fatherProtection = 0
124 return 0
126 def isHidden( self ):
127 try:
128 return self._hideFromUnauthorizedUsers
129 except AttributeError:
130 self._hideFromUnauthorizedUsers = 0
131 return 0
133 def setHidden(self, hidden):
134 self._hideFromUnauthorizedUsers = hidden
135 self._p_changed = 1
137 def isProtected( self ):
138 """tells whether the associated resource is read protected or not"""
139 return (self._getAccessProtection() + self._getFatherProtection()) > 0
141 def isItselfProtected(self):
142 return self._getAccessProtection() > 0
144 def setProtection( self, protected ):
145 self._accessProtection = protected
146 self._p_changed = 1
148 def isFatherProtected(self):
149 return self._getFatherProtection()
151 def setFatherProtection( self, protected ):
152 self._fatherProtection = protected
153 self._p_changed = 1
155 def grantAccess(self, principal):
156 """grants read access for the related resource to the specified
157 principal"""
159 if principal not in self.allowed and isinstance(principal, (AvatarUserWrapper, GroupWrapper)):
160 self.allowed.append(principal)
161 self._p_changed = 1
162 signals.acl.access_granted.send(self, principal=principal)
164 def getAccessEmail(self):
165 try:
166 return self.allowedEmail
167 except:
168 self.allowedEmail = []
169 return self.allowedEmail
171 def revokeAccess( self, principal ):
172 """revokes read access for the related resource to the specified
173 principal"""
174 #ToDo: Revoking access to a user is quite more complex than this. We can
175 # just remove the user, but if there's a group in the list that
176 # contains the user, the access to the user is not revoked, he still
177 # has access. But dunno what to do in that case: raise an exception??
178 # Certainly removing the user from the group or the group itself
179 # wouldn't be acceptable solutions.
180 if principal in self.allowed:
181 self.allowed.remove( principal )
182 self._p_changed = 1
183 signals.acl.access_revoked.send(self, principal=principal)
185 def setAccessKey( self, key="" ):
186 self.accessKey = key
188 def getAccessKey( self ):
189 try:
190 return self.accessKey
191 except:
192 self.setAccessKey()
193 return ""
195 def canKeyAccess( self, key ):
198 if self.getAccessKey()!="":
199 if key == self.getAccessKey():
200 return True
201 return False
203 @classmethod
204 def isHarvesterIP( cls, ip ):
205 minfo = info.HelperMaKaCInfo.getMaKaCInfoInstance()
206 ipList = minfo.getIPBasedACLMgr().get_full_access_acl()
208 return ip in ipList
210 def canIPAccess(self, ip):
212 # Domain protection
213 if not self.getRequiredDomainList():
214 return True
215 return any(domain.belongsTo(ip) for domain in self.getRequiredDomainList())
217 def isAdmin(self, av):
218 return av and av.user and av.user.is_admin
220 def canUserAccess(self, av):
221 if self.isAdmin(av):
222 return True
223 for principal in self.allowed:
224 if principal is None:
225 self.revokeAccess(principal)
226 continue
227 if isinstance(principal, (AvatarUserWrapper, GroupWrapper)) and principal.containsUser(av):
228 return True
229 if isinstance(av, AvatarUserWrapper):
230 for email in av.getEmails():
231 if email in self.getAccessEmail():
232 self.grantAccess(av)
233 av.linkTo(self.getOwner(), "manager")
234 return False
236 def getAccessList(self):
237 """returns a list of those principals which have access privileges"""
238 for principal in list(self.allowed):
239 if principal is None:
240 self.revokeAccess(principal)
241 elif isinstance(principal, AvatarUserWrapper) and principal.user is None:
242 self.revokeAccess(principal)
243 return self.allowed
245 def getModificationEmail(self):
246 try:
247 return self.managersEmail
248 except:
249 self.managersEmail = []
250 return self.managersEmail
252 def grantModificationEmail(self, email):
253 """looks if the email is in the managersEmail list (list with the users with access to modification)
254 and if it's not it adds the email to the list
255 Returns True is email was added to the list, False if it was already there.
257 if not email.lower() in map(lambda x: x.lower(), self.getModificationEmail()):
258 self.getModificationEmail().append(email)
259 self._p_changed = 1
260 signals.acl.modification_granted.send(self, principal=email)
261 return True
262 return False
264 def revokeModificationEmail(self, email):
265 if email in self.getModificationEmail():
266 self.getModificationEmail().remove(email)
267 self._p_changed = 1
268 signals.acl.modification_revoked.send(self, principal=email)
270 def grantModification( self, principal ):
271 """grants modification access for the related resource to the specified
272 principal"""
273 # ToDo: should the groups allowed to be managers?
274 if principal not in self.managers and (isinstance(principal, (AvatarUserWrapper, GroupWrapper))):
275 self.managers.append(principal)
276 self._p_changed = 1
277 signals.acl.modification_granted.send(self, principal=principal)
279 def revokeModification( self, principal ):
280 """revokes modification access for the related resource to the
281 specified principal"""
282 if principal in self.managers:
283 self.managers.remove( principal )
284 self._p_changed = 1
285 signals.acl.modification_revoked.send(self, principal=principal)
287 def canModify(self, user):
288 """tells whether the specified user has modification privileges"""
289 if user and user.user.is_admin:
290 return True
292 for principal in self.managers:
293 if isinstance(principal, (AvatarUserWrapper, GroupWrapper)) and principal.containsUser(user):
294 return True
295 ret = False
296 if isinstance(user, AvatarUserWrapper):
297 for email in user.getEmails():
298 if email in self.getModificationEmail():
299 self.grantModification(user)
300 self.revokeModificationEmail(email)
301 ret = True
302 user.linkTo(self.getOwner(), "manager")
303 return ret
305 def getModifierList( self ):
306 """returns a list of those principals which have modification
307 privileges"""
308 for principal in list(self.managers):
309 if principal is None:
310 self.revokeModification(principal)
311 elif isinstance(principal, AvatarUserWrapper) and principal.user is None:
312 self.revokeModification(principal)
313 return self.managers
315 def requireDomain( self, domain ):
316 """adds a domain to the required domain list
318 if domain in self.requiredDomains:
319 return
320 self.requiredDomains.append( domain )
321 self._p_changed = 1
323 def freeDomain( self, domain ):
326 if domain not in self.requiredDomains:
327 return
328 self.requiredDomains.remove( domain )
329 self._p_changed = 1
331 def getRequiredDomainList( self ):
334 return self.requiredDomains
336 def getAnyDomainProtection(self):
338 Checks if the element is protected by domain at any level. It stops checking
339 when it finds an explicitly public or private parent.
341 Returns the list of domains from which the item can be accessed.
344 if self.getAccessProtectionLevel() == 0:
345 owner = self.getOwner().getOwner()
347 # inheriting - get protection from parent
348 if owner:
349 return owner.getAccessController().getAnyDomainProtection()
350 else:
351 # strangely enough, the root category has 2 states
352 # 0 -> inheriting (public) and 1 -> restricted
353 # so, in this case, we really want the category's list
354 return self.getRequiredDomainList()
355 else:
356 return self.getRequiredDomainList()
358 def isDomainProtected(self):
359 if self.getRequiredDomainList():
360 return 1
361 return 0
363 def getAnyContactInfo(self):
364 parent = self.getOwner().getOwner()
365 if not self.getContactInfo() and parent and hasattr(parent, 'getAccessController'):
366 return parent.getAccessController().getAnyContactInfo()
367 else:
368 return self.getContactInfo()
370 def getContactInfo(self):
371 """Defines who to contact in case of access control error.
372 One can use this info to display it along with the exception message"""
373 try:
374 if self.contactInfo:
375 pass
376 except AttributeError:
377 self.contactInfo = ""
378 return self.contactInfo
380 def setContactInfo(self, info):
381 self.contactInfo = info
383 def _grantSubmission(self, av):
384 if av not in self.getSubmitterList():
385 self.submitters.append(av)
386 self._p_changed = 1
388 def grantSubmission(self, sb):
389 """Grants submission privileges for the specified user
391 av = self._getAvatarByEmail(sb.getEmail())
392 if av and av.isActivated():
393 self._grantSubmission(av)
394 elif sb.getEmail():
395 self.getOwner().getConference().getPendingQueuesMgr().addSubmitter(sb, self.getOwner(), False)
397 def _revokeSubmission(self, av):
398 if av in self.getSubmitterList():
399 self.submitters.remove(av)
400 self._p_changed = 1
402 def revokeSubmission(self, sb):
403 """Removes submission privileges for the specified user
405 av = self._getAvatarByEmail(sb.getEmail())
406 self.getOwner().getConference().getPendingQueuesMgr().removeSubmitter(sb, self.getOwner())
407 self._revokeSubmission(av)
409 def _getAvatarByEmail(self, email):
410 from MaKaC.user import AvatarHolder
411 ah = AvatarHolder()
412 avatars = ah.match({"email": email}, exact=1, searchInAuthenticators=False)
413 if not avatars:
414 avatars = ah.match({"email": email}, exact=1)
415 for av in avatars:
416 if av.hasEmail(email):
417 return av
418 return None
420 def getSubmitterList(self, no_groups=False):
421 """Gives the list of users with submission privileges
423 try:
424 return self.submitters
425 except AttributeError:
426 self.submitters = []
427 return self.submitters
429 def canUserSubmit(self, user):
430 """Tells whether a user can submit material
432 return user in self.getSubmitterList()
434 def addNonInheritingChildren(self, obj):
435 self.nonInheritingChildren.add(obj)
436 self._p_changed = 1
438 def removeNonInheritingChildren(self, obj):
439 self.nonInheritingChildren.discard(obj)
440 self._p_changed = 1
442 def getNonInheritingChildren(self):
443 return self.nonInheritingChildren
445 def setNonInheritingChildren(self, nonInheritingChildren):
446 self.nonInheritingChildren = nonInheritingChildren
448 def updateNonInheritingChildren(self, elem, delete=False):
449 if delete or elem.getAccessController().getAccessProtectionLevel() == 0:
450 self.removeNonInheritingChildren(elem)
451 else:
452 self.addNonInheritingChildren(elem)
453 self._p_changed = 1
455 @isFullyAccess(-1)
456 def isFullyPublic(self):
457 pass
459 @isFullyAccess(1)
460 def isFullyPrivate(self):
461 pass
463 @getChildren(-1)
464 def getProtectedChildren(self):
465 pass
467 @getChildren(1)
468 def getPublicChildren(self):
469 pass
472 class CategoryAC(AccessController):
474 def __init__( self ):
475 pass
478 class AccessWrapper:
479 """This class encapsulates the information about an access to the system. It
480 must be initialised by clients if they want to perform access
481 control operations over the objects in the system.
484 def __init__(self, user=None):
485 self._currentUser = user
487 def setUser( self, newAvatar ):
488 self._currentUser = newAvatar
489 ContextManager.set('currentUser', self._currentUser)
491 def getUser( self ):
492 return self._currentUser