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