Skip to content

Same repository gets mirrored, fetched and diffed twice when callers vary path casing #24

Description

@matt-edmondson

What's wrong

The repository allow-list is deliberately case-insensitive. RepositoryPattern.Translate compiles every configured pattern with RegexOptions.IgnoreCase, and the type's own remarks say why: "Matching is case insensitive because forge repository names are, and a pattern that fails only because someone typed Studio is a support ticket rather than a control." So for a configured pattern studio/game.git, IRepositoryAllowList.IsAllowed("github", "Studio/Game.git") and IsAllowed("github", "studio/game.git") both return true.

However, every identity derived downstream from that same client-supplied repositoryPath (taken verbatim from the request URL by StateRouteParser.Parse) is case-sensitive:

  • MirrorStore.TryResolve builds the on-disk mirror directory by Path.Combine-ing the path segments verbatim, so on a case-sensitive filesystem (this ships as a Linux container per the README/Dockerfile) Studio/Game.git and studio/game.git resolve to two different mirror.git directories.
  • MirrorKey.ToFlightKey() ($"{Upstream}\n{RepositoryPath}") is used both for SingleFlight clone/fetch coalescing in MirrorFetcher, and — via repository.Key.ToFlightKey() in BranchStateHandler.ComputeAsync — as the Repository component of DiffKey, so the two spellings never share fetch coalescing or a DiffCache entry either.
  • CredentialAdmission.Key hashes $"{upstream}\n{repositoryPath}\n{authorization}" (ordinal), so the two spellings are admitted (and separately probed with git ls-remote) independently too.

Concrete failure scenario

Two callers (or the same caller at different times — e.g. a client whose local remote URL preserves whatever casing was used when the repository was cloned) address the same upstream repository as Studio/Game.git and studio/game.git. Both pass the allow-list because it treats them as equivalent, but the service ends up: cloning and keeping two full bare mirrors of the same repository on disk, fetching it twice on every polling heartbeat, computing and caching branch diffs twice, and probing ls-remote admission twice. This silently doubles disk, network and CPU cost for that repository and defeats the service's central design goal stated in the README — "does that work once, centrally, for many clients" — for any deployment where callers don't consistently use one casing.

Suggested fix / acceptance criteria

  • Normalize repositoryPath to one canonical case immediately after the allow-list check succeeds in BranchStateHandler.Resolve, before it is used to build the mirror directory, the SingleFlight key, the DiffKey, or the admission key — so every downstream identity is derived from the canonical form instead of the caller's literal spelling.
  • Add a test asserting that two requests for the same repository differing only in path casing resolve to the same mirror directory, share one fetch/coalescing key, and share one DiffCache entry.

Activity

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions