Package flumotion :: Package component :: Package bouncers :: Module bouncer
[hide private]

Source Code for Module flumotion.component.bouncers.bouncer

  1  # -*- Mode: Python -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 21   
 22  """ 
 23  Base class and implementation for bouncer components, who perform 
 24  authentication services for other components. 
 25   
 26  Bouncers receive keycards, defined in L{flumotion.common.keycards}, and 
 27  then authenticate them. 
 28   
 29  Passing a keycard over a PB connection will copy all of the keycard's 
 30  attributes to a remote side, so that bouncer authentication can be 
 31  coupled with PB. Bouncer implementations have to make sure that they 
 32  never store sensitive data as an attribute on a keycard. 
 33   
 34  Keycards have three states: REQUESTING, AUTHENTICATED, and REFUSED. When 
 35  a keycard is first passed to a bouncer, it has the state REQUESTING. 
 36  Bouncers should never read the 'state' attribute on a keycard for any 
 37  authentication-related purpose, since it comes from the remote side. 
 38  Typically, a bouncer will only set the 'state' attribute to 
 39  AUTHENTICATED or REFUSED once it has the information to make such a 
 40  decision. 
 41   
 42  Authentication of keycards is performed in the authenticate() method, 
 43  which takes a keycard as an argument. The Bouncer base class' 
 44  implementation of this method will perform some common checks (e.g., is 
 45  the bouncer enabled, is the keycard of the correct type), and then 
 46  dispatch to the do_authenticate method, which is expected to be 
 47  overridden by subclasses. 
 48   
 49  Implementations of do_authenticate should eventually return a keycard 
 50  with the state AUTHENTICATED or REFUSED. It is acceptable for this 
 51  method to return either a keycard or a deferred that will eventually 
 52  return a keycard. 
 53   
 54  FIXME: Currently, a return value of 'None' is treated as rejecting the 
 55  keycard. This is unintuitive. 
 56   
 57  Challenge-response authentication may be implemented in 
 58  do_authenticate(), by returning a keycard still in the state REQUESTING 
 59  but with extra attributes annotating the keycard. The remote side would 
 60  then be expected to set a response on the card, resubmit, at which point 
 61  authentication could be performed. The exact protocol for this depends 
 62  on the particular keycard class and set of bouncers that can 
 63  authenticate that keycard class. 
 64   
 65  It is expected that a bouncer implementation keeps references on the 
 66  currently active set of authenticated keycards. These keycards can then 
 67  be revoked at any time by the bouncer, which will be effected through an 
 68  'expireKeycard' call. When the code that requested the keycard detects 
 69  that the keycard is no longer necessary, it should notify the bouncer 
 70  via calling 'removeKeycardId'. 
 71   
 72  The above process is leak-prone, however; if for whatever reason, the 
 73  remote side is unable to remove the keycard, the keycard will never be 
 74  removed from the bouncer's state. For that reason there is a more robust 
 75  method: if the keycard has a 'ttl' attribute, then it will be expired 
 76  automatically after 'keycard.ttl' seconds have passed. The remote side 
 77  is then responsible for periodically telling the bouncer which keycards 
 78  are still valid via the 'keepAlive' call, which resets the TTL on the 
 79  given set of keycards. 
 80   
 81  Note that with automatic expiry via the TTL attribute, it is still 
 82  preferred, albeit not strictly necessary, that callers of authenticate() 
 83  call removeKeycardId when the keycard is no longer used. 
 84  """ 
 85   
 86  import md5 
 87  import random 
 88  import time 
 89   
 90  from twisted.internet import defer, reactor 
 91   
 92  from flumotion.common import interfaces, keycards, errors 
 93  from flumotion.common.poller import Poller 
 94  from flumotion.common.componentui import WorkerComponentUIState 
 95   
 96  from flumotion.component import component 
 97  from flumotion.twisted import flavors, credentials 
 98   
 99  __all__ = ['Bouncer'] 
