root/xcap/authentication.py

Revision 416, 9.1 kB (checked in by Denis Bilenko <denis@ag-projects.com>, 5 weeks ago)

fixed: using openser backend with enable_publish_xcapdiff=yes failed with AttributeError: 'module' object has no attribute 'ServerConfig'

Line 
1# Copyright (C) 2007 AG Projects.
2#
3
4"""XCAP authentication module"""
5
6# XXX this module should be either renamed or refactored as it does more then just auth.
7
8from xcap import tweaks; tweaks.tweak_BasicCredentialFactory()
9
10from zope.interface import Interface, implements
11
12from twisted.internet import defer
13from twisted.python import failure
14from twisted.cred import credentials, portal, checkers, error as credError
15from twisted.web2 import http, server, stream, responsecode
16from twisted.web2.auth.wrapper import HTTPAuthResource, UnauthorizedResponse
17
18from application.configuration.datatypes import StringList
19
20from xcap import __version__
21from xcap.config import ConfigFile, ConfigSection
22from xcap.appusage import getApplicationForURI, namespaces
23from xcap.errors import ResourceNotFound
24from xcap.uri import XCAPUser, XCAPUri
25from xcap.root_uris import root_uris
26
27
28# body of 404 error message to render when user requests xcap-root
29# it's html, because XCAP root is often published on the web.
30# NOTE: there're no plans to convert other error messages to html.
31# Since a web-browser is not the primary tool for accessing XCAP server, text/plain
32# is easier for clients to present to user/save to logs/etc.
33WELCOME = ('<html><head><title>Not Found</title></head>'
34           '<body><h1>Not Found</h1>XCAP server does not serve anything '
35           'directly under XCAP Root URL. You have to be more specific.'
36           '<br><br>'
37           '<address><a href="http://www.openxcap.org">OpenXCAP/%s</address>'
38           '</body></html>') % __version__
39
40
41class AuthenticationConfig(ConfigSection):
42    _datatypes = {'trusted_peers': StringList,
43                  'default_realm': str}
44    default_realm = None
45    trusted_peers = []
46
47configuration = ConfigFile()
48configuration.read_settings('Authentication', AuthenticationConfig)
49
50def parseNodeURI(node_uri, default_realm):
51    """Parses the given Node URI, containing the XCAP root, document selector,
52       and node selector, and returns an XCAPUri instance if succesful."""
53    xcap_root = None
54    for uri in root_uris:
55        if node_uri.startswith(uri):
56            xcap_root = uri
57            break
58    if xcap_root is None:
59        raise ResourceNotFound("XCAP root not found for URI: %s" % node_uri)
60    resource_selector = node_uri[len(xcap_root):]
61    if not resource_selector or resource_selector=='/':
62        raise ResourceNotFound(WELCOME)
63    r = XCAPUri(xcap_root, resource_selector, namespaces)
64    if r.user.domain is None:
65        r.user.domain = default_realm
66    return r
67
68
69class ITrustedPeerCredentials(credentials.ICredentials):
70
71    def checkPeer(self, trusted_peers):
72        pass
73
74
75class TrustedPeerCredentials:
76    implements(ITrustedPeerCredentials)
77
78    def __init__(self, peer):
79        self.peer = peer
80
81    def checkPeer(self, trusted_peers):
82        return self.peer in trusted_peers
83
84## credentials checkers
85
86class TrustedPeerChecker:
87
88    implements(checkers.ICredentialsChecker)
89    credentialInterfaces = (ITrustedPeerCredentials,)
90
91    def __init__(self, trusted_peers):
92        self.trusted_peers = trusted_peers
93
94    def requestAvatarId(self, credentials):
95        """Return the avatar ID for the credentials which must have a 'peer' attribute,
96           or an UnauthorizedLogin in case of a failure."""
97        if credentials.checkPeer(self.trusted_peers):
98            return defer.succeed(credentials.peer)
99        return defer.fail(credError.UnauthorizedLogin())
100
101## avatars
102
103class IAuthUser(Interface):
104    pass
105
106class ITrustedPeer(Interface):
107    pass
108
109class AuthUser(str):
110    """Authenticated XCAP User avatar."""
111    implements(IAuthUser)
112
113class TrustedPeer(str):
114    """Trusted peer avatar."""
115    implements(ITrustedPeer)
116
117## realm
118
119class XCAPAuthRealm(object):
120    """XCAP authentication realm. Receives an avatar ID (a string identifying the user)
121       and a list of interfaces the avatar needs to support. It returns an avatar that
122       encapsulates data about that user."""
123    implements(portal.IRealm)
124
125    def requestAvatar(self, avatarId, mind, *interfaces):
126        if IAuthUser in interfaces:
127            return IAuthUser, AuthUser(avatarId)
128        elif ITrustedPeer in interfaces:
129            return ITrustedPeer, TrustedPeer(avatarId)
130
131        raise NotImplementedError("Only IAuthUser and ITrustedPeer interfaces are supported")
132
133def get_cred(request, default_realm):
134    auth = request.headers.getHeader('authorization')
135    if auth:
136        typ, data = auth
137        if typ == 'basic':
138            return data.decode('base64').split(':', 1)[0], default_realm
139        elif typ == 'digest':
140            raise NotImplementedError
141    return None, default_realm
142
143## authentication wrapper for XCAP resources
144class XCAPAuthResource(HTTPAuthResource):
145
146    def allowedMethods(self):
147        return ('GET', 'PUT', 'DELETE')
148
149    def _updateRealm(self, realm):
150        """Updates the realm of the attached credential factories."""
151        for factory in self.credentialFactories.values():
152            factory.realm = realm
153
154    def authenticate(self, request):
155        """Authenticates an XCAP request."""
156        uri = request.scheme + "://" + request.host + request.uri
157        xcap_uri = parseNodeURI(uri, AuthenticationConfig.default_realm)
158        request.xcap_uri = xcap_uri
159        if xcap_uri.doc_selector.context=='global':
160            return defer.succeed(self.wrappedResource)
161
162        ## For each request the authentication realm must be
163        ## dinamically deducted from the XCAP request URI
164        realm = xcap_uri.user.domain
165
166        if not xcap_uri.user.username:
167            # for 'global' requests there's no username@domain in the URI,
168            # so we will use username and domain from Authorization header
169            xcap_uri.user.username, xcap_uri.user.domain = get_cred(request, AuthenticationConfig.default_realm)
170
171        self._updateRealm(realm)
172        remote_addr = request.remoteAddr.host
173        if AuthenticationConfig.trusted_peers:
174            return self.portal.login(TrustedPeerCredentials(remote_addr),
175                                     None,
176                                     ITrustedPeer
177                                     ).addCallbacks(self._loginSucceeded,
178                                                    self._trustedPeerLoginFailed,
179                                                    (request,), None,
180                                                    (request,), None)
181        return HTTPAuthResource.authenticate(self, request)
182
183    def _trustedPeerLoginFailed(self, result, request):
184        """If the peer is not trusted, fallback to HTTP basic/digest authentication."""
185        return HTTPAuthResource.authenticate(self, request)
186
187    def _loginSucceeded(self, avatar, request):
188        """Authorizes an XCAP request after it has been authenticated."""
189       
190        interface, avatar_id = avatar ## the avatar is the authenticated XCAP User
191        xcap_uri = request.xcap_uri
192
193        application = getApplicationForURI(xcap_uri)
194
195        if not application:
196            raise ResourceNotFound
197
198        if interface is IAuthUser and application.is_authorized(XCAPUser.parse(avatar_id), xcap_uri):
199            return HTTPAuthResource._loginSucceeded(self, avatar, request)
200        elif interface is ITrustedPeer:
201            return HTTPAuthResource._loginSucceeded(self, avatar, request)
202        else:
203            return failure.Failure(
204                      http.HTTPError(
205                        UnauthorizedResponse(
206                        self.credentialFactories,
207                        request.remoteAddr)))
208
209    def locateChild(self, request, seg):
210        """
211        Authenticate the request then return the C{self.wrappedResource}
212        and the unmodified segments.
213        We're not using path location, we want to fall back to the renderHTTP() call.
214        """
215        #return self.authenticate(request), seg
216        return self, server.StopTraversal
217
218    def renderHTTP(self, request):
219        """
220        Authenticate the request then return the result of calling renderHTTP
221        on C{self.wrappedResource}
222        """
223        if request.method not in self.allowedMethods():
224            response = http.Response(responsecode.NOT_ALLOWED)
225            response.headers.setHeader("allow", self.allowedMethods())
226            return response
227
228        def _renderResource(resource):
229            return resource.renderHTTP(request)
230
231        def _finished_reading(ignore, result):
232            data = ''.join(result)
233            request.attachment = data
234            d = self.authenticate(request)
235            d.addCallback(_renderResource)
236            return d
237
238        if request.method in ('PUT', 'DELETE'):
239            # we need to authenticate the request after all the attachment stream
240            # has been read
241            # QQQ DELETE doesn't have any attachments, does it? nor does GET.
242            # QQQ Reading attachment when there isn't one won't hurt, will it?
243            # QQQ So why don't we just do it all the time for all requests?
244            data = []
245            d = stream.readStream(request.stream, data.append)
246            d.addCallback(_finished_reading, data)
247            return d
248        else:
249            d = self.authenticate(request)
250            d.addCallback(_renderResource)
251
252        return d
Note: See TracBrowser for help on using the browser.