Statistics
| Branch: | Tag: | Revision:

root / astakosclient / astakosclient / __init__.py @ 1ecb12b5

History | View | Annotate | Download (11.4 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 urllib
37
import hashlib
38
from copy import copy
39

    
40
import simplejson
41
from astakosclient.utils import retry, scheme_to_class
42
from astakosclient.errors import \
43
    AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \
44
    NoUserName, NoUUID
45

    
46

    
47
# --------------------------------------------------------------------
48
# Astakos Client Class
49

    
50
def get_token_from_cookie(request, cookie_name):
51
    """Extract token from the cookie name provided
52

53
    Cookie should be in the same form as astakos
54
    service sets its cookie contents:
55
        <user_uniq>|<user_token>
56

57
    """
58
    try:
59
        cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
60
        return cookie_content.split("|")[1]
61
    except:
62
        return None
63

    
64

    
65
class AstakosClient():
66
    """AstakosClient Class Implementation"""
67

    
68
    # ----------------------------------
69
    def __init__(self, astakos_url, retry=0,
70
                 use_pool=False, pool_size=8, logger=None):
71
        """Intialize AstakosClient Class
72

73
        Keyword arguments:
74
        astakos_url -- i.e https://accounts.example.com (string)
75
        use_pool    -- use objpool for http requests (boolean)
76
        retry       -- how many time to retry (integer)
77
        logger      -- pass a different logger
78

79
        """
80
        if logger is None:
81
            logging.basicConfig(
82
                format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
83
                datefmt='%Y-%m-%d %H:%M:%S',
84
                level=logging.INFO)
85
            logger = logging.getLogger("astakosclient")
86
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
87
                     "use_pool = %s" % (astakos_url, use_pool))
88

    
89
        if not astakos_url:
90
            m = "Astakos url not given"
91
            logger.error(m)
92
            raise ValueError(m)
93

    
94
        # Check for supported scheme
95
        p = urlparse.urlparse(astakos_url)
96
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
97
        if conn_class is None:
98
            m = "Unsupported scheme: %s" % p.scheme
99
            logger.error(m)
100
            raise ValueError(m)
101

    
102
        # Save astakos_url etc. in our class
103
        self.retry = retry
104
        self.logger = logger
105
        self.netloc = p.netloc
106
        self.scheme = p.scheme
107
        self.conn_class = conn_class
108

    
109
    # ----------------------------------
110
    @retry
111
    def _call_astakos(self, token, request_path,
112
                      headers=None, body=None, method="GET"):
113
        """Make the actual call to Astakos Service"""
114
        hashed_token = hashlib.sha1()
115
        hashed_token.update(token)
116
        self.logger.debug(
117
            "Make a %s request to %s using token %s "
118
            "with headers %s and body %s"
119
            % (method, request_path, hashed_token.hexdigest(), headers, body))
120

    
121
        # Check Input
122
        if not token:
123
            m = "Token not given"
124
            self.logger.error(m)
125
            raise ValueError(m)
126
        if headers is None:
127
            headers = {}
128
        if body is None:
129
            body = {}
130
        if request_path[0] != '/':
131
            request_path = "/" + request_path
132

    
133
        # Build request's header and body
134
        kwargs = {}
135
        kwargs['headers'] = copy(headers)
136
        kwargs['headers']['X-Auth-Token'] = token
137
        if body:
138
            kwargs['body'] = copy(body)
139
            kwargs['headers'].setdefault(
140
                'content-type', 'application/octet-stream')
141
        kwargs['headers'].setdefault('content-length',
142
                                     len(body) if body else 0)
143

    
144
        try:
145
            # Get the connection object
146
            with self.conn_class(self.netloc) as conn:
147
                # Send request
148
                (data, status) = \
149
                    _do_request(conn, method, request_path, **kwargs)
150
        except Exception as err:
151
            self.logger.error("Failed to send request: %s" % repr(err))
152
            raise AstakosClientException(str(err))
153

    
154
        # Return
155
        self.logger.debug("Request returned with status %s" % status)
156
        if status == 400:
157
            raise BadRequest(data)
158
        elif status == 401:
159
            raise Unauthorized(data)
160
        elif status == 403:
161
            raise Forbidden(data)
162
        elif status == 404:
163
            raise NotFound(data)
164
        elif status < 200 or status >= 300:
165
            raise AstakosClientException(data, status)
166
        return simplejson.loads(unicode(data))
167

    
168
    # ------------------------
169
    def get_user_info(self, token, usage=False):
170
        """Authenticate user and get user's info as a dictionary
171

172
        Keyword arguments:
173
        token   -- user's token (string)
174
        usage   -- return usage information for user (boolean)
175

176
        In case of success return user information (json parsed format).
177
        Otherwise raise an AstakosClientException.
178

179
        """
180
        # Send request
181
        auth_path = "/im/authenticate"
182
        if usage:
183
            auth_path += "?usage=1"
184
        return self._call_astakos(token, auth_path)
185

    
186
    # ----------------------------------