100  __version__ = "$Rev: 6982 $" 
101   
102   
103 -class BouncerMedium(component.BaseComponentMedium):
104 105 logCategory = 'bouncermedium'
106 - def remote_authenticate(self, keycard):
107 """ 108 Authenticates the given keycard. 109 110 @type keycard: L{flumotion.common.keycards.Keycard} 111 """ 112 return self.comp.authenticate(keycard)
113
114 - def remote_keepAlive(self, issuerName, ttl):
115 """ 116 Resets the expiry timeout for keycards issued by issuerName. 117 118 @param issuerName: the issuer for which keycards should be kept 119 alive; that is to say, keycards with the 120 attribute 'issuerName' set to this value will 121 have their ttl values reset. 122 @type issuerName: str 123 @param ttl: the new expiry timeout 124 @type ttl: number 125 """ 126 return self.comp.keepAlive(issuerName, ttl)
127
128 - def remote_removeKeycardId(self, keycardId):
129 try: 130 self.comp.removeKeycardId(keycardId) 131 # FIXME: at least have an exception name please 132 except KeyError: 133 self.warning('Could not remove keycard id %s' % keycardId)
134
135 - def remote_expireKeycardId(self, keycardId):
136 """ 137 Called by bouncer views to expire keycards. 138 """ 139 return self.comp.expireKeycardId(keycardId)
140
141 - def remote_setEnabled(self, enabled):
142 return self.comp.setEnabled(enabled)
143
144 - def remote_getEnabled(self):
145 return self.comp.getEnabled()
146
147 -class Bouncer(component.BaseComponent):
148 """ 149 I am the base class for all bouncers. 150 151 @cvar keycardClasses: tuple of all classes of keycards this bouncer can 152 authenticate, in order of preference 153 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard} 154 class objects 155 """ 156 keycardClasses = () 157 componentMediumClass = BouncerMedium 158 logCategory = 'bouncer' 159 160 KEYCARD_EXPIRE_INTERVAL = 2 * 60 # expire every 2 minutes 161
162 - def init(self):
163 self._idCounter = 0 164 self._idFormat = time.strftime('%Y%m%d%H%M%S-%%d') 165 self._keycards = {} # keycard id -> Keycard 166 self._keycardDatas = {} # keycard id -> data in uiState 167 self.uiState.addListKey('keycards') 168 169 self._expirer = Poller(self._expire, 170 self.KEYCARD_EXPIRE_INTERVAL, 171 start=False) 172 self.enabled = True
173
174 - def setDomain(self, name):
175 self.domain = name
176
177 - def getDomain(self):
178 return self.domain
179
180 - def typeAllowed(self, keycard):
181 """ 182 Verify if the keycard is an instance of a Keycard class specified 183 in the bouncer's keycardClasses variable. 184 """ 185 return isinstance(keycard, self.keycardClasses)
186
187 - def setEnabled(self, enabled):
188 if not enabled and self.enabled: 189 # If we were enabled and are being set to disabled, eject the warp 190 # core^w^w^w^wexpire all existing keycards 191 self.expireAllKeycards() 192 self._expirer.stop() 193 194 self.enabled = enabled
195
196 - def getEnabled(self):
197 return self.enabled
198
199 - def do_stop(self):
200 self.setEnabled(False) 201 return defer.succeed(True)
202
203 - def _expire(self):
204 for k in self._keycards.values(): 205 if hasattr(k, 'ttl'): 206 k.ttl -= self._expirer.timeout 207 if k.ttl <= 0: 208 self.expireKeycardId(k.id)
209
210 - def authenticate(self, keycard):
211 if not self.typeAllowed(keycard): 212 self.warning('keycard %r is not an allowed keycard class', keycard) 213 return None 214 215 if self.enabled: 216 if not self._expirer.running and hasattr(keycard, 'ttl'): 217 self.debug('installing keycard timeout poller') 218 self._expirer.start() 219 return defer.maybeDeferred(self.do_authenticate, keycard) 220 else: 221 self.debug("Bouncer disabled, refusing authentication") 222 return None
223
224 - def do_authenticate(self, keycard):
225 """ 226 Must be overridden by subclasses. 227 228 Authenticate the given keycard. 229 Return the keycard with state AUTHENTICATED to authenticate, 230 with state REQUESTING to continue the authentication process, 231 or None to deny the keycard, or a deferred which should have the same 232 eventual value. 233 """ 234 raise NotImplementedError("authenticate not overridden")
235
236 - def hasKeycard(self, keycard):
237 return keycard in self._keycards.values()
238
239 - def generateKeycardId(self):
240 # FIXME: what if it already had one ? 241 # FIXME: deal with wraparound ? 242 keycardId = self._idFormat % self._idCounter 243 self._idCounter += 1 244 return keycardId
245
246 - def addKeycard(self, keycard):
247 # give keycard an id and store it in our hash 248 if self._keycards.has_key(keycard.id): 249 # already in there 250 return 251 252 keycardId = self.generateKeycardId() 253 keycard.id = keycardId 254 255 if hasattr(keycard, 'ttl') and keycard.ttl <= 0: 256 self.log('immediately expiring keycard %r', keycard) 257 return 258 259 self._keycards[keycardId] = keycard 260 data = keycard.getData() 261 self._keycardDatas[keycardId] = data 262 263 self.uiState.append('keycards', data) 264 self.debug("added keycard with id %s, ttl %r", keycard.id, 265 getattr(keycard, 'ttl', None))
266
267 - def removeKeycard(self, keycard):
268 if not self._keycards.has_key(keycard.id): 269 raise KeyError 270 271 del self._keycards[keycard.id] 272 273 data = self._keycardDatas[keycard.id] 274 self.uiState.remove('keycards', data) 275 del self._keycardDatas[keycard.id] 276 self.info("removed keycard with id %s" % keycard.id)
277
278 - def removeKeycardId(self, keycardId):
279 self.debug("removing keycard with id %s" % keycardId) 280 if not self._keycards.has_key(keycardId): 281 raise KeyError 282 283 keycard = self._keycards[keycardId] 284 self.removeKeycard(keycard)
285
286 - def keepAlive(self, issuerName, ttl):
287 for k in self._keycards.itervalues(): 288 if hasattr(k, 'issuerName') and k.issuerName == issuerName: 289 k.ttl = ttl
290
291 - def expireAllKeycards(self):
292 return defer.DeferredList( 293 [self.expireKeycardId(keycardId) 294 for keycardId in self._keycards.keys()])
295
296 - def expireKeycardId(self, keycardId):
297 self.log("expiring keycard with id %r", keycardId) 298 if not self._keycards.has_key(keycardId): 299 raise KeyError 300 301 keycard = self._keycards[keycardId] 302 self.removeKeycardId(keycardId) 303 304 if self.medium: 305 return self.medium.callRemote('expireKeycard', 306 keycard.requesterId, keycard.id) 307 else: 308 return defer.succeed(None)
309
310 -class TrivialBouncer(Bouncer):
311 """ 312 A very trivial bouncer implementation. 313 314 Useful as a concrete bouncer class for which all users are accepted whenever 315 the bouncer is enabled. 316 """ 317 keycardClasses = (keycards.KeycardGeneric,) 318
319 - def do_authenticate(self, keycard):
320 self.addKeycard(keycard) 321 keycard.state = keycards.AUTHENTICATED 322 323 return keycard
324
325 -class ChallengeResponseBouncer(Bouncer):
326 """ 327 A base class for Challenge-Response bouncers 328 """ 329 330 challengeResponseClasses = () 331
332 - def init(self):
333 self._checker = None 334 self._challenges = {} 335 self._db = {}
336
337 - def setChecker(self, checker):
338 self._checker = checker
339
340 - def addUser(self, user, salt, *args):
341 self._db[user] = salt 342 self._checker.addUser(user, *args)
343
344 - def _requestAvatarIdCallback(self, PossibleAvatarId, keycard):
345 # authenticated, so return the keycard with state authenticated 346 keycard.state = keycards.AUTHENTICATED 347 self.addKeycard(keycard) 348 if not keycard.avatarId: 349 keycard.avatarId = PossibleAvatarId 350 self.info('authenticated login of "%s"' % keycard.avatarId) 351 self.debug('keycard %r authenticated, id %s, avatarId %s' % ( 352 keycard, keycard.id, keycard.avatarId)) 353 354 return keycard
355
356 - def _requestAvatarIdErrback(self, failure, keycard):
357 failure.trap(errors.NotAuthenticatedError) 358 # FIXME: we want to make sure the "None" we return is returned 359 # as coming from a callback, ie the deferred 360 self.removeKeycard(keycard) 361 self.info('keycard %r refused, Unauthorized' % keycard) 362 return None
363
364 - def do_authenticate(self, keycard):
365 # at this point we add it so there's an ID for challenge-response 366 self.addKeycard(keycard) 367 368 # check if the keycard is ready for the checker, based on the type 369 if isinstance(keycard, self.challengeResponseClasses): 370 # Check if we need to challenge it 371 if not keycard.challenge: 372 self.debug('putting challenge on keycard %r' % keycard) 373 keycard.challenge = credentials.cryptChallenge() 374 if keycard.username in self._db: 375 keycard.salt = self._db[keycard.username] 376 else: 377 # random-ish salt, otherwise it's too obvious 378 string = str(random.randint(pow(10,10), pow(10, 11))) 379 md = md5.new() 380 md.update(string) 381 keycard.salt = md.hexdigest()[:2] 382 self.debug("user not found, inventing bogus salt") 383 self.debug("salt %s, storing challenge for id %s" % ( 384 keycard.salt, keycard.id)) 385 # we store the challenge locally to verify against tampering 386 self._challenges[keycard.id] = keycard.challenge 387 return keycard 388 389 if keycard.response: 390 # Check if the challenge has been tampered with 391 if self._challenges[keycard.id] != keycard.challenge: 392 self.removeKeycard(keycard) 393 self.info('keycard %r refused, challenge tampered with' % 394 keycard) 395 return None 396 del self._challenges[keycard.id] 397 398 # use the checker 399 self.debug('submitting keycard %r to checker' % keycard) 400 d = self._checker.requestAvatarId(keycard) 401 d.addCallback(self._requestAvatarIdCallback, keycard) 402 d.addErrback(self._requestAvatarIdErrback, keycard) 403 return d
404