__init__.py 16 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, InvalidResponse
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)
166 167 168 169 170 171 172 173

        try:
            if data:
                return simplejson.loads(unicode(data))
            else:
                return ""
        except Exception as err:
            raise InvalidResponse(str(err), data)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
174 175

    # ------------------------
176
    # GET /im/authenticate
177
    def get_user_info(self, token, usage=False):
178
        """Authenticate user and get user's info as a dictionary
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
179 180

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
181
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
182 183 184 185 186 187
        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
188
        # Send request
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
189 190 191
        auth_path = "/im/authenticate"
        if usage:
            auth_path += "?usage=1"
192
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
193 194

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

210
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
211 212 213
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
214
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
215 216 217 218 219 220 221
        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"
222
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
223

224 225
    def get_username(self, token, uuid):
        """Return the user name of a uuid (see get_usernames)"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
226 227 228
        if not uuid:
            m = "No uuid was given"
            self.logger.error(m)
229
            raise BadValue(m)
230
        uuid_dict = self.get_usernames(token, [uuid])
231 232 233
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
234
            raise NoUserName(uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
235

236
    def service_get_usernames(self, token, uuids):
237 238
        """Return a uuid_catalog dict using a service's token"""
        req_path = "/service/api/user_catalogs"
239
        return self._uuid_catalog(token, uuids, req_path)
240

241
    def service_get_username(self, token, uuid):
242 243 244 245
        """Return the displayName of a uuid using a service's token"""
        if not uuid:
            m = "No uuid was given"
            self.logger.error(m)
246
            raise BadValue(m)
247
        uuid_dict = self.service_get_usernames(token, [uuid])
248 249 250
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
251
            raise NoUserName(uuid)
252

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

269
    def get_uuids(self, token, display_names):
270 271 272 273 274 275 276 277 278 279 280
        """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"
281
        return self._displayname_catalog(token, display_names, req_path)
282

283
    def get_uuid(self, token, display_name):
284 285 286 287
        """Return the uuid of a name (see getUUIDs)"""
        if not display_name:
            m = "No display_name was given"
            self.logger.error(m)
288
            raise BadValue(m)
289
        name_dict = self.get_uuids(token, [display_name])
290 291 292 293
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
294

295
    def service_get_uuids(self, token, display_names):
296 297
        """Return a display_name catalog using a service's token"""
        req_path = "/service/api/user_catalogs"
298
        return self._displayname_catalog(token, display_names, req_path)
299

300
    def service_get_uuid(self, token, display_name):
301 302 303 304
        """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)
305
            raise BadValue(m)
306
        name_dict = self.service_get_uuids(token, [display_name])
307 308 309 310
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
311

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
312
    # ----------------------------------
313
    # GET "/im/get_services"
314
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
315
        """Return a list of dicts with the registered services"""
316 317 318 319 320 321 322
        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
323

324 325 326
    # ----------------------------------
    # GET "/astakos/api/quotas"
    def get_quotas(self, token):
327 328 329 330 331 332 333 334 335
        """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

        """
336 337
        return self._call_astakos(token, "/astakos/api/quotas")

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 363 364 365 366 367 368 369
    # ----------------------------------
    # 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)

370 371 372 373 374 375 376 377 378 379 380 381 382 383
    # ----------------------------------
    # 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")

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
    # ----------------------------------
    # GET "/astakos/api/commissions/<serial>
    def get_commission_info(self, token, serial):
        """Get Description of a Commission

        Keyword arguments:
        token   -- user's token (string)
        serial  -- commission's id (int)

        In case of success return a dict of dicts containing
        informations (details) about the requests commission

        """
        if not serial:
            m = "Commissions serial not given"
            self.logger.error(m)
            raise BadValue(m)

        path = "/astakos/api/commissions/" + str(serial)
        return self._call_astakos(token, path)

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
    # ----------------------------------
    # POST "astakos/api/commissions/<serial>/action"
    def issue_commission_action(self, token, serial, action):
        """Issue a commission action

        Keyword arguments:
        token   -- user's token (string)
        serial  -- commission's id (int)
        action  -- action to perform, currently accept/reject (string)

        In case of success return nothing.

        """
        if not serial:
            m = "Commissions serial not given"
            self.logger.error(m)
            raise BadValue(m)
        if not action:
            m = "Action not given"
            self.logger.error(m)
            raise BadValue(m)

        path = "/astakos/api/commissions/" + str(serial) + "/action"
        req_headers = {'content-type': 'application/json'}
        req_body = simplejson.dumps({str(action): ""})
        self._call_astakos(token, path, req_headers, req_body, "POST")

    def issue_commission_accept(self, token, serial):
        """Issue a commission accept (see issue_commission_action)"""
        self.issue_commission_action(token, serial, "accept")

    def issue_commission_reject(self, token, serial):
        """Issue a commission reject (see issue_commission_reject)"""
        self.issue_commission_action(token, serial, "reject")

440

441 442
# --------------------------------------------------------------------
# Private functions
443 444
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
445
def _do_request(conn, method, url, **kwargs):
446 447 448 449 450 451
    """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)
452 453
    message = response.reason
    return (message, data, status)