Fall back to anonymous REST when SSO blocks actions/* resolution by nodeselector · Pull Request #75 · github/gh-actions-lock · GitHub
Skip to content

Fall back to anonymous REST when SSO blocks actions/* resolution - #75

Merged
nodeselector merged 7 commits into
mainfrom
nodeselector-sso-fallback-anonymous-auth
Jun 30, 2026
Merged

nodeselector merged 7 commits into
mainfrom
nodeselector-sso-fallback-anonymous-auth

Conversation

@nodeselector

Copy link
Copy Markdown
Collaborator

Motivation

In Codespaces (and similar environments), the GITHUB_TOKEN cannot be SSO-authorized for cross-enterprise orgs like actions. This means every API call to resolve actions/checkout, actions/setup-go, etc. fails with a SAML 403 -- even though those repos are fully public. The tool becomes unusable for any workflow that references actions/*.

Approach

When a SAML enforcement error is detected for a given org, probe that org's public visibility with an unauthenticated HEAD /orgs/{owner} request. If the org is publicly visible, retry the failed operation anonymously (no Authorization header). This covers:

  • GraphQL resolution (ResolveActionFiles) -- the primary ref-to-SHA + action.yml fetch
  • REST endpoints -- ListBranches, ListTags, CompareCommits, Releases, CommitSHA, repoMetadata
  • GraphQL reachability (BatchBranchContains) -- falls back to per-branch anonymous REST compare
  • GraphQL peel (PeelTagObject) -- returns zero result on SAML (callers already handle gracefully)

The eligibility probe result is cached in a sync.Map for the process lifetime, so each org is only probed once.

Key design decisions

  • No hardcoded org list. Eligibility is determined dynamically via an anonymous probe, so any public org with SAML enforcement gets the fallback automatically.
  • anonHTTP client on Client struct. Respects GH_ACTIONS_LOCK_INSECURE for integration tests (TLS skip), and anonBase() skips the api. prefix for IP:port hostnames used by the stub server.
  • Surgical error matching. Only triggers on HTTP 403 with "SAML enforcement" in the error message -- won't interfere with rate limits or other 403s.

Testing

  • Unit tests: TestSSOFallbackEligible, TestResolveActionFiles_SSOFallbackForActionsOrg, TestResolveActionFiles_SSONoFallbackForNonActionsOrg
  • Integration scenario: sso_anon_fallback -- stub returns SAML 403 for auth'd requests, 200 for anonymous; validates the full pipeline resolves successfully
  • Verified live against api.github.com that anonymous resolution of actions/checkout@v4 returns the correct SHA and action.yml content

When the authenticated GraphQL path hits SAML enforcement for the
actions org, retry those refs using unauthenticated REST. The actions/*
repos are public so anonymous access works, avoiding hard failures in
environments like Codespaces where the token can't be SSO-authorized
for the cross-enterprise actions org.
The initial GraphQL-only fallback missed REST endpoints (ListBranches,
ListTags, CompareCommits, Releases, CommitSHA, repoMetadata) and the
GraphQL reachability/peel queries. All of these fail identically when
the token is SAML-blocked for the actions org.

Now any REST 403 that matches SAML enforcement for an SSO-fallback-
eligible owner retries the same endpoint anonymously. GraphQL
BatchBranchContains falls back to per-branch anonymous REST compare
calls. PeelTagObject returns a zero result (callers already handle
that gracefully).
Instead of maintaining a static map of eligible orgs, probe the org
with an unauthenticated HEAD /orgs/{owner} request. If it returns 200
the org is publicly visible and anonymous fallback will work. Results
are cached in a sync.Map for the process lifetime.
Stub server returns SAML 403 for authenticated requests (Authorization
header present) and 200 with synthetic data for anonymous requests.
Validates the full fallback pipeline: GraphQL fails, probe confirms
org is public, anonymous REST resolves the action successfully.

Also wires the anon HTTP client through the Client struct so it
respects GH_ACTIONS_LOCK_INSECURE (TLS skip) in integration tests,
and makes anonBase() skip the 'api.' prefix for IP:port hostnames
used by the stub server.
GitHub Advanced Security started work on behalf of nodeselector June 30, 2026 15:53
GitHub Advanced Security finished work on behalf of nodeselector June 30, 2026 15:53
@nodeselector
nodeselector marked this pull request as ready for review June 30, 2026 15:54
Copilot AI review requested due to automatic review settings June 30, 2026 15:54
IsSAMLEnforcement was requiring api.HTTPError (REST 403) but the GraphQL
resolution path produces plain errors via ssoRequiredMessage. Now it also
matches plain errors whose message contains SAML enforcement indicators.

Also fixed retrySSOWithAnonymous to use IsSAMLEnforcement instead of a
hardcoded string check for 'SSO authorization required', which missed the
'SAML enforcement' variant from real GitHub responses.

Removed trailing newline in anon_fallback_test.go that failed gofmt.
GitHub Advanced Security started work on behalf of nodeselector June 30, 2026 15:59
GitHub Advanced Security finished work on behalf of nodeselector June 30, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Not ready to approve

There are a few correctness/operational issues in the new fallback implementation (cached probe behavior, URL path escaping, caching consistency, and error signaling) that should be addressed before merging.

Pull request overview

This PR adds an anonymous REST fallback path when authenticated GitHub API access is blocked by organization SAML/SSO enforcement (notably affecting public actions/* repos in Codespaces-like environments), restoring the ability to resolve and pin actions/* references.

Changes:

  • Add an anonymous probe + REST request helpers to retry certain SAML-blocked operations without an Authorization header when the owner is publicly visible.
  • Wire fallback behavior into GraphQL action-file resolution and several REST-backed endpoints (branches/tags/compare/metadata/releases/commit SHA), plus GraphQL reachability/peel behavior.
  • Add unit + integration coverage via a new stubbed scenario (sso_anon_fallback) and targeted tests for eligibility and resolution behavior.
File summaries
File Description
test/scenarios/catalog.yml Adds a new integration scenario entry validating SSO→anonymous fallback behavior.
test/integration/run.rb Implements stub-server behavior for the new SSO anonymous fallback scenario.
internal/ghapi/tagsource.go Adds anonymous REST retry for releases listing and commit SHA resolution when SAML-blocked.
internal/ghapi/repos.go Adds anonymous REST retry for branches/tags listing, repo metadata fetch, and compare-commits logic.
internal/ghapi/graphql_reachability.go Adds REST compare fallback when GraphQL reachability is SAML-blocked.
internal/ghapi/graphql_peel.go Treats SAML-blocked peel queries as non-fatal (returns zero result).
internal/ghapi/graphql_action_files.go Retries per-ref SSO failures via anonymous REST for fallback-eligible owners.
internal/ghapi/client.go Adds an anonymous HTTP client on the ghapi Client (honors insecure TLS in integration tests).
internal/ghapi/anon_fallback.go Introduces eligibility probing, SAML detection helper, and anonymous REST helper methods.
internal/ghapi/anon_fallback_test.go Adds unit tests covering eligibility probing and ResolveActionFiles anonymous fallback behavior.

Review details

  • Files reviewed: 10/10 changed files
  • Comments generated: 4
  • Review effort level: Low

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/ghapi/anon_fallback.go
Comment thread internal/ghapi/anon_fallback.go
Comment thread internal/ghapi/repos.go
Comment thread internal/ghapi/graphql_reachability.go
Instead of returning a zero result when SAML blocks the GraphQL peel,
actually call the REST git/tags and git/commits endpoints anonymously
to determine object type and peel annotated tags to their commit.
GitHub Advanced Security started work on behalf of nodeselector June 30, 2026 16:02
GitHub Advanced Security finished work on behalf of nodeselector June 30, 2026 16:02
- Don't cache false on transport/construction errors in SSOFallbackEligible;
  only cache after receiving an HTTP response so transient failures don't
  permanently disable fallback.
- Escape file path segments in anonGetFileContent to handle special chars.
- Populate branchListCache with anonymous results so singleflight/caching
  still works on the fallback path.
- Return error from anonBatchBranchContains when all compare calls fail
  instead of silently returning 'no matches'.
GitHub Advanced Security started work on behalf of nodeselector June 30, 2026 16:06
GitHub Advanced Security finished work on behalf of nodeselector June 30, 2026 16:07
@nodeselector
nodeselector merged commit 72db63e into main Jun 30, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants