Brian Sutherland
2010-12-16 cfe26c0de34069c37ab0fb40b87d96f65d5c4e44
commit | author | age
993216 1 from zope.interface import implements
TS 2
b482a1 3 from repoze.who.interfaces import IAPI
993216 4 from repoze.who.interfaces import IAPIFactory
TS 5 from repoze.who.interfaces import IIdentifier
6 from repoze.who.interfaces import IAuthenticator
7 from repoze.who.interfaces import IChallenger
8 from repoze.who.interfaces import IMetadataProvider
9
10
11 def get_api(environ):
12     return environ.get('repoze.who.api')
13
14
15 class APIFactory(object):
16     implements(IAPIFactory)
17
18     def __init__(self,
19                  identifiers=(),
20                  authenticators=(),
21                  challengers=(),
22                  mdproviders=(),
23                  request_classifier=None,
24                  challenge_decider=None,
b482a1 25                  remote_user_key = 'REMOTE_USER',
993216 26                  logger=None,
TS 27                 ):
28         self.identifiers = identifiers
29         self.authenticators = authenticators
30         self.challengers = challengers
31         self.mdproviders = mdproviders
32         self.request_classifier = request_classifier
33         self.challenge_decider = challenge_decider
b482a1 34         self.remote_user_key = remote_user_key
993216 35         self.logger = logger
TS 36
37     def __call__(self, environ):
38         """ See IAPIFactory.
39         """
40         api = environ.get('repoze.who.api')
41         if api is None:
b482a1 42             api = environ['repoze.who.api'] = API(environ, 
TS 43                                                   self.identifiers,
993216 44                                                   self.authenticators,
TS 45                                                   self.challengers,
46                                                   self.mdproviders,
47                                                   self.request_classifier,
48                                                   self.challenge_decider,
b482a1 49                                                   self.remote_user_key,
993216 50                                                   self.logger,
TS 51                                                  )
52         return api
53
54
55 def verify(plugin, iface):
56     from zope.interface.verify import verifyObject
57     verifyObject(iface, plugin, tentative=True)
58
59  
60 def make_registries(identifiers, authenticators, challengers, mdproviders):
61     from zope.interface.verify import BrokenImplementation
62     interface_registry = {}
63     name_registry = {}
64
65     for supplied, iface in [ (identifiers, IIdentifier),
66                              (authenticators, IAuthenticator),
67                              (challengers, IChallenger),
68                              (mdproviders, IMetadataProvider)]:
69
70         for name, value in supplied:
71             try:
72                 verify(value, iface)
73             except BrokenImplementation, why:
74                 why = str(why)
75                 raise ValueError(str(name) + ': ' + why)
76             L = interface_registry.setdefault(iface, [])
77             L.append(value)
78             name_registry[name] = value
79
80     return interface_registry, name_registry
81
82
83 def match_classification(iface, plugins, classification):
84     result = []
85     for plugin in plugins:
86         
87         plugin_classifications = getattr(plugin, 'classifications', {})
88         iface_classifications = plugin_classifications.get(iface)
89         if not iface_classifications: # good for any
90             result.append(plugin)
91             continue
92         if classification in iface_classifications:
93             result.append(plugin)
94
95     return result
96
97
98 class API(object):
b482a1 99     implements(IAPI)
993216 100
TS 101     def __init__(self,
b482a1 102                  environ,
993216 103                  identifiers,
TS 104                  authenticators,
105                  challengers,
106                  mdproviders,
107                  request_classifier,
108                  challenge_decider,
b482a1 109                  remote_user_key,
993216 110                  logger,
TS 111                 ):
b482a1 112         self.environ = environ
993216 113         (self.interface_registry,
TS 114          self.name_registry) = make_registries(identifiers, authenticators,
115                                                challengers, mdproviders)
9a8e60 116         self.identifiers = identifiers
993216 117         self.authenticators = authenticators
TS 118         self.challengers = challengers
119         self.mdproviders = mdproviders
120         self.challenge_decider = challenge_decider
b482a1 121         self.remote_user_key = remote_user_key
993216 122         self.logger = logger
b482a1 123         classification = self.classification = (request_classifier and
TS 124                                                 request_classifier(environ))
125         logger and logger.info('request classification: %s' % classification)
9a8e60 126         
b482a1 127     def authenticate(self):
TS 128
129         ids = self._identify()
130             
131         # ids will be list of tuples: [ (IIdentifier, identity) ]
132         if ids:
133             auth_ids = self._authenticate(ids)
134
135             # auth_ids will be a list of five-tuples in the form
136             #  ( (auth_rank, id_rank), authenticator, identifier, identity,
137             #    userid )
138             #
139             # When sorted, its first element will represent the "best"
140             # identity for this request.
141
142             if auth_ids:
143                 auth_ids.sort()
144                 best = auth_ids[0]
145                 rank, authenticator, identifier, identity, userid = best
146                 identity = Identity(identity) # dont show contents at print
147                 identity['authenticator'] = authenticator
148                 identity['identifier'] = identifier
149
150                 # allow IMetadataProvider plugins to scribble on the identity
151                 self._add_metadata(identity)
152
153                 # add the identity to the environment; a downstream
154                 # application can mutate it to do an 'identity reset'
155                 # as necessary, e.g. identity['login'] = 'foo',
156                 # identity['password'] = 'bar'
157                 self.environ['repoze.who.identity'] = identity
158                 # set the REMOTE_USER
159                 self.environ[self.remote_user_key] = userid
160                 return identity
161
162         self.logger and self.logger.info(
163                         'no identities found, not authenticating')
164
165     def challenge(self, status='403 Forbidden', app_headers=()):
166         """ See IAPI.
167         """
168         identity = self.environ.get('repoze.who.identity', {})
169         identifier = identity.get('identifier')
170
171         logger = self.logger
172
173         forget_headers = []
174
175         if identifier:
176             id_forget_headers = identifier.forget(self.environ, identity)
177             if id_forget_headers is not None:
178                 forget_headers.extend(id_forget_headers)
179                 logger and logger.info('forgetting via headers from %s: %s'
180                                        % (identifier, forget_headers))
181
182         candidates = self.interface_registry.get(IChallenger, ())
52bc23 183         logger and logger.debug('challengers registered: %s' % repr(candidates))
b482a1 184         plugins = match_classification(IChallenger, candidates,
TS 185                                        self.classification)
52bc23 186         logger and logger.debug('challengers matched for '
TS 187                                'classification "%s": %s'
188                                     % (self.classification, plugins))
b482a1 189         for plugin in plugins:
TS 190             app = plugin.challenge(self.environ, status, app_headers,
191                                    forget_headers)
192             if app is not None:
193                 # new WSGI application
194                 logger and logger.info(
195                     'challenger plugin %s "challenge" returned an app' % (
196                     plugin))
197                 return app
198
199         # signifies no challenge
200         logger and logger.info('no challenge app returned')
201         return None
202
b213af 203     def remember(self, identity=None):
b482a1 204         """ See IAPI.
TS 205         """
206         headers = ()
b213af 207         if identity is None:
TS 208             identity = self.environ.get('repoze.who.identity', {})
b482a1 209         identifier = identity.get('identifier')
TS 210         if identifier:
cfe26c 211             got_headers = identifier.remember(self.environ, identity)
BS 212             if got_headers:
213                 headers = got_headers
b482a1 214                 logger = self.logger
TS 215                 logger and logger.info('remembering via headers from %s: %s'
216                                         % (identifier, headers))
217         return headers
218
b213af 219     def forget(self, identity=None):
b482a1 220         """ See IAPI.
TS 221         """
222         headers = ()
b213af 223         if identity is None:
TS 224             identity = self.environ.get('repoze.who.identity', {})
b482a1 225         identifier = identity.get('identifier')
TS 226         if identifier:
cfe26c 227             got_headers = identifier.forget(self.environ, identity)
BS 228             if got_headers:
229                 headers = got_headers
b482a1 230                 logger = self.logger
TS 231                 logger and logger.info('forgetting via headers from %s: %s'
232                                         % (identifier, headers))
233         return headers
234
9a8e60 235     def login(self, credentials, identifier_name=None):
TS 236         """ See IAPI.
237         """
238         if identifier_name is not None:
239             identifier = self.name_registry[identifier_name]
240         else:
241             identifier = self.identifiers[0][1]
242         # Pretend that the given identifier extracted the identity.
243         authenticated = self._authenticate([(identifier, credentials)])
244         if authenticated:
245             # and therefore can remember it
246             rank, plugin, identifier, identity, userid = authenticated[0]
247             headers = identifier.remember(self.environ, identity)
248             return identity, headers
249         else:
250             # or forget it
251             headers = identifier.forget(self.environ, None)
252             return None, headers
253
924f24 254     def logout(self, identifier_name=None):
TS 255         """ See IAPI.
256         """
257         if identifier_name is not None:
258             identifier = self.name_registry[identifier_name]
259         else:
260             identifier = self.identifiers[0][1]
261         # Pretend that the given identifier extracted the identity.
e24a75 262         headers = identifier.forget(self.environ, None)
CM 263
264         # we need to remove the identity for hybrid middleware/api usages to
265         # work correctly: middleware calls ``remember`` unconditionally "on
266         # the way out", and if an identity is found, competing login headers
267         # will be set.
268         if 'repoze.who.identity' in self.environ:
269             del self.environ['repoze.who.identity']
270
271         return headers
924f24 272
b482a1 273     def _identify(self):
TS 274         """ See IAPI.
275         """
993216 276         logger = self.logger
TS 277         candidates = self.interface_registry.get(IIdentifier, ())
52bc23 278         logger and self.logger.debug('identifier plugins registered: %s' %
993216 279                                     (candidates,))
b482a1 280         plugins = match_classification(IIdentifier, candidates,
TS 281                                        self.classification)
52bc23 282         logger and self.logger.debug(
993216 283             'identifier plugins matched for '
b482a1 284             'classification "%s": %s' % (self.classification, plugins))
993216 285
TS 286         results = []
287         for plugin in plugins:
b482a1 288             identity = plugin.identify(self.environ)
993216 289             if identity is not None:
TS 290                 logger and logger.debug(
291                     'identity returned from %s: %s' % (plugin, identity))
292                 results.append((plugin, identity))
293             else:
294                 logger and logger.debug(
295                     'no identity returned from %s (%s)' % (plugin, identity))
296
297         logger and logger.debug('identities found: %s' % (results,))
298         return results
299
b482a1 300     def _authenticate(self, identities):
TS 301         """ See IAPI.
302         """
993216 303         logger = self.logger
TS 304         candidates = self.interface_registry.get(IAuthenticator, [])
52bc23 305         logger and self.logger.debug('authenticator plugins registered: %s' %
993216 306                                     candidates)
TS 307         plugins = match_classification(IAuthenticator, candidates,
b482a1 308                                        self.classification)
52bc23 309         logger and self.logger.debug(
993216 310             'authenticator plugins matched for '
b482a1 311             'classification "%s": %s' % (self.classification, plugins))
993216 312
TS 313         auth_rank = 0
b213af 314         results = []
993216 315
TS 316         for plugin in plugins:
b213af 317             identifier_rank = 0
993216 318             for identifier, identity in identities:
b482a1 319                 userid = plugin.authenticate(self.environ, identity)
993216 320                 if userid is not None:
TS 321                     logger and logger.debug(
322                         'userid returned from %s: "%s"' % (plugin, userid))
323
324                     # stamp the identity with the userid
325                     identity['repoze.who.userid'] = userid
326                     rank = (auth_rank, identifier_rank)
327                     results.append(
328                         (rank, plugin, identifier, identity, userid)
329                         )
330                 else:
331                     logger and logger.debug(
332                         'no userid returned from %s: (%s)' % (
333                         plugin, userid))
334                 identifier_rank += 1
335             auth_rank += 1
336
337         logger and logger.debug('identities authenticated: %s' % (results,))
338         return results
339
b482a1 340     def _add_metadata(self, identity):
TS 341         """ See IAPI.
342         """
993216 343         candidates = self.interface_registry.get(IMetadataProvider, ())
TS 344         plugins = match_classification(IMetadataProvider, candidates,
b482a1 345                                        self.classification)        
993216 346         for plugin in plugins:
b482a1 347             plugin.add_metadata(self.environ, identity)
993216 348
b482a1 349 class Identity(dict):
TS 350     """ dict subclass: prevent members from being rendered during print
351     """
352     def __repr__(self):
353         return '<repoze.who identity (hidden, dict-like) at %s>' % id(self)
354     __str__ = __repr__