def test_parse_rfc3339_datetime(self): # Sanity round-trip test with current time. now = utils.utcnow() parsed = utils.parse_rfc3339_datetime(now.isoformat() + 'Z') self.assertEqual(now, parsed) ok_cases = [ ('2017-08-17T04:21:32.722952943Z', (2017, 8, 17, 4, 21, 32, 722953)), ('2017-08-17T04:21:32Z', (2017, 8, 17, 4, 21, 32, 0)), ('1972-01-01T10:00:20.021-05:00', (1972, 1, 1, 15, 0, 20, 21000)), ('1972-01-01T10:00:20.021+05:00', (1972, 1, 1, 5, 0, 20, 21000)), ('1985-04-12T23:20:50.52Z', (1985, 4, 12, 23, 20, 50, 520000)), ('1996-12-19T16:39:57-08:00', (1996, 12, 20, 0, 39, 57, 0)), ] for val, expected in ok_cases: parsed = utils.parse_rfc3339_datetime(val) self.assertIsNone(parsed.tzinfo) self.assertEqual(datetime.datetime(*expected), parsed) bad_cases = [ '', '1985-04-12T23:20:50.52', # no timezone '2017:08:17T04:21:32Z', # bad base format '2017-08-17T04:21:32.7229529431Z' , # more than nano second precision '2017-08-17T04:21:32Zblah', # trailing data '1972-01-01T10:00:20.021-0500', # bad timezone format ] for val in bad_cases: with self.assertRaises(ValueError): utils.parse_rfc3339_datetime(val)
def _mint_oauth_token_via_grant(grant_token, oauth_scopes, audit_tags): """Does the RPC to the token server to exchange a grant for an access token. Args: grant_token: a token generated by get_oauth_token_grant. oauth_scopes: list of strings with requested OAuth scopes. audit_tags: list with information tags to send with the RPC, for logging. Returns: (new token, datetime when it expires). Raises: PermissionError if the token server forbids the usage. MisconfigurationError if the service account is misconfigured. InternalError if the RPC fails unexpectedly. """ resp = _call_token_server( 'MintOAuthTokenViaGrant', { 'grantToken': grant_token, 'oauthScope': oauth_scopes, 'minValidityDuration': MIN_TOKEN_LIFETIME_SEC, 'auditTags': _common_audit_tags() + audit_tags, }) try: access_token = str(resp['accessToken']) service_version = str(resp['serviceVersion']) expiry = utils.parse_rfc3339_datetime(resp['expiry']) except (KeyError, ValueError) as exc: logging.error('Bad response from the token server (%s):\n%r', exc, resp) raise InternalError( 'Bad response from the token server, see server logs') logging.info('The token server replied, its version: %s', service_version) return access_token, expiry
def _mint_oauth_token_grant(service_account, end_user, validity_duration): """Does the actual RPC to the token server to generate OAuth token grant. Args: service_account: a service account email to use. end_user: identity of the end user (the one who posts Swarming task). validity_duration: timedelta with how long the returned token should live. Returns: (new token, datetime when it expires). Raises: PermissionError if the token server forbids the usage. MisconfigurationError if the service account is misconfigured. InternalError if the RPC fails unexpectedly. """ resp = _call_token_server( 'MintOAuthTokenGrant', { 'serviceAccount': service_account, 'validityDuration': int(validity_duration.total_seconds()), 'endUser': end_user.to_bytes(), 'auditTags': _common_audit_tags(), }) try: grant_token = str(resp['grantToken']) service_version = str(resp['serviceVersion']) expiry = utils.parse_rfc3339_datetime(resp['expiry']) except (KeyError, ValueError) as exc: logging.error('Bad response from the token server (%s):\n%r', exc, resp) raise InternalError( 'Bad response from the token server, see server logs') logging.info('The token server replied, its version: %s', service_version) return grant_token, expiry
def test_get_access_token_async(self): orig_get_access_token_async = service_account.get_access_token_async expire_time = '2014-10-02T15:01:23.045123456Z' @ndb.tasklet def urlfetch_mock(**kwargs): class Response(dict): def __init__(self, *args, **kwargs): super(Response, self).__init__(*args, **kwargs) self.status_code = 200 self.content = json.dumps(self) mock_dict = { "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/": Response({ "accessToken": 'foobartoken', "expireTime": expire_time, }) } for url_prefix, response in mock_dict.iteritems(): if kwargs['url'].find(url_prefix) == 0: raise ndb.Return(response) raise Exception('url not found in mock: %s' % kwargs['url']) self.mock(service_account, '_urlfetch', urlfetch_mock) @ndb.tasklet def get_access_token_async_mock(scopes, service_account_key=None, act_as=None, min_lifetime_sec=5 * 60): if service_account_key: raise ndb.Return("FAKETOKENFAKETOKEN") result = yield orig_get_access_token_async(scopes, service_account_key, act_as, min_lifetime_sec) raise ndb.Return(result) # Wrap get_access_token to mock out local signing self.mock(service_account, 'get_access_token_async', get_access_token_async_mock) # Quick self check on mock of local-signer based flow self.assertEqual( "FAKETOKENFAKETOKEN", service_account.get_access_token_async( ["a", "b"], service_account_key=FAKE_SECRET_KEY).get_result()) res = service_account.get_access_token_async( ["c"], service_account_key=None, act_as="*****@*****.**").get_result() self.assertEqual( ('foobartoken', int( utils.datetime_to_timestamp( utils.parse_rfc3339_datetime(expire_time)) / 1e6)), res)
def _mint_service_account_token(service_account, realm, oauth_scopes, audit_tags): """Does the RPC to the token server to get an access token using realm. Args: service_account: a service account email to use. realm: a realm name to use. oauth_scopes: list of strings with requested OAuth scopes. audit_tags: list with information tags to send with the RPC, for logging. Raises: PermissionError if the token server forbids the usage. MisconfigurationError if the service account is misconfigured. InternalError if the RPC fails unexpectedly. """ # extract LUCI project from realm '<project>:<realm>'. luci_project = realm.split(':')[0] assert luci_project resp = _call_token_server( 'MintServiceAccountToken', { # tokenserver.minter.SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN 'tokenKind': 1, 'serviceAccount': service_account, 'realm': realm, 'oauthScope': oauth_scopes, 'minValidityDuration': MIN_TOKEN_LIFETIME_SEC, 'auditTags': _common_audit_tags() + audit_tags, }, luci_project) try: access_token = str(resp['token']) service_version = str(resp['serviceVersion']) expiry = utils.parse_rfc3339_datetime(resp['expiry']) except (KeyError, ValueError) as exc: logging.error('Bad response from the token server (%s):\n%r', exc, resp) raise InternalError( 'Bad response from the token server, see server logs') logging.info('The token server replied, its version: %s', service_version) return access_token, expiry
def _mint_oauth_token_async(token_factory, email, scopes, lifetime_sec=0, delegates=None): """Creates a new access token using IAM credentials API.""" # Query IAM credentials generateAccessToken API to obtain an OAuth token for # a given service account. Maximum lifetime is 1 hour. And can be obtained # through a chain of delegates. logging.info('Refreshing the access token for %s with scopes %s', email, scopes) request_body = {'scope': scopes} if delegates: request_body['delegates'] = delegates if lifetime_sec > 0: # Api accepts number of seconds with trailing 's' request_body['lifetime'] = '%ds' % lifetime_sec http_auth, _ = yield token_factory() response = yield _call_async( url='https://iamcredentials.googleapis.com/v1/projects/-/' 'serviceAccounts/%s:generateAccessToken' % urllib.parse.quote_plus(email), method='POST', headers={ 'Accept': 'application/json', 'Authorization': 'Bearer %s' % http_auth, 'Content-Type': 'application/json; charset=utf-8', }, payload=utils.encode_to_json(request_body), ) expired_at = int( utils.datetime_to_timestamp( utils.parse_rfc3339_datetime(response['expireTime'])) / 1e6) raise ndb.Return({ 'access_token': response['accessToken'], 'exp_ts': expired_at, })