__init__.py 30.7 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, parse_request, check_input
43
from astakosclient.errors import \
44
    AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \
45
    NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse
46 47 48 49 50
from .keypath import get_path
from .services import astakos_services


# Customize astakos_services here?
51 52


53 54 55 56
def join_urls(a, b):
    """join_urls from synnefo.lib"""
    return a.rstrip("/") + "/" + b.lstrip("/")

57 58
# --------------------------------------------------------------------
# Astakos API urls
59
UI_PREFIX = get_path(astakos_services, 'astakos_ui.prefix')
60
ACCOUNTS_PREFIX = get_path(astakos_services, 'astakos_account.prefix')
61
ACCOUNTS_PREFIX = join_urls(ACCOUNTS_PREFIX, 'v1.0')
62 63 64
API_AUTHENTICATE = join_urls(ACCOUNTS_PREFIX, "authenticate")
API_USERCATALOGS = join_urls(ACCOUNTS_PREFIX, "user_catalogs")
API_SERVICE_USERCATALOGS = join_urls(ACCOUNTS_PREFIX, "service/user_catalogs")
65
API_GETSERVICES = join_urls(UI_PREFIX, "get_services")
66 67 68 69 70 71
API_RESOURCES = join_urls(ACCOUNTS_PREFIX, "resources")
API_QUOTAS = join_urls(ACCOUNTS_PREFIX, "quotas")
API_SERVICE_QUOTAS = join_urls(ACCOUNTS_PREFIX, "service_quotas")
API_COMMISSIONS = join_urls(ACCOUNTS_PREFIX, "commissions")
API_COMMISSIONS_ACTION = join_urls(API_COMMISSIONS, "action")
API_FEEDBACK = join_urls(ACCOUNTS_PREFIX, "feedback")
72 73 74
API_PROJECTS = join_urls(ACCOUNTS_PREFIX, "projects")
API_APPLICATIONS = join_urls(API_PROJECTS, "apps")
API_MEMBERSHIPS = join_urls(API_PROJECTS, "memberships")
75 76 77

# --------------------------------------------------------------------
# Astakos Keystone API urls
78
IDENTITY_PREFIX = get_path(astakos_services, 'astakos_identity.prefix')
79
IDENTITY_PREFIX = join_urls(IDENTITY_PREFIX, "v2.0")
80
API_TOKENS = join_urls(IDENTITY_PREFIX, "tokens")
81 82


83
# --------------------------------------------------------------------
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
84
# Astakos Client Class
85

86
def get_token_from_cookie(request, cookie_name):
87 88 89 90 91 92 93 94 95 96 97 98 99 100
    """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
101 102 103 104
class AstakosClient():
    """AstakosClient Class Implementation"""

    # ----------------------------------
105 106
    def __init__(self, astakos_url, retry=0,
                 use_pool=False, pool_size=8, logger=None):
107
        """Initialize AstakosClient Class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
108 109 110 111

        Keyword arguments:
        astakos_url -- i.e https://accounts.example.com (string)
        use_pool    -- use objpool for http requests (boolean)
112
        retry       -- how many time to retry (integer)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
