Skip to content
Merged
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
17 changes: 17 additions & 0 deletions internal/googleapi/service_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ var newServiceAccountTokenSource = func(ctx context.Context, keyJSON []byte, sub
}

func tokenSourceForServiceAccountScopes(ctx context.Context, email string, scopes []string) (oauth2.TokenSource, string, bool, error) {
// GOG_SA_KEY_PATH allows callers (e.g. proxy wrappers) to specify
// the SA key file directly, decoupling the account email (used as
// the impersonation subject) from the key file location.
if envPath := os.Getenv("GOG_SA_KEY_PATH"); envPath != "" {
data, readErr := os.ReadFile(envPath) //nolint:gosec // caller-provided path
if readErr != nil {
return nil, "", false, fmt.Errorf("read service account key from GOG_SA_KEY_PATH: %w", readErr)
}

ts, tokenErr := newServiceAccountTokenSource(ctx, data, email, scopes)
if tokenErr != nil {
return nil, "", false, tokenErr
}

return ts, envPath, true, nil
}

saPath, err := config.ServiceAccountPath(email)
if err != nil {
return nil, "", false, fmt.Errorf("service account path: %w", err)
Expand Down
56 changes: 55 additions & 1 deletion internal/googleapi/service_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"testing"
)

func generateTestSAKeyJSON(t *testing.T, clientEmail string) []byte {
func generateTestSAKeyJSON(t *testing.T, clientEmail string) []byte { //nolint:unparam // test helper; callers always use saEmail but the parameter keeps intent clear
t.Helper()

key, err := rsa.GenerateKey(rand.Reader, 2048)
Expand Down Expand Up @@ -67,6 +69,58 @@ func TestNewServiceAccountTokenSource_Impersonation(t *testing.T) {
}
}

func TestTokenSourceForServiceAccountScopes_GOG_SA_KEY_PATH(t *testing.T) {
const saEmail = "sa@test-project.iam.gserviceaccount.com"
const userEmail = "user@example.com"

keyJSON := generateTestSAKeyJSON(t, saEmail)

// Write SA key to a temp file.
dir := t.TempDir()

keyPath := filepath.Join(dir, "sa-key.json")

if err := os.WriteFile(keyPath, keyJSON, 0o600); err != nil {
t.Fatalf("write key file: %v", err)
}

// Set GOG_SA_KEY_PATH so tokenSourceForServiceAccountScopes reads
// the key from this path instead of deriving it from the email.
t.Setenv("GOG_SA_KEY_PATH", keyPath)

ts, path, ok, err := tokenSourceForServiceAccountScopes(
context.Background(), userEmail,
[]string{"https://www.googleapis.com/auth/calendar"},
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if !ok {
t.Fatal("expected ok=true")
}

if ts == nil {
t.Fatal("expected non-nil token source")
}

if path != keyPath {
t.Fatalf("expected path=%q, got %q", keyPath, path)
}
}

func TestTokenSourceForServiceAccountScopes_GOG_SA_KEY_PATH_NotFound(t *testing.T) {
t.Setenv("GOG_SA_KEY_PATH", "/nonexistent/sa-key.json")

_, _, _, err := tokenSourceForServiceAccountScopes(
context.Background(), "user@example.com",
[]string{"https://www.googleapis.com/auth/calendar"},
)
if err == nil {
t.Fatal("expected error for nonexistent key path")
}
}

func TestNewServiceAccountTokenSource_EmptySubject(t *testing.T) {
const saEmail = "sa@test-project.iam.gserviceaccount.com"
keyJSON := generateTestSAKeyJSON(t, saEmail)
Expand Down
Loading