diff --git a/auth/grpctransport/grpctransport.go b/auth/grpctransport/grpctransport.go index b4ba3dcbfa9a..5c3bc66f9981 100644 --- a/auth/grpctransport/grpctransport.go +++ b/auth/grpctransport/grpctransport.go @@ -87,6 +87,9 @@ type Options struct { // configured for the client, which will be compared to the universe domain // that is separately configured for the credentials. UniverseDomain string + // APIKey specifies an API key to be used as the basis for authentication. + // If set DetectOpts are ignored. + APIKey string // InternalOptions are NOT meant to be set directly by consumers of this // package, they should only be set by generated client code. @@ -109,7 +112,8 @@ func (o *Options) validate() error { if o.InternalOptions != nil && o.InternalOptions.SkipValidation { return nil } - hasCreds := o.Credentials != nil || + hasCreds := o.APIKey != "" || + o.Credentials != nil || (o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) || (o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "") if o.DisableAuthentication && hasCreds { @@ -237,8 +241,15 @@ func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, er return nil, err } - // Authentication can only be sent when communicating over a secure connection. - if !opts.DisableAuthentication { + if opts.APIKey != "" { + grpcOpts = append(grpcOpts, + grpc.WithPerRPCCredentials(&grpcKeyProvider{ + apiKey: opts.APIKey, + metadata: opts.Metadata, + secure: secure, + }), + ) + } else if !opts.DisableAuthentication { metadata := opts.Metadata var creds *auth.Credentials @@ -283,6 +294,26 @@ func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, er return grpc.DialContext(ctx, endpoint, grpcOpts...) } +// grpcKeyProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials. +type grpcKeyProvider struct { + apiKey string + metadata map[string]string + secure bool +} + +func (g *grpcKeyProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + metadata := make(map[string]string, len(g.metadata)+1) + metadata["X-goog-api-key"] = g.apiKey + for k, v := range g.metadata { + metadata[k] = v + } + return metadata, nil +} + +func (g *grpcKeyProvider) RequireTransportSecurity() bool { + return g.secure +} + // grpcCredentialsProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials. type grpcCredentialsProvider struct { creds *auth.Credentials diff --git a/auth/grpctransport/grpctransport_test.go b/auth/grpctransport/grpctransport_test.go index 456a06196a64..78e1b84f4aea 100644 --- a/auth/grpctransport/grpctransport_test.go +++ b/auth/grpctransport/grpctransport_test.go @@ -389,6 +389,28 @@ func TestNewClient_DetectedServiceAccount(t *testing.T) { } } +func TestGRPCKeyProvider_GetRequestMetadata(t *testing.T) { + apiKey := "MY_API_KEY" + reason := "MY_REQUEST_REASON" + ts := grpcKeyProvider{ + apiKey: apiKey, + metadata: map[string]string{ + "X-goog-request-reason": reason, + }, + } + got, err := ts.GetRequestMetadata(context.Background()) + if err != nil { + t.Fatal(err) + } + want := map[string]string{ + "X-goog-api-key": ts.apiKey, + "X-goog-request-reason": reason, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } +} + type staticTP struct { tok *auth.Token }