__init__.py 13.9 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, BadValue, QuotaLimit
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):
71
        """Initialize AstakosClient Class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
72 73 74 75

        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
                     "use_pool = %s" % (astakos_url, use_pool))

        if not astakos_url:
            m = "Astakos url not given"
            logger.error(m)
92
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
93 94 95

        # 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
            m = "Unsupported scheme: %s" % p.scheme
            logger.error(m)
100
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
101

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

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
132 133
        # Build request's header and body
        kwargs = {}
134
        kwargs['headers'] = copy(headers)
135 136
        if token is not None:
            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
            # Get the connection object
            with self.conn_class(self.netloc) as conn:
                # Send request
148
                (message, data, status) = \
149
                    _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(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)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
166 167 168
        return simplejson.loads(unicode(data))

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

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
174
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
175 176 177 178 179 180
        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
181
        # Send request
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
182 183 184
        auth_path = "/im/authenticate"
        if usage:
            auth_path += "?usage=1"
185
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
186 187

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

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

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
207
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
208 209 210 211 212 213 214
        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"
215
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
216

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

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

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

262
    def get_uuids(self, token, display_names):
263 264 265 266 267 268 269 270 271 272 273
        """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"
274
        return self._displayname_catalog(token, display_names, req_path)
275

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

288
    def service_get_uuids(self, token, display_names):
289 290
        """Return a display_name catalog using a service's token"""
        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 295 296 297
        """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)
298
            raise BadValue(m)
299
        name_dict = self.service_get_uuids(token, [display_name])
300 301 302 303
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
304

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
305
    # ----------------------------------
306
    # GET "/im/get_services"
307
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
308
        """Return a list of dicts with the registered services"""
309 310 311 312 313 314 315
        return self._call_astakos(None, "/im/get_services")

    # ----------------------------------
    # GET "/astakos/api/resources"
    def get_resources(self):
        """Return a dict of dicts with the available resources"""
        return self._call_astakos(None, "/astakos/api/resources")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
316

317 318 319
    # ----------------------------------
    # GET "/astakos/api/quotas"
    def get_quotas(self, token):
320 321 322 323 324 325 326 327 328
        """Get user's quotas

        Keyword arguments:
        token   -- user's token (string)

        In case of success return a dict of dicts with user's current quotas.
        Otherwise raise an AstakosClientException

        """
329 330
        return self._call_astakos(token, "/astakos/api/quotas")

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
    # ----------------------------------
    # POST "/astakos/api/commisions"
    def issue_commission(self, token, request):
        """Issue a commission

        Keyword arguments:
        token   -- user's token (string)
        request -- commision request (dict)

        In case of success return commission's id (int).
        Otherwise raise an AstakosClientException.

        """
        req_headers = {'content-type': 'application/json'}
        req_body = simplejson.dumps(request)
        try:
            response = self._call_astakos(token, "/astakos/api/commissions",
                                          req_headers, req_body, "POST")
        except AstakosClientException as err:
            if err.status == 413:
                raise QuotaLimit(err.message, err.details)
            else:
                raise

        if "serial" in response:
            return response['serial']
        else:
            m = "issue_commission_core request returned %s. No serial found" \
                % response
            self.logger.error(m)
            raise AstakosClientException(m)

363 364 365 366 367 368 369 370 371 372 373 374 375 376
    # ----------------------------------
    # GET "/astakos/api/commissions"
    def get_pending_commissions(self, token):
        """Get Pending Commissions

        Keyword arguments:
        token   -- user's token (string)

        In case of success return a list of pending commissions' ids
        (list of integers)

        """
        return self._call_astakos(token, "/astakos/api/commissions")

377

378 379
# --------------------------------------------------------------------
# Private functions
380 381
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
382
def _do_request(conn, method, url, **kwargs):
383 384 385 386 387 388
    """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)
389 390
    message = response.reason
    return (message, data, status)