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