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

import logging
import urlparse
36
import urllib
37
import hashlib
38
from copy import copy
39 40

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


47
# --------------------------------------------------------------------
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
48
# Astakos Client Class
49

50
def get_token_from_cookie(request, cookie_name):
51 52 53 54 55 56 57 58 59 60 61 62 63 64
    """Extract token from the cookie name provided

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

    """
    try:
        cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
        return cookie_content.split("|")[1]
    except:
        return None


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
65 66 67 68
class AstakosClient():
    """AstakosClient Class Implementation"""

    # ----------------------------------
69 70
    def __init__(self, astakos_url, retry=0,
                 use_pool=False, pool_size=8, logger=None):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
71 72 73 74 75
        """Intialize AstakosClient Class

        Keyword arguments:
        astakos_url -- i.e https://accounts.example.com (string)
        use_pool    -- use objpool for http requests (boolean)
76
        retry       -- how many time to retry (integer)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
77 78 79 80
        logger      -- pass a different logger

        """
        if logger is None:
81 82 83 84
            logging.basicConfig(
                format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
                datefmt='%Y-%m-%d %H:%M:%S',
                level=logging.INFO)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
85
            logger = logging.getLogger("astakosclient")
86
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
87 88 89 90 91 92 93 94 95
                     "use_pool = %s" % (astakos_url, use_pool))

        if not astakos_url:
            m = "Astakos url not given"
            logger.error(m)
            raise ValueError(m)

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
102
        # Save astakos_url etc. in our class
103
        self.retry = retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
104 105 106
        self.logger = logger
        self.netloc = p.netloc
        self.scheme = p.scheme
107
        self.conn_class = conn_class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
108

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

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

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

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

        # Return
        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:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
165 166 167 168
            raise AstakosClientException(data, status)
        return simplejson.loads(unicode(data))

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

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
173
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
174 175 176 177 178 179
        usage   -- return usage information for user (boolean)

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

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

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

200
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
201 202 203
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
204
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
205 206 207 208 209 210 211
        uuids   -- list of user ids (list of strings)

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

        """
        req_path = "/user_catalogs"
212
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
213

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

226
    def service_get_usernames(self, token, uuids):
227 228
        """Return a uuid_catalog dict using a service's token"""
        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 233 234 235 236
        """Return the displayName of a uuid using a service's token"""
        if not uuid:
            m = "No uuid was given"
            self.logger.error(m)
            raise ValueError(m)
237
        uuid_dict = self.service_get_usernames(token, [uuid])
238 239 240
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
241
            raise NoUserName(uuid)
242

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

257
    def get_uuids(self, token, display_names):
258 259 260 261 262 263 264 265 266 267 268
        """Return a displayname_catalog for the given names

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

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

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

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

283
    def service_get_uuids(self, token, display_names):
284 285
        """Return a display_name catalog using a service's token"""
        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 290 291 292 293
        """Return the uuid of a name using a service's token"""
        if not display_name:
            m = "No display_name was given"
            self.logger.error(m)
            raise ValueError(m)
294
        name_dict = self.service_get_uuids(token, [display_name])
295 296 297 298
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
299

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

305

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