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
Exemple #4
0
    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,
    })