113 114 115 116
        logger      -- pass a different logger

        """
        if logger is None:
117 118 119 120
            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
121
            logger = logging.getLogger("astakosclient")
122
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
123 124
                     "use_pool = %s" % (astakos_url, use_pool))

125
        check_input("__init__", logger, astakos_url=astakos_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
126 127 128

        # Check for supported scheme
        p = urlparse.urlparse(astakos_url)
129
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
130
        if conn_class is None:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
131 132
            m = "Unsupported scheme: %s" % p.scheme
            logger.error(m)
133
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
134

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
135
        # Save astakos_url etc. in our class
136
        self.retry = retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
137 138 139
        self.logger = logger
        self.netloc = p.netloc
        self.scheme = p.scheme
140
        self.path = p.path.rstrip('/')
141
        self.conn_class = conn_class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
142

143 144
    # ----------------------------------
    @retry
145 146
    def _call_astakos(self, token, request_path, headers=None,
                      body=None, method="GET", log_body=True):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
147
        """Make the actual call to Astakos Service"""
148 149 150 151 152 153
        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
154
        self.logger.debug(
155
            "Make a %s request to %s %s with headers %s and body %s"
156 157
            % (method, request_path, using_token, headers,
               body if log_body else "(not logged)"))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
158

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
159
        # Check Input
160 161 162 163
        if headers is None:
            headers = {}
        if body is None:
            body = {}
164
        path = self.path + "/" + request_path.strip('/')
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
165

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
166 167
        # Build request's header and body
        kwargs = {}
168
        kwargs['headers'] = copy(headers)
169 170
        if token is not None:
            kwargs['headers']['X-Auth-Token'] = token
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
171
        if body:
172
            kwargs['body'] = copy(body)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
173 174 175 176 177 178
            kwargs['headers'].setdefault(
                'content-type', 'application/octet-stream')
        kwargs['headers'].setdefault('content-length',
                                     len(body) if body else 0)

        try:
179 180 181
            # Get the connection object
            with self.conn_class(self.netloc) as conn:
                # Send request
182
                (message, data, status) = \
183
                    _do_request(conn, method, path, **kwargs)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
184
        except Exception as err:
185
            self.logger.error("Failed to send request: %s" % repr(err))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
186 187 188 189
            raise AstakosClientException(str(err))

        # Return
        self.logger.debug("Request returned with status %s" % status)
190
        if status == 400:
191
            raise BadRequest(message, data)
192
        elif status == 401:
193
            raise Unauthorized(message, data)
194
        elif status == 403:
195
            raise Forbidden(message, data)
196
        elif status == 404:
197
            raise NotFound(message, data)
198
        elif status < 200 or status >= 300:
199
            raise AstakosClientException(message, data, status)
200 201 202 203 204

        try:
            if data:
                return simplejson.loads(unicode(data))
            else:
205
                return None
206
        except Exception as err:
207 208
            msg = "Cannot parse response \"%s\" with simplejson: %s"
            self.logger.error(msg % (data, str(err)))
209
            raise InvalidResponse(str(err), data)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
210 211

    # ------------------------
212
    # do a GET to ``API_AUTHENTICATE``
213
    def get_user_info(self, token, usage=False):
214
        """Authenticate user and get user's info as a dictionary
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
215 216

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
217
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
218 219 220 221 222 223
        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
224
        # Send request
225
        auth_path = copy(API_AUTHENTICATE)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
226 227
        if usage:
            auth_path += "?usage=1"
228
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
229 230

    # ----------------------------------
231
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
232
    #   with {'uuids': uuids}
233
    def _uuid_catalog(self, token, uuids, req_path):
234
        req_headers = {'content-type': 'application/json'}
235
        req_body = parse_request({'uuids': uuids}, self.logger)
236
        data = self._call_astakos(
237
            token, req_path, req_headers, req_body, "POST")
238 239 240
        if "uuid_catalog" in data:
            return data.get("uuid_catalog")
        else:
241
            m = "_uuid_catalog request returned %s. No uuid_catalog found" \
242 243 244
                % data
            self.logger.error(m)
            raise AstakosClientException(m)
245

246
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
247 248 249
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
250
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
251 252 253 254 255 256
        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

        """
257
        req_path = copy(API_USERCATALOGS)
258
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
259

260 261
    def get_username(self, token, uuid):
        """Return the user name of a uuid (see get_usernames)"""
262
        check_input("get_username", self.logger, uuid=uuid)
263
        uuid_dict = self.get_usernames(token, [uuid])
264 265 266
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
267
            raise NoUserName(uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
268

269
    def service_get_usernames(self, token, uuids):
270
        """Return a uuid_catalog dict using a service's token"""
271
        req_path = copy(API_SERVICE_USERCATALOGS)
272
        return self._uuid_catalog(token, uuids, req_path)
273

274
    def service_get_username(self, token, uuid):
