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()
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
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('/')
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)
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), }
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
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, ))
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