mirror of https://github.com/sbt/sbt.git
[2.x] fix: Preserve '=' in remote cache header values (#9534)
**Problem**
`remoteCacheHeaders` entries are parsed in GrpcActionCacheStore.AuthCallCredentials
with h.split("="), which splits on every =. Java's split discards trailing empty
strings, so a Basic auth header such as authorization=Basic dXNlcjpwdw== produces
exactly two elements and matches List(k, v) with the base64 padding silently removed.
The truncated credential is rejected by the cache server with UNAUTHENTICATED.
**Solution**
Split on the first = only, keeping the remainder of the string verbatim as the header
value. The error case narrows to a header containing no = at all.
Generated-by: Claude Opus 5
This commit is contained in:
parent
bb141109e1
commit
56c3633874
|
|
@ -142,9 +142,12 @@ object GrpcActionCacheStore:
|
|||
|
||||
class AuthCallCredentials(remoteHeaders: List[String]) extends CallCredentials:
|
||||
val pairs = remoteHeaders.map: h =>
|
||||
h.split("=").toList match
|
||||
// Split on the first '=' only. Splitting on every '=' would drop trailing
|
||||
// padding from values such as Basic auth credentials ("Basic dXNlcjpwdw==")
|
||||
// and reject values that legitimately contain '='.
|
||||
h.split("=", 2).toList match
|
||||
case List(k, v) => Metadata.Key.of(k, Metadata.ASCII_STRING_MARSHALLER) -> v
|
||||
case _ => sys.error("remote header must contain one '='")
|
||||
case _ => sys.error("remote header must contain '='")
|
||||
override def applyRequestMetadata(
|
||||
requestInfo: CallCredentials.RequestInfo,
|
||||
executor: java.util.concurrent.Executor,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ object GrpcActionCacheStoreTest extends verify.BasicTestSuite:
|
|||
// Distinct Deadline instances derived at call time, not a single shared frozen one.
|
||||
assert(!deadline1.eq(deadline2))
|
||||
|
||||
// Regression test: header values may legitimately contain '=' -- Basic auth credentials
|
||||
// end in base64 padding. Splitting on every '=' silently truncated the value, so the
|
||||
// server rejected the credential with UNAUTHENTICATED while the build still succeeded,
|
||||
// leaving the cache permanently empty with no error reported.
|
||||
test("header values retain '=' such as base64 padding"):
|
||||
val twoPad = GrpcActionCacheStore.AuthCallCredentials(List("authorization=Basic dXNlcjpwdw=="))
|
||||
val (key, value) = twoPad.pairs.head
|
||||
assert(key.name == "authorization")
|
||||
assert(value == "Basic dXNlcjpwdw==")
|
||||
|
||||
val onePad = GrpcActionCacheStore.AuthCallCredentials(List("authorization=Basic dXNlcjpwdzE="))
|
||||
assert(onePad.pairs.head._2 == "Basic dXNlcjpwdzE=")
|
||||
|
||||
// An interior '=' is part of the value, not a second separator.
|
||||
val interior = GrpcActionCacheStore.AuthCallCredentials(List("x-api-key=ab=cd"))
|
||||
assert(interior.pairs.head._2 == "ab=cd")
|
||||
|
||||
// No '=' at all remains an error.
|
||||
intercept[RuntimeException]:
|
||||
GrpcActionCacheStore.AuthCallCredentials(List("bogus")).pairs
|
||||
|
||||
private def newStore(): GrpcActionCacheStore =
|
||||
val base = Files.createTempDirectory("grpc-action-cache-test")
|
||||
val disk = DiskActionCacheStore(base, PlainVirtualFileConverter.converter)
|
||||
|
|
|
|||
Loading…
Reference in New Issue