275
        """Return the displayName of a uuid using a service's token"""
276
        check_input("service_get_username", self.logger, uuid=uuid)
277
        uuid_dict = self.service_get_usernames(token, [uuid])
278 279 280
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
281
            raise NoUserName(uuid)
282

283
    # ----------------------------------
284
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
285
    #   with {'displaynames': display_names}
286
    def _displayname_catalog(self, token, display_names, req_path):
287
        req_headers = {'content-type': 'application/json'}
288
        req_body = parse_request({'displaynames': display_names}, self.logger)
289
        data = self._call_astakos(
290
            token, req_path, req_headers, req_body, "POST")
291 292 293
        if "displayname_catalog" in data:
            return data.get("displayname_catalog")
        else:
294
            m = "_displayname_catalog request returned %s. " \
295 296 297
                "No displayname_catalog found" % data
            self.logger.error(m)
            raise AstakosClientException(m)
298

299
    def get_uuids(self, token, display_names):
300 301 302 303 304 305 306 307 308 309
        """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

        """
310
        req_path = copy(API_USERCATALOGS)
311
        return self._displayname_catalog(token, display_names, req_path)
312

313
    def get_uuid(self, token, display_name):
314
        """Return the uuid of a name (see getUUIDs)"""
315
        check_input("get_uuid", self.logger, display_name=display_name)
316
        name_dict = self.get_uuids(token, [display_name])
317 318 319 320
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
321

322
    def service_get_uuids(self, token, display_names):
323
        """Return a display_name catalog using a service's token"""
324
        req_path = copy(API_SERVICE_USERCATALOGS)
325
        return self._displayname_catalog(token, display_names, req_path)
326

327
    def service_get_uuid(self, token, display_name):
328
        """Return the uuid of a name using a service's token"""
329
        check_input("service_get_uuid", self.logger, display_name=display_name)
330
        name_dict = self.service_get_uuids(token, [display_name])
331 332 333 334
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
335

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
336
    # ----------------------------------
337
    # do a GET to ``API_GETSERVICES``
338
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
339
        """Return a list of dicts with the registered services"""
340
        return self._call_astakos(None, copy(API_GETSERVICES))
341 342

    # ----------------------------------
343
    # do a GET to ``API_RESOURCES``
344 345
    def get_resources(self):
        """Return a dict of dicts with the available resources"""
346
        return self._call_astakos(None, copy(API_RESOURCES))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
347

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
    # ----------------------------------
    # do a POST to ``API_FEEDBACK``
    def send_feedback(self, token, message, data):
        """Send feedback to astakos service

        keyword arguments:
        token       -- user's token (string)
        message     -- Feedback message
        data        -- Additional information about service client status

        In case of success return nothing.
        Otherwise raise an AstakosClientException

        """
        check_input("send_feedback", self.logger, message=message, data=data)
        path = copy(API_FEEDBACK)
        req_body = urllib.urlencode(
            {'feedback_msg': message, 'feedback_data': data})
366 367
        self._call_astakos(token, path, None, req_body, "POST")

368 369
    # ----------------------------------
    # do a POST to ``API_TOKENS``
370
    def get_endpoints(self, token, uuid=None):
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
        """ Fallback call for authenticate

        Keyword arguments:
        token   -- user's token (string)
        uuid    -- user's uniq id

        It returns back the token as well as information about the token
        holder and the services he/she can acess (in json format).
        In case of error raise an AstakosClientException.

        """
        req_path = copy(API_TOKENS)
        req_headers = {'content-type': 'application/json'}
        body = {'auth': {'token': {'id': token}}}
        if uuid is not None:
            body['auth']['tenantName'] = uuid
        req_body = parse_request(body, self.logger)
        return self._call_astakos(token, req_path, req_headers,
                                  req_body, "POST", False)

391
    # ----------------------------------
392
    # do a GET to ``API_QUOTAS``
393
    def get_quotas(self, token):
394 395 396 397 398 399 400 401 402
        """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

        """
403
        return self._call_astakos(token, copy(API_QUOTAS))
