示例#1
0
    def __init__(self, options=None, session=None):
        if options and options.get('baseUrl'):
            raise exceptions.TaskclusterFailure(
                'baseUrl option is no longer allowed')
        o = copy.deepcopy(self.classOptions)
        o.update(_defaultConfig)
        if options:
            o.update(options)
        if not o.get('rootUrl'):
            raise exceptions.TaskclusterFailure('rootUrl option is required')

        credentials = o.get('credentials')
        if credentials:
            for x in ('accessToken', 'clientId', 'certificate'):
                value = credentials.get(x)
                if value and not isinstance(value, six.binary_type):
                    try:
                        credentials[x] = credentials[x].encode('ascii')
                    except:
                        s = '%s (%s) must be unicode encodable' % (
                            x, credentials[x])
                        raise exceptions.TaskclusterAuthFailure(s)

        self.options = o
        if 'credentials' in o:
            log.debug('credentials key scrubbed from logging output')
        log.debug(dict((k, v) for k, v in o.items() if k != 'credentials'))

        if session:
            self.session = session
        else:
            self.session = self._createSession()
示例#2
0
 def buildUrl(self, methodName, *args, **kwargs):
     entry = self.funcinfo.get(methodName)
     if not entry:
         raise exceptions.TaskclusterFailure(
             'Requested method "%s" not found in API Reference' %
             methodName)
     apiArgs = self._processArgs(entry, *args, **kwargs)
     route = self._subArgsInRoute(entry, apiArgs)
     return self.options['baseUrl'] + '/' + route
 def buildUrl(self, methodName, *args, **kwargs):
     entry = self.funcinfo.get(methodName)
     if not entry:
         raise exceptions.TaskclusterFailure(
             'Requested method "%s" not found in API Reference' %
             methodName)
     routeParams, _, query, _, _ = self._processArgs(entry, *args, **kwargs)
     route = self._subArgsInRoute(entry, routeParams)
     if query:
         route += '?' + urllib.parse.urlencode(query)
     return self.options['baseUrl'] + '/' + route
示例#4
0
    def _subArgsInRoute(self, entry, args):
        """ Given a route like "/task/<taskId>/artifacts" and a mapping like
        {"taskId": "12345"}, return a string like "/task/12345/artifacts"
        """

        route = entry['route']

        for arg, val in six.iteritems(args):
            toReplace = "<%s>" % arg
            if toReplace not in route:
                raise exceptions.TaskclusterFailure(
                    'Arg %s not found in route for %s' % (arg, entry['name']))
            val = urllib.parse.quote(str(val).encode("utf-8"), '')
            route = route.replace("<%s>" % arg, val)

        return route.lstrip('/')
示例#5
0
    def _makeApiCall(self, entry, *args, **kwargs):
        """ This function is used to dispatch calls to other functions
        for a given API Reference entry"""

        payload = None
        _args = list(args)
        _kwargs = copy.deepcopy(kwargs)

        if 'input' in entry:
            if len(args) > 0:
                payload = _args.pop()
            else:
                raise exceptions.TaskclusterFailure(
                    'Payload is required as last positional arg')
        apiArgs = self._processArgs(entry, *_args, **_kwargs)
        route = self._subArgsInRoute(entry, apiArgs)
        log.debug('Route is: %s', route)

        return self._makeHttpRequest(entry['method'], route, payload)
示例#6
0
def createTemporaryCredentials(clientId,
                               accessToken,
                               start,
                               expiry,
                               scopes,
                               name=None):
    """ Create a set of temporary credentials

    Callers should not apply any clock skew; clock drift is accounted for by
    auth service.

    clientId: the issuing clientId
    accessToken: the issuer's accessToken
    start: start time of credentials (datetime.datetime)
    expiry: expiration time of credentials, (datetime.datetime)
    scopes: list of scopes granted
    name: credential name (optional)

    Returns a dictionary in the form:
        { 'clientId': str, 'accessToken: str, 'certificate': str}
    """

    for scope in scopes:
        if not isinstance(scope, six.string_types):
            raise exceptions.TaskclusterFailure('Scope must be string')

    # Credentials can only be valid for 31 days.  I hope that
    # this is validated on the server somehow...

    if expiry - start > datetime.timedelta(days=31):
        raise exceptions.TaskclusterFailure('Only 31 days allowed')

    # We multiply times by 1000 because the auth service is JS and as a result
    # uses milliseconds instead of seconds
    cert = dict(
        version=1,
        scopes=scopes,
        start=calendar.timegm(start.utctimetuple()) * 1000,
        expiry=calendar.timegm(expiry.utctimetuple()) * 1000,
        seed=utils.slugId() + utils.slugId(),
    )

    # if this is a named temporary credential, include the issuer in the certificate
    if name:
        cert['issuer'] = utils.toStr(clientId)

    sig = ['version:' + utils.toStr(cert['version'])]
    if name:
        sig.extend([
            'clientId:' + utils.toStr(name),
            'issuer:' + utils.toStr(clientId),
        ])
    sig.extend([
        'seed:' + utils.toStr(cert['seed']), 'start:' +
        utils.toStr(cert['start']), 'expiry:' +
        utils.toStr(cert['expiry']), 'scopes:'
    ] + scopes)
    sigStr = '\n'.join(sig).encode()

    if isinstance(accessToken, six.text_type):
        accessToken = accessToken.encode()
    sig = hmac.new(accessToken, sigStr, hashlib.sha256).digest()

    cert['signature'] = utils.encodeStringForB64Header(sig)

    newToken = hmac.new(accessToken, cert['seed'], hashlib.sha256).digest()
    newToken = utils.makeB64UrlSafe(
        utils.encodeStringForB64Header(newToken)).replace(b'=', b'')

    return {
        'clientId': name or clientId,
        'accessToken': newToken,
        'certificate': utils.dumpJson(cert),
    }
