Skip to content

Multi-feed-per-host with mixed valid + stale settings.xml entries: dropped feed inherits other feed's PAT (host-keyed credMap) #7

Description

@clee704

Summary

When ~/.m2/settings.xml has multiple <server> entries that resolve to the same host, and one PAT is valid while another is stale, dropping the stale entry doesn't produce an Entra fallback for that feed. The plugin's credMap is keyed by host, so the still-valid entry occupies the host slot and the dropped feed inherits its credentials. Fetches against the dropped feed will 401 because the valid PAT isn't scoped to it.

Found during PR #6 self-review (round 4) — pre-existing latent bug that v0.0.10's probe-then-drop behavior makes user-visible with a misleading "falling through to Entra token acquisition" log.

Reproducer

~/.m2/settings.xml:

<server><id>FeedA</id><username>azure</username><password>VALID-pat-for-FeedA</password></server>
<server><id>FeedB</id><username>azure</username><password>STALE-pat</password></server>

build.sbt:

resolvers += "FeedA" at "https://pkgs.dev.azure.com/myorg/_packaging/FeedA/maven/v1"
resolvers += "FeedB" at "https://pkgs.dev.azure.com/myorg/_packaging/FeedB/maven/v1"
addSbtPlugin("dev.chungmin" % "sbt-azure-devops-credentials" % "0.0.10")

sbt update:

  • Probe of FeedA → 200 → kept as Credentials("realm", "pkgs.dev.azure.com", "azure", VALID-pat)
  • Probe of FeedB → 401 → dropped → buildCredentialsFromMavenSettings returns None for FeedB
  • Log: [info] pkgs.dev.azure.com: settings.xml credentials returned 401; falling through to Entra token acquisition.misleading

But:

  • buildCredentialsWithAccessToken: credMap = {pkgs.dev.azure.com -> FeedA cred}; for FeedB resolver, credMap.contains(host) is true → no Entra cred generated
  • updateCoursierConf: looks up FeedB resolver's host → finds FeedA's cred → registers Authentication(azure, VALID-pat) against resolver id FeedB
  • Coursier sends FeedA's PAT when fetching from FeedB → feed returns 401 → user sees confusing failure

Why the bug is pre-existing, not introduced by 0.0.10

Even on 0.0.9 with the same shape (FeedA settings entry, no FeedB entry), credMap = {host -> FeedA cred} and FeedB ends up with FeedA's PAT applied. The host-keying is sbt/Ivy's native credential matching model — Credentials carries a host, not a resolver name. The plugin's credMap reflects that.

0.0.10 didn't introduce the bug; it just made the user-visible failure mode worse by adding a misleading "falling through to Entra" log over the same broken substrate.

Affected code paths

AzureDevOpsCredentialsPlugin.scala in master:

  1. buildCredentialsWithAccessToken:

    val credMap = existingCredentials.collect {
      case credential: DirectCredentials =>
        credential.host -> credential                   // ← host key
    }.toMap
    resolvers.collect {
      case repo: MavenRepo =>
        val host = uri.getHost
        else { None }                                   // ← host-key collision skips Entra gen
    }
  2. updateCoursierConf — same credMap keying and same credMap.get(host) lookup, then registers repo.name -> Authentication(...) with the host-matched credential.

Proposed fix

Switch from host-keyed to per-resolver-keyed credential matching, OR maintain a separate (host, resolverName) index so dropped-settings entries don't shadow each other via the host slot. Concrete design needs to decide:

  • Coursier-only support? The clean fix lives in authenticationByRepositoryId (per-repo.name). Ivy matches by host and can't carry two credentials for the same host with reliable precedence.
  • Per-resolver Entra cred generation? A fresh Entra token applied to N resolvers on the same host needs N Credentials instances each tagged with the right host (and ideally per-resolver Coursier auth entry).
  • Backward compat? Existing users relying on the host-keyed precedence (one settings entry → shared across same-host resolvers) shouldn't break.

Reasonable v1 scope: fix the Coursier path, document the Ivy limitation, leave the existing host-key behavior as the default fallback when settings entries are valid.

Current workarounds (documented in README "Limitations")

  1. Consolidate to one PAT per host scoped to all your feeds (works with current code).
  2. Set azureDevOpsValidateExistingCredentials := "never" to disable the probe — pre-0.0.10 "trust both, let fetch fail on the stale one" surfaces a clean 401 instead of the misleading "falling through" log.
  3. Use legacy <org>.pkgs.visualstudio.com URLs so different orgs land on different hostnames (within-org multi-feed still collides).

Repro test (would land with the fix)

it should "generate per-resolver Entra creds when a sibling resolver has a valid settings entry" in {
  // FeedA: valid settings entry; FeedB: stale settings entry (dropped by probe).
  // After buildCredentialsWithAccessToken: assert FeedB gets an Entra cred,
  // not silently inheriting FeedA's PAT.
  // After updateCoursierConf: authenticationByRepositoryId("FeedB") must use
  // the Entra token, not FeedA's PAT.
}

Severity

  • User-visible impact: build fails with 401 on the dropped feed; log says "falling through to Entra" so user thinks plugin handled it.
  • Real-world frequency: uncommon. Most ADO users have one consolidated PAT or no settings entries.
  • Discovered via: PR feat: probe stale settings.xml entries and fall through to Entra #6 self-review round 4 by an LLM reviewer cross-checking the per-URI cache fix's downstream coverage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions