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

Source Code for Module flumotion.component.bouncers.plug

  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 random 
 87  import time 
 88   
 89  from twisted.internet import defer, reactor 
 90   
 91  from flumotion.common import keycards, common, errors, python 
 92  from flumotion.common.poller import Poller 
 93  from flumotion.component.plugs import base as pbase 
 94  from flumotion.twisted import credentials 
 95   
 96  __all__ = ['BouncerPlug'] 
 97  __version__ = "$Rev: 7990 $" 
 98   
 99   
100 -class BouncerPlug(pbase.ComponentPlug, common.InitMixin):
101 """ 102 I am the base class for all bouncer plugs. 103 104 FIXME: expireKeycardIds has been added to the component bouncer. 105 Because the plug version is not yet used, and will be 106 refactored/redesigned in a near future, the modifications 107 have not been duplicated here. 108 109 @cvar keycardClasses: tuple of all classes of keycards this bouncer can 110 authenticate, in order of preference 111 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard} 112 class objects 113 """ 114 keycardClasses = () 115 logCategory = 'bouncer' 116 117 KEYCARD_EXPIRE_INTERVAL = 2 * 60 118
119 - def __init__(self, *args, **kwargs):
120 pbase.ComponentPlug.__init__(self, *args, **kwargs) 121 common.InitMixin.__init__(self)
122
123 - def init(self):
124 self.medium = None 125 self.enabled = True 126 self._idCounter = 0 127 self._idFormat = time.strftime('%Y%m%d%H%M%S-%%d') 128 self._keycards = {} # keycard id -> Keycard 129 130 self._expirer = Poller(self._expire, 131 self.KEYCARD_EXPIRE_INTERVAL, 132 start=False)
133
134 - def typeAllowed(self, keycard):
135 """ 136 Verify if the keycard is an instance of a Keycard class specified 137 in the bouncer's keycardClasses variable. 138 """ 139 return isinstance(keycard, self.keycardClasses)
140
141 - def setEnabled(self, enabled):
142 if not enabled and self.enabled: 143 # If we were enabled and are being set to disabled, eject the warp 144 # core^w^w^w^wexpire all existing keycards 145 self.expireAllKeycards() 146 self._expirer.stop() 147 148 self.enabled = enabled
149
150 - def setMedium(self, medium):
151 self.medium = medium
152
153 - def stop(self, component):
154 self.setEnabled(False)
155
156 - def _expire(self):
157 for k in self._keycards.values(): 158 if hasattr(k, 'ttl'): 159 k.ttl -= self._expirer.timeout 160 if k.ttl <= 0: 161 self.expireKeycardId(k.id)
162
163 - def authenticate(self, keycard):
164 if not self.typeAllowed(keycard): 165 self.warning('keycard %r is not an allowed keycard class', keycard) 166 return None 167 168 if self.enabled: 169 if not self._expirer.running and hasattr(keycard, 'ttl'): 170 self.debug('installing keycard timeout poller') 171 self._expirer.start() 172 return defer.maybeDeferred(self.do_authenticate, keycard) 173 else: 174 self.debug("Bouncer disabled, refusing authentication") 175 return None
176
177 - def do_authenticate(self, keycard):
178 """ 179 Must be overridden by subclasses. 180 181 Authenticate the given keycard. 182 Return the keycard with state AUTHENTICATED to authenticate, 183 with state REQUESTING to continue the authentication process, 184 or None to deny the keycard, or a deferred which should have the same 185 eventual value. 186 """ 187 raise NotImplementedError("authenticate not overridden")
188
189 - def hasKeycard(self, keycard):
190 return keycard in self._keycards.values()
191
192 - def generateKeycardId(self):
193 keycardId = self._idFormat % self._idCounter 194 self._idCounter += 1 195 return keycardId
196
197 - def addKeycard(self, keycard):
198 """ 199 Adds a keycard to the bouncer. 200 Can be called with the same keycard more than one time. 201 If the keycard has already been added successfully, 202 adding it again will succeed and return True. 203 204 @param keycard: the keycard to add. 205 @return: if the bouncer accepts the keycard. 206 """ 207 # give keycard an id and store it in our hash 208 if keycard.id in self._keycards: 209 # already in there 210 return True 211 212 keycard.id = self.generateKeycardId() 213 214 if hasattr(keycard, 'ttl') and keycard.ttl <= 0: 215 self.log('immediately expiring keycard %r', keycard) 216 return False 217 218 self._keycards[keycard.id] = keycard 219 220 self.debug("added keycard with id %s" % keycard.id) 221 return True
222
223 - def removeKeycard(self, keycard):
224 if not keycard.id in self._keycards: 225 raise KeyError 226 227 del self._keycards[keycard.id] 228 229 self.debug("removed keycard with id %s" % keycard.id)
230
231 - def removeKeycardId(self, keycardId):
232 self.debug("removing keycard with id %s" % keycardId) 233 if not keycardId in self._keycards: 234 raise KeyError 235 236 keycard = self._keycards[keycardId] 237 self.removeKeycard(keycard)
238
239 - def keepAlive(self, issuerName, ttl):
240 for k in self._keycards.itervalues(): 241 if hasattr(k, 'issuerName') and k.issuerName == issuerName: 242 k.ttl = ttl
243
244 - def expireAllKeycards(self):
245 return defer.DeferredList( 246 [self.expireKeycardId(keycardId) 247 for keycardId in self._keycards.keys()])
248
249 - def expireKeycardId(self, keycardId):
250 self.log("expiring keycard with id %r", keycardId) 251 if not keycardId in self._keycards: 252 raise KeyError 253 254 keycard = self._keycards.pop(keycardId) 255 256 return self.medium.callRemote('expireKeycard', 257 keycard.requesterId, keycard.id)
258 259
260 -class BouncerTrivialPlug(BouncerPlug):
261 """ 262 A very trivial bouncer implementation. 263 264 Useful as a concrete bouncer class for which all users are 265 accepted whenever the bouncer is enabled. 266 """ 267 keycardClasses = (keycards.KeycardGeneric, ) 268
269 - def do_authenticate(self, keycard):
270 if not self.addKeycard(keycard): 271 keycard.state = keycards.REFUSED 272 return keycard 273 keycard.state = keycards.AUTHENTICATED 274 return keycard
275 276
277 -class ChallengeResponseBouncerPlug(BouncerPlug):
278 """ 279 A base class for Challenge-Response bouncers 280 """ 281 282 challengeResponseClasses = () 283
284 - def init(self):
285 self._checker = None 286 self._challenges = {} 287 self._db = {}
288
289 - def setChecker(self, checker):
290 self._checker = checker
291
292 - def addUser(self, user, salt, *args):
293 self._db[user] = salt 294 self._checker.addUser(user, *args)
295
296 - def _requestAvatarIdCallback(self, PossibleAvatarId, keycard):
297 # authenticated, so return the keycard with state authenticated 298 keycard.state = keycards.AUTHENTICATED 299 self.addKeycard(keycard) 300 if not keycard.avatarId: 301 keycard.avatarId = PossibleAvatarId 302 self.info('authenticated login of "%s"' % keycard.avatarId) 303 self.debug('keycard %r authenticated, id %s, avatarId %s' % ( 304 keycard, keycard.id, keycard.avatarId)) 305 306 return keycard
307
308 - def _requestAvatarIdErrback(self, failure, keycard):
309 failure.trap(errors.NotAuthenticatedError) 310 # FIXME: we want to make sure the "None" we return is returned 311 # as coming from a callback, ie the deferred 312 self.removeKeycard(keycard) 313 self.info('keycard %r refused, Unauthorized' % keycard) 314 return None
315
316 - def do_authenticate(self, keycard):
317 # at this point we add it so there's an ID for challenge-response 318 if not self.addKeycard(keycard): 319 keycard.state = keycards.REFUSED 320 return keycard 321 322 # check if the keycard is ready for the checker, based on the type 323 if isinstance(keycard, self.challengeResponseClasses): 324 # Check if we need to challenge it 325 if not keycard.challenge: 326 self.debug('putting challenge on keycard %r' % keycard) 327 keycard.challenge = credentials.cryptChallenge() 328 if keycard.username in self._db: 329 keycard.salt = self._db[keycard.username] 330 else: 331 # random-ish salt, otherwise it's too obvious 332 string = str(random.randint(pow(10, 10), pow(10, 11))) 333 md = python.md5() 334 md.update(string) 335 keycard.salt = md.hexdigest()[:2] 336 self.debug("user not found, inventing bogus salt") 337 self.debug("salt %s, storing challenge for id %s" % ( 338 keycard.salt, keycard.id)) 339 # we store the challenge locally to verify against tampering 340 self._challenges[keycard.id] = keycard.challenge 341 return keycard 342 343 if keycard.response: 344 # Check if the challenge has been tampered with 345 if self._challenges[keycard.id] != keycard.challenge: 346 self.removeKeycard(keycard) 347 self.info('keycard %r refused, challenge tampered with' % 348 keycard) 349 return None 350 del self._challenges[keycard.id] 351 352 # use the checker 353 self.debug('submitting keycard %r to checker' % keycard) 354 d = self._checker.requestAvatarId(keycard) 355 d.addCallback(self._requestAvatarIdCallback, keycard) 356 d.addErrback(self._requestAvatarIdErrback, keycard) 357 return d
358