示例#7
0
    def _processArgs(self, entry, *_args, **_kwargs):
        """ Given an entry, positional and keyword arguments, figure out what
        the query-string options, payload and api arguments are.
        """

        # We need the args to be a list so we can mutate them
        args = list(_args)
        kwargs = copy.deepcopy(_kwargs)

        reqArgs = entry['args']
        routeParams = {}

        query = {}
        payload = None
        kwApiArgs = {}

        paginationHandler = None
        paginationLimit = None

        # There are three formats for calling methods:
        #   1. method(v1, v1, payload)
        #   2. method(payload, k1=v1, k2=v2)
        #   3. method(payload=payload, query=query, params={k1: v1, k2: v2})
        if len(kwargs) == 0:
            if 'input' in entry and len(args) == len(reqArgs) + 1:
                payload = args.pop()
            if len(args) != len(reqArgs):
                log.debug(args)
                log.debug(reqArgs)
                raise exceptions.TaskclusterFailure(
                    'Incorrect number of positional arguments')
            log.debug('Using method(v1, v2, payload) calling convention')
        else:
            # We're considering kwargs which are the api route parameters to be
            # called 'flat' because they're top level keys.  We're special
            # casing calls which have only api-arg kwargs and possibly a payload
            # value and handling them directly.
            isFlatKwargs = True
            if len(kwargs) == len(reqArgs):
                for arg in reqArgs:
                    if not kwargs.get(arg, False):
                        isFlatKwargs = False
                        break
                if 'input' in entry and len(args) != 1:
                    isFlatKwargs = False
                if 'input' not in entry and len(args) != 0:
                    isFlatKwargs = False
                else:
                    pass  # We're using payload=, query= and param=
            else:
                isFlatKwargs = False

            # Now we're going to handle the two types of kwargs.  The first is
            # 'flat' ones, which are where the api params
            if isFlatKwargs:
                if 'input' in entry:
                    payload = args.pop()
                kwApiArgs = kwargs
                log.debug(
                    'Using method(payload, k1=v1, k2=v2) calling convention')
                warnings.warn(
                    "The method(payload, k1=v1, k2=v2) calling convention will soon be deprecated",
                    PendingDeprecationWarning)
            else:
                kwApiArgs = kwargs.get('params', {})
                payload = kwargs.get('payload', None)
                query = kwargs.get('query', {})
                paginationHandler = kwargs.get('paginationHandler', None)
                paginationLimit = kwargs.get('paginationLimit', None)
                log.debug(
                    'Using method(payload=payload, query=query, params={k1: v1, k2: v2}) calling convention'
                )

        if 'input' in entry and isinstance(payload, type(None)):
            raise exceptions.TaskclusterFailure('Payload is required')

        # These all need to be rendered down to a string, let's just check that
        # they are up front and fail fast
        for arg in args:
            if not isinstance(arg, six.string_types) and not isinstance(
                    arg, int):
                raise exceptions.TaskclusterFailure(
                    'Positional arg "%s" to %s is not a string or int' %
                    (arg, entry['name']))

        for name, arg in six.iteritems(kwApiArgs):
            if not isinstance(arg, six.string_types) and not isinstance(
                    arg, int):
                raise exceptions.TaskclusterFailure(
                    'KW arg "%s: %s" to %s is not a string or int' %
                    (name, arg, entry['name']))

        if len(args) > 0 and len(kwApiArgs) > 0:
            raise exceptions.TaskclusterFailure(
                'Specify either positional or key word arguments')

        # We know for sure that if we don't give enough arguments that the call
        # should fail.  We don't yet know if we should fail because of two many
        # arguments because we might be overwriting positional ones with kw ones
        if len(reqArgs) > len(args) + len(kwApiArgs):
            raise exceptions.TaskclusterFailure(
                '%s takes %d args, only %d were given' %
                (entry['name'], len(reqArgs), len(args) + len(kwApiArgs)))

        # We also need to error out when we have more positional args than required
        # because we'll need to go through the lists of provided and required args
        # at the same time.  Not disqualifying early means we'll get IndexErrors if
        # there are more positional arguments than required
        if len(args) > len(reqArgs):
            raise exceptions.TaskclusterFailure(
                '%s called with too many positional args', entry['name'])

        i = 0
        for arg in args:
            log.debug('Found a positional argument: %s', arg)
            routeParams[reqArgs[i]] = arg
            i += 1

        log.debug('After processing positional arguments, we have: %s',
                  routeParams)

        routeParams.update(kwApiArgs)

        log.debug('After keyword arguments, we have: %s', routeParams)

        if len(reqArgs) != len(routeParams):
            errMsg = '%s takes %s args, %s given' % (
                entry['name'], ','.join(reqArgs), routeParams.keys())
            log.error(errMsg)
            raise exceptions.TaskclusterFailure(errMsg)

        for reqArg in reqArgs:
            if reqArg not in routeParams:
                errMsg = '%s requires a "%s" argument which was not provided' % (
                    entry['name'], reqArg)
                log.error(errMsg)
                raise exceptions.TaskclusterFailure(errMsg)

        return routeParams, payload, query, paginationHandler, paginationLimit
