diff --git a/plugins/common/http/config.go b/plugins/common/http/config.go index dbd335676..aad17fd6e 100644 --- a/plugins/common/http/config.go +++ b/plugins/common/http/config.go @@ -2,10 +2,12 @@ package httpconfig import ( "context" + "fmt" "net/http" "time" "github.com/benbjohnson/clock" + "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/plugins/common/cookie" @@ -30,12 +32,12 @@ type HTTPClientConfig struct { func (h *HTTPClientConfig) CreateClient(ctx context.Context, log telegraf.Logger) (*http.Client, error) { tlsCfg, err := h.ClientConfig.TLSConfig() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to set TLS config: %w", err) } prox, err := h.HTTPProxy.Proxy() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to set proxy: %w", err) } transport := &http.Transport{ diff --git a/plugins/outputs/http/README.md b/plugins/outputs/http/README.md index 20471d465..c1c002d5f 100644 --- a/plugins/outputs/http/README.md +++ b/plugins/outputs/http/README.md @@ -28,6 +28,9 @@ format by default. # token_url = "https://indentityprovider/oauth2/v1/token" # scopes = ["urn:opc:idm:__myscopes__"] + ## Goole API Auth + # google_application_credentials = "/etc/telegraf/example_secret.json" + ## Optional TLS Config # tls_ca = "/etc/telegraf/ca.pem" # tls_cert = "/etc/telegraf/cert.pem" @@ -104,6 +107,14 @@ format by default. # non_retryable_statuscodes = [409, 413] ``` +### Google API Auth + +The `google_application_credentials` setting is used with Google Cloud APIs. It specifies the json key file. To learn about creating Google service accounts, consult Google's +[oauth2 service account documentation][create_service_account]. An example use case is a metrics proxy deployed to +Cloud Run. In this example, the service account must have the "run.routes.invoke" permission. + +[create_service_account]: https://cloud.google.com/docs/authentication/production#create_service_account + ### Optional Cookie Authentication Settings The optional Cookie Authentication Settings will retrieve a cookie from the diff --git a/plugins/outputs/http/http.go b/plugins/outputs/http/http.go index 1c6a7b29a..b046c1dad 100644 --- a/plugins/outputs/http/http.go +++ b/plugins/outputs/http/http.go @@ -19,6 +19,8 @@ import ( httpconfig "github.com/influxdata/telegraf/plugins/common/http" "github.com/influxdata/telegraf/plugins/outputs" "github.com/influxdata/telegraf/plugins/serializers" + "golang.org/x/oauth2" + "google.golang.org/api/idtoken" ) const ( @@ -50,6 +52,10 @@ type HTTP struct { awsCfg *awsV2.Config internalaws.CredentialConfig + + // Google API Auth + CredentialsFile string `toml:"google_application_credentials"` + oauth2Token *oauth2.Token } func (h *HTTP) SetSerializer(serializer serializers.Serializer) { @@ -168,6 +174,15 @@ func (h *HTTP) writeMetric(reqBody []byte) error { req.SetBasicAuth(h.Username, h.Password) } + // google api auth + if h.CredentialsFile != "" { + token, err := h.getAccessToken(context.Background(), h.URL) + if err != nil { + return err + } + token.SetAuthHeader(req) + } + req.Header.Set("User-Agent", internal.ProductToken()) req.Header.Set("Content-Type", defaultContentType) if h.ContentEncoding == "gzip" { @@ -220,3 +235,23 @@ func init() { } }) } + +func (h *HTTP) getAccessToken(ctx context.Context, audience string) (*oauth2.Token, error) { + if h.oauth2Token.Valid() { + return h.oauth2Token, nil + } + + ts, err := idtoken.NewTokenSource(ctx, audience, idtoken.WithCredentialsFile(h.CredentialsFile)) + if err != nil { + return nil, fmt.Errorf("error creating oauth2 token source: %s", err) + } + + token, err := ts.Token() + if err != nil { + return nil, fmt.Errorf("error fetching oauth2 token: %s", err) + } + + h.oauth2Token = token + + return token, nil +} diff --git a/plugins/outputs/http/http_test.go b/plugins/outputs/http/http_test.go index 40c246b53..50f690ebc 100644 --- a/plugins/outputs/http/http_test.go +++ b/plugins/outputs/http/http_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "testing" "time" @@ -526,6 +527,79 @@ func TestOAuthClientCredentialsGrant(t *testing.T) { } } +func TestOAuthAuthorizationCodeGrant(t *testing.T) { + ts := httptest.NewServer(http.NotFoundHandler()) + defer ts.Close() + + u, err := url.Parse(fmt.Sprintf("http://%s", ts.Listener.Addr().String())) + require.NoError(t, err) + + tmpDir := t.TempDir() + tmpFile, err := os.CreateTemp(tmpDir, "test_key_file") + require.NoError(t, err) + + tmpTokenURI := u.String() + "/token" + data := []byte(fmt.Sprintf("{\n \"type\": \"service_account\",\n \"project_id\": \"my-project\",\n \"private_key_id\": \"223423436436453645363456\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIICXAIBAAKBgQDX7Plvu0MJtA9TrusYtQnAogsdiYJZd9wfFIjH5FxE3SWJ4KAIE+yRWRqcqX8XnpieQLaNsfXhDPWLkWngTDydk4NO/jlAQk0e6+9+NeiZ2ViIHmtXERb9CyiiWUmo+YCd69lhzSEIMK9EPBSDHQTgQMtEfGak03G5rx3MCakE1QIDAQABAoGAOjRU4Lt3zKvO3d3u3ZAfet+zY1jn3DolCfO9EzUJcj6ymcIFIWhNgrikJcrCyZkkxrPnAbcQ8oNNxTuDcMTcKZbnyUnlQj5NtVuty5Q+zgf3/Q2pRhaE+TwrpOJ+ETtVp9R/PrPN2NC5wPo289fPNWFYkd4DPbdWZp5AJHz1XYECQQD3kKpinJxMYp9FQ1Qj1OkxGln0KPgdqRYjjW/rXI4/hUodfg+xXWHPFSGj3AgEjQIvuengbOAeH3qowF1uxVTlAkEA30hXM3EbboMCDQzNRNkkV9EiZ0MZXhj1aIGl+sQZOmOeFdcdjGkDdsA42nmaYqXCD9KAvc+S/tGJaa0Qg0VhMQJAb2+TAqh0Qn3yK39PFIH2JcAy1ZDLfq5p5L75rfwPm9AnuHbSIYhjSo+8gMG+ai3+2fTZrcfUajrJP8S3SfFRcQJBANQQPOHatxcKzlPeqMaPBXlyY553mAxK4CnVmPLGdL+EBYzwtlu5EVUj09uMSxkOHXYxk5yzHQVvtXbsrBZBOsECQBJLlkMjJmXrIIdLPmHQWL3bm9MMg1PqzupSEwz6cyrGuIIm/X91pDyxCHaKYWp38FXBkYAgohI8ow5/sgRvU5w=\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"test-service-account-email@example.iam.gserviceaccount.com\",\n \"client_id\": \"110300009813738675309\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"%s\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/test-service-account-email@example.iam.gserviceaccount.com\"\n}", tmpTokenURI)) + _, err = tmpFile.Write(data) + require.NoError(t, err) + + require.NoError(t, tmpFile.Close()) + + const token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg2NzUzMDliMjJiMDFiZTU2YzIxM2M5ODU0MGFiNTYzYmZmNWE1OGMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwOi8vMTI3LjAuMC4xOjU4MDI1LyIsImF6cCI6InRlc3Qtc2VydmljZS1hY2NvdW50LWVtYWlsQGV4YW1wbGUuY29tIiwiZW1haWwiOiJ0ZXN0LXNlcnZpY2UtYWNjb3VudC1lbWFpbEBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjk0NjY4NDgwMCwiaWF0Ijo5NDY2ODEyMDAsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMudGVzdC5jb20iLCJzdWIiOiIxMTAzMDAwMDk4MTM3Mzg2NzUzMDkifQ.qi2LsXP2o6nl-rbYKUlHAgTBY0QoU7Nhty5NGR4GMdc8OoGEPW-vlD0WBSaKSr11vyFcIO4ftFDWXElo9Ut-AIQPKVxinsjHIU2-LoIATgI1kyifFLyU_pBecwcI4CIXEcDK5wEkfonWFSkyDZHBeZFKbJXlQXtxj0OHvQ-DEEepXLuKY6v3s4U6GyD9_ppYUy6gzDZPYUbfPfgxCj_Jbv6qkLU0DiZ7F5-do6X6n-qkpgCRLTGHcY__rn8oe8_pSimsyJEeY49ZQ5lj4mXkVCwgL9bvL1_eW1p6sgbHaBnPKVPbM7S1_cBmzgSonm__qWyZUxfDgNdigtNsvzBQTg" + + tests := []struct { + name string + plugin *HTTP + handler TestHandlerFunc + tokenHandler TestHandlerFunc + }{ + { + name: "no credentials file", + plugin: &HTTP{ + URL: u.String(), + }, + handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + require.Len(t, r.Header["Authorization"], 0) + w.WriteHeader(http.StatusOK) + }, + }, + { + name: "success", + plugin: &HTTP{ + URL: u.String() + "/write", + CredentialsFile: tmpFile.Name(), + }, + tokenHandler: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + authHeader := fmt.Sprintf(`{"id_token":"%s"}`, token) + _, err = w.Write([]byte(authHeader)) + require.NoError(t, err) + }, + handler: func(t *testing.T, w http.ResponseWriter, r *http.Request) { + require.Equal(t, []string{"Bearer " + token}, r.Header["Authorization"]) + w.WriteHeader(http.StatusOK) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/write": + tt.handler(t, w, r) + case "/token": + tt.tokenHandler(t, w, r) + } + }) + + tt.plugin.SetSerializer(influx.NewSerializer()) + require.NoError(t, tt.plugin.Connect()) + require.NoError(t, tt.plugin.Write([]telegraf.Metric{getMetric()})) + require.NoError(t, err) + }) + } +} + func TestDefaultUserAgent(t *testing.T) { ts := httptest.NewServer(http.NotFoundHandler()) defer ts.Close()