lib/openagents/scv/codex_credential_store/gcp_secret_manager.ex

58e6347eeb72 · 3 KB

defmodule OpenAgents.SCV.CodexCredentialStore.GcpSecretManager do
  @moduledoc "Google Secret Manager storage for versioned SCV Codex authentication homes."

  @behaviour OpenAgents.SCV.CodexCredentialStore

  alias OpenAgents.SCV.DriverAccount

  @impl true
  def put(%DriverAccount{} = account, auth_json) when is_binary(auth_json) do
    with {:ok, secret_ref} <- secret_ref(account),
         {:ok, token} <- access_token(),
         {:ok, response} <-
           Req.post(
             request_options(
               url: "#{api_base()}/v1/#{secret_ref}:addVersion",
               auth: {:bearer, token},
               json: %{payload: %{data: Base.encode64(auth_json)}}
             )
           ),
         200 <- response.status,
         %{"name" => version_name} <- response.body,
         {:ok, version} <- parse_version(version_name) do
      {:ok, version}
    else
      _error -> {:error, :credential_store_failed}
    end
  end

  @impl true
  def fetch(%DriverAccount{credential_version: version} = account) when is_integer(version) do
    with {:ok, secret_ref} <- secret_ref(account),
         {:ok, token} <- access_token(),
         {:ok, response} <-
           Req.get(
             request_options(
               url: "#{api_base()}/v1/#{secret_ref}/versions/#{version}:access",
               auth: {:bearer, token}
             )
           ),
         200 <- response.status,
         %{"payload" => %{"data" => encoded}} <- response.body,
         {:ok, auth_json} <- Base.decode64(encoded) do
      {:ok, auth_json}
    else
      _error -> {:error, :credential_not_found}
    end
  end

  def fetch(%DriverAccount{}), do: {:error, :credential_not_found}

  defp access_token do
    with {:ok, response} <-
           Req.get(
             request_options(
               url:
                 "#{metadata_base()}/computeMetadata/v1/instance/service-accounts/default/token",
               headers: [{"metadata-flavor", "Google"}]
             )
           ),
         200 <- response.status,
         %{"access_token" => token} when is_binary(token) <- response.body do
      {:ok, token}
    else
      _error -> {:error, :workload_identity_unavailable}
    end
  end

  defp request_options(options) do
    configured = Keyword.get(config(), :request_options, [])
    Keyword.merge(configured, options)
  end

  defp secret_ref(%DriverAccount{secret_ref: "projects/" <> _rest = ref}), do: {:ok, ref}
  defp secret_ref(%DriverAccount{}), do: {:error, :credential_reference_invalid}

  defp parse_version(name) do
    case Regex.run(~r{/versions/([0-9]+)\z}, name, capture: :all_but_first) do
      [encoded] ->
        case Integer.parse(encoded) do
          {version, ""} when version > 0 -> {:ok, version}
          _invalid -> {:error, :credential_version_invalid}
        end

      _invalid ->
        {:error, :credential_version_invalid}
    end
  end

  defp api_base,
    do: Keyword.get(config(), :secret_manager_api_base, "https://secretmanager.googleapis.com")

  defp metadata_base,
    do: Keyword.get(config(), :metadata_api_base, "http://metadata.google.internal")

  defp config, do: Application.fetch_env!(:openagents, :scv_codex)
end