Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / web / _auth / wrapper.py
1 # -*- test-case-name: twisted.web.test.test_httpauth -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 A guard implementation which supports HTTP header-based authentication
7 schemes.
8
9 If no I{Authorization} header is supplied, an anonymous login will be
10 attempted by using a L{Anonymous} credentials object.  If such a header is
11 supplied and does not contain allowed credentials, or if anonymous login is
12 denied, a 401 will be sent in the response along with I{WWW-Authenticate}
13 headers for each of the allowed authentication schemes.
14 """
15
16 from zope.interface import implements
17
18 from twisted.python import log
19 from twisted.python.components import proxyForInterface
20 from twisted.web.resource import IResource, ErrorPage
21 from twisted.web import util
22 from twisted.cred import error
23 from twisted.cred.credentials import Anonymous
24
25
26 class UnauthorizedResource(object):
27     """
28     Simple IResource to escape Resource dispatch
29     """
30     implements(IResource)
31     isLeaf = True
32
33
34     def __init__(self, factories):
35         self._credentialFactories = factories
36
37
38     def render(self, request):
39         """
40         Send www-authenticate headers to the client
41         """
42         def generateWWWAuthenticate(scheme, challenge):
43             l = []
44             for k,v in challenge.iteritems():
45                 l.append("%s=%s" % (k, quoteString(v)))
46             return "%s %s" % (scheme, ", ".join(l))
47
48         def quoteString(s):
49             return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
50
51         request.setResponseCode(401)
52         for fact in self._credentialFactories:
53             challenge = fact.getChallenge(request)
54             request.responseHeaders.addRawHeader(
55                 'www-authenticate',
56                 generateWWWAuthenticate(fact.scheme, challenge))
57         if request.method == 'HEAD':
58             return ''
59         return 'Unauthorized'
60
61
62     def getChildWithDefault(self, path, request):
63         """
64         Disable resource dispatch
65         """
66         return self
67
68
69
70 class HTTPAuthSessionWrapper(object):
71     """
72     Wrap a portal, enforcing supported header-based authentication schemes.
73
74     @ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
75         avatars.
76
77     @ivar _credentialFactories: A list of L{ICredentialFactory} providers which
78         will be used to decode I{Authorization} headers into L{ICredentials}
79         providers.
80     """
81     implements(IResource)
82     isLeaf = False
83
84     def __init__(self, portal, credentialFactories):
85         """
86         Initialize a session wrapper
87
88         @type portal: C{Portal}
89         @param portal: The portal that will authenticate the remote client
90
91         @type credentialFactories: C{Iterable}
92         @param credentialFactories: The portal that will authenticate the
93             remote client based on one submitted C{ICredentialFactory}
94         """
95         self._portal = portal
96         self._credentialFactories = credentialFactories
97
98
99     def _authorizedResource(self, request):
100         """
101         Get the L{IResource} which the given request is authorized to receive.
102         If the proper authorization headers are present, the resource will be
103         requested from the portal.  If not, an anonymous login attempt will be
104         made.
105         """
106         authheader = request.getHeader('authorization')
107         if not authheader:
108             return util.DeferredResource(self._login(Anonymous()))
109
110         factory, respString = self._selectParseHeader(authheader)
111         if factory is None:
112             return UnauthorizedResource(self._credentialFactories)
113         try:
114             credentials = factory.decode(respString, request)
115         except error.LoginFailed:
116             return UnauthorizedResource(self._credentialFactories)
117         except:
118             log.err(None, "Unexpected failure from credentials factory")
119             return ErrorPage(500, None, None)
120         else:
121             return util.DeferredResource(self._login(credentials))
122
123
124     def render(self, request):
125         """
126         Find the L{IResource} avatar suitable for the given request, if
127         possible, and render it.  Otherwise, perhaps render an error page
128         requiring authorization or describing an internal server failure.
129         """
130         return self._authorizedResource(request).render(request)
131
132
133     def getChildWithDefault(self, path, request):
134         """
135         Inspect the Authorization HTTP header, and return a deferred which,
136         when fired after successful authentication, will return an authorized
137         C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
138         be returned, essentially halting further dispatch on the wrapped
139         resource and all children
140         """
141         # Don't consume any segments of the request - this class should be
142         # transparent!
143         request.postpath.insert(0, request.prepath.pop())
144         return self._authorizedResource(request)
145
146
147     def _login(self, credentials):
148         """
149         Get the L{IResource} avatar for the given credentials.
150
151         @return: A L{Deferred} which will be called back with an L{IResource}
152             avatar or which will errback if authentication fails.
153         """
154         d = self._portal.login(credentials, None, IResource)
155         d.addCallbacks(self._loginSucceeded, self._loginFailed)
156         return d
157
158
159     def _loginSucceeded(self, (interface, avatar, logout)):
160         """
161         Handle login success by wrapping the resulting L{IResource} avatar
162         so that the C{logout} callback will be invoked when rendering is
163         complete.
164         """
165         class ResourceWrapper(proxyForInterface(IResource, 'resource')):
166             """
167             Wrap an L{IResource} so that whenever it or a child of it
168             completes rendering, the cred logout hook will be invoked.
169
170             An assumption is made here that exactly one L{IResource} from
171             among C{avatar} and all of its children will be rendered.  If
172             more than one is rendered, C{logout} will be invoked multiple
173             times and probably earlier than desired.
174             """
175             def getChildWithDefault(self, name, request):
176                 """
177                 Pass through the lookup to the wrapped resource, wrapping
178                 the result in L{ResourceWrapper} to ensure C{logout} is
179                 called when rendering of the child is complete.
180                 """
181                 return ResourceWrapper(self.resource.getChildWithDefault(name, request))
182
183             def render(self, request):
184                 """
185                 Hook into response generation so that when rendering has
186                 finished completely (with or without error), C{logout} is
187                 called.
188                 """
189                 request.notifyFinish().addBoth(lambda ign: logout())
190                 return super(ResourceWrapper, self).render(request)
191
192         return ResourceWrapper(avatar)
193
194
195     def _loginFailed(self, result):
196         """
197         Handle login failure by presenting either another challenge (for
198         expected authentication/authorization-related failures) or a server
199         error page (for anything else).
200         """
201         if result.check(error.Unauthorized, error.LoginFailed):
202             return UnauthorizedResource(self._credentialFactories)
203         else:
204             log.err(
205                 result,
206                 "HTTPAuthSessionWrapper.getChildWithDefault encountered "
207                 "unexpected error")
208             return ErrorPage(500, None, None)
209
210
211     def _selectParseHeader(self, header):
212         """
213         Choose an C{ICredentialFactory} from C{_credentialFactories}
214         suitable to use to decode the given I{Authenticate} header.
215
216         @return: A two-tuple of a factory and the remaining portion of the
217             header value to be decoded or a two-tuple of C{None} if no
218             factory can decode the header value.
219         """
220         elements = header.split(' ')
221         scheme = elements[0].lower()
222         for fact in self._credentialFactories:
223             if fact.scheme == scheme:
224                 return (fact, ' '.join(elements[1:]))
225         return (None, None)