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 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
116
127
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
136 if not enabled and self.enabled:
137
138
139 self.expireAllKeycards()
140 self._expirer.stop()
141
142 self.enabled = enabled
143
146
147 - def stop(self, component):
149
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
170
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
184 return keycard in self._keycards.values()
185
187 id = self._idFormat % self._idCounter
188 self._idCounter += 1
189 return id
190
207
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
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
226 for k in self._keycards.itervalues():
227 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
228 k.ttl = ttl
229
233
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
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
258
260 """
261 A base class for Challenge-Response bouncers
262 """
263
264 challengeResponseClasses = ()
265
267 self._checker = None
268 self._challenges = {}
269 self._db = {}
270
272 self._checker = checker
273
274 - def addUser(self, user, salt, *args):
275 self._db[user] = salt
276 self._checker.addUser(user, *args)
277
289
297
299
300 self.addKeycard(keycard)
301
302
303 if isinstance(keycard, self.challengeResponseClasses):
304
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
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
320 self._challenges[keycard.id] = keycard.challenge
321 return keycard
322
323 if keycard.response:
324
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
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