Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-client / astakosclient / __init__.py @ 8f2d7ede

History | View | Annotate | Download (8.7 kB)

1
# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import urlparse
36
import httplib
37
import urllib
38
from copy import copy
39

    
40
import simplejson
41
import objpool.http
42

    
43

    
44
# --------------------------------------------------------------------
45
# Astakos Client Exception
46
class AstakosClientException(Exception):
47
    def __init__(self, message, status=0):
48
        self.message = message
49
        self.status = status
50

    
51
    def __str__(self):
52
        return repr(self.message)
53

    
54

    
55
# --------------------------------------------------------------------
56
# Astakos Client Class
57

    
58
def getTokenFromCookie(request, cookie_name):
59
    """Extract token from the cookie name provided
60

61
    Cookie should be in the same form as astakos
62
    service sets its cookie contents:
63
        <user_uniq>|<user_token>
64

65
    """
66
    try:
67
        cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
68
        return cookie_content.split("|")[1]
69
    except:
70
        return None
71

    
72

    
73
class AstakosClient():
74
    """AstakosClient Class Implementation"""
75

    
76
    # ----------------------------------
77
    def __init__(self, astakos_url, use_pool=False, retry=0, logger=None):
78
        """Intialize AstakosClient Class
79

80
        Keyword arguments:
81
        astakos_url -- i.e https://accounts.example.com (string)
82
        use_pool    -- use objpool for http requests (boolean)
83
        retry       -- how many time to retry (integer)
84
        logger      -- pass a different logger
85

86
        """
87
        if logger is None:
88
            logger = logging.getLogger("astakosclient")
89
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
90
                     "use_pool = %s" % (astakos_url, use_pool))
91

    
92
        if not astakos_url:
93
            m = "Astakos url not given"
94
            logger.error(m)
95
            raise ValueError(m)
96

    
97
        # Check for supported scheme
98
        p = urlparse.urlparse(astakos_url)
99
        conn_class = _scheme_to_class(p.scheme, use_pool)
100
        if conn_class is None:
101
            m = "Unsupported scheme: %s" % p.scheme
102
            logger.error(m)
103
            raise ValueError(m)
104

    
105
        # Save astakos_url etc. in our class
106
        self.retry = retry
107
        self.logger = logger
108
        self.netloc = p.netloc
109
        self.scheme = p.scheme
110
        self.conn_class = conn_class
111

    
112
    # ----------------------------------
113
    def retry(func):
114
        def decorator(self, *args, **kwargs):
115
            attemps = 0
116
            while True:
117
                try:
118
                    return func(self, *args, **kwargs)
119
                except AstakosClientException as err:
120
                    is_last_attempt = attemps == self.retry
121
                    if is_last_attempt:
122
                        raise err
123
                    if err.status == 401 or err.status == 404:
124
                        # In case of Unauthorized response
125
                        # or Not Found return immediately
126
                        raise err
127
                    attemps += 1
128
        return decorator
129

    
130
    # ----------------------------------
131
    @retry
132
    def _callAstakos(self, token, request_path,
133
                     headers=None, body=None, method="GET"):
134
        """Make the actual call to Astakos Service"""
135
        self.logger.debug(
136
            "Make a %s request to %s with headers %s "
137
            "and body %s" % (method, request_path, headers, body))
138

    
139
        # Check Input
140
        if not token:
141
            m = "Token not given"
142
            self.logger.error(m)
143
            raise ValueError(m)
144
        if headers is None:
145
            headers = {}
146
        if body is None:
147
            body = {}
148

    
149
        # Build request's header and body
150
        kwargs = {}
151
        kwargs['headers'] = copy(headers)
152
        kwargs['headers']['X-Auth-Token'] = token
153
        if body:
154
            kwargs['body'] = copy(body)
155
            kwargs['headers'].setdefault(
156
                'content-type', 'application/octet-stream')
157
        kwargs['headers'].setdefault('content-length',
158
                                     len(body) if body else 0)
159

    
160
        # Get the connection object
161
        conn = self.conn_class(self.netloc)
162

    
163
        # Send request
164
        try:
165
            (data, status) = _doRequest(conn, method, request_path, **kwargs)
166
        except Exception as err:
167
            self.logger.error("Failed to send request: %s" % err)
168
            raise AstakosClientException(str(err))
169
        finally:
170
            conn.close()
171

    
172
        # Return
173
        self.logger.debug("Request returned with status %s" % status)
174
        if status < 200 or status >= 300:
175
            raise AstakosClientException(data, status)
176
        return simplejson.loads(unicode(data))
177

    
178
    # ------------------------
179
    def authenticate(self, token, usage=False):
180
        """Check if user is authenticated Astakos user
181

182
        Keyword arguments:
183
        token   -- user's token (string)
184
        usage   -- return usage information for user (boolean)
185

186
        In case of success return user information (json parsed format).
187
        Otherwise raise an AstakosClientException.
188

189
        """
190
        # Send request
191
        auth_path = "/im/authenticate"
192
        if usage:
193
            auth_path += "?usage=1"
194
        return self._callAstakos(token, auth_path)
195

    
196
    # ----------------------------------
197
    def getDisplayNames(self, token, uuids):
198
        """Return a uuid_catalog dictionary for the given uuids
199

200
        Keyword arguments:
201
        token   -- user's token (string)
202
        uuids   -- list of user ids (list of strings)
203

204
        The returned uuid_catalog is a dictionary with uuids as
205
        keys and the corresponding user names as values
206

207
        """
208
        req_headers = {'content-type': 'application/json'}
209
        req_body = simplejson.dumps({'uuids': uuids})
210
        req_path = "/user_catalogs"
211

    
212
        data = self._callAstakos(
213
            token, req_path, req_headers, req_body, "POST")
214
        # XXX: check if exists
215
        return data.get("uuid_catalog")
216

    
217
    def getDisplayName(self, token, uuid):
218
        """Return the displayName of a uuid (see getDisplayNames)"""
219
        if not uuid:
220
            m = "No uuid was given"
221
            self.logger.error(m)
222
            raise ValueError(m)
223
        uuid_dict = self.getDisplayNames(token, [uuid])
224
        # XXX: check if exists
225
        return uuid_dict.get(uuid)
226

    
227

    
228
# --------------------------------------------------------------------
229
# Private functions
230
def _scheme_to_class(scheme, use_pool):
231
    """Return the appropriate conn class for given scheme"""
232
    if scheme == "http":
233
        if use_pool:
234
            return _objpoolHttpScheme
235
        else:
236
            return httplib.HTTPConnection
237
    elif scheme == "https":
238
        if use_pool:
239
            return _objpoolHttpsScheme
240
        else:
241
            return httplib.HTTPSConnection
242
    else:
243
        return None
244

    
245

    
246
def _objpoolHttpScheme(netloc):
247
    """Intialize the appropriate objpool.http class"""
248
    return objpool.http.get_http_connection(netloc, "http")
249

    
250

    
251
def _objpoolHttpsScheme(netloc):
252
    """Intialize the appropriate objpool.http class"""
253
    return objpool.http.get_http_connection(netloc, "https")
254

    
255

    
256
def _doRequest(conn, method, url, **kwargs):
257
    """The actual request. This function can easily be mocked"""
258
    conn.request(method, url, **kwargs)
259
    response = conn.getresponse()
260
    length = response.getheader('content-length', None)
261
    data = response.read(length)
262
    status = int(response.status)
263
    return (data, status)