Skip to content

fix(auth): auto-detect GitHub Actions WIF with proper audience, host validation, and lazy GSM init - #2109

Merged
Andriy Knysh (aknysh) merged 14 commits into
cloudposse:mainfrom
shirkevich:fix/gcp-wif-ci
Feb 27, 2026
Merged

Andriy Knysh (aknysh) merged 14 commits into
cloudposse:mainfrom
shirkevich:fix/gcp-wif-ci

Conversation

@shirkevich

@shirkevich Mikhail Shirkov (shirkevich) commented Feb 25, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fix GCP Workload Identity Federation (WIF) for GitHub Actions CI environments by auto-detecting the OIDC token source, constructing the correct audience, and validating token URL hosts.

Core fixes (GitHub Actions WIF)

  • Auto-detect GitHub Actions OIDC when token_source is not explicitly configured — detects ACTIONS_ID_TOKEN_REQUEST_URL and defaults to URL-based token fetch.
  • Auto-construct WIF audience from project_number, workload_identity_pool_id, and workload_identity_provider_id so the OIDC token's aud claim matches what GCP STS expects. Without this, GitHub defaults to the repo owner URL (e.g., https://github.com/org), which STS rejects.
  • Validate env-sourced OIDC URLs against a GitHub Actions host whitelist (*.actions.githubusercontent.com) instead of skipping host validation entirely.
  • Fail fast on empty audience when running in GitHub Actions with incomplete WIF config, instead of hitting a cryptic STS audience mismatch error downstream.

Supporting fixes

  • Defer GSM client creation to first use via ensureClient() instead of eagerly creating it in the constructor — solves the chicken-and-egg problem where store init during config loading happens before auth sets credentials.
  • Treat ADC client_id/client_secret as an atomic pair — custom ID without matching secret falls back to the full default pair to prevent invalid_client errors.
  • Drop unused error return from resolveADCClientCredentials() (always returns a valid pair).
  • Add gitleaks:allow marker to public gcloud OAuth secret.
  • Add //nolint:forbidigo to os.Getenv calls in auth code that runs before Viper/flags are wired.
  • Add missing docstrings and perf.Track calls per project conventions.
  • Make tests hermetic — replace external GitHub URL dependency with httptest servers; remove redundant os.Unsetenv calls.

Why

GCP WIF authentication was failing in GitHub Actions CI with three sequential errors:

  1. token_source not configured — nil token source with no auto-detection
  2. token URL host not allowed — GitHub Actions uses dynamic subdomains
  3. audience mismatch — OIDC aud claim defaulted to repo owner URL instead of WIF provider resource name

References

  • Tested in CI via pre-release binaries on shirkevich/atmos

…mic pair

Defer Google Secret Manager client initialization to first use via
ensureClient() instead of eagerly creating it in the constructor. This
fixes a chicken-and-egg problem where config loading creates stores
before auth credentials (e.g., GOOGLE_OAUTH_ACCESS_TOKEN from WIF) are
established, causing "failed to create client: could not find default
credentials" errors in CI.

Also treat ADC client_id/client_secret as an atomic pair: a custom
client_id without a matching secret falls back to the full default pair
rather than producing a mismatched combination that fails OAuth refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m Medium size PR label Feb 25, 2026
@coderabbitai

coderabbitai Bot commented Feb 25, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ADC client-credentials resolution with env/ADC-file/default fallbacks; switches GSM store to lazy client initialization, adds secret helper methods and GetKey; refactors GCP WIF token flow to accept/auto-detect token sources (including GitHub Actions) and updates tests to match lazy/init and token-source changes.

Changes

Cohort / File(s) Summary
GCP ADC credential resolution
pkg/auth/cloud/gcp/setup.go
Adds ADC client_id/secret resolution: env var overrides, ADC JSON parsing, ADC path discovery, fallback to default public gcloud client pair, and formatTokenExpiry for zero-time/RFC3339 UTC formatting.
GCP ADC tests
pkg/auth/cloud/gcp/setup_test.go
Adds/renames tests to assert fallback-to-defaults when ADC secret/file absent and verifies behavior when custom client_id is provided without a secret.
GSM store lazy init & helpers
pkg/store/google_secret_manager_store.go
Removes eager client init in constructor; centralizes lazy init in ensureClient(); adds createReplicationFromLocations, unexported createSecret/addSecretVersion, and public GetKey.
GSM store tests & mocks
pkg/store/google_secret_manager_store_test.go, pkg/store/identity_test.go
Extends MockGSMClient (CreateSecret/AddSecretVersion/AccessSecretVersion/Close), adds lazy-init test (TestNewGSMStore_LazyClientCreation), updates tests to mark initOnce and avoid eager client creation; adjusts expectations for constructor behavior.
GCP WIF token handling
pkg/auth/providers/gcp_wif/provider.go, pkg/auth/providers/gcp_wif/provider_test.go
Refactors token retrieval helpers to accept a tokenSource; auto-detects GitHub Actions OIDC when token_source is nil and routes to URL-based retrieval; centralizes helpers (wifAudience, getHTTPClient, getStsURL), tightens host validation, updates scopes/defaults and numerous tests for new flows and signatures.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Provider as Provider (gcp_wif)
participant Env as Environment (GitHub Actions)
participant TokenSrc as Token Source (env/file/url)
participant STS as Google STS
participant ADC as ADC resolver
Provider->>Provider: resolve token_source (explicit or nil)
alt token_source nil + GitHub Actions env present
Provider->>Env: read ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN
Env-->>Provider: id_token (URL+token) or url hint
Provider->>STS: exchange id_token -> STS token (use wifAudience)
STS-->>Provider: STS token
else explicit token_source (env/file/url)
Provider->>TokenSrc: obtain id_token (env/file/url)
TokenSrc-->>Provider: id_token
Provider->>STS: exchange id_token -> STS token
STS-->>Provider: STS token
end
Note right of ADC: ADC resolver checks env/ADC file/default client_id+secret
ADC-->>Provider: client_id, client_secret

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • aknysh
  • osterman
  • mcalhoun
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: auto-detecting GitHub Actions WIF, adding proper audience and host validation, and implementing lazy GSM client initialization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/auth/cloud/gcp/setup_test.go (1)

157-158: Drop manual os.Unsetenv in this test and keep env state fully test-scoped.

resolveADCClientCredentials() uses Getenv, so t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "") is sufficient here; the explicit unset is redundant.