404

405
    # ----------------------------------
406
    # do a GET to ``API_SERVICE_QUOTAS``
407
    def service_get_quotas(self, token, user=None):
408 409 410 411
        """Get all quotas for resources associated with the service

        Keyword arguments:
        token   -- service's token (string)
412
        user    -- optionally, the uuid of a specific user
413 414

        In case of success return a dict of dicts of dicts with current quotas
415
        for all users, or of a specified user, if user argument is set.
416 417 418
        Otherwise raise an AstakosClientException

        """
419
        query = copy(API_SERVICE_QUOTAS)
420 421 422
        if user is not None:
            query += "?user=" + user
        return self._call_astakos(token, query)
423

424
    # ----------------------------------
425
    # do a POST to ``API_COMMISSIONS``
426 427 428 429
    def issue_commission(self, token, request):
        """Issue a commission

        Keyword arguments:
430
        token   -- service's token (string)
431 432 433 434 435 436 437
        request -- commision request (dict)

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

        """
        req_headers = {'content-type': 'application/json'}
438
        req_body = parse_request(request, self.logger)
439
        try:
440
            response = self._call_astakos(token, copy(API_COMMISSIONS),
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
                                          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)

456
    def issue_one_commission(self, token, holder, source, provisions,
457
                             name="", force=False, auto_accept=False):
458 459 460 461 462 463
        """Issue one commission (with specific holder and source)

        keyword arguments:
        token       -- service's token (string)
        holder      -- user's id (string)
        source      -- commission's source (ex system) (string)
464
        provisions  -- resources with their quantity (dict from string to int)
465
        name        -- description of the commission (string)
466 467 468 469 470 471 472 473
        force       -- force this commission (boolean)
        auto_accept -- auto accept this commission (boolean)

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

        """
474 475 476
        check_input("issue_one_commission", self.logger,
                    holder=holder, source=source,
                    provisions=provisions)
477 478 479 480

        request = {}
        request["force"] = force
        request["auto_accept"] = auto_accept
481
        request["name"] = name
482 483
        try:
            request["provisions"] = []
484
            for resource, quantity in provisions.iteritems():
485 486 487 488 489 490 491 492 493
                t = {"holder": holder, "source": source,
                     "resource": resource, "quantity": quantity}
                request["provisions"].append(t)
        except Exception as err:
            self.logger.error(str(err))
            raise BadValue(str(err))

        return self.issue_commission(token, request)

494
    # ----------------------------------
495
    # do a GET to ``API_COMMISSIONS``
496 497 498 499
    def get_pending_commissions(self, token):
        """Get Pending Commissions

        Keyword arguments:
500
        token   -- service's token (string)
501 502 503 504 505

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

        """
506
        return self._call_astakos(token, copy(API_COMMISSIONS))
507

508
    # ----------------------------------
509
    # do a GET to ``API_COMMISSIONS``/<serial>
510 511 512 513
    def get_commission_info(self, token, serial):
        """Get Description of a Commission

        Keyword arguments:
514
        token   -- service's token (string)
515 516 517
        serial  -- commission's id (int)

        In case of success return a dict of dicts containing
518
        informations (details) about the requested commission
519 520

        """
521
        check_input("get_commission_info", self.logger, serial=serial)
522

523
        path = API_COMMISSIONS + "/" + str(serial)
524 525
        return self._call_astakos(token, path)

526
    # ----------------------------------
527
    # do a POST to ``API_COMMISSIONS``/<serial>/action"
528 529
    def commission_action(self, token, serial, action):
        """Perform a commission action
530 531

        Keyword arguments:
532
        token   -- service's token (string)
533 534 535 536 537 538
        serial  -- commission's id (int)
        action  -- action to perform, currently accept/reject (string)

        In case of success return nothing.

        """
539 540
        check_input("commission_action", self.logger,
                    serial=serial, action=action)
541

542
        path = API_COMMISSIONS + "/" + str(serial) + "/action"
543
        req_headers = {'content-type': 'application/json'}
