1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 interfaces, keycards, errors, python
92 from flumotion.common.poller import Poller
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 __version__ = "$Rev$"
100
101 EXPIRE_BLOCK_SIZE = 100
102
103
105
106 logCategory = 'bouncermedium'
107
109 """
110 Authenticates the given keycard.
111
112 @type keycard: L{flumotion.common.keycards.Keycard}
113 """
114 return self.comp.authenticate(keycard)
115
117 """
118 Resets the expiry timeout for keycards issued by issuerName.
119
120 @param issuerName: the issuer for which keycards should be kept
121 alive; that is to say, keycards with the
122 attribute 'issuerName' set to this value will
123 have their ttl values reset.
124 @type issuerName: str
125 @param ttl: the new expiry timeout
126 @type ttl: number
127 """
128 return self.comp.keepAlive(issuerName, ttl)
129
131 try:
132 self.comp.removeKeycardId(keycardId)
133
134 except KeyError:
135 self.warning('Could not remove keycard id %s' % keycardId)
136
138 """
139 Called by bouncer views to expire keycards.
140 """
141 return self.comp.expireKeycardId(keycardId)
142
144 """
145 Called by bouncer views to expire multiple keycards.
146 """
147 return self.comp.expireKeycardIds(keycardIds)
148
151
154
155
156 -class Bouncer(component.BaseComponent):
157 """
158 I am the base class for all bouncer components.
159
160 @cvar keycardClasses: tuple of all classes of keycards this bouncer can
161 authenticate, in order of preference
162 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard}
163 class objects
164 """
165 keycardClasses = ()
166 componentMediumClass = BouncerMedium
167 logCategory = 'bouncer'
168
169 KEYCARD_EXPIRE_INTERVAL = 2 * 60
170
180
181 - def setDomain(self, name):
183
184 - def getDomain(self):
186
188 """
189 Verify if the keycard is an instance of a Keycard class specified
190 in the bouncer's keycardClasses variable.
191 """
192 return isinstance(keycard, self.keycardClasses)
193
195
196 def callAndPassthru(result, method, *args):
197 method(*args)
198 return result
199
200 if not enabled and self.enabled:
201
202
203 self.enabled = False
204 self._expirer.stop()
205 d = self.expireAllKeycards()
206 d.addCallback(callAndPassthru, self.on_disabled)
207 return d
208 self.enabled = enabled
209 d = defer.succeed(0)
210 d.addCallback(callAndPassthru, self.on_enabled)
211 return d
212
215
218
236
238 """
239 Override to expire keycards managed by sub-classes.
240
241 @param elapsed: time in second since the last expiration call.
242 @type elapsed: int
243 @returns: if there is more keycard to expire. If False is returned,
244 the expirer poller MAY be stopped.
245 @rtype: bool
246 """
247 for k in self._keycards.values():
248 if hasattr(k, 'ttl'):
249 k.ttl -= elapsed
250 if k.ttl <= 0:
251 self.expireKeycardId(k.id)
252 return len(self._keycards) > 0
253
255 """
256 Override to check keycards before authentication steps.
257 Should return True if the keycard is valid, False otherwise.
258 #FIXME: This belong to the base bouncer class
259
260 @param keycard: the keycard that should be validated
261 before authentication
262 @type keycard: flumotion.common.keycards.Keycard
263 @returns: True if the keycard is accepted, False otherwise
264 @rtype: bool
265 """
266 return True
267
269 """
270 Must be overridden by subclasses.
271
272 Authenticate the given keycard.
273 Return the keycard with state AUTHENTICATED to authenticate,
274 with state REQUESTING to continue the authentication process,
275 or REFUSED to deny the keycard or a deferred which should
276 have the same eventual value.
277
278 FIXME: Currently, a return value of 'None' is treated
279 as rejecting the keycard. This is unintuitive.
280
281 FIXME: in fact, for authentication sessions like challenge/response,
282 returning a keycard with state REFUSED instead of None
283 will not work properly and may enter in an asynchronous infinit loop.
284 """
285 raise NotImplementedError("authenticate not overridden")
286
288 """
289 Override to update sub-class specific data related to keycards.
290 Called when the base bouncer accepts and references a new keycard.
291 """
292
294 """
295 Override to cleanup sub-class specific data related to keycards.
296 Called when the base bouncer has cleanup his references to a keycard.
297 """
298
300 """
301 Override to initialize sub-class specific data
302 when the bouncer is enabled.
303 """
304
306 """
307 Override to cleanup sub-class specific data
308 when the bouncer is disabled.
309 """
310
312 return keycard in self._keycards.values()
313
315
316
317 keycardId = self._idFormat % self._idCounter
318 self._idCounter += 1
319 return keycardId
320
322 """
323 Adds a keycard to the bouncer.
324 Can be called with the same keycard more than one time.
325 If the keycard has already been added successfully,
326 adding it again will succeed and return True.
327
328 @param keycard: the keycard to add.
329 @return: if the bouncer accepts the keycard.
330 """
331
332 if keycard.id in self._keycards:
333
334 return True
335
336 keycardId = self.generateKeycardId()
337 keycard.id = keycardId
338
339 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
340 self.log('immediately expiring keycard %r', keycard)
341 return False
342
343 self._addKeycard(keycard)
344 return True
345
354
356 self.debug("removing keycard with id %s" % keycardId)
357 if not keycardId in self._keycards:
358 raise KeyError
359
360 keycard = self._keycards[keycardId]
361 self.removeKeycard(keycard)
362
364 for k in self._keycards.itervalues():
365 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
366 k.ttl = ttl
367
370
384
390
392 keycardBlock = keycardIds[:EXPIRE_BLOCK_SIZE]
393 keycardIds = keycardIds[EXPIRE_BLOCK_SIZE:]
394 idByReq = {}
395
396 for keycardId in keycardBlock:
397 if keycardId in self._keycards:
398 keycard = self._keycards[keycardId]
399 requesterId = keycard.requesterId
400 idByReq.setdefault(requesterId, []).append(keycardId)
401 self.removeKeycardId(keycardId)
402
403 if not (idByReq and self.medium):
404 finished.callback(total)
405 return
406
407 defs = [self.medium.callRemote('expireKeycards', rid, ids)
408 for rid, ids in idByReq.items()]
409 dl = defer.DeferredList(defs, consumeErrors=True)
410
411 def countExpirations(results, total):
412 return sum([v for s, v in results if s and v]) + total
413
414 dl.addCallback(countExpirations, total)
415 dl.addCallback(self._expireNextKeycardBlock, keycardIds, finished)
416
427
433
434
436 """
437 I am a bouncer that handle pending authentication sessions.
438 I am storing the last keycard of an authenticating session.
439 """
440
442
443 self._sessions = {}
444
446
447 self._sessions.clear()
448
450 """
451 Extracts session info from a keycard.
452 Used by updateAuthSession to store session info.
453 Must be overridden by subclasses.
454 """
455 raise NotImplementedError()
456
458 """
459 Tells if a keycard is related to a pending authentication session.
460 It basically check if the id of the keycard is known.
461
462 @param keycard: the keycard to check
463 @type keycard: flumotion.common.keycards.Keycard
464 @returns: if a pending authentication session associated
465 with the specified keycard exists.
466
467 @rtype: bool
468 """
469 return (keycard.id is not None) and (keycard.id in self._sessions)
470
472 """
473 @return: the last updated keycard for the authentication session
474 associated with the specified keycard
475 @rtype: flumotion.common.keycards.Keycard or None
476 """
477 data = keycard.id and self._sessions.get(keycard.id, None)
478 return data and data[1]
479
481 """
482 Starts an authentication session with a keycard.
483 The keycard id will be generated and set.
484 The session info will be extracted from the keycard
485 by calling the method do_extractKeycardInfo, and can
486 be retrieved by calling getAuthSessionInfo.
487
488 If a the keycard already have and id, and there is
489 an authentication session with this id, the session info
490 is updated from the keycard, and it return True.
491
492 @param keycard: the keycard to update from.
493 @type keycard: flumotion.common.keycards.Keycard
494 @return: if the bouncer accepts the keycard.
495 """
496
497 if self.hasAuthSession(keycard):
498
499 self._updateInfoFromKeycard(keycard)
500 return True
501
502 if keycard.id:
503 self.warning("keycard %r already has an id, but no "
504 "authentication session", keycard)
505 keycard.state = keycards.REFUSED
506 return False
507
508 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
509 self.log('immediately expiring keycard %r', keycard)
510 keycard.state = keycards.REFUSED
511 return False
512
513
514 keycardId = self.generateKeycardId()
515 keycard.id = keycardId
516
517 self._updateInfoFromKeycard(keycard)
518
519 self.debug("started authentication session with with id %s, ttl %r",
520 keycard.id, getattr(keycard, 'ttl', None))
521 return True
522
524 """
525 Updates an authentication session with the last keycard.
526 The session info will be extracted from the keycard
527 by calling the method do_extractKeycardInfo, and can
528 be retrieved by calling getAuthSessionInfo.
529
530 @param keycard: the keycard to update from.
531 @type keycard: flumotion.common.keycards.Keycard
532 """
533
534 if self.hasAuthSession(keycard):
535
536 self._updateInfoFromKeycard(keycard)
537 else:
538 keycard.state = keycards.REFUSED
539
541 """
542 Cancels the authentication session associated
543 with the specified keycard.
544 Used when doing challenge/response authentication.
545 @raise KeyError: when there is no session associated with the keycard.
546 """
547 keycard.state = keycards.REFUSED
548 del self._sessions[keycard.id]
549
551 """
552 Confirms the authentication session represented
553 by the specified keycard is authenticated.
554 This will add the specified keycard to the
555 bouncer keycard list like addKeycard would do
556 but without changing the keycard id.
557 The authentication session data is cleaned up.
558
559 If the bouncer already have a keycard with the same id,
560 the authentication is confirmed but the bouncer keycard
561 is NOT updated. FIXME: is it what we want ? ? ?
562
563 @param keycard: the keycard to add to the bouncer list.
564 @type keycard: flumotion.common.keycards.Keycard
565 @return: if the bouncer accepts the keycard.
566 """
567 keycardId = keycard.id
568
569 if keycardId not in self._sessions:
570 self.warning("unknown authentication session, or pending keycard "
571 "expired for id %s", keycardId)
572 keycard.state = keycards.REFUSED
573 return False
574
575 del self._sessions[keycardId]
576
577
578 if keycardId in self._keycards:
579 self.debug("confirming an authentication session we already "
580 "know about with id %s", keycardId)
581 keycard.state = keycards.AUTHENTICATED
582 return True
583
584
585 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
586 self.log('immediately expiring keycard %r', keycard)
587 keycard.state = keycards.REFUSED
588 return False
589
590 keycard.state = keycards.AUTHENTICATED
591 self._addKeycard(keycard)
592 return True
593
595 """
596 Updates the authentication session data.
597 Can be used bu subclasses to modify the data directly.
598 """
599 ttl, _oldData = self._sessions.get(keycard.id, (None, None))
600 if ttl is None:
601 ttl = getattr(keycard, 'ttl', None)
602 self._sessions[keycard.id] = (ttl, data)
603
605 cont = Bouncer.do_expireKeycards(self, elapsed)
606 for id, (ttl, data) in self._sessions.items():
607 if ttl is not None:
608 ttl -= elapsed
609 self._sessions[id] = (ttl, data)
610 if ttl <= 0:
611 del self._sessions[id]
612
613 return cont and len(self._sessions) > 0
614
619
620
622 """
623 A very trivial bouncer implementation.
624
625 Useful as a concrete bouncer class for which all users are
626 accepted whenever the bouncer is enabled.
627 """
628 keycardClasses = (keycards.KeycardGeneric, )
629
636
637
639 """
640 A base class for Challenge-Response bouncers
641 """
642
643 challengeResponseClasses = ()
644
646 self._checker = None
647 self._challenges = {}
648 self._db = {}
649
651 self._checker = checker
652
653 - def addUser(self, user, salt, *args):
654 self._db[user] = salt
655 self._checker.addUser(user, *args)
656
658 return getattr(keycard, 'challenge', None)
659
675
689
691 if isinstance(keycard, self.challengeResponseClasses):
692
693 if not self.hasAuthSession(keycard):
694 if not self.startAuthSession(keycard):
695
696 keycard.state = keycards.REFUSED
697 return None
698 self.debug('putting challenge on keycard %r' % keycard)
699 keycard.challenge = credentials.cryptChallenge()
700 if keycard.username in self._db:
701 keycard.salt = self._db[keycard.username]
702 else:
703
704 string = str(random.randint(pow(10, 10), pow(10, 11)))
705 md = python.md5()
706 md.update(string)
707 keycard.salt = md.hexdigest()[:2]
708 self.debug("user not found, inventing bogus salt")
709 self.debug("salt %s, storing challenge for id %s"
710 % (keycard.salt, keycard.id))
711 self.updateAuthSession(keycard)
712 return keycard
713 else:
714
715 challenge = self.getAuthSessionInfo(keycard)
716 if challenge != keycard.challenge:
717 self.info('keycard %r refused, challenge tampered with'
718 % keycard)
719 self.cancelAuthSession(keycard)
720 keycard.state = keycards.REFUSED
721 return None
722 else:
723
724
725 if not self.startAuthSession(keycard):
726
727 keycard.state = keycards.REFUSED
728 return None
729
730
731 self.debug('submitting keycard %r to checker' % keycard)
732 d = self._checker.requestAvatarId(keycard)
733 d.addCallback(self._requestAvatarIdCallback, keycard)
734 d.addErrback(self._requestAvatarIdErrback, keycard)
735 return d
736