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, common
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
101
102 logCategory = 'bouncermedium'
104 """
105 Authenticates the given keycard.
106
107 @type keycard: L{flumotion.common.keycards.Keycard}
108 """
109 return self.comp.authenticate(keycard)
110
112 """
113 Resets the expiry timeout for keycards issued by issuerName.
114
115 @param issuerName: the issuer for which keycards should be kept
116 alive; that is to say, keycards with the
117 attribute 'issuerName' set to this value will
118 have their ttl values reset.
119 @type issuerName: str
120 @param ttl: the new expiry timeout
121 @type ttl: number
122 """
123 return self.comp.keepAlive(issuerName, ttl)
124
126 try:
127 self.comp.removeKeycardId(keycardId)
128
129 except KeyError:
130 self.warning('Could not remove keycard id %s' % keycardId)
131
133 """
134 Called by bouncer views to expire keycards.
135 """
136 return self.comp.expireKeycardId(keycardId)
137
140
141 -class Bouncer(component.BaseComponent):
142 """
143 I am the base class for all bouncers.
144
145 @cvar keycardClasses: tuple of all classes of keycards this bouncer can
146 authenticate, in order of preference
147 @type keycardClasses: tuple of L{flumotion.common.keycards.Keycard}
148 class objects
149 """
150 keycardClasses = ()
151 componentMediumClass = BouncerMedium
152 logCategory = 'bouncer'
153
154 KEYCARD_EXPIRE_INTERVAL = 2 * 60
155
167
168 - def setDomain(self, name):
170
171 - def getDomain(self):
173
175 """
176 Verify if the keycard is an instance of a Keycard class specified
177 in the bouncer's keycardClasses variable.
178 """
179 return isinstance(keycard, self.keycardClasses)
180
182 if not enabled and self.enabled:
183
184
185 self.expireAllKeycards()
186 self._expirer.stop()
187
188 self.enabled = enabled
189
193
195 for k in self._keycards.values():
196 if hasattr(k, 'ttl'):
197 k.ttl -= self._expirer.timeout
198 if k.ttl <= 0:
199 self.expireKeycardId(k.id)
200
214
216 """
217 Must be overridden by subclasses.
218
219 Authenticate the given keycard.
220 Return the keycard with state AUTHENTICATED to authenticate,
221 with state REQUESTING to continue the authentication process,
222 or None to deny the keycard, or a deferred which should have the same
223 eventual value.
224 """
225 raise NotImplementedError("authenticate not overridden")
226
228 return keycard in self._keycards.values()
229
231
232
233 id = self._idFormat % self._idCounter
234 self._idCounter += 1
235 return id
236
257
259 id = keycard.id
260 if not self._keycards.has_key(id):
261 raise KeyError
262
263 del self._keycards[id]
264
265 data = self._keycardDatas[id]
266 self.uiState.remove('keycards', data)
267 del self._keycardDatas[id]
268 self.info("removed keycard with id %s" % id)
269
271 self.debug("removing keycard with id %s" % id)
272 if not self._keycards.has_key(id):
273 raise KeyError
274
275 keycard = self._keycards[id]
276 self.removeKeycard(keycard)
277
279 for k in self._keycards.itervalues():
280 if hasattr(k, 'issuerName') and k.issuerName == issuerName:
281 k.ttl = ttl
282
286
300
302 """
303 A very trivial bouncer implementation.
304
305 Useful as a concrete bouncer class for which all users are accepted whenever
306 the bouncer is enabled.
307 """
308 keycardClasses = (keycards.KeycardGeneric,)
309
315
317 """
318 A base class for Challenge-Response bouncers
319 """
320
321 challengeResponseClasses = ()
322
324 self._checker = None
325 self._challenges = {}
326 self._db = {}
327
329 self._checker = checker
330
331 - def addUser(self, user, salt, *args):
332 self._db[user] = salt
333 self._checker.addUser(user, *args)
334
346
354
356
357 self.addKeycard(keycard)
358
359
360 if isinstance(keycard, self.challengeResponseClasses):
361
362 if not keycard.challenge:
363 self.debug('putting challenge on keycard %r' % keycard)
364 keycard.challenge = credentials.cryptChallenge()
365 if keycard.username in self._db:
366 keycard.salt = self._db[keycard.username]
367 else:
368
369 string = str(random.randint(pow(10,10), pow(10, 11)))
370 md = md5.new()
371 md.update(string)
372 keycard.salt = md.hexdigest()[:2]
373 self.debug("user not found, inventing bogus salt")
374 self.debug("salt %s, storing challenge for id %s" % (
375 keycard.salt, keycard.id))
376
377 self._challenges[keycard.id] = keycard.challenge
378 return keycard
379
380 if keycard.response:
381
382 if self._challenges[keycard.id] != keycard.challenge:
383 self.removeKeycard(keycard)
384 self.info('keycard %r refused, challenge tampered with' %
385 keycard)
386 return None
387 del self._challenges[keycard.id]
388
389
390 self.debug('submitting keycard %r to checker' % keycard)
391 d = self._checker.requestAvatarId(keycard)
392 d.addCallback(self._requestAvatarIdCallback, keycard)
393 d.addErrback(self._requestAvatarIdErrback, keycard)
394 return d
395