Proposed cleanup
 	t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", filepath.Join(tmp, "missing.json"))
 	t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "")
-	os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET")

Based on learnings: In the cloudposse/atmos repo, tests that manipulate environment variables should use testing.T.Setenv for automatic setup/teardown instead of os.Setenv/Unsetenv.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup_test.go` around lines 157 - 158, The test currently
calls t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "") and then immediately calls
os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET"); remove the manual os.Unsetenv call
so the test relies solely on testing.T.Setenv for scope-managed environment
changes; update the test in pkg/auth/cloud/gcp/setup_test.go where
resolveADCClientCredentials() is exercised to only use t.Setenv(...) and not
os.Unsetenv(...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/auth/cloud/gcp/setup_test.go`:
- Around line 157-158: The test currently calls
t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "") and then immediately calls
os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET"); remove the manual os.Unsetenv call
so the test relies solely on testing.T.Setenv for scope-managed environment
changes; update the test in pkg/auth/cloud/gcp/setup_test.go where
resolveADCClientCredentials() is exercised to only use t.Setenv(...) and not
os.Unsetenv(...).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee6a167 and 6c3df98.

📒 Files selected for processing (5)
  • pkg/auth/cloud/gcp/setup.go
  • pkg/auth/cloud/gcp/setup_test.go
  • pkg/store/google_secret_manager_store.go
  • pkg/store/google_secret_manager_store_test.go
  • pkg/store/identity_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 25, 2026
Comment thread pkg/auth/cloud/gcp/setup.go
When token_source is not configured in the WIF provider spec, auto-detect
GitHub Actions by checking ACTIONS_ID_TOKEN_REQUEST_URL and default to
URL-based OIDC token fetch. This eliminates the need for explicit
token_source config in CI.

Also adds docstrings to all functions in changed files to satisfy the 80%
docstring coverage threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pkg/auth/cloud/gcp/setup.go (1)

176-180: ⚠️ Potential issue | 🟡 Minor

Support Windows gcloud default credentials path.

On Windows, gcloud stores ADC at %APPDATA%\gcloud\application_default_credentials.json (e.g., C:\Users\<USERNAME>\AppData\Roaming\gcloud\...), but the current code only checks the Unix path. This will fail to find credentials on Windows systems.

The fix should check the APPDATA environment variable before falling back to the Unix home directory:

