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 md5
87 import random
88 import time
89
90 from twisted.internet import defer, reactor
91
92 from flumotion.common import interfaces, keycards, errors
93 from flumotion.common.poller import Poller
94 from flumotion.common.componentui import WorkerComponentUIState
95
96 from flumotion.component import component
97 from flumotion.twisted import flavors, credentials
98
99 __all__ = ['Bouncer']
100 __version__ = "$Rev: 6982 $"
101
102
104
105 logCategory = 'bouncermedium'
107 """
108 Authenticates the given keycard.
109
110 @type keycard: L{flumotion.common.keycards.Keycard}
111 """
112 return self.comp.authenticate(keycard)
113
115 """
116 Resets the expiry timeout for keycards issued by issuerName.
117
118 @param issuerName: the issuer for which keycards should be kept
119 alive; that is to say, keycards with the
120 attribute 'issuerName' set to this value will
121 have their ttl values reset.
122 @type issuerName: str
123 @param ttl: the new expiry timeout
124 @type ttl: number
125 """
126 return self.comp.keepAlive(issuerName, ttl)
127
129 try:
130 self.comp.removeKeycardId(keycardId)
131
132 except KeyError:
133 self.warning('Could not remove keycard id %s' % keycardId)
134
136 """
137 Called by bouncer views to expire keycards.
138 """
139 return self.comp.expireKeycardId(keycardId)
140
143
146
147 -class Bouncer(component.BaseComponent):
148 """
149 I am the base class for all bouncers.
150
151 @cvar keycardClasses: tuple of all classes of keycards this bouncer can
152 authenticate, in order of preference
153 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard}
154 class objects
155 """
156 keycardClasses = ()
157 componentMediumClass = BouncerMedium
158 logCategory = 'bouncer'
159
160 KEYCARD_EXPIRE_INTERVAL = 2 * 60
161
173
174 - def setDomain(self, name):
176
177 - def getDomain(self):
179
181 """
182 Verify if the keycard is an instance of a Keycard class specified
183 in the bouncer's keycardClasses variable.
184 """
185 return isinstance(keycard, self.keycardClasses)
186
188 if not enabled and self.enabled:
189
190
191 self.expireAllKeycards()
192 self._expirer.stop()
193
194 self.enabled = enabled
195
198
202
209
211 if not self.typeAllowed(keycard):
212 self.warning('keycard %r is not an allowed keycard class', keycard)
213 return None
214
215 if self.enabled:
216 if not self._expirer.running and hasattr(keycard, 'ttl'):
217 self.debug('installing keycard timeout poller')
218 self._expirer.start()
219 return defer.maybeDeferred(self.do_authenticate, keycard)
220 else:
221 self.debug("Bouncer disabled, refusing authentication")
222 return None
223
225 """
226 Must be overridden by subclasses.
227
228 Authenticate the given keycard.
229 Return the keycard with state AUTHENTICATED to authenticate,
230 with state REQUESTING to continue the authentication process,
231 or None to deny the keycard, or a deferred which should have the same
232 eventual value.
233 """
234 raise NotImplementedError("authenticate not overridden")
235
237 return keycard in self._keycards.values()
238
240
241
242 keycardId = self._idFormat % self._idCounter
243 self._idCounter += 1
244 return keycardId
245
266
277
279 self.debug("removing keycard with id %s" % keycardId)
280 if not self._keycards.has_key(keycardId):
281 raise KeyError
282
283 keycard = self._keycards[keycardId]
284 self.removeKeycard(keycard)
285
287 for k in self._keycards.itervalues():
288 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
289 k.ttl = ttl
290
295
309
311 """
312 A very trivial bouncer implementation.
313
314 Useful as a concrete bouncer class for which all users are accepted whenever
315 the bouncer is enabled.
316 """
317 keycardClasses = (keycards.KeycardGeneric,)
318
324
326 """
327 A base class for Challenge-Response bouncers
328 """
329
330 challengeResponseClasses = ()
331
333 self._checker = None
334 self._challenges = {}
335 self._db = {}
336
338 self._checker = checker
339
340 - def addUser(self, user, salt, *args):
341 self._db[user] = salt
342 self._checker.addUser(user, *args)
343
355
363
365
366 self.addKeycard(keycard)
367
368
369 if isinstance(keycard, self.challengeResponseClasses):
370
371 if not keycard.challenge:
372 self.debug('putting challenge on keycard %r' % keycard)
373 keycard.challenge = credentials.cryptChallenge()
374 if keycard.username in self._db:
375 keycard.salt = self._db[keycard.username]
376 else:
377
378 string = str(random.randint(pow(10,10), pow(10, 11)))
379 md = md5.new()
380 md.update(string)
381 keycard.salt = md.hexdigest()[:2]
382 self.debug("user not found, inventing bogus salt")
383 self.debug("salt %s, storing challenge for id %s" % (
384 keycard.salt, keycard.id))
385
386 self._challenges[keycard.id] = keycard.challenge
387 return keycard
388
389 if keycard.response:
390
391 if self._challenges[keycard.id] != keycard.challenge:
392 self.removeKeycard(keycard)
393 self.info('keycard %r refused, challenge tampered with' %
394 keycard)
395 return None
396 del self._challenges[keycard.id]
397
398
399 self.debug('submitting keycard %r to checker' % keycard)
400 d = self._checker.requestAvatarId(keycard)
401 d.addCallback(self._requestAvatarIdCallback, keycard)
402 d.addErrback(self._requestAvatarIdErrback, keycard)
403 return d
404