Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions plugins/gcloud/bq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gcloud

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func BqCLI() schema.Executable {
return schema.Executable{
Name: "BigQuery CLI",
Runs: []string{"bq"},
DocsURL: sdk.URL("https://cloud.google.com/bigquery/docs/bq-command-line-tool"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.NotWithoutArgs(),
),
Uses: []schema.CredentialUsage{
{
Name: credname.ServiceAccountKey,
},
},
}
}
29 changes: 29 additions & 0 deletions plugins/gcloud/gcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gcloud

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func GCloudCLI() schema.Executable {
return schema.Executable{
Name: "Google Cloud CLI",
Runs: []string{"gcloud"},
DocsURL: sdk.URL("https://cloud.google.com/sdk/gcloud"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.NotWithoutArgs(),
needsauth.NotWhenContainsArgs("auth"),
needsauth.NotWhenContainsArgs("config"),
needsauth.NotWhenContainsArgs("info"),
needsauth.NotWhenContainsArgs("components"),
),
Uses: []schema.CredentialUsage{
{
Name: credname.ServiceAccountKey,
},
},
}
}
25 changes: 25 additions & 0 deletions plugins/gcloud/gsutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gcloud

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func GsutilCLI() schema.Executable {
return schema.Executable{
Name: "Google Cloud Storage CLI",
Runs: []string{"gsutil"},
DocsURL: sdk.URL("https://cloud.google.com/storage/docs/gsutil"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.NotWithoutArgs(),
),
Uses: []schema.CredentialUsage{
{
Name: credname.ServiceAccountKey,
},
},
}
}
24 changes: 24 additions & 0 deletions plugins/gcloud/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package gcloud

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/schema"
)

func New() schema.Plugin {
return schema.Plugin{
Name: "gcloud",
Platform: schema.PlatformInfo{
Name: "Google Cloud Platform",
Homepage: sdk.URL("https://cloud.google.com"),
},
Credentials: []schema.CredentialType{
ServiceAccountKey(),
},
Executables: []schema.Executable{
GCloudCLI(),
GsutilCLI(),
BqCLI(),
},
}
}
194 changes: 194 additions & 0 deletions plugins/gcloud/service_account_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package gcloud

import (
"context"
"encoding/json"
"os"
"path/filepath"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/importer"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func ServiceAccountKey() schema.CredentialType {
return schema.CredentialType{
Name: credname.ServiceAccountKey,
DocsURL: sdk.URL("https://cloud.google.com/iam/docs/keys-create-delete"),
ManagementURL: sdk.URL("https://console.cloud.google.com/iam-admin/serviceaccounts"),
Fields: []schema.CredentialField{
{
Name: fieldname.Credential,
MarkdownDescription: "The JSON credential content for a service account key or authorized user credential.",
Secret: true,
},
{
Name: fieldname.ProjectID,
MarkdownDescription: "The default GCP project to use.",
Optional: true,
},
{
Name: fieldname.Account,
MarkdownDescription: "The account email associated with the credential.",
Optional: true,
},
},
DefaultProvisioner: GCPProvisioner{},
Importer: importer.TryAll(
TryGCloudApplicationDefaultCredentialsFile(),
TryGoogleApplicationCredentialsEnvVar(),
),
}
}

// GCPProvisioner writes the credential JSON to a temporary file and sets
// GOOGLE_APPLICATION_CREDENTIALS to point at it. This approach works with
// all GCP tools (gcloud, gsutil, bq, client libraries, Terraform).
type GCPProvisioner struct{}

func (p GCPProvisioner) Description() string {
return "Provision GCP credential as a temporary JSON file and set GOOGLE_APPLICATION_CREDENTIALS"
}

func (p GCPProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) {
credJSON := in.ItemFields[fieldname.Credential]
var cred gcpCredentialFile
if err := json.Unmarshal([]byte(credJSON), &cred); err != nil {
out.AddError(errInvalidJSON)
return
}

outPath := filepath.Join(in.TempDir, "gcloud-credentials.json")
out.AddSecretFile(outPath, []byte(credJSON))
out.AddEnvVar("GOOGLE_APPLICATION_CREDENTIALS", outPath)
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE tells the gcloud CLI to use
// this credential file directly, bypassing its own auth store.
// GOOGLE_APPLICATION_CREDENTIALS alone is only respected by client
// libraries, not by gcloud CLI commands.
out.AddEnvVar("CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE", outPath)

if projectID, ok := in.ItemFields[fieldname.ProjectID]; ok && projectID != "" {
out.AddEnvVar("CLOUDSDK_CORE_PROJECT", projectID)
} else if cred.ProjectID != "" {
out.AddEnvVar("CLOUDSDK_CORE_PROJECT", cred.ProjectID)
}

if account, ok := in.ItemFields[fieldname.Account]; ok && account != "" {
out.AddEnvVar("CLOUDSDK_CORE_ACCOUNT", account)
} else if cred.ClientEmail != "" {
out.AddEnvVar("CLOUDSDK_CORE_ACCOUNT", cred.ClientEmail)
}
}

func (p GCPProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) {
// Temp files are automatically cleaned up by the SDK.
}

var errInvalidJSON = &jsonError{}

type jsonError struct{}

func (e *jsonError) Error() string {
return "credential field does not contain valid JSON"
}

// TryGCloudApplicationDefaultCredentialsFile imports credentials from the
// gcloud application default credentials file at ~/.config/gcloud/application_default_credentials.json.
//
// This file may contain either a service_account key (long-lived, never expires
// unless explicitly deleted) or an authorized_user credential generated by
// "gcloud auth application-default login". Authorized user credentials contain
// a refresh token whose lifetime depends on the Google account type:
//
// - Personal Gmail accounts: refresh tokens are long-lived and only expire
// if revoked, unused for 6 months, or the password is changed.
// - Google Workspace / Cloud Identity accounts: refresh tokens are subject
// to session length policies configured by the org admin (typically 1–24
// hours), after which reauthentication is required.
//
// Both types are imported. Users with managed org accounts should prefer
// service account keys for a more durable credential.
func TryGCloudApplicationDefaultCredentialsFile() sdk.Importer {
return importer.TryFile("~/.config/gcloud/application_default_credentials.json", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {
var cred gcpCredentialFile
if err := contents.ToJSON(&cred); err != nil {
out.AddError(err)
return
}

if cred.Type == "" {
return
}

candidate := sdk.ImportCandidate{
Fields: map[sdk.FieldName]string{
fieldname.Credential: contents.ToString(),
},
}

if cred.ProjectID != "" {
candidate.Fields[fieldname.ProjectID] = cred.ProjectID
}

if cred.ClientEmail != "" {
candidate.NameHint = importer.SanitizeNameHint(cred.ClientEmail)
}

out.AddCandidate(candidate)
})
}

// TryGoogleApplicationCredentialsEnvVar imports credentials from the file
// pointed to by the GOOGLE_APPLICATION_CREDENTIALS environment variable.
func TryGoogleApplicationCredentialsEnvVar() sdk.Importer {
return func(ctx context.Context, in sdk.ImportInput, out *sdk.ImportOutput) {
attempt := out.NewAttempt(importer.SourceEnvVars("GOOGLE_APPLICATION_CREDENTIALS"))

filePath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
if filePath == "" {
return
}

contents, err := os.ReadFile(filePath)
if err != nil {
attempt.AddError(err)
return
}

var cred gcpCredentialFile
if err := json.Unmarshal(contents, &cred); err != nil {
attempt.AddError(err)
return
}

if cred.Type == "" {
return
}

candidate := sdk.ImportCandidate{
Fields: map[sdk.FieldName]string{
fieldname.Credential: string(contents),
},
}

if cred.ProjectID != "" {
candidate.Fields[fieldname.ProjectID] = cred.ProjectID
}

if cred.ClientEmail != "" {
candidate.NameHint = importer.SanitizeNameHint(cred.ClientEmail)
}

attempt.AddCandidate(candidate)
}
}

// gcpCredentialFile represents the minimal structure of a GCP credential JSON file,
// supporting both service_account and authorized_user types.
type gcpCredentialFile struct {
Type string `json:"type"`
ProjectID string `json:"project_id,omitempty"`
ClientEmail string `json:"client_email,omitempty"`
}
Loading