Proposed Windows compatibility fix
 func adcCredentialsPath() string {
 	if path := strings.TrimSpace(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")); path != "" {
 		return path
 	}
 	if configDir := strings.TrimSpace(os.Getenv("CLOUDSDK_CONFIG")); configDir != "" {
 		return filepath.Join(configDir, "application_default_credentials.json")
 	}
+	// Windows: %APPDATA%\gcloud; Unix: ~/.config/gcloud
+	if appData := os.Getenv("APPDATA"); appData != "" {
+		return filepath.Join(appData, "gcloud", "application_default_credentials.json")
+	}
 	home, err := os.UserHomeDir()
 	if err != nil {
 		return ""
 	}
 	return filepath.Join(home, ".config", "gcloud", "application_default_credentials.json")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup.go` around lines 176 - 180, The code that builds the
gcloud ADC path only uses os.UserHomeDir (Unix-style ~/.config/gcloud/...),
which misses Windows' %APPDATA%\gcloud\application_default_credentials.json;
update the function that currently calls os.UserHomeDir to first check
os.Getenv("APPDATA") and if non-empty return filepath.Join(appData, "gcloud",
"application_default_credentials.json"), otherwise fall back to using
os.UserHomeDir and filepath.Join(home, ".config", "gcloud",
"application_default_credentials.json"); keep the existing failure behavior of
returning "" when neither APPDATA nor UserHomeDir is available and reference the
os.Getenv("APPDATA") check and the filepath.Join calls to locate where to change
the logic.
pkg/store/google_secret_manager_store.go (2)

413-418: ⚠️ Potential issue | 🟠 Major

Handle PermissionDenied explicitly in GetKey.

Right now PermissionDenied falls into ErrAccessSecret, unlike Get, which loses error specificity and consistency for callers using typed error handling.

Suggested fix
  if err != nil {
-     if status.Code(err) == codes.NotFound {
-         return nil, fmt.Errorf(errWrapFormatWithID, ErrResourceNotFound, secretName, err)
-     }
-     return nil, fmt.Errorf(errWrapFormat, ErrAccessSecret, err)
+     if st, ok := status.FromError(err); ok {
+         switch st.Code() {
+         case codes.NotFound:
+             return nil, fmt.Errorf(errWrapFormatWithID, ErrResourceNotFound, secretName, err)
+         case codes.PermissionDenied:
+             return nil, fmt.Errorf(errWrapFormatWithID, ErrPermissionDenied, fmt.Sprintf("secret %s", secretName), err)
+         }
+     }
+     return nil, fmt.Errorf(errWrapFormat, ErrAccessSecret, err)
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/store/google_secret_manager_store.go` around lines 413 - 418, In GetKey's
error handling (inside function GetKey), explicitly check for status.Code(err)
== codes.PermissionDenied and return a wrapped error using the
ErrPermissionDenied sentinel (use the same errWrapFormatWithID pattern with
secretName and err) instead of letting it fall through to ErrAccessSecret; keep
the existing NotFound handling and fallback to ErrAccessSecret for other codes.

386-431: 🛠️ Refactor suggestion | 🟠 Major

Add the required perf tracker in public GetKey.

GetKey is a non-trivial public method and should include the standard defer perf.Track(...)() hook at function entry (with the required blank line after it).

As per coding guidelines "Add defer perf.Track(atmosConfig, "pkg.FuncName")() + blank line to all public functions, use nil if no atmosConfig param."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/store/google_secret_manager_store.go` around lines 386 - 431, Add the
standard perf tracker at the top of the public GetKey method: insert a defer
perf.Track(nil, "pkg.GetKey")() immediately after the function entry (as the
first executable statement) and include a blank line after that defer; also add
the perf import if missing so GSMStore.GetKey compiles. Ensure the tracker uses
nil for atmosConfig per guidelines and references the GetKey method name.
pkg/store/google_secret_manager_store_test.go (1)

24-60: ⚠️ Potential issue | 🟠 Major

Switch to mockgen for GSMClient mock generation.

Add a //go:generate directive to google_secret_manager_store.go above the GSMClient interface and generate the mock, just like the other interfaces in the pkg/store package. This keeps the mock in sync with interface changes and aligns with the codebase pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/store/google_secret_manager_store_test.go` around lines 24 - 60, Add a
//go:generate directive above the GSMClient interface in
google_secret_manager_store.go and remove the hand-written MockGSMClient type in
google_secret_manager_store_test.go; then run the same mock generation command
used elsewhere in pkg/store to produce an up-to-date mock for GSMClient (so
generated mock matches the GSMClient interface and follows the repo pattern),
ensuring references to GSMClient, CreateSecret, AddSecretVersion,
AccessSecretVersion, and Close are present in the generated mock.
pkg/auth/providers/gcp_wif/provider_test.go (1)

410-439: ⚠️ Potential issue | 🟠 Major

Mock the HTTP request in TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions to eliminate external dependency and strengthen assertions.

The test currently attempts a real HTTP request to token.actions.githubusercontent.com and only asserts that an error occurs without verifying the intended behavior—it checks that a specific substring is absent rather than confirming the code path works correctly. This can pass on unrelated failures and is non-deterministic.

Use a custom RoundTripper to intercept the request, verify the Authorization header, and return a deterministic response. Then assert the actual token value is extracted correctly. Consider applying the same pattern to both nil-token_source test cases as a table-driven structure to cover multiple scenarios (no GitHub Actions env, GitHub Actions with success, GitHub Actions with fetch failure).

The suggested refactor pattern aligns with how other tests in this file (e.g., TestGetTokenFromURL) use httptest to mock services deterministically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider_test.go` around lines 410 - 439, Modify
TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions to mock the HTTP call
instead of hitting the external URL: create a custom RoundTripper or httptest
server and assign it to p.httpClient, intercept requests to verify the
Authorization header contains the ACTIONS_ID_TOKEN_REQUEST_TOKEN value, return a
deterministic JSON token response, then call p.getOIDCToken(ctx) and assert the
returned token matches the mocked response (and still assert it does not return
errUtils.ErrInvalidProviderConfig). Use the same mocking pattern for the other
nil token_source case if helpful to convert tests into a small table-driven set
covering no-GHA, GHA-success, and GHA-failure scenarios so behavior is
deterministic.
🧹 Nitpick comments (3)
pkg/auth/providers/gcp_wif/provider.go (2)

305-308: Consider limiting error body read size.

When the token request fails, the entire response body is read without a size limit. A malicious or misconfigured server could return a very large body, consuming memory.

Suggested fix
 	if resp.StatusCode != http.StatusOK {
-		body, _ := io.ReadAll(resp.Body)
+		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
 		return "", fmt.Errorf("%w: token request failed: %s: %s", errUtils.ErrAuthenticationFailed, resp.Status, string(body))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 305 - 308, The error
handling in the token request checks resp.StatusCode but reads the entire
resp.Body via io.ReadAll which can OOM on large responses; update the failure
branch in the token fetch logic to read only a bounded amount (e.g., wrap
resp.Body with io.LimitReader and read up to a safe max like 4KB) and handle
read errors, then include that truncated body in the fmt.Errorf return (still
using errUtils.ErrAuthenticationFailed and resp.Status) so you preserve context
while preventing unbounded memory usage.

376-379: Same unbounded body read pattern here.

Similar to the token URL request, the STS error response body is read without a size limit.

Suggested fix
 	if resp.StatusCode != http.StatusOK {
-		body, _ := io.ReadAll(resp.Body)
+		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
 		return nil, fmt.Errorf("%w: STS error: %s: %s", errUtils.ErrAuthenticationFailed, resp.Status, string(body))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 376 - 379, The STS error
handling reads resp.Body without a size limit; change the read to use a bounded
reader (e.g. io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize))) and
introduce a sensible constant like maxErrorBodySize (e.g. 4–16 KB) to avoid
unbounded memory use; update the error message to include the possibly truncated
body and ensure resp.Body is closed as before; apply this change around the STS
response check that constructs the fmt.Errorf with
errUtils.ErrAuthenticationFailed and resp.Status.
pkg/auth/cloud/gcp/setup_test.go (1)

158-159: Redundant unset after setting to empty.

t.Setenv already sets the variable and handles cleanup. The subsequent os.Unsetenv is redundant and may conflict with t.Setenv's cleanup logic.

Simplify by removing the redundant unset
-	t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "")
-	os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET")
+	os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET")

Or if you want to ensure it's truly unset (not empty string), just use os.Unsetenv alone since t.Setenv will capture the original value for restoration anyway.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup_test.go` around lines 158 - 159, Remove the
redundant os.Unsetenv call after using t.Setenv for the
ATMOS_GCP_ADC_CLIENT_SECRET variable: keep the
t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "") line (which records original value
and restores it after the test) and delete the subsequent
os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET") to avoid conflicting cleanup logic;
alternatively, if you prefer not to use t.Setenv, replace the t.Setenv call with
a single os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET") instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/store/google_secret_manager_store_test.go`:
- Around line 89-90: The helper gsmClientSecretCreationMock currently hardcodes
"test-project" for the parent string; update it to use the function parameter
projectID (e.g., build parent with fmt.Sprintf("projects/%s", projectID)) so the
mock respects the passed projectID; verify any other occurrences inside
gsmClientSecretCreationMock that reference the project ID are replaced to use
the projectID parameter and keep the rest of the mock (MockGSMClient
expectations) unchanged.

---

Outside diff comments:
In `@pkg/auth/cloud/gcp/setup.go`:
- Around line 176-180: The code that builds the gcloud ADC path only uses
os.UserHomeDir (Unix-style ~/.config/gcloud/...), which misses Windows'
%APPDATA%\gcloud\application_default_credentials.json; update the function that
currently calls os.UserHomeDir to first check os.Getenv("APPDATA") and if
non-empty return filepath.Join(appData, "gcloud",
"application_default_credentials.json"), otherwise fall back to using
os.UserHomeDir and filepath.Join(home, ".config", "gcloud",
"application_default_credentials.json"); keep the existing failure behavior of
returning "" when neither APPDATA nor UserHomeDir is available and reference the
os.Getenv("APPDATA") check and the filepath.Join calls to locate where to change
the logic.

