1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """
19 Base class and implementation for bouncer components, who perform
20 authentication services for other components.
21
22 Bouncers receive keycards, defined in L{flumotion.common.keycards}, and
23 then authenticate them.
24
25 Passing a keycard over a PB connection will copy all of the keycard's
26 attributes to a remote side, so that bouncer authentication can be
27 coupled with PB. Bouncer implementations have to make sure that they
28 never store sensitive data as an attribute on a keycard.
29
30 Keycards have three states: REQUESTING, AUTHENTICATED, and REFUSED. When
31 a keycard is first passed to a bouncer, it has the state REQUESTING.
32 Bouncers should never read the 'state' attribute on a keycard for any
33 authentication-related purpose, since it comes from the remote side.
34 Typically, a bouncer will only set the 'state' attribute to
35 AUTHENTICATED or REFUSED once it has the information to make such a
36 decision.
37
38 Authentication of keycards is performed in the authenticate() method,
39 which takes a keycard as an argument. The Bouncer base class'
40 implementation of this method will perform some common checks (e.g., is
41 the bouncer enabled, is the keycard of the correct type), and then
42 dispatch to the do_authenticate method, which is expected to be
43 overridden by subclasses.
44
45 Implementations of do_authenticate should eventually return a keycard
46 with the state AUTHENTICATED or REFUSED. It is acceptable for this
47 method to return either a keycard or a deferred that will eventually
48 return a keycard.
49
50 FIXME: Currently, a return value of 'None' is treated as rejecting the
51 keycard. This is unintuitive.
52
53 Challenge-response authentication may be implemented in
54 do_authenticate(), by returning a keycard still in the state REQUESTING
55 but with extra attributes annotating the keycard. The remote side would
56 then be expected to set a response on the card, resubmit, at which point
57 authentication could be performed. The exact protocol for this depends
58 on the particular keycard class and set of bouncers that can
59 authenticate that keycard class.
60
61 It is expected that a bouncer implementation keeps references on the
62 currently active set of authenticated keycards. These keycards can then
63 be revoked at any time by the bouncer, which will be effected through an
64 'expireKeycard' call. When the code that requested the keycard detects
65 that the keycard is no longer necessary, it should notify the bouncer
66 via calling 'removeKeycardId'.
67
68 The above process is leak-prone, however; if for whatever reason, the
69 remote side is unable to remove the keycard, the keycard will never be
70 removed from the bouncer's state. For that reason there is a more robust
71 method: if the keycard has a 'ttl' attribute, then it will be expired
72 automatically after 'keycard.ttl' seconds have passed. The remote side
73 is then responsible for periodically telling the bouncer which keycards
74 are still valid via the 'keepAlive' call, which resets the TTL on the
75 given set of keycards.
76
77 Note that with automatic expiry via the TTL attribute, it is still
78 preferred, albeit not strictly necessary, that callers of authenticate()
79 call removeKeycardId when the keycard is no longer used.
80 """
81
82 import random
83 import time
84
85 from twisted.internet import defer, reactor
86
87 from flumotion.common import keycards, errors, python, poller
88 from flumotion.common.componentui import WorkerComponentUIState
89
90 from flumotion.component import component
91 from flumotion.twisted import credentials
92
93 __all__ = ['Bouncer']
94 __version__ = "$Rev$"
95
96
97 EXPIRE_BLOCK_SIZE = 100
98
99
101
102 logCategory = 'bouncermedium'
103
105 """
106 Authenticates the given keycard.
107
108 @type keycard: L{flumotion.common.keycards.Keycard}
109 """
110 return self.comp.authenticate(keycard)
111
113 """
114 Resets the expiry timeout for keycards issued by issuerName.
115
116 @param issuerName: the issuer for which keycards should be kept
117 alive; that is to say, keycards with the
118 attribute 'issuerName' set to this value will
119 have their ttl values reset.
120 @type issuerName: str
121 @param ttl: the new expiry timeout
122 @type ttl: number
123 """
124 return self.comp.keepAlive(issuerName, ttl)
125
127 try:
128 self.comp.removeKeycardId(keycardId)
129
130 except KeyError:
131 self.warning('Could not remove keycard id %s' % keycardId)
132
134 """
135 Called by bouncer views to expire keycards.
136 """
137 return self.comp.expireKeycardId(keycardId)
138
140 """
141 Called by bouncer views to expire multiple keycards.
142 """
143 return self.comp.expireKeycardIds(keycardIds)
144
147
150
151
152 -class Bouncer(component.BaseComponent):
153 """
154 I am the base class for all bouncer components.
155
156 @cvar keycardClasses: tuple of all classes of keycards this bouncer can
157 authenticate, in order of preference
158 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard}
159 class objects
160 """
161 keycardClasses = ()
162 componentMediumClass = BouncerMedium
163 logCategory = 'bouncer'
164
165 KEYCARD_EXPIRE_INTERVAL = 2 * 60
166
176
177 - def setDomain(self, name):
179
180 - def getDomain(self):
182
184 """
185 Verify if the keycard is an instance of a Keycard class specified
186 in the bouncer's keycardClasses variable.
187 """
188 return isinstance(keycard, self.keycardClasses)
189
191
192 def callAndPassthru(result, method, *args):
193 method(*args)
194 return result
195
196 if not enabled and self.enabled:
197
198
199 self.enabled = False
200 self._expirer.stop()
201 d = self.expireAllKeycards()
202 d.addCallback(callAndPassthru, self.on_disabled)
203 return d
204 self.enabled = enabled
205 d = defer.succeed(0)
206 d.addCallback(callAndPassthru, self.on_enabled)
207 return d
208
211
214
232
234 """
235 Override to expire keycards managed by sub-classes.
236
237 @param elapsed: time in second since the last expiration call.
238 @type elapsed: int
239 @returns: if there is more keycard to expire. If False is returned,
240 the expirer poller MAY be stopped.
241 @rtype: bool
242 """
243 for k in self._keycards.values():
244 if hasattr(k, 'ttl'):
245 k.ttl -= elapsed
246 if k.ttl <= 0:
247 self.expireKeycardId(k.id)
248 return len(self._keycards) > 0
249
251 """
252 Override to check keycards before authentication steps.
253 Should return True if the keycard is valid, False otherwise.
254 #FIXME: This belong to the base bouncer class
255
256 @param keycard: the keycard that should be validated
257 before authentication
258 @type keycard: flumotion.common.keycards.Keycard
259 @returns: True if the keycard is accepted, False otherwise
260 @rtype: bool
261 """
262 return True
263
265 """
266 Must be overridden by subclasses.
267
268 Authenticate the given keycard.
269 Return the keycard with state AUTHENTICATED to authenticate,
270 with state REQUESTING to continue the authentication process,
271 or REFUSED to deny the keycard or a deferred which should
272 have the same eventual value.
273
274 FIXME: Currently, a return value of 'None' is treated
275 as rejecting the keycard. This is unintuitive.
276
277 FIXME: in fact, for authentication sessions like challenge/response,
278 returning a keycard with state REFUSED instead of None
279 will not work properly and may enter in an asynchronous infinit loop.
280 """
281 raise NotImplementedError("authenticate not overridden")
282
284 """
285 Override to update sub-class specific data related to keycards.
286 Called when the base bouncer accepts and references a new keycard.
287 """
288
290 """
291 Override to cleanup sub-class specific data related to keycards.
292 Called when the base bouncer has cleanup his references to a keycard.
293 """
294
296 """
297 Override to initialize sub-class specific data
298 when the bouncer is enabled.
299 """
300
302 """
303 Override to cleanup sub-class specific data
304 when the bouncer is disabled.
305 """
306
308 return keycard in self._keycards.values()
309
311
312
313 keycardId = self._idFormat % self._idCounter
314 self._idCounter += 1
315 return keycardId
316
318 """
319 Adds a keycard to the bouncer.
320 Can be called with the same keycard more than one time.
321 If the keycard has already been added successfully,
322 adding it again will succeed and return True.
323
324 @param keycard: the keycard to add.
325 @return: if the bouncer accepts the keycard.
326 """
327
328 if keycard.id in self._keycards:
329
330 return True
331
332 keycardId = self.generateKeycardId()
333 keycard.id = keycardId
334
335 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
336 self.log('immediately expiring keycard %r', keycard)
337 return False
338
339 self._addKeycard(keycard)
340 return True
341
350
352 self.debug("removing keycard with id %s" % keycardId)
353 if not keycardId in self._keycards:
354 raise KeyError
355
356 keycard = self._keycards[keycardId]
357 self.removeKeycard(keycard)
358
360 for k in self._keycards.itervalues():
361 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
362 k.ttl = ttl
363
366
380
386
388
389
390
391
392
393 keycardBlock = keycardIds[:EXPIRE_BLOCK_SIZE]
394 keycardIds = keycardIds[EXPIRE_BLOCK_SIZE:]
395 idByReq = {}
396
397 for keycardId in keycardBlock:
398 if keycardId in self._keycards:
399 keycard = self._keycards[keycardId]
400 requesterId = keycard.requesterId
401 idByReq.setdefault(requesterId, []).append(keycardId)
402 self.removeKeycardId(keycardId)
403
404 if not (idByReq and self.medium):
405
406
407
408 finished.callback(total)
409 return
410
411 defs = [self.medium.callRemote('expireKeycards', rid, ids)
412 for rid, ids in idByReq.items()]
413 dl = defer.DeferredList(defs, consumeErrors=True)
414
415 def countExpirations(results, total):
416 return sum([v for s, v in results if s and v]) + total
417
418 dl.addCallback(countExpirations, total)
419 dl.addCallback(self._expireNextKeycardBlock, keycardIds, finished)
420
431
437
438
440 """
441 I am a bouncer that handle pending authentication sessions.
442 I am storing the last keycard of an authenticating session.
443 """
444
446
447 self._sessions = {}
448
450
451 self._sessions.clear()
452
454 """
455 Extracts session info from a keycard.
456 Used by updateAuthSession to store session info.
457 Must be overridden by subclasses.
458 """
459 raise NotImplementedError()
460
462 """
463 Tells if a keycard is related to a pending authentication session.
464 It basically check if the id of the keycard is known.
465
466 @param keycard: the keycard to check
467 @type keycard: flumotion.common.keycards.Keycard
468 @returns: if a pending authentication session associated
469 with the specified keycard exists.
470
471 @rtype: bool
472 """
473 return (keycard.id is not None) and (keycard.id in self._sessions)
474
476 """
477 @return: the last updated keycard for the authentication session
478 associated with the specified keycard
479 @rtype: flumotion.common.keycards.Keycard or None
480 """
481 data = keycard.id and self._sessions.get(keycard.id, None)
482 return data and data[1]
483
485 """
486 Starts an authentication session with a keycard.
487 The keycard id will be generated and set.
488 The session info will be extracted from the keycard
489 by calling the method do_extractKeycardInfo, and can
490 be retrieved by calling getAuthSessionInfo.
491
492 If a the keycard already have and id, and there is
493 an authentication session with this id, the session info
494 is updated from the keycard, and it return True.
495
496 @param keycard: the keycard to update from.
497 @type keycard: flumotion.common.keycards.Keycard
498 @return: if the bouncer accepts the keycard.
499 """
500
501 if self.hasAuthSession(keycard):
502
503 self._updateInfoFromKeycard(keycard)
504 return True
505
506 if keycard.id:
507 self.warning("keycard %r already has an id, but no "
508 "authentication session", keycard)
509 keycard.state = keycards.REFUSED
510 return False
511
512 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
513 self.log('immediately expiring keycard %r', keycard)
514 keycard.state = keycards.REFUSED
515 return False
516
517
518 keycardId = self.generateKeycardId()
519 keycard.id = keycardId
520
521 self._updateInfoFromKeycard(keycard)
522
523 self.debug("started authentication session with with id %s, ttl %r",
524 keycard.id, getattr(keycard, 'ttl', None))
525 return True
526
528 """
529 Updates an authentication session with the last keycard.
530 The session info will be extracted from the keycard
531 by calling the method do_extractKeycardInfo, and can
532 be retrieved by calling getAuthSessionInfo.
533
534 @param keycard: the keycard to update from.
535 @type keycard: flumotion.common.keycards.Keycard
536 """
537
538 if self.hasAuthSession(keycard):
539
540 self._updateInfoFromKeycard(keycard)
541 else:
542 keycard.state = keycards.REFUSED
543
545 """
546 Cancels the authentication session associated
547 with the specified keycard.
548 Used when doing challenge/response authentication.
549 @raise KeyError: when there is no session associated with the keycard.
550 """
551 keycard.state = keycards.REFUSED
552 del self._sessions[keycard.id]
553
555 """
556 Confirms the authentication session represented
557 by the specified keycard is authenticated.
558 This will add the specified keycard to the
559 bouncer keycard list like addKeycard would do
560 but without changing the keycard id.
561 The authentication session data is cleaned up.
562
563 If the bouncer already have a keycard with the same id,
564 the authentication is confirmed but the bouncer keycard
565 is NOT updated. FIXME: is it what we want ? ? ?
566
567 @param keycard: the keycard to add to the bouncer list.
568 @type keycard: flumotion.common.keycards.Keycard
569 @return: if the bouncer accepts the keycard.
570 """
571 keycardId = keycard.id
572
573 if keycardId not in self._sessions:
574 self.warning("unknown authentication session, or pending keycard "
575 "expired for id %s", keycardId)
576 keycard.state = keycards.REFUSED
577 return False
578
579 del self._sessions[keycardId]
580
581
582 if keycardId in self._keycards:
583 self.debug("confirming an authentication session we already "
584 "know about with id %s", keycardId)
585 keycard.state = keycards.AUTHENTICATED
586 return True
587
588
589 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
590 self.log('immediately expiring keycard %r', keycard)
591 keycard.state = keycards.REFUSED
592 return False
593
594 keycard.state = keycards.AUTHENTICATED
595 self._addKeycard(keycard)
596 return True
597
599 """
600 Updates the authentication session data.
601 Can be used bu subclasses to modify the data directly.
602 """
603 ttl, _oldData = self._sessions.get(keycard.id, (None, None))
604 if ttl is None:
605 ttl = getattr(keycard, 'ttl', None)
606 self._sessions[keycard.id] = (ttl, data)
607
609 cont = Bouncer.do_expireKeycards(self, elapsed)
610 for sessionId, (ttl, data) in self._sessions.items():
611 if ttl is not None:
612 ttl -= elapsed
613 self._sessions[sessionId] = (ttl, data)
614 if ttl <= 0:
615 del self._sessions[sessionId]
616
617 return cont and len(self._sessions) > 0
618
623
624
626 """
627 A very trivial bouncer implementation.
628
629 Useful as a concrete bouncer class for which all users are
630 accepted whenever the bouncer is enabled.
631 """
632 keycardClasses = (keycards.KeycardGeneric, )
633
640
641
643 """
644 A base class for Challenge-Response bouncers
645 """
646
647 challengeResponseClasses = ()
648
650 self._checker = None
651 self._challenges = {}
652 self._db = {}
653
655 self._checker = checker
656
657 - def addUser(self, user, salt, *args):
658 self._db[user] = salt
659 self._checker.addUser(user, *args)
660
662 return getattr(keycard, 'challenge', None)
663
679
693
695 if isinstance(keycard, self.challengeResponseClasses):
696
697 if not self.hasAuthSession(keycard):
698 if not self.startAuthSession(keycard):
699
700 keycard.state = keycards.REFUSED
701 return None
702 self.debug('putting challenge on keycard %r' % keycard)
703 keycard.challenge = credentials.cryptChallenge()
704 if keycard.username in self._db:
705 keycard.salt = self._db[keycard.username]
706 else:
707
708 string = str(random.randint(pow(10, 10), pow(10, 11)))
709 md = python.md5()
710 md.update(string)
711 keycard.salt = md.hexdigest()[:2]
712 self.debug("user not found, inventing bogus salt")
713 self.debug("salt %s, storing challenge for id %s"
714 % (keycard.salt, keycard.id))
715 self.updateAuthSession(keycard)
716 return keycard
717 else:
718
719 challenge = self.getAuthSessionInfo(keycard)
720 if challenge != keycard.challenge:
721 self.info('keycard %r refused, challenge tampered with'
722 % keycard)
723 self.cancelAuthSession(keycard)
724 keycard.state = keycards.REFUSED
725 return None
726 else:
727
728
729 if not self.startAuthSession(keycard):
730
731 keycard.state = keycards.REFUSED
732 return None
733
734
735 self.debug('submitting keycard %r to checker' % keycard)
736 d = self._checker.requestAvatarId(keycard)
737 d.addCallback(self._requestAvatarIdCallback, keycard)
738 d.addErrback(self._requestAvatarIdErrback, keycard)
739 return d
740