544
        req_body = parse_request({str(action): ""}, self.logger)
545 546
        self._call_astakos(token, path, req_headers, req_body, "POST")

547 548 549
    def accept_commission(self, token, serial):
        """Accept a commission (see commission_action)"""
        self.commission_action(token, serial, "accept")
550

551 552 553
    def reject_commission(self, token, serial):
        """Reject a commission (see commission_action)"""
        self.commission_action(token, serial, "reject")
554

555
    # ----------------------------------
556
    # do a POST to ``API_COMMISSIONS_ACTION``
557 558 559 560 561 562 563 564 565 566 567 568 569
    def resolve_commissions(self, token, accept_serials, reject_serials):
        """Resolve multiple commissions at once

        Keyword arguments:
        token           -- service's token (string)
        accept_serials  -- commissions to accept (list of ints)
        reject_serials  -- commissions to reject (list of ints)

        In case of success return a dict of dicts describing which
        commissions accepted, which rejected and which failed to
        resolved.

        """
570 571 572
        check_input("resolve_commissions", self.logger,
                    accept_serials=accept_serials,
                    reject_serials=reject_serials)
573

574
        path = copy(API_COMMISSIONS_ACTION)
575 576 577 578 579 580
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request({"accept": accept_serials,
                                  "reject": reject_serials},
                                 self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809
    # ----------------------------
    # do a GET to ``API_PROJECTS``
    def get_projects(self, token, name=None, state=None, owner=None):
        """Retrieve all accessible projects

        Arguments:
        token -- user's token (string)
        name  -- filter by name (optional)
        state -- filter by state (optional)
        owner -- filter by owner (optional)

        In case of success, return a list of project descriptions.
        """
        path = API_PROJECTS
        filters = {}
        if name is not None:
            filters["name"] = name
        if state is not None:
            filters["state"] = state
        if owner is not None:
            filters["owner"] = owner
        req_headers = {'content-type': 'application/json'}
        req_body = (parse_request({"filter": filters}, self.logger)
                    if filters else None)
        return self._call_astakos(token, path, req_headers, req_body)

    # -----------------------------------------
    # do a GET to ``API_PROJECTS``/<project_id>
    def get_project(self, token, project_id):
        """Retrieve project description, if accessible

        Arguments:
        token      -- user's token (string)
        project_id -- project identifier

        In case of success, return project description.
        """
        path = join_urls(API_PROJECTS, str(project_id))
        return self._call_astakos(token, path)

    # -----------------------------
    # do a POST to ``API_PROJECTS``
    def create_project(self, token, specs):
        """Submit application to create a new project

        Arguments:
        token -- user's token (string)
        specs -- dict describing a project

        In case of success, return project and application identifiers.
        """
        path = API_PROJECTS
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request(specs, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # ------------------------------------------
    # do a POST to ``API_PROJECTS``/<project_id>
    def modify_project(self, token, project_id, specs):
        """Submit application to modify an existing project

        Arguments:
        token      -- user's token (string)
        project_id -- project identifier
        specs      -- dict describing a project

        In case of success, return project and application identifiers.
        """
        path = join_urls(API_PROJECTS, str(project_id))
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request(specs, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # -------------------------------------------------
    # do a POST to ``API_PROJECTS``/<project_id>/action
    def project_action(self, token, project_id, action, reason=""):
        """Perform action on a project

        Arguments:
        token      -- user's token (string)
        project_id -- project identifier
        action     -- action to perform, one of "suspend", "unsuspend",
                      "terminate", "reinstate"
        reason     -- reason of performing the action

        In case of success, return nothing.
        """
        path = join_urls(API_PROJECTS, str(project_id))
        path = join_urls(path, "action")
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request({action: reason}, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # --------------------------------
    # do a GET to ``API_APPLICATIONS``
    def get_applications(self, token, project=None):
        """Retrieve all accessible applications

        Arguments:
        token   -- user's token (string)
        project -- filter by project (optional)

        In case of success, return a list of application descriptions.
        """
        path = API_APPLICATIONS
        req_headers = {'content-type': 'application/json'}
        body = {"project": project} if project is not None else None
        req_body = parse_request(body, self.logger) if body else None
        return self._call_astakos(token, path, req_headers, req_body)

    # -----------------------------------------
    # do a GET to ``API_APPLICATIONS``/<app_id>
    def get_application(self, token, app_id):
        """Retrieve application description, if accessible

        Arguments:
        token  -- user's token (string)
        app_id -- application identifier

        In case of success, return application description.
        """
        path = join_urls(API_APPLICATIONS, str(app_id))
        return self._call_astakos(token, path)

    # -------------------------------------------------
    # do a POST to ``API_APPLICATIONS``/<app_id>/action
    def application_action(self, token, app_id, action, reason=""):
        """Perform action on an application

        Arguments:
        token  -- user's token (string)
        app_id -- application identifier
        action -- action to perform, one of "approve", "deny",
                  "dismiss", "cancel"
        reason -- reason of performing the action

        In case of success, return nothing.
        """
        path = join_urls(API_APPLICATIONS, str(app_id))
        path = join_urls(path, "action")
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request({action: reason}, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # -------------------------------
    # do a GET to ``API_MEMBERSHIPS``
    def get_memberships(self, token, project=None):
        """Retrieve all accessible memberships

        Arguments:
        token   -- user's token (string)
        project -- filter by project (optional)

        In case of success, return a list of membership descriptions.
        """
        path = API_MEMBERSHIPS
        req_headers = {'content-type': 'application/json'}
        body = {"project": project} if project is not None else None
        req_body = parse_request(body, self.logger) if body else None
        return self._call_astakos(token, path, req_headers, req_body)

    # -----------------------------------------
    # do a GET to ``API_MEMBERSHIPS``/<memb_id>
    def get_membership(self, token, memb_id):
        """Retrieve membership description, if accessible

        Arguments:
        token   -- user's token (string)
        memb_id -- membership identifier

        In case of success, return membership description.
        """
        path = join_urls(API_MEMBERSHIPS, str(memb_id))
        return self._call_astakos(token, path)

    # -------------------------------------------------
    # do a POST to ``API_MEMBERSHIPS``/<memb_id>/action
    def membership_action(self, token, memb_id, action, reason=""):
        """Perform action on a membership

        Arguments:
        token   -- user's token (string)
        memb_id -- membership identifier
        action  -- action to perform, one of "leave", "cancel", "accept",
                   "reject", "remove"
        reason  -- reason of performing the action

        In case of success, return nothing.
        """
        path = join_urls(API_MEMBERSHIPS, str(memb_id))
        path = join_urls(path, "action")
        req_headers = {'content-type': 'application/json'}
        req_body = parse_request({action: reason}, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # --------------------------------
    # do a POST to ``API_MEMBERSHIPS``
    def join_project(self, token, project_id):
        """Join a project

        Arguments:
        token      -- user's token (string)
        project_id -- project identifier

        In case of success, return membership identifier.
        """
        path = API_MEMBERSHIPS
        req_headers = {'content-type': 'application/json'}
        body = {"join": {"project": project_id}}
        req_body = parse_request(body, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")

    # --------------------------------
    # do a POST to ``API_MEMBERSHIPS``
    def enroll_member(self, token, project_id, email):
        """Enroll a user in a project

        Arguments:
        token      -- user's token (string)
        project_id -- project identifier
        email      -- user identified by email

        In case of success, return membership identifier.
        """
        path = API_MEMBERSHIPS
        req_headers = {'content-type': 'application/json'}
        body = {"enroll": {"project": project_id, "user": email}}
        req_body = parse_request(body, self.logger)
        return self._call_astakos(token, path, req_headers, req_body, "POST")
810

811 812
# --------------------------------------------------------------------
# Private functions
813 814
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
815
def _do_request(conn, method, url, **kwargs):
816 817 818 819 820 821
    """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)
822 823
    message = response.reason
    return (message, data, status)