187
    def _uuid_catalog(self, token, uuids, req_path):
188
        req_headers = {'content-type': 'application/json'}
189
        req_body = simplejson.dumps({'uuids': uuids})
190
        data = self._call_astakos(
191
            token, req_path, req_headers, req_body, "POST")
192
        if "uuid_catalog" in data:
193
            return data.get("uuid_catalog")
194
        else:
195
            m = "_uuid_catalog request returned %s. No uuid_catalog found" \
196
                % data
197
            self.logger.error(m)
198
            raise AstakosClientException(m)
199

    
200
    def get_usernames(self, token, uuids):
201
        """Return a uuid_catalog dictionary for the given uuids
202

203
        Keyword arguments:
204
        token   -- user's token (string)
205
        uuids   -- list of user ids (list of strings)
206

207
        The returned uuid_catalog is a dictionary with uuids as
208
        keys and the corresponding user names as values
209

210
        """
211
        req_path = "/user_catalogs"
212
        return self._uuid_catalog(token, uuids, req_path)
213

    
214
    def get_username(self, token, uuid):
215
        """Return the user name of a uuid (see get_usernames)"""
216
        if not uuid:
217
            m = "No uuid was given"
218
            self.logger.error(m)
219
            raise ValueError(m)
220
        uuid_dict = self.get_usernames(token, [uuid])
221
        if uuid in uuid_dict:
222
            return uuid_dict.get(uuid)
223
        else:
224
            raise NoUserName(uuid)
225

    
226
    def service_get_usernames(self, token, uuids):
227
        """Return a uuid_catalog dict using a service's token"""
228
        req_path = "/service/api/user_catalogs"
229
        return self._uuid_catalog(token, uuids, req_path)
230

    
231
    def service_get_username(self, token, uuid):
232
        """Return the displayName of a uuid using a service's token"""
233
        if not uuid:
234
            m = "No uuid was given"
235
            self.logger.error(m)
236
            raise ValueError(m)
237
        uuid_dict = self.service_get_usernames(token, [uuid])
238
        if uuid in uuid_dict:
239
            return uuid_dict.get(uuid)
240
        else:
241
            raise NoUserName(uuid)
242

    
243
    # ----------------------------------
244
    def _displayname_catalog(self, token, display_names, req_path):
245
        req_headers = {'content-type': 'application/json'}
246
        req_body = simplejson.dumps({'displaynames': display_names})
247
        data = self._call_astakos(
248
            token, req_path, req_headers, req_body, "POST")
249
        if "displayname_catalog" in data:
250
            return data.get("displayname_catalog")
251
        else:
252
            m = "_displayname_catalog request returned %s. " \
253
                "No displayname_catalog found" % data
254
            self.logger.error(m)
255
            raise AstakosClientException(m)
256

    
257
    def get_uuids(self, token, display_names):
258
        """Return a displayname_catalog for the given names
259

260
        Keyword arguments:
261
        token           -- user's token (string)
262
        display_names   -- list of user names (list of strings)
263

264
        The returned displayname_catalog is a dictionary with
265
        the names as keys and the corresponding uuids as values
266

267
        """
268
        req_path = "/user_catalogs"
269
        return self._displayname_catalog(token, display_names, req_path)
270

    
271
    def get_uuid(self, token, display_name):
272
        """Return the uuid of a name (see getUUIDs)"""
273
        if not display_name:
274
            m = "No display_name was given"
275
            self.logger.error(m)
276
            raise ValueError(m)
277
        name_dict = self.get_uuids(token, [display_name])
278
        if display_name in name_dict:
279
            return name_dict.get(display_name)
280
        else:
281
            raise NoUUID(display_name)
282

    
283
    def service_get_uuids(self, token, display_names):
284
        """Return a display_name catalog using a service's token"""
285
        req_path = "/service/api/user_catalogs"
286
        return self._displayname_catalog(token, display_names, req_path)
287

    
288
    def service_get_uuid(self, token, display_name):
289
        """Return the uuid of a name using a service's token"""
290
        if not display_name:
291
            m = "No display_name was given"
292
            self.logger.error(m)
293
            raise ValueError(m)
294
        name_dict = self.service_get_uuids(token, [display_name])
295
        if display_name in name_dict:
296
            return name_dict.get(display_name)
297
        else:
298
            raise NoUUID(display_name)
299

    
300
    # ----------------------------------
301
    def get_services(self):
302
        """Return a list of dicts with the registered services"""
303
        return self._call_astakos("dummy token", "/im/get_services")
304

    
305

    
306
# --------------------------------------------------------------------
307
# Private functions
308
# We want _doRequest to be a distinct function
309
# so that we can replace it during unit tests.
310
def _do_request(conn, method, url, **kwargs):
311
    """The actual request. This function can easily be mocked"""
312
    conn.request(method, url, **kwargs)
313
    response = conn.getresponse()
314
    length = response.getheader('content-length', None)
315
    data = response.read(length)
316
    status = int(response.status)
317
    return (data, status)