从 Google Cloud Build 发出授权请求

Making authorised requests from Google Cloud Build

我正在尝试在 Google Cloud Build 中设置部署路径。为此,我想:

  1. 运行单元测试
  2. 在没有流量的情况下部署到云端运行
  3. 运行 集成测试
  4. 迁移云中的流量运行

我已经完成了大部分设置,但我的集成测试包括对 Cloud 运行 的几次调用,以验证经过身份验证的调用 return 200 和未经身份验证的 return 401。我遇到困难的是从 Cloud Build 发出签名请求。当手动部署和 运行 集成测试时,它们可以工作,但不是来自 Cloud Build。

理想情况下,我想使用 Cloud Build 服务帐户来调用 Cloud 运行 就像我通常在 AWS 中所做的那样,但我找不到如何从 Cloud 访问它 运行纳尔。因此,我改为从 Secret Manager 检索凭证文件。此凭据文件来自新创建的具有 Cloud 运行 调用者角色的服务帐户:

steps:
  - name: gcr.io/cloud-builders/gcloud
    id: get-github-ssh-secret
    entrypoint: 'bash'
    args: [ '-c', 'gcloud secrets version access latest --secret=name-of-secret > /root/service-account/credentials.json' ]
    volumes:
      - name: 'service-account'
        path: /root/service-account
...
  - name: python:3.8.7
    id: integration-tests
    entrypoint: /bin/sh
    args:
      - '-c'
      - |-
        if [ $_STAGE != "prod" ]; then 
          python -m pip install -r requirements.txt
          python -m pytest test/integration --disable-warnings ; 
        fi
    volumes:
      - name: 'service-account'
        path: /root/service-account

对于集成测试,我创建了一个名为 Authorizer 的 class,并且尝试了 __get_authorized_header_for_cloud_build__get_authorized_header_for_cloud_build2

import json
import time
import urllib
from typing import Optional

import google.auth
import requests
from google import auth
from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
import jwt


class Authorizer(object):
    cloudbuild_credential_path = "/root/service-account/credentials.json"

    # Permissions to request for Access Token
    scopes = ["https://www.googleapis.com/auth/cloud-platform"]

    def get_authorized_header(self, receiving_service_url) -> dict:
        auth_header = self.__get_authorized_header_for_current_user() \
                      or self.__get_authorized_header_for_cloud_build(receiving_service_url)
        return auth_header

    def __get_authorized_header_for_current_user(self) -> Optional[dict]:
        credentials, _ = auth.default()
        auth_req = google.auth.transport.requests.Request()
        credentials.refresh(auth_req)
        if hasattr(credentials, "id_token"):
            authorized_header = {"Authorization": f'Bearer {credentials.id_token}'}
            auth_req.session.close()
            print("Got auth header for current user with auth.default()")
            return authorized_header

    def __get_authorized_header_for_cloud_build2(self, receiving_service_url) -> dict:
        credentials = service_account.Credentials.from_service_account_file(
            self.cloudbuild_credential_path, scopes=self.scopes)
        auth_req = google.auth.transport.requests.Request()
        credentials.refresh(auth_req)
        return {"Authorization": f'Bearer {credentials.token}'}

    def __get_authorized_header_for_cloud_build(self, receiving_service_url) -> dict:
        with open(self.cloudbuild_credential_path, 'r') as f:
            data = f.read()
        credentials_json = json.loads(data)

        signed_jwt = self.__create_signed_jwt(credentials_json, receiving_service_url)
        token = self.__exchange_jwt_for_token(signed_jwt)
        return {"Authorization": f'Bearer {token}'}

    def __create_signed_jwt(self, credentials_json, run_service_url):
        iat = time.time()
        exp = iat + 3600
        payload = {
            'iss': credentials_json['client_email'],
            'sub': credentials_json['client_email'],
            'target_audience': run_service_url,
            'aud': 'https://www.googleapis.com/oauth2/v4/token',
            'iat': iat,
            'exp': exp
        }
        additional_headers = {
            'kid': credentials_json['private_key_id']
        }
        signed_jwt = jwt.encode(
            payload,
            credentials_json['private_key'],
            headers=additional_headers,
            algorithm='RS256'
        )
        return signed_jwt

    def __exchange_jwt_for_token(self, signed_jwt):
        body = {
            'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion': signed_jwt
        }
        token_request = requests.post(
            url='https://www.googleapis.com/oauth2/v4/token',
            headers={
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            data=urllib.parse.urlencode(body)
        )
        return token_request.json()['id_token']

所以当 运行 在本地时,__get_authorized_header_for_current_user 正在被使用并且可以工作。在 Cloud Build 中 运行 时,使用 __get_authorized_header_for_cloud_build。但即使暂时禁用 __get_authorized_header_for_current_user 并让 cloudbuild_credential_path 引用我本地电脑上的 json 文件,它仍然会收到 401。即使我从凭据文件所有者权限中授予服务帐户。另一种尝试是 __get_authorized_header_for_cloud_build 我尝试自己更多地获取令牌而不是包裹,但仍然是 401.

为了完整起见,集成测试看起来有点像这样:

class NameOfViewIntegrationTestCase(unittest.TestCase):
    base_url = "https://**.a.run.app"
    name_of_call_url = base_url + "/name-of-call"

    def setUp(self) -> None:
        self._authorizer = Authorizer()

    def test_name_of_call__authorized__ok_result(self) -> None:
        # Arrange
        url = self.name_of_call_url 

        # Act
        response = requests.post(url, headers=self._authorizer.get_authorized_header(url))

        # Arrange
        self.assertTrue(response.ok, msg=f'{response.status_code}: {response.text}')

知道我做错了什么吗?如果您需要任何澄清,请告诉我。提前致谢!

首先,你的代码太复杂了。如果您想根据运行时环境利用应用程序默认凭证 (ADC),仅这些行就足够了

from google.oauth2.id_token import fetch_id_token
from google.auth.transport import requests
r = requests.Request()
print(fetch_id_token(r,"<AUDIENCE>"))

在Google 云平台上,由于metadata server,将使用环境服务帐户。在您的本地环境中,您需要将环境变量 GOOGLE_APPLICATION_CREDENTIALS 设置为服务帐户密钥文件的路径

注意:您只能使用服务帐户凭据(在 GCP 或您的环境中)生成 id_token,使用您的用户帐户是不可能的


这里的问题是,它在 Cloud Build 上不起作用。我不知道为什么,但无法使用 Cloud Build 元数据服务器生成 id_token。所以,我写了 an article on this 和一个可能的解决方法