示例#8
0
    def buildSignedUrl(self, methodName, *args, **kwargs):
        """ Build a signed URL.  This URL contains the credentials needed to access
        a resource."""

        if 'expiration' in kwargs:
            expiration = kwargs['expiration']
            del kwargs['expiration']
        else:
            expiration = self.options['signedUrlExpiration']

        expiration = int(
            time.time() +
            expiration)  # Mainly so that we throw if it's not a number

        requestUrl = self.buildUrl(methodName, *args, **kwargs)

        if not self._hasCredentials():
            raise exceptions.TaskclusterAuthFailure('Invalid Hawk Credentials')

        clientId = utils.toStr(self.options['credentials']['clientId'])
        accessToken = utils.toStr(self.options['credentials']['accessToken'])

        def genBewit():
            # We need to fix the output of get_bewit.  It returns a url-safe base64
            # encoded string, which contains a list of tokens separated by '\'.
            # The first one is the clientId, the second is an int, the third is
            # url-safe base64 encoded MAC, the fourth is the ext param.
            # The problem is that the nested url-safe base64 encoded MAC must be
            # base64 (i.e. not url safe) or server-side will complain.

            # id + '\\' + exp + '\\' + mac + '\\' + options.ext;
            resource = mohawk.base.Resource(
                credentials={
                    'id': clientId,
                    'key': accessToken,
                    'algorithm': 'sha256',
                },
                method='GET',
                ext=utils.toStr(self.makeHawkExt()),
                url=requestUrl,
                timestamp=expiration,
                nonce='',
                # content='',
                # content_type='',
            )
            bewit = mohawk.bewit.get_bewit(resource)
            return bewit.rstrip('=')

        bewit = genBewit()

        if not bewit:
            raise exceptions.TaskclusterFailure('Did not receive a bewit')

        u = urllib.parse.urlparse(requestUrl)

        qs = u.query
        if qs:
            qs += '&'
        qs += 'bewit=%s' % bewit

        return urllib.parse.urlunparse((
            u.scheme,
            u.netloc,
            u.path,
            u.params,
            qs,
            u.fragment,
        ))
示例#9
0
    def _processArgs(self, entry, *args, **kwargs):
        """ Take the list of required arguments, positional arguments
        and keyword arguments and return a dictionary which maps the
        value of the given arguments to the required parameters.

        Keyword arguments will overwrite positional arguments.
        """

        reqArgs = entry['args']
        data = {}

        # These all need to be rendered down to a string, let's just check that
        # they are up front and fail fast
        for arg in list(args) + [kwargs[x] for x in kwargs]:
            if not isinstance(arg, six.string_types) and not isinstance(
                    arg, int):
                raise exceptions.TaskclusterFailure(
                    'Arguments "%s" to %s is not a string or int' %
                    (arg, entry['name']))

        if len(args) > 0 and len(kwargs) > 0:
            raise exceptions.TaskclusterFailure(
                'Specify either positional or key word arguments')

        # We know for sure that if we don't give enough arguments that the call
        # should fail.  We don't yet know if we should fail because of two many
        # arguments because we might be overwriting positional ones with kw ones
        if len(reqArgs) > len(args) + len(kwargs):
            raise exceptions.TaskclusterFailure(
                '%s takes %d args, only %d were given' %
                (entry['name'], len(reqArgs), len(args) + len(kwargs)))

        # We also need to error out when we have more positional args than required
        # because we'll need to go through the lists of provided and required args
        # at the same time.  Not disqualifying early means we'll get IndexErrors if
        # there are more positional arguments than required
        if len(args) > len(reqArgs):
            raise exceptions.TaskclusterFailure(
                '%s called with too many positional args', entry['name'])

        i = 0
        for arg in args:
            log.debug('Found a positional argument: %s', arg)
            data[reqArgs[i]] = arg
            i += 1

        log.debug('After processing positional arguments, we have: %s', data)

        data.update(kwargs)

        log.debug('After keyword arguments, we have: %s', data)

        if len(reqArgs) != len(data):
            errMsg = '%s takes %s args, %s given' % (
                entry['name'], ','.join(reqArgs), data.keys())
            log.error(errMsg)
            raise exceptions.TaskclusterFailure(errMsg)

        for reqArg in reqArgs:
            if reqArg not in data:
                errMsg = '%s requires a "%s" argument which was not provided' % (
                    entry['name'], reqArg)
                log.error(errMsg)
                raise exceptions.TaskclusterFailure(errMsg)

        return data