package integrations import ( "context" "encoding/json" "fmt" "github.com/porter-dev/porter/api/types" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "gorm.io/gorm" ) // GCPIntegration is an auth mechanism that uses a GCP service account to // authenticate type GCPIntegration struct { gorm.Model // The id of the user that linked this auth mechanism UserID uint `json:"user_id"` // The project that this integration belongs to ProjectID uint `json:"project_id"` // The GCP project id where the service account for this auth mechanism persists GCPProjectID string `json:"gcp_project_id"` // The GCP service account email for this credential GCPSAEmail string `json:"gcp_sa_email"` // The GCP user email that linked this service account GCPUserEmail string `json:"gcp-user-email"` // The GCP region, which may or may not be used by the integration GCPRegion string `json:"gcp_region"` // ------------------------------------------------------------------ // All fields encrypted before storage. // ------------------------------------------------------------------ // KeyData for a service account for GCP connectors GCPKeyData []byte `json:"gcp_key_data"` } func (g *GCPIntegration) ToGCPIntegrationType() *types.GCPIntegration { return &types.GCPIntegration{ CreatedAt: g.CreatedAt, ID: g.ID, UserID: g.UserID, ProjectID: g.ProjectID, GCPProjectID: g.GCPProjectID, GCPSAEmail: g.GCPSAEmail, } } // GetBearerToken retrieves a bearer token for a GCP account func (g *GCPIntegration) GetBearerToken( ctx context.Context, getTokenCache GetTokenCacheFunc, setTokenCache SetTokenCacheFunc, scopes ...string, ) (*oauth2.Token, error) { cache, err := getTokenCache(ctx) // check the token cache for a non-expired token if cache != nil { if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 { return &oauth2.Token{ AccessToken: string(cache.Token), Expiry: cache.Expiry, }, nil } } creds, err := google.CredentialsFromJSON( ctx, g.GCPKeyData, scopes..., ) if err != nil { return nil, fmt.Errorf("failed to get credentials from json: %w", err) } tok, err := creds.TokenSource.Token() if err != nil { return nil, fmt.Errorf("failed to get token from credentials: %w", err) } // update the token cache err = setTokenCache(ctx, tok.AccessToken, tok.Expiry) if err != nil { return nil, fmt.Errorf("non-fatal error setting token cache: %w", err) } return tok, nil } // credentialsFile is the unmarshalled representation of a GCP credentials file. // Source; golang.org/x/oauth2/google type credentialsFile struct { Type string `json:"type"` // serviceAccountKey or userCredentialsKey // Service Account fields ClientEmail string `json:"client_email"` PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` TokenURL string `json:"token_uri"` ProjectID string `json:"project_id"` // User Credential fields // (These typically come from gcloud auth.) ClientSecret string `json:"client_secret"` ClientID string `json:"client_id"` RefreshToken string `json:"refresh_token"` // External Account fields Audience string `json:"audience"` SubjectTokenType string `json:"subject_token_type"` TokenURLExternal string `json:"token_url"` TokenInfoURL string `json:"token_info_url"` ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` // CredentialSource externalaccount.CredentialSource `json:"credential_source"` QuotaProjectID string `json:"quota_project_id"` } func GCPProjectIDFromJSON(jsonData []byte) (string, error) { var f credentialsFile if err := json.Unmarshal(jsonData, &f); err != nil { return "", err } return f.ProjectID, nil } // PopulateGCPMetadata uses the credentials file to get the GCP SA name and // project ID, and attaches it to the GCP credential func (g *GCPIntegration) PopulateGCPMetadata() { var f credentialsFile if err := json.Unmarshal(g.GCPKeyData, &f); err != nil { return } g.GCPProjectID = f.ProjectID g.GCPSAEmail = f.ClientEmail }