internal/resources/providers/gcplib/auth/credentials.go (107 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package auth import ( "context" "encoding/json" "errors" "fmt" "os" "golang.org/x/oauth2/google" "google.golang.org/api/option" "github.com/elastic/cloudbeat/internal/config" "github.com/elastic/cloudbeat/internal/infra/clog" ) type GcpFactoryConfig struct { // organizations/%s or projects/%s Parent string ClientOpts []option.ClientOption } type ConfigProviderAPI interface { GetGcpClientConfig(ctx context.Context, cfg config.GcpConfig, log *clog.Logger) (*GcpFactoryConfig, error) } type GoogleAuthProviderAPI interface { FindDefaultCredentials(ctx context.Context) (*google.Credentials, error) } type ConfigProvider struct { AuthProvider GoogleAuthProviderAPI } var ErrMissingOrgId = errors.New("organization ID is required for organization account type") var ErrInvalidCredentialsJSON = errors.New("invalid credentials JSON") var ErrProjectNotFound = errors.New("no project ID was found") func (p *ConfigProvider) GetGcpClientConfig(ctx context.Context, cfg config.GcpConfig, log *clog.Logger) (*GcpFactoryConfig, error) { // used in cloud shell flow (and development) if cfg.CredentialsJSON == "" && cfg.CredentialsFilePath == "" { return p.getApplicationDefaultCredentials(ctx, cfg, log) } // used in the manual flow return p.getCustomCredentials(ctx, cfg, log) } func (p *ConfigProvider) getGcpFactoryConfig(ctx context.Context, cfg config.GcpConfig, clientOpts []option.ClientOption) (*GcpFactoryConfig, error) { parent, err := getGcpConfigParentValue(ctx, *p, cfg) if err != nil { return nil, err } return &GcpFactoryConfig{ Parent: parent, ClientOpts: clientOpts, }, nil } // https://cloud.google.com/docs/authentication/application-default-credentials func (p *ConfigProvider) getApplicationDefaultCredentials(ctx context.Context, cfg config.GcpConfig, log *clog.Logger) (*GcpFactoryConfig, error) { log.Info("getDefaultCredentialsConfig create credentials options") return p.getGcpFactoryConfig(ctx, cfg, nil) } func (p *ConfigProvider) getCustomCredentials(ctx context.Context, cfg config.GcpConfig, log *clog.Logger) (*GcpFactoryConfig, error) { log.Info("getCustomCredentialsConfig create credentials options") var opts []option.ClientOption if cfg.CredentialsFilePath != "" { if err := validateJSONFromFile(cfg.CredentialsFilePath); err != nil { return nil, err } log.Infof("Appending credentials file path to gcp client options: %s", cfg.CredentialsFilePath) opts = append(opts, option.WithCredentialsFile(cfg.CredentialsFilePath)) } if cfg.CredentialsJSON != "" { if !json.Valid([]byte(cfg.CredentialsJSON)) { return nil, ErrInvalidCredentialsJSON } log.Info("Appending credentials JSON to client options") opts = append(opts, option.WithCredentialsJSON([]byte(cfg.CredentialsJSON))) } return p.getGcpFactoryConfig(ctx, cfg, opts) } func (p *ConfigProvider) getProjectId(ctx context.Context, cfg config.GcpConfig) (string, error) { if cfg.ProjectId != "" { return cfg.ProjectId, nil } // Try to get project ID from metadata server in case we are running on GCP VM cred, err := p.AuthProvider.FindDefaultCredentials(ctx) if err == nil { return cred.ProjectID, nil } return "", ErrProjectNotFound } func validateJSONFromFile(filePath string) error { if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { return fmt.Errorf("file %q cannot be found", filePath) } b, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("the file %q cannot be read", filePath) } if !json.Valid(b) { return fmt.Errorf("the file %q does not contain valid JSON", filePath) } return nil } func getGcpConfigParentValue(ctx context.Context, provider ConfigProvider, cfg config.GcpConfig) (string, error) { switch cfg.AccountType { case config.OrganizationAccount: if cfg.OrganizationId == "" { return "", ErrMissingOrgId } return fmt.Sprintf("organizations/%s", cfg.OrganizationId), nil case config.SingleAccount: projectId, err := provider.getProjectId(ctx, cfg) if err != nil { return "", fmt.Errorf("failed to get project ID: %v", err) } return fmt.Sprintf("projects/%s", projectId), nil default: return "", fmt.Errorf("invalid gcp account type: %s", cfg.AccountType) } }