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 keycards, common, errors, python
92 from flumotion.common.poller import Poller
93 from flumotion.component.plugs import base as pbase
94 from flumotion.twisted import credentials
95
96 __all__ = ['BouncerPlug']
97 __version__ = "$Rev: 7990 $"
98
99
100 -class BouncerPlug(pbase.ComponentPlug, common.InitMixin):
101 """
102 I am the base class for all bouncer plugs.
103
104 FIXME: expireKeycardIds has been added to the component bouncer.
105 Because the plug version is not yet used, and will be
106 refactored/redesigned in a near future, the modifications
107 have not been duplicated here.
108
109 @cvar keycardClasses: tuple of all classes of keycards this bouncer can
110 authenticate, in order of preference
111 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard}
112 class objects
113 """
114 keycardClasses = ()
115 logCategory = 'bouncer'
116
117 KEYCARD_EXPIRE_INTERVAL = 2 * 60
118
122
133
135 """
136 Verify if the keycard is an instance of a Keycard class specified
137 in the bouncer's keycardClasses variable.
138 """
139 return isinstance(keycard, self.keycardClasses)
140
142 if not enabled and self.enabled:
143
144
145 self.expireAllKeycards()
146 self._expirer.stop()
147
148 self.enabled = enabled
149
152
153 - def stop(self, component):
155
157 for k in self._keycards.values():
158 if hasattr(k, 'ttl'):
159 k.ttl -= self._expirer.timeout
160 if k.ttl <= 0:
161 self.expireKeycardId(k.id)
162
176
178 """
179 Must be overridden by subclasses.
180
181 Authenticate the given keycard.
182 Return the keycard with state AUTHENTICATED to authenticate,
183 with state REQUESTING to continue the authentication process,
184 or None to deny the keycard, or a deferred which should have the same
185 eventual value.
186 """
187 raise NotImplementedError("authenticate not overridden")
188
190 return keycard in self._keycards.values()
191
193 keycardId = self._idFormat % self._idCounter
194 self._idCounter += 1
195 return keycardId
196
198 """
199 Adds a keycard to the bouncer.
200 Can be called with the same keycard more than one time.
201 If the keycard has already been added successfully,
202 adding it again will succeed and return True.
203
204 @param keycard: the keycard to add.
205 @return: if the bouncer accepts the keycard.
206 """
207
208 if keycard.id in self._keycards:
209
210 return True
211
212 keycard.id = self.generateKeycardId()
213
214 if hasattr(keycard, 'ttl') and keycard.ttl <= 0:
215 self.log('immediately expiring keycard %r', keycard)
216 return False
217
218 self._keycards[keycard.id] = keycard
219
220 self.debug("added keycard with id %s" % keycard.id)
221 return True
222
224 if not keycard.id in self._keycards:
225 raise KeyError
226
227 del self._keycards[keycard.id]
228
229 self.debug("removed keycard with id %s" % keycard.id)
230
232 self.debug("removing keycard with id %s" % keycardId)
233 if not keycardId in self._keycards:
234 raise KeyError
235
236 keycard = self._keycards[keycardId]
237 self.removeKeycard(keycard)
238
240 for k in self._keycards.itervalues():
241 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
242 k.ttl = ttl
243
248
250 self.log("expiring keycard with id %r", keycardId)
251 if not keycardId in self._keycards:
252 raise KeyError
253
254 keycard = self._keycards.pop(keycardId)
255
256 return self.medium.callRemote('expireKeycard',
257 keycard.requesterId, keycard.id)
258
259
261 """
262 A very trivial bouncer implementation.
263
264 Useful as a concrete bouncer class for which all users are
265 accepted whenever the bouncer is enabled.
266 """
267 keycardClasses = (keycards.KeycardGeneric, )
268
275
276
278 """
279 A base class for Challenge-Response bouncers
280 """
281
282 challengeResponseClasses = ()
283
285 self._checker = None
286 self._challenges = {}
287 self._db = {}
288
290 self._checker = checker
291
292 - def addUser(self, user, salt, *args):
293 self._db[user] = salt
294 self._checker.addUser(user, *args)
295
307
315
317
318 if not self.addKeycard(keycard):
319 keycard.state = keycards.REFUSED
320 return keycard
321
322
323 if isinstance(keycard, self.challengeResponseClasses):
324
325 if not keycard.challenge:
326 self.debug('putting challenge on keycard %r' % keycard)
327 keycard.challenge = credentials.cryptChallenge()
328 if keycard.username in self._db:
329 keycard.salt = self._db[keycard.username]
330 else:
331
332 string = str(random.randint(pow(10, 10), pow(10, 11)))
333 md = python.md5()
334 md.update(string)
335 keycard.salt = md.hexdigest()[:2]
336 self.debug("user not found, inventing bogus salt")
337 self.debug("salt %s, storing challenge for id %s" % (
338 keycard.salt, keycard.id))
339
340 self._challenges[keycard.id] = keycard.challenge
341 return keycard
342
343 if keycard.response:
344
345 if self._challenges[keycard.id] != keycard.challenge:
346 self.removeKeycard(keycard)
347 self.info('keycard %r refused, challenge tampered with' %
348 keycard)
349 return None
350 del self._challenges[keycard.id]
351
352
353 self.debug('submitting keycard %r to checker' % keycard)
354 d = self._checker.requestAvatarId(keycard)
355 d.addCallback(self._requestAvatarIdCallback, keycard)
356 d.addErrback(self._requestAvatarIdErrback, keycard)
357 return d
358