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, common 
 93  from flumotion.common.componentui import WorkerComponentUIState 
 94   
 95  from flumotion.component import component 
 96  from flumotion.twisted import flavors, credentials 
 97   
 98  __all__ = ['Bouncer'] 
 99   
100 -class BouncerMedium(component.BaseComponentMedium):
101 102 logCategory = 'bouncermedium'
103 - def remote_authenticate(self, keycard):
104 """ 105 Authenticates the given keycard. 106 107 @type keycard: L{flumotion.common.keycards.Keycard} 108 """ 109 return self.comp.authenticate(keycard)
110
111 - def remote_keepAlive(self, issuerName, ttl):
112 """ 113 Resets the expiry timeout for keycards issued by issuerName. 114 115 @param issuerName: the issuer for which keycards should be kept 116 alive; that is to say, keycards with the 117 attribute 'issuerName' set to this value will 118 have their ttl values reset. 119 @type issuerName: str 120 @param ttl: the new expiry timeout 121 @type ttl: number 122 """ 123 return self.comp.keepAlive(issuerName, ttl)
124
125 - def remote_removeKeycardId(self, keycardId):
126 try: 127 self.comp.removeKeycardId(keycardId) 128 # FIXME: at least have an exception name please 129 except KeyError: 130 self.warning('Could not remove keycard id %s' % keycardId)
131
132 - def remote_expireKeycardId(self, keycardId):
133 """ 134 Called by bouncer views to expire keycards. 135 """ 136 return self.comp.expireKeycardId(keycardId)
137
138 - def remote_setEnabled(self, enabled):
139 return self.comp.setEnabled(enabled)
140
141 -class Bouncer(component.BaseComponent):
142 """ 143 I am the base class for all bouncers. 144 145 @cvar keycardClasses: tuple of all classes of keycards this bouncer can 146 authenticate, in order of preference 147 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard} 148 class objects 149 """ 150 keycardClasses = () 151 componentMediumClass = BouncerMedium 152 logCategory = 'bouncer' 153 154 KEYCARD_EXPIRE_INTERVAL = 2 * 60 # expire every 2 minutes 155
156 - def init(self):
157 self._idCounter = 0 158 self._idFormat = time.strftime('%Y%m%d%H%M%S-%%d') 159 self._keycards = {} # keycard id -> Keycard 160 self._keycardDatas = {} # keycard id -> data in uiState 161 self.uiState.addListKey('keycards') 162 163 self._expirer = common.Poller(self._expire, 164 self.KEYCARD_EXPIRE_INTERVAL, 165 start=False) 166 self.enabled = True
167
168 - def setDomain(self, name):
169 self.domain = name
170
171 - def getDomain(self):
172 return self.domain
173
174 - def typeAllowed(self, keycard):
175 """ 176 Verify if the keycard is an instance of a Keycard class specified 177 in the bouncer's keycardClasses variable. 178 """ 179 return isinstance(keycard, self.keycardClasses)
180
181 - def setEnabled(self, enabled):
182 if not enabled and self.enabled: 183 # If we were enabled and are being set to disabled, eject the warp 184 # core^w^w^w^wexpire all existing keycards 185 self.expireAllKeycards() 186 self._expirer.stop() 187 188 self.enabled = enabled
189
190 - def do_stop(self):
191 self.setEnabled(False) 192 return defer.succeed(True)
193
194 - def _expire(self):
195 for k in self._keycards.values(): 196 if hasattr(k, 'ttl'): 197 k.ttl -= self._expirer.timeout 198 if k.ttl <= 0: 199 self.expireKeycardId(k.id)
200
201 - def authenticate(self, keycard):
202 if not self.typeAllowed(keycard): 203 self.warning('keycard %r is not an allowed keycard class', keycard) 204 return None 205 206 if self.enabled: 207 if not self._expirer.running and hasattr(keycard, 'ttl'): 208 self.debug('installing keycard timeout poller') 209 self._expirer.start() 210 return defer.maybeDeferred(self.do_authenticate, keycard) 211 else: 212 self.debug("Bouncer disabled, refusing authentication") 213 return None
214
215 - def do_authenticate(self, keycard):
216 """ 217 Must be overridden by subclasses. 218 219 Authenticate the given keycard. 220 Return the keycard with state AUTHENTICATED to authenticate, 221 with state REQUESTING to continue the authentication process, 222 or None to deny the keycard, or a deferred which should have the same 223 eventual value. 224 """ 225 raise NotImplementedError("authenticate not overridden")
226
227 - def hasKeycard(self, keycard):
228 return keycard in self._keycards.values()
229
230 - def generateKeycardId(self):
231 # FIXME: what if it already had one ? 232 # FIXME: deal with wraparound ? 233 id = self._idFormat % self._idCounter 234 self._idCounter += 1 235 return id
236
237 - def addKeycard(self, keycard):
238 # give keycard an id and store it in our hash 239 if self._keycards.has_key(keycard.id): 240 # already in there 241 return 242 243 id = self.generateKeycardId() 244 keycard.id = id 245 246 if hasattr(keycard, 'ttl') and keycard.ttl <= 0: 247 self.log('immediately expiring keycard %r', keycard) 248 return 249 250 self._keycards[id] = keycard 251 data = keycard.getData() 252 self._keycardDatas[id] = data 253 254 self.uiState.append('keycards', data) 255 self.debug("added keycard with id %s, ttl %r", keycard.id, 256 getattr(keycard, 'ttl', None))
257
258 - def removeKeycard(self, keycard):
259 id = keycard.id 260 if not self._keycards.has_key(id): 261 raise KeyError 262 263 del self._keycards[id] 264 265 data = self._keycardDatas[id] 266 self.uiState.remove('keycards', data) 267 del self._keycardDatas[id] 268 self.info("removed keycard with id %s" % id)
269
270 - def removeKeycardId(self, id):
271 self.debug("removing keycard with id %s" % id) 272 if not self._keycards.has_key(id): 273 raise KeyError 274 275 keycard = self._keycards[id] 276 self.removeKeycard(keycard)
277
278 - def keepAlive(self, issuerName, ttl):
279 for k in self._keycards.itervalues(): 280 if hasattr(k, 'issuerName') and k.issuerName == issuerName: 281 k.ttl = ttl
282
283 - def expireAllKeycards(self):
284 return defer.DeferredList( 285 [self.expireKeycardId(id) for id in self._keycards.keys()])
286
287 - def expireKeycardId(self, id):
288 self.log("expiring keycard with id %r", id) 289 if not self._keycards.has_key(id): 290 raise KeyError 291 292 keycard = self._keycards[id] 293 self.removeKeycardId(id) 294 295 if self.medium: 296 return self.medium.callRemote('expireKeycard', 297 keycard.requesterId, keycard.id) 298 else: 299 return defer.succeed(None)
300
301 -class TrivialBouncer(Bouncer):
302 """ 303 A very trivial bouncer implementation. 304 305 Useful as a concrete bouncer class for which all users are accepted whenever 306 the bouncer is enabled. 307 """ 308 keycardClasses = (keycards.KeycardGeneric,) 309
310 - def do_authenticate(self, keycard):
311 self.addKeycard(keycard) 312 keycard.state = keycards.AUTHENTICATED 313 314 return keycard
315
316 -class ChallengeResponseBouncer(Bouncer):
317 """ 318 A base class for Challenge-Response bouncers 319 """ 320 321 challengeResponseClasses = () 322
323 - def init(self):
324 self._checker = None 325 self._challenges = {} 326 self._db = {}
327
328 - def setChecker(self, checker):
329 self._checker = checker
330
331 - def addUser(self, user, salt, *args):
332 self._db[user] = salt 333 self._checker.addUser(user, *args)
334
335 - def _requestAvatarIdCallback(self, PossibleAvatarId, keycard):
336 # authenticated, so return the keycard with state authenticated 337 keycard.state = keycards.AUTHENTICATED 338 self.addKeycard(keycard) 339 if not keycard.avatarId: 340 keycard.avatarId = PossibleAvatarId 341 self.info('authenticated login of "%s"' % keycard.avatarId) 342 self.debug('keycard %r authenticated, id %s, avatarId %s' % ( 343 keycard, keycard.id, keycard.avatarId)) 344 345 return keycard
346
347 - def _requestAvatarIdErrback(self, failure, keycard):
348 failure.trap(errors.NotAuthenticatedError) 349 # FIXME: we want to make sure the "None" we return is returned 350 # as coming from a callback, ie the deferred 351 self.removeKeycard(keycard) 352 self.info('keycard %r refused, Unauthorized' % keycard) 353 return None
354
355 - def do_authenticate(self, keycard):
356 # at this point we add it so there's an ID for challenge-response 357 self.addKeycard(keycard) 358 359 # check if the keycard is ready for the checker, based on the type 360 if isinstance(keycard, self.challengeResponseClasses): 361 # Check if we need to challenge it 362 if not keycard.challenge: 363 self.debug('putting challenge on keycard %r' % keycard) 364 keycard.challenge = credentials.cryptChallenge() 365 if keycard.username in self._db: 366 keycard.salt = self._db[keycard.username] 367 else: 368 # random-ish salt, otherwise it's too obvious 369 string = str(random.randint(pow(10,10), pow(10, 11))) 370 md = md5.new() 371 md.update(string) 372 keycard.salt = md.hexdigest()[:2] 373 self.debug("user not found, inventing bogus salt") 374 self.debug("salt %s, storing challenge for id %s" % ( 375 keycard.salt, keycard.id)) 376 # we store the challenge locally to verify against tampering 377 self._challenges[keycard.id] = keycard.challenge 378 return keycard 379 380 if keycard.response: 381 # Check if the challenge has been tampered with 382 if self._challenges[keycard.id] != keycard.challenge: 383 self.removeKeycard(keycard) 384 self.info('keycard %r refused, challenge tampered with' % 385 keycard) 386 return None 387 del self._challenges[keycard.id] 388 389 # use the checker 390 self.debug('submitting keycard %r to checker' % keycard) 391 d = self._checker.requestAvatarId(keycard) 392 d.addCallback(self._requestAvatarIdCallback, keycard) 393 d.addErrback(self._requestAvatarIdErrback, keycard) 394 return d
395