In `@pkg/auth/providers/gcp_wif/provider_test.go`:
- Around line 410-439: Modify
TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions to mock the HTTP call
instead of hitting the external URL: create a custom RoundTripper or httptest
server and assign it to p.httpClient, intercept requests to verify the
Authorization header contains the ACTIONS_ID_TOKEN_REQUEST_TOKEN value, return a
deterministic JSON token response, then call p.getOIDCToken(ctx) and assert the
returned token matches the mocked response (and still assert it does not return
errUtils.ErrInvalidProviderConfig). Use the same mocking pattern for the other
nil token_source case if helpful to convert tests into a small table-driven set
covering no-GHA, GHA-success, and GHA-failure scenarios so behavior is
deterministic.

In `@pkg/store/google_secret_manager_store_test.go`:
- Around line 24-60: Add a //go:generate directive above the GSMClient interface
in google_secret_manager_store.go and remove the hand-written MockGSMClient type
in google_secret_manager_store_test.go; then run the same mock generation
command used elsewhere in pkg/store to produce an up-to-date mock for GSMClient
(so generated mock matches the GSMClient interface and follows the repo
pattern), ensuring references to GSMClient, CreateSecret, AddSecretVersion,
AccessSecretVersion, and Close are present in the generated mock.

In `@pkg/store/google_secret_manager_store.go`:
- Around line 413-418: In GetKey's error handling (inside function GetKey),
explicitly check for status.Code(err) == codes.PermissionDenied and return a
wrapped error using the ErrPermissionDenied sentinel (use the same
errWrapFormatWithID pattern with secretName and err) instead of letting it fall
through to ErrAccessSecret; keep the existing NotFound handling and fallback to
ErrAccessSecret for other codes.
- Around line 386-431: Add the standard perf tracker at the top of the public
GetKey method: insert a defer perf.Track(nil, "pkg.GetKey")() immediately after
the function entry (as the first executable statement) and include a blank line
after that defer; also add the perf import if missing so GSMStore.GetKey
compiles. Ensure the tracker uses nil for atmosConfig per guidelines and
references the GetKey method name.

---

Nitpick comments:
In `@pkg/auth/cloud/gcp/setup_test.go`:
- Around line 158-159: Remove the redundant os.Unsetenv call after using
t.Setenv for the ATMOS_GCP_ADC_CLIENT_SECRET variable: keep the
t.Setenv("ATMOS_GCP_ADC_CLIENT_SECRET", "") line (which records original value
and restores it after the test) and delete the subsequent
os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET") to avoid conflicting cleanup logic;
alternatively, if you prefer not to use t.Setenv, replace the t.Setenv call with
a single os.Unsetenv("ATMOS_GCP_ADC_CLIENT_SECRET") instead.

In `@pkg/auth/providers/gcp_wif/provider.go`:
- Around line 305-308: The error handling in the token request checks
resp.StatusCode but reads the entire resp.Body via io.ReadAll which can OOM on
large responses; update the failure branch in the token fetch logic to read only
a bounded amount (e.g., wrap resp.Body with io.LimitReader and read up to a safe
max like 4KB) and handle read errors, then include that truncated body in the
fmt.Errorf return (still using errUtils.ErrAuthenticationFailed and resp.Status)
so you preserve context while preventing unbounded memory usage.
- Around line 376-379: The STS error handling reads resp.Body without a size
limit; change the read to use a bounded reader (e.g.
io.ReadAll(io.LimitReader(resp.Body, maxErrorBodySize))) and introduce a
sensible constant like maxErrorBodySize (e.g. 4–16 KB) to avoid unbounded memory
use; update the error message to include the possibly truncated body and ensure
resp.Body is closed as before; apply this change around the STS response check
that constructs the fmt.Errorf with errUtils.ErrAuthenticationFailed and
resp.Status.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3df98 and 2dcd5ed.

📒 Files selected for processing (6)
  • pkg/auth/cloud/gcp/setup.go
  • pkg/auth/cloud/gcp/setup_test.go
  • pkg/auth/providers/gcp_wif/provider.go
  • pkg/auth/providers/gcp_wif/provider_test.go
  • pkg/store/google_secret_manager_store.go
  • pkg/store/google_secret_manager_store_test.go

Comment thread pkg/store/google_secret_manager_store_test.go Outdated
GitHub Actions uses dynamic subdomains for ACTIONS_ID_TOKEN_REQUEST_URL
(e.g., run-actions-1-azure-eastus.actions.githubusercontent.com) which
don't match the static allowlist. When the URL comes from the env var,
skip host validation entirely — GitHub Actions controls that URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/auth/providers/gcp_wif/provider_test.go`:
- Around line 426-438: The test uses a real GitHub URL causing flakiness; make
it deterministic by replacing the outbound call with an httptest server and
injecting its URL via the ACTIONS_ID_TOKEN_REQUEST_URL env var (keep
ACTIONS_ID_TOKEN_REQUEST_TOKEN for the bearer token), then call
Provider.getOIDCToken with the Provider instance (use the existing http.Client)
and assert the exact expected outcome: either a successful token string when the
test server returns a valid token payload or a specific authentication error
type/message when the server returns an auth failure; ensure the test server
validates the incoming Authorization header and body to exercise the same code
paths as getOIDCToken.

In `@pkg/auth/providers/gcp_wif/provider.go`:
- Around line 269-274: The current check in provider.go skips host validation
when fromEnv is true, allowing any HTTPS URL from ACTIONS_ID_TOKEN_REQUEST_URL;
change the logic in the token URL validation (the block referencing fromEnv,
AllowedHosts, and hostAllowed(u, allowedHosts)) so that host validation is not
bypassed: if token_source.allowed_hosts is set, use hostAllowed(u, allowedHosts)
as now; if allowed_hosts is empty and the URL came from the environment, only
accept HTTPS hosts that match the known GitHub Actions domains (e.g.,
actions.githubusercontent.com and its
run-actions-*.actions.githubusercontent.com subdomains) and reject all others;
when rejecting, return the same errUtils.ErrInvalidProviderConfig error with a
clear message. Ensure the scheme check (https) remains enforced and keep
references to fromEnv, AllowedHosts, hostAllowed, and
errUtils.ErrInvalidProviderConfig so reviewers can locate the change.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2dcd5ed and dce8d76.

📒 Files selected for processing (2)
  • pkg/auth/providers/gcp_wif/provider.go
  • pkg/auth/providers/gcp_wif/provider_test.go

Comment thread pkg/auth/providers/gcp_wif/provider_test.go Outdated
Comment thread pkg/auth/providers/gcp_wif/provider.go Outdated
…ctions

When fetching OIDC token from GitHub Actions without an explicit audience,
auto-construct the WIF provider resource name as the audience parameter.
Without this, GitHub defaults the aud claim to the repo owner URL
(e.g., https://github.com/org) which GCP STS rejects as audience mismatch.

Also refactors exchangeToken to reuse the new wifAudience() helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pkg/auth/providers/gcp_wif/provider.go (2)

428-440: Consider returning an error instead of empty string.

If wifAudience() returns empty due to missing fields, exchangeToken will send an empty audience to STS, yielding a confusing GCP error. While Validate() guards against this in Authenticate(), a defensive check here could help catch misuse in other call paths.

🛡️ Optional defensive approach
-func (p *Provider) wifAudience() string {
+func (p *Provider) wifAudience() (string, error) {
 	pool := p.poolID()
 	provider := p.providerID()
 	if p.spec.ProjectNumber == "" || pool == "" || provider == "" {
-		return ""
+		return "", fmt.Errorf("%w: cannot construct WIF audience: missing project_number, pool, or provider", errUtils.ErrInvalidProviderConfig)
 	}
 	return fmt.Sprintf(
 		"//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s",
 		p.spec.ProjectNumber, pool, provider,
-	)
+	), nil
 }

Then update callers to handle the error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 428 - 440, wifAudience
currently returns an empty string when required fields are missing which lets
callers like exchangeToken send an invalid audience to STS; change wifAudience()
to return (string, error) and return a descriptive error if
p.spec.ProjectNumber, pool or provider are empty, then update callers (notably
exchangeToken and any other usages) to handle and propagate that error (or
return it up through Authenticate/Validate) so no empty audience is ever sent;
keep Validate() as-is but treat wifAudience as a defensive check used by
wifAudience(), exchangeToken(), and Authenticate().

331-347: Simplify case handling in hostAllowed.

Both allowed and host are already lowercased before the comparison. Using strings.EqualFold afterwards is redundant for host and only needed for u.Host. You could streamline this.

♻️ Suggested simplification
 func hostAllowed(u *url.URL, allowedHosts []string) bool {
 	host := strings.ToLower(u.Hostname())
+	hostWithPort := strings.ToLower(u.Host)
 	if host == "" {
 		return false
 	}
 	for _, allowed := range allowedHosts {
 		allowed = strings.ToLower(strings.TrimSpace(allowed))
 		if allowed == "" {
 			continue
 		}
-		if strings.EqualFold(allowed, u.Host) || strings.EqualFold(allowed, host) {
+		if allowed == hostWithPort || allowed == host {
 			return true
 		}
 	}
 	return false
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 331 - 347, hostAllowed
currently lowercases host and allowed but then uses strings.EqualFold
redundantly; simplify by lowercasing and trimming allowed once, then compare
using simple equality against the lowercased u.Hostname() and the lowercased
u.Host (if needed). Update the hostAllowed function: compute host :=
strings.ToLower(u.Hostname()) and normAllowed :=
strings.ToLower(strings.TrimSpace(allowed)), skip empty normAllowed, and use
direct equality checks (normAllowed == host or normAllowed ==
strings.ToLower(u.Host)) instead of strings.EqualFold to remove redundancy while
preserving correct comparisons for u.Host and host.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/auth/providers/gcp_wif/provider.go`:
- Around line 428-440: wifAudience currently returns an empty string when
required fields are missing which lets callers like exchangeToken send an
invalid audience to STS; change wifAudience() to return (string, error) and
return a descriptive error if p.spec.ProjectNumber, pool or provider are empty,
then update callers (notably exchangeToken and any other usages) to handle and
propagate that error (or return it up through Authenticate/Validate) so no empty
audience is ever sent; keep Validate() as-is but treat wifAudience as a
defensive check used by wifAudience(), exchangeToken(), and Authenticate().
- Around line 331-347: hostAllowed currently lowercases host and allowed but
then uses strings.EqualFold redundantly; simplify by lowercasing and trimming
allowed once, then compare using simple equality against the lowercased
u.Hostname() and the lowercased u.Host (if needed). Update the hostAllowed
function: compute host := strings.ToLower(u.Hostname()) and normAllowed :=
strings.ToLower(strings.TrimSpace(allowed)), skip empty normAllowed, and use
direct equality checks (normAllowed == host or normAllowed ==
strings.ToLower(u.Host)) instead of strings.EqualFold to remove redundancy while
preserving correct comparisons for u.Host and host.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dce8d76 and e06f639.

📒 Files selected for processing (1)
  • pkg/auth/providers/gcp_wif/provider.go

Code review cleanup: add doc comment for getTokenFromURL and
adcClientCredentials, add perf.Track to Validate() and Environment().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
pkg/auth/providers/gcp_wif/provider.go (1)

275-281: ⚠️ Potential issue | 🟠 Major

Host validation bypass for env-sourced URLs still needs hardening.

The past review flagged this: skipping host validation entirely when fromEnv=true creates a token exfiltration risk if ACTIONS_ID_TOKEN_REQUEST_URL is poisoned. The bearer token would be sent to an attacker-controlled server.

Consider validating against known GitHub Actions OIDC hosts even for environment-sourced URLs:

🔐 Suggested hardening
 	allowedHosts := ts.AllowedHosts
-	if !fromEnv && len(allowedHosts) > 0 && !hostAllowed(u, allowedHosts) {
+	if fromEnv {
+		host := strings.ToLower(u.Hostname())
+		if host != "token.actions.githubusercontent.com" && !strings.HasSuffix(host, ".actions.githubusercontent.com") {
+			return "", fmt.Errorf("%w: token URL host %q is not a trusted GitHub Actions OIDC host", errUtils.ErrInvalidProviderConfig, u.Hostname())
+		}
+	} else if len(allowedHosts) > 0 && !hostAllowed(u, allowedHosts) {
 		return "", fmt.Errorf("%w: token URL host %q is not allowed; set token_source.allowed_hosts to override", errUtils.ErrInvalidProviderConfig, u.Hostname())
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 275 - 281, The current
logic in provider.go skips host validation when fromEnv is true, which allows
env-sourced ACTIONS_ID_TOKEN_REQUEST_URL to point at attacker-controlled hosts;
instead, keep a whitelist check by augmenting the existing allowedHosts check:
define a small constant list of canonical GitHub Actions OIDC hosts (e.g.,
"actions.githubusercontent.com" and the run-actions subdomain pattern) and when
fromEnv==true validate u (the parsed token URL) against either ts.AllowedHosts
or this known-GHA whitelist using hostAllowed; if neither matches return the
same ErrInvalidProviderConfig error. Update the validation around
allowedHosts/hostAllowed and ensure the error message references u.Hostname() as
before.
🧹 Nitpick comments (1)
pkg/auth/cloud/gcp/setup.go (1)

116-117: Consider using Viper for ATMOS_ prefixed environment variables.

The guideline states that ATMOS_ environment variables should use viper.BindEnv. Currently, these are read directly via os.Getenv. This works but diverges from the Atmos pattern where config flows through Viper → atmosConfig.Settings.

If this function had access to atmosConfig, you'd bind these in cmd/root.go and read from settings. Since this is a lower-level utility, the direct os.Getenv is pragmatic for now, but worth noting for future consistency.

As per coding guidelines: "Environment variables MUST use viper.BindEnv("ATMOS_VAR", "ATMOS_VAR", "FALLBACK") - ATMOS_ prefix required."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup.go` around lines 116 - 117, The code in setup.go
reads ATMOS_GCP_ADC_CLIENT_ID and ATMOS_GCP_ADC_CLIENT_SECRET directly via
os.Getenv into customClientID and customClientSecret, which violates the
project's guideline to use viper.BindEnv for ATMOS_ variables; fix this by
binding those env vars (e.g., viper.BindEnv("ATMOS_GCP_ADC_CLIENT_ID",
"ATMOS_GCP_ADC_CLIENT_ID") and similarly for CLIENT_SECRET) — ideally in
cmd/root.go where atmosConfig is initialized — and then read them via
atmosConfig.Settings or viper.GetString (replace the os.Getenv usages that
populate customClientID and customClientSecret) so the values flow through
Viper/atmosConfig.Settings per project convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/auth/providers/gcp_wif/provider.go`:
- Around line 275-281: The current logic in provider.go skips host validation
when fromEnv is true, which allows env-sourced ACTIONS_ID_TOKEN_REQUEST_URL to
point at attacker-controlled hosts; instead, keep a whitelist check by
augmenting the existing allowedHosts check: define a small constant list of
canonical GitHub Actions OIDC hosts (e.g., "actions.githubusercontent.com" and
the run-actions subdomain pattern) and when fromEnv==true validate u (the parsed
token URL) against either ts.AllowedHosts or this known-GHA whitelist using
hostAllowed; if neither matches return the same ErrInvalidProviderConfig error.
Update the validation around allowedHosts/hostAllowed and ensure the error
message references u.Hostname() as before.

---

Nitpick comments:
In `@pkg/auth/cloud/gcp/setup.go`:
- Around line 116-117: The code in setup.go reads ATMOS_GCP_ADC_CLIENT_ID and
ATMOS_GCP_ADC_CLIENT_SECRET directly via os.Getenv into customClientID and
customClientSecret, which violates the project's guideline to use viper.BindEnv
for ATMOS_ variables; fix this by binding those env vars (e.g.,
viper.BindEnv("ATMOS_GCP_ADC_CLIENT_ID", "ATMOS_GCP_ADC_CLIENT_ID") and
similarly for CLIENT_SECRET) — ideally in cmd/root.go where atmosConfig is
initialized — and then read them via atmosConfig.Settings or viper.GetString
(replace the os.Getenv usages that populate customClientID and
customClientSecret) so the values flow through Viper/atmosConfig.Settings per
project convention.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e06f639 and 438017a.

📒 Files selected for processing (2)
  • pkg/auth/cloud/gcp/setup.go
  • pkg/auth/providers/gcp_wif/provider.go

… whitelist

Previously, when the OIDC token URL came from ACTIONS_ID_TOKEN_REQUEST_URL,
host validation was skipped entirely. This could allow an attacker who
controls that env var to redirect token fetches to arbitrary hosts.

Now env-sourced URLs are validated against known GitHub Actions OIDC hosts
(*.actions.githubusercontent.com). Explicitly configured URLs continue
to use the user-supplied allowed_hosts list.

Also adds //nolint:forbidigo to ATMOS_GCP_ADC_CLIENT_* env reads in
setup.go, matching the established auth package pattern for env vars
read during runtime auth (before Viper/flags are wired).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@shirkevich

Copy link
Copy Markdown
Collaborator Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pkg/auth/providers/gcp_wif/provider_test.go (1)

423-439: ⚠️ Potential issue | 🟠 Major

Test still depends on network; use httptest server instead.

This test sets ACTIONS_ID_TOKEN_REQUEST_URL to a real GitHub domain and relies on the request failing. It'll be flaky in CI and the assertion (NotContains) is weak. Spin up an httptest.NewTLSServer, inject its URL, and assert the exact outcome.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider_test.go` around lines 423 - 439, The test
TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions is flaky because it
hits a real GitHub URL instead of a local test server; replace the external
dependency by creating an httptest.NewTLSServer that returns a deterministic
error/response, set ACTIONS_ID_TOKEN_REQUEST_URL to the test server URL and
ACTIONS_ID_TOKEN_REQUEST_TOKEN accordingly, then call p.getOIDCToken and assert
the exact expected error/HTTP status/response text from the httptest server (not
just that the error doesn't contain "token_source not configured") to make the
test hermetic and deterministic while keeping the test focused on
Provider.getOIDCToken behavior.
🧹 Nitpick comments (2)
pkg/auth/providers/gcp_wif/provider_test.go (1)

173-175: Minor: t.Setenv("") + os.Unsetenv is redundant.

t.Setenv("") sets the var to empty (still exists), then os.Unsetenv removes it. Since you want "not set," just call os.Unsetenv directly. The t.Setenv call here doesn't add value.

This same pattern appears at lines 521-522 and 679-680.

🔧 Suggested simplification
 	// Test missing env var - ensure it's unset for this test.
-	t.Setenv("TEST_OIDC_TOKEN", "")
 	os.Unsetenv("TEST_OIDC_TOKEN")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider_test.go` around lines 173 - 175, Remove
the redundant t.Setenv("") calls and use only os.Unsetenv to ensure the
environment variable is truly unset when testing getTokenFromEnv; specifically,
in the test code around the call to p.getTokenFromEnv(p.spec.TokenSource) (and
the equivalent spots that repeat this pattern), delete the
t.Setenv("TEST_OIDC_TOKEN", "") lines and keep the
os.Unsetenv("TEST_OIDC_TOKEN") so the tests reflect an unset variable when
calling p.getTokenFromEnv.
pkg/auth/providers/gcp_wif/provider.go (1)

295-304: Consider a defensive check when audience cannot be computed.

If wifAudience() returns empty (due to missing ProjectNumber/pool/provider), no audience param gets added. The STS exchange will later fail with a confusing audience mismatch error.

Since Authenticate() validates first, this shouldn't happen in normal flow. But if getTokenFromURL is ever called in isolation (tests, future refactors), an explicit check here could surface the real issue earlier.

🔧 Optional: fail fast on missing audience
 audience := ts.Audience
 if audience == "" && fromEnv {
     audience = p.wifAudience()
+    if audience == "" {
+        return "", fmt.Errorf("%w: cannot compute WIF audience; ensure project_number, pool_id, and provider_id are configured", errUtils.ErrInvalidProviderConfig)
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/providers/gcp_wif/provider.go` around lines 295 - 304, Add a
defensive "fail fast" check when computing the audience in the token URL
construction: after resolving audience (audience := ts.Audience / audience =
p.wifAudience()), if audience is still empty then return an explicit error
indicating missing WIF audience instead of proceeding; update getTokenFromURL
(and callers if needed) to propagate that error so callers like Authenticate()
or isolated tests surface the real configuration problem early. Ensure the error
message references the missing ProjectNumber/pool/provider context to make
debugging easier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/auth/cloud/gcp/setup.go`:
- Line 110: The hardcoded public OAuth secret assigned to defaultClientSecret is
triggering gitleaks; add an explicit gitleaks allow marker to the same line (in
addition to the existing //nolint:gosec) so the scanner recognizes this
intentional public value; update the declaration of defaultClientSecret in
pkg/auth/cloud/gcp/setup.go to include a gitleaks suppression comment (for
example adding "gitleaks:allow" or the repo's accepted allow marker) preserving
the existing explanatory comment.

---

Duplicate comments:
In `@pkg/auth/providers/gcp_wif/provider_test.go`:
- Around line 423-439: The test
TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions is flaky because it
hits a real GitHub URL instead of a local test server; replace the external
dependency by creating an httptest.NewTLSServer that returns a deterministic
error/response, set ACTIONS_ID_TOKEN_REQUEST_URL to the test server URL and
ACTIONS_ID_TOKEN_REQUEST_TOKEN accordingly, then call p.getOIDCToken and assert
the exact expected error/HTTP status/response text from the httptest server (not
just that the error doesn't contain "token_source not configured") to make the
test hermetic and deterministic while keeping the test focused on
Provider.getOIDCToken behavior.

---

Nitpick comments:
In `@pkg/auth/providers/gcp_wif/provider_test.go`:
- Around line 173-175: Remove the redundant t.Setenv("") calls and use only
os.Unsetenv to ensure the environment variable is truly unset when testing
getTokenFromEnv; specifically, in the test code around the call to
p.getTokenFromEnv(p.spec.TokenSource) (and the equivalent spots that repeat this
pattern), delete the t.Setenv("TEST_OIDC_TOKEN", "") lines and keep the
os.Unsetenv("TEST_OIDC_TOKEN") so the tests reflect an unset variable when
calling p.getTokenFromEnv.

In `@pkg/auth/providers/gcp_wif/provider.go`:
- Around line 295-304: Add a defensive "fail fast" check when computing the
audience in the token URL construction: after resolving audience (audience :=
ts.Audience / audience = p.wifAudience()), if audience is still empty then
return an explicit error indicating missing WIF audience instead of proceeding;
update getTokenFromURL (and callers if needed) to propagate that error so
callers like Authenticate() or isolated tests surface the real configuration
problem early. Ensure the error message references the missing
ProjectNumber/pool/provider context to make debugging easier.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 438017a and 5b27edc.

📒 Files selected for processing (3)
  • pkg/auth/cloud/gcp/setup.go
  • pkg/auth/providers/gcp_wif/provider.go
  • pkg/auth/providers/gcp_wif/provider_test.go

Comment thread pkg/auth/cloud/gcp/setup.go Outdated
@codecov

codecov Bot commented Feb 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.54%. Comparing base (ff71e77) to head (b2b6054).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2109      +/-   ##
==========================================
+ Coverage   76.50%   76.54%   +0.04%     
==========================================
  Files         832      832              
  Lines       79391    79407      +16     
==========================================
+ Hits        60736    60783      +47     
+ Misses      14860    14834      -26     
+ Partials     3795     3790       -5     
Flag Coverage Δ
unittests 76.54% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/auth/cloud/gcp/setup.go 80.00% <100.00%> (+1.05%) ⬆️
pkg/auth/providers/gcp_wif/provider.go 94.87% <100.00%> (+2.01%) ⬆️
pkg/store/google_secret_manager_store.go 78.46% <ø> (-0.31%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@osterman Erik Osterman (Cloud Posse) (osterman) added the patch A minor, backward compatible change label Feb 26, 2026
- Add gitleaks:allow marker to public gcloud OAuth secret (setup.go)
- Fail fast when env-sourced OIDC URL cannot construct WIF audience
  (missing project_number/pool/provider), instead of hitting a cryptic
  STS audience mismatch downstream
- Make TestGetOIDCToken_NilTokenSource_AutoDetectsGitHubActions
  hermetic: replace real GitHub URL with local httptest server
- Remove redundant os.Unsetenv calls (t.Setenv provides cleanup)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@shirkevich

Copy link
Copy Markdown
Collaborator Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
pkg/auth/cloud/gcp/setup.go (2)

172-176: Add nolint:forbidigo for consistency.

Lines 116-119 annotate os.Getenv calls with //nolint:forbidigo explaining they're called before Viper is wired. The same rationale applies here for GOOGLE_APPLICATION_CREDENTIALS and CLOUDSDK_CONFIG.

♻️ Proposed fix
 func adcCredentialsPath() string {
+	//nolint:forbidigo // Direct os.Getenv required: called during auth before Viper/flags are wired.
 	if path := strings.TrimSpace(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")); path != "" {
 		return path
 	}
+	//nolint:forbidigo // Direct os.Getenv required: called during auth before Viper/flags are wired.
 	if configDir := strings.TrimSpace(os.Getenv("CLOUDSDK_CONFIG")); configDir != "" {
 		return filepath.Join(configDir, "application_default_credentials.json")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup.go` around lines 172 - 176, In adcCredentialsPath(),
add the same `//nolint:forbidigo` annotations used earlier for os.Getenv to the
two getenv calls that read "GOOGLE_APPLICATION_CREDENTIALS" and
"CLOUDSDK_CONFIG" so the linter knows these environment reads occur before Viper
is wired; update the lines that call os.Getenv in the adcCredentialsPath
function to include `//nolint:forbidigo` immediately after the call sites to
maintain consistency with the other getenv annotations.

101-148: Unused error return value.

The function signature includes an error return but all paths return nil. Consider either simplifying to (string, string) or adding a brief comment indicating the error is reserved for future validation.

Not blocking—current code is functionally correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/auth/cloud/gcp/setup.go` around lines 101 - 148, The
resolveADCClientCredentials function currently declares an error return but
never returns a non-nil error; remove the unused error from the signature and
return only (string, string) from resolveADCClientCredentials (update the
function declaration and all call sites to accept two return values), or
alternatively keep the error in the signature but add a brief comment above
resolveADCClientCredentials explaining the error slot is reserved for future
validation; prefer the first option (drop the error) for clarity and adjust
callers accordingly (e.g., any code currently doing "id, secret, err :=
resolveADCClientCredentials()" must be changed to "id, secret :=
resolveADCClientCredentials()").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/auth/cloud/gcp/setup.go`:
- Around line 172-176: In adcCredentialsPath(), add the same
`//nolint:forbidigo` annotations used earlier for os.Getenv to the two getenv
calls that read "GOOGLE_APPLICATION_CREDENTIALS" and "CLOUDSDK_CONFIG" so the
linter knows these environment reads occur before Viper is wired; update the
lines that call os.Getenv in the adcCredentialsPath function to include
`//nolint:forbidigo` immediately after the call sites to maintain consistency
with the other getenv annotations.
- Around line 101-148: The resolveADCClientCredentials function currently
declares an error return but never returns a non-nil error; remove the unused
error from the signature and return only (string, string) from
resolveADCClientCredentials (update the function declaration and all call sites
to accept two return values), or alternatively keep the error in the signature
but add a brief comment above resolveADCClientCredentials explaining the error
slot is reserved for future validation; prefer the first option (drop the error)
for clarity and adjust callers accordingly (e.g., any code currently doing "id,
secret, err := resolveADCClientCredentials()" must be changed to "id, secret :=
resolveADCClientCredentials()").

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5b27edc and 1ef4ba2.

📒 Files selected for processing (3)
  • pkg/auth/cloud/gcp/setup.go
  • pkg/auth/providers/gcp_wif/provider.go
  • pkg/auth/providers/gcp_wif/provider_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/auth/providers/gcp_wif/provider.go

…ials

The function always returns a valid credential pair (falling back to
public gcloud defaults), so the error return was dead code. Removing
it simplifies the caller in SetupFiles and the three test call sites.

Also adds //nolint:forbidigo to os.Getenv calls in adcCredentialsPath()
for consistency with the ATMOS_GCP_ADC_* env reads above.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@shirkevich

Copy link
Copy Markdown
Collaborator Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@shirkevich Mikhail Shirkov (shirkevich) changed the title fix(auth): defer GSM client creation and treat ADC credentials as atomic pair fix(auth): auto-detect GitHub Actions WIF with proper audience, host validation, and lazy GSM init Feb 27, 2026
…t mock

gsmClientSecretCreationMock was ignoring its projectID parameter and
hardcoding "test-project". All callers happened to pass "test-project"
so this was latent, but the mock should respect its contract.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 27, 2026
In GitHub Actions, token_source is now auto-detected from
ACTIONS_ID_TOKEN_REQUEST_URL. The audience is auto-constructed
from project_number/pool_id/provider_id, and the token URL is
validated against known GitHub Actions OIDC hosts.

Updated providers.mdx to show the simplified config (no
token_source block needed) and documented all auto-detection
behavior. Updated blog post to split ADC and WIF examples and
explain the zero-config GitHub Actions experience.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@aknysh Andriy Knysh (aknysh) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aknysh
Andriy Knysh (aknysh) merged commit 5009216 into cloudposse:main Feb 27, 2026
56 checks passed
@github-actions

github-actions Bot commented Mar 3, 2026

Copy link
Copy Markdown

These changes were released in v1.208.0.

Igor Rodionov (goruha) added a commit that referenced this pull request Mar 9, 2026
…raform-plan

* osterman/native-ci-terraform: (28 commits)
  feat: Add source cache TTL for JIT-vendored components (#2138)
  feat: Per-target version overrides in vendor manifests (#2141)
  docs: Add PRD for browser-based auth in aws/user identity (#1887)
  docs: Add EKS kubeconfig authentication integration PRD (#1884)
  fix: correct marketplace.json schema and update docs with install/uninstall commands (#2142)
  fix: propagate auth to all YAML functions in multi-component execution (#2140)
  fix: Use atmos_component for source provisioner workdir paths (#2137)
  Fix identity prompts to respect --interactive flag (#2130)
  Increase PR size thresholds to accommodate AI-assisted development (#2136)
  docs: Add Azure authentication provider documentation (#2132)
  fix: propagate component-type level dependencies through stack processor (#2127)
  fix: Add retry and missing workflow step properties to all schema copies (#2113)
  Exclude unsupported windows/arm from goreleaser build matrix (#2133)
  Add AI Agent Skills for LLM-Powered Infrastructure Development (#2121)
  Fix: Convert toolchain paths to absolute in PATH to resolve exec.LookPath failures (#2095)
  Fix workdir collision for component instances sharing base component (#2093)
  fix(auth): propagate TTY state to subprocesses for SSO device flow in workflows (#2126)
  fix(security): prevent SSRF in GitHub OIDC token URL handling (CWE-918) (#2106)
  Fix #2112: add workflow_retry definition and retry property to workflow step schema (#2114)
  fix(auth): auto-detect GitHub Actions WIF with proper audience, host validation, and lazy GSM init (#2109)
  ...
@github-actions

github-actions Bot commented Mar 9, 2026

Copy link
Copy Markdown

These changes were released in v1.208.1-test.9.

@github-actions

github-actions Bot commented Mar 9, 2026

Copy link
Copy Markdown

These changes were released in v1.208.1-test.10.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch A minor, backward compatible change size/l Large size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants