Files
threshold-plugin-gcp/internal/credential/config.go
T

218 lines
6.9 KiB
Go
Raw Normal View History

2024-09-22 23:21:58 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package credential
import (
"context"
"encoding/json"
"fmt"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/impersonate"
googleOption "google.golang.org/api/option"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// impersonateServiceAccount is a function variable that creates a TokenSource
var impersonateServiceAccountFn = func(
ctx context.Context,
config impersonate.CredentialsConfig,
opts ...googleOption.ClientOption,
) (oauth2.TokenSource, error) {
return impersonate.CredentialsTokenSource(ctx, config, opts...)
}
// findDefaultCredentialsFn is a function variable that finds the default credentials
var findDefaultCredentialsFn = func(ctx context.Context, scopes ...string) (*google.Credentials, error) {
return google.FindDefaultCredentials(ctx, scopes...)
}
// serviceAccountTokenLifetime is the lifetime of the generated GCP authentication token.
// The token is valid for 1 hour.
const serviceAccountTokenLifetime = time.Hour
// Config is the configuration for the GCP credential.
type Config struct {
ProjectId string
PrivateKey string
PrivateKeyId string
2024-10-21 10:47:06 -04:00
Zone string
2024-09-22 23:21:58 -04:00
ClientEmail string
TargetServiceAccountId string
Scopes []string
}
// credentials represents a simplified version of the GCP credentials file format.
type credentials struct {
ClientEmail string `json:"client_email"`
Type string `json:"type"`
ProjectId string `json:"project_id"`
PrivateKey string `json:"private_key"`
PrivateKeyId string `json:"private_key_id"`
}
// toCredentials converts the config to credentials.
func (c *Config) toCredentials() *credentials {
return &credentials{
Type: "service_account",
ProjectId: c.ProjectId,
PrivateKey: c.PrivateKey,
PrivateKeyId: c.PrivateKeyId,
ClientEmail: c.ClientEmail,
}
}
2024-10-18 11:11:10 -04:00
// clone returns a copy of the configuration.
func (c *Config) clone() *Config {
return &Config{
ProjectId: c.ProjectId,
PrivateKey: c.PrivateKey,
PrivateKeyId: c.PrivateKeyId,
ClientEmail: c.ClientEmail,
TargetServiceAccountId: c.TargetServiceAccountId,
Scopes: c.Scopes,
2024-09-22 23:21:58 -04:00
}
}
2024-10-18 11:11:10 -04:00
// GenerateCredentials generates GCP credentials based on the provided configuration.
2024-09-22 23:21:58 -04:00
// It supports Service Account Key, Service Account Impersonation, and ADC.
2024-10-18 11:11:10 -04:00
// If the credentials are already generated, it will return the cached credentials.
func (c *Config) GenerateCredentials(ctx context.Context) (*google.Credentials, error) {
2024-09-22 23:21:58 -04:00
var creds *google.Credentials
var err error
switch {
case c.PrivateKey != "" && c.ClientEmail != "" && c.TargetServiceAccountId != "":
creds, err = c.credentialsFromImpersonation(ctx)
2024-10-18 11:11:10 -04:00
if err != nil {
return nil, status.Errorf(
codes.Internal,
"failed to generate credentials from service account impersonation: %v", err,
)
}
2024-09-22 23:21:58 -04:00
case c.PrivateKey != "" && c.ClientEmail != "":
creds, err = c.credentialsFromServiceAccountKey(ctx)
2024-10-18 11:11:10 -04:00
if err != nil {
return nil, status.Errorf(
codes.Internal,
"failed to generate credentials from service account key: %v", err,
)
}
2024-09-22 23:21:58 -04:00
default:
creds, err = c.credentialsFromADC(ctx)
2024-10-18 11:11:10 -04:00
if err != nil {
return nil, status.Errorf(
codes.Internal,
"failed to generate credentials from Application Default Credentials: %v", err,
)
}
2024-09-22 23:21:58 -04:00
}
return creds, nil
}
// credentialsFromServiceAccountKey generates the credentials from the service account key.
func (c *Config) credentialsFromServiceAccountKey(ctx context.Context) (*google.Credentials, error) {
if c.PrivateKey == "" {
return nil, status.Error(codes.InvalidArgument, "private_key is required")
}
if c.ClientEmail == "" {
return nil, status.Error(codes.InvalidArgument, "client_email is required")
}
credBytes, err := json.Marshal(c.toCredentials())
if err != nil {
return nil, status.Error(codes.Internal, "error marshaling credentials")
}
creds, err := google.CredentialsFromJSON(ctx, credBytes, c.Scopes...)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse credentials: %v", err)
}
return creds, nil
}
// credentialsFromImpersonation generates the credentials from the impersonation.
func (c *Config) credentialsFromImpersonation(ctx context.Context) (*google.Credentials, error) {
if c.PrivateKey == "" {
return nil, status.Error(codes.InvalidArgument, "private_key is required")
}
if c.ClientEmail == "" {
return nil, status.Error(codes.InvalidArgument, "client_email is required")
}
if c.TargetServiceAccountId == "" {
return nil, status.Error(codes.InvalidArgument, "target_service_account_id is required")
}
credBytes, err := json.Marshal(c.toCredentials())
if err != nil {
return nil, status.Error(codes.Internal, "error marshaling credentials")
}
creds, err := impersonateServiceAccount(ctx, c.ProjectId, credBytes, c.TargetServiceAccountId, c.Scopes)
if err != nil {
return nil, status.Errorf(codes.Internal, "error impersonating service account: %v", err)
}
return creds, nil
}
// credentialsFromADC generates the credentials from the Application Default Credentials (ADC).
func (c *Config) credentialsFromADC(ctx context.Context) (*google.Credentials, error) {
creds, err := findDefaultCredentialsFn(ctx, c.Scopes...)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to find default credentials: %v", err)
}
return creds, nil
}
2024-10-21 10:47:06 -04:00
// IsRotatable returns a boolean indicating if the credentials are rotatable.
func (c *Config) IsRotatable() bool {
if c == nil {
return false
}
switch {
case len(c.TargetServiceAccountId) > 0 && len(c.PrivateKey) > 0 && len(c.ClientEmail) > 0:
return true
case len(c.PrivateKey) > 0 && len(c.ClientEmail) > 0:
return true
default:
return false
}
}
2024-09-22 23:21:58 -04:00
// impersonateServiceAccount impersonates the service account.
// This is used to impersonate the service account to generate an access token.
func impersonateServiceAccount(
ctx context.Context,
projectId string,
baseCredentialsJSON []byte,
targetServiceAccountId string,
scopes []string,
) (*google.Credentials, error) {
if baseCredentialsJSON == nil {
return nil, status.Error(codes.InvalidArgument, "base_service_account is required")
}
if targetServiceAccountId == "" {
return nil, status.Error(codes.InvalidArgument, "target_service_account_id is required")
}
if len(scopes) == 0 {
return nil, status.Error(codes.InvalidArgument, "scope is required")
}
ts, err := impersonateServiceAccountFn(ctx, impersonate.CredentialsConfig{
TargetPrincipal: targetServiceAccountId,
Scopes: scopes,
Lifetime: serviceAccountTokenLifetime,
}, googleOption.WithCredentialsJSON(baseCredentialsJSON))
if err != nil {
return nil, fmt.Errorf("failed to create impersonated token source: %v", err)
}
return &google.Credentials{
ProjectID: projectId,
TokenSource: ts,
}, nil
}