Statistics
| Branch: | Tag: | Revision:

root / astakosclient / astakosclient / __init__.py @ c4644612

History | View | Annotate | Download (12 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
        """Initialize 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
        if token is not None:
115
            hashed_token = hashlib.sha1()
116
            hashed_token.update(token)
117
            using_token = "using token %s" % (hashed_token.hexdigest())
118
        else:
119
            using_token = "without using token"
120
        self.logger.debug(
121
            "Make a %s request to %s %s with headers %s and body %s"
122
            % (method, request_path, using_token, headers, body))
123

    
124
        # Check Input
125
        if headers is None:
126
            headers = {}
127
        if body is None:
128
            body = {}
129
        if request_path[0] != '/':
130
            request_path = "/" + request_path
131

    
132
        # Build request's header and body
133
        kwargs = {}
134
        kwargs['headers'] = copy(headers)
135
        if token is not None:
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
                (message, 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(message, data)
158
        elif status == 401:
159
            raise Unauthorized(message, data)
160
        elif status == 403:
161
            raise Forbidden(message, data)
162
        elif status == 404:
163
            raise NotFound(message, data)
164
        elif status < 200 or status >= 300:
165
            raise AstakosClientException(message, data, status)
166
        return simplejson.loads(unicode(data))
167

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

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

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

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

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

    
203
    def get_usernames(self, token, uuids):
204
        """Return a uuid_catalog dictionary for the given uuids
205

206
        Keyword arguments:
207
        token   -- user's token (string)
208
        uuids   -- list of user ids (list of strings)
209

210
        The returned uuid_catalog is a dictionary with uuids as
211
        keys and the corresponding user names as values
212

213
        """
214
        req_path = "/user_catalogs"
215
        return self._uuid_catalog(token, uuids, req_path)
216

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

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

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

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

    
262
    def get_uuids(self, token, display_names):
263
        """Return a displayname_catalog for the given names
264

265
        Keyword arguments:
266
        token           -- user's token (string)
267
        display_names   -- list of user names (list of strings)
268

269
        The returned displayname_catalog is a dictionary with
270
        the names as keys and the corresponding uuids as values
271

272
        """
273
        req_path = "/user_catalogs"
274
        return self._displayname_catalog(token, display_names, req_path)
275

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

    
288
    def service_get_uuids(self, token, display_names):
289
        """Return a display_name catalog using a service's token"""
290
        req_path = "/service/api/user_catalogs"
291
        return self._displayname_catalog(token, display_names, req_path)
292

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

    
305
    # ----------------------------------
306
    # GET "/im/get_services"
307
    def get_services(self):
308
        """Return a list of dicts with the registered services"""
309
        return self._call_astakos(None, "/im/get_services")
310

    
311
    # ----------------------------------
312
    # GET "/astakos/api/resources"
313
    def get_resources(self):
314
        """Return a dict of dicts with the available resources"""
315
        return self._call_astakos(None, "/astakos/api/resources")
316

    
317

    
318
# --------------------------------------------------------------------
319
# Private functions
320
# We want _doRequest to be a distinct function
321
# so that we can replace it during unit tests.
322
def _do_request(conn, method, url, **kwargs):
323
    """The actual request. This function can easily be mocked"""
324
    conn.request(method, url, **kwargs)
325
    response = conn.getresponse()
326
    length = response.getheader('content-length', None)
327
    data = response.read(length)
328
    status = int(response.status)
329
    message = response.reason
330
    return (message, data, status)