This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-connectors.git


The following commit(s) were added to refs/heads/main by this push:
     new ae7c53315 Google: close the credentials file when parsing it fails 
(#1928)
ae7c53315 is described below

commit ae7c53315a8b2faf9b4908cff836b24573813847
Author: PJ Fanning <[email protected]>
AuthorDate: Sun Sep 6 22:51:20 2026 +0100

    Google: close the credentials file when parsing it fails (#1928)
    
    * Google: close the credentials file when parsing it fails
    
    Neither ServiceAccountCredentials nor UserAccessCredentials closed the file 
in
    a finally, so a parse failure leaked the descriptor. That is the routine 
path:
    the application-default provider tries the service account parser first, 
and on
    a machine set up with `gcloud auth application-default login` the well-known
    file is an authorized user file, so the first parse always throws before
    falling through to the parser that can read it.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * Update 
google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
    
    Co-authored-by: Philippus Baalman <[email protected]>
    
    * Update 
google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
    
    Co-authored-by: Philippus Baalman <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Philippus Baalman <[email protected]>
---
 .../google/auth/ServiceAccountCredentials.scala    |  5 +-
 .../google/auth/UserAccessCredentials.scala        |  5 +-
 .../connectors/google/auth/CredentialsSpec.scala   | 75 ++++++++++++++++++++++
 3 files changed, 81 insertions(+), 4 deletions(-)

diff --git 
a/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/ServiceAccountCredentials.scala
 
b/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/ServiceAccountCredentials.scala
index b96c8bced..8792f4620 100644
--- 
a/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/ServiceAccountCredentials.scala
+++ 
b/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/ServiceAccountCredentials.scala
@@ -42,8 +42,9 @@ private[connectors] object ServiceAccountCredentials {
           c.getString("private-key"))
       } else {
         val src = Source.fromFile(c.getString("path"))
-        val credentials = 
JsonParser(src.mkString).convertTo[ServiceAccountCredentialsFile]
-        src.close()
+        val credentials =
+          try JsonParser(src.mkString).convertTo[ServiceAccountCredentialsFile]
+          finally src.close()
         (credentials.project_id, credentials.client_email, 
credentials.private_key)
       }
     }
diff --git 
a/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/UserAccessCredentials.scala
 
b/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/UserAccessCredentials.scala
index 102690cc7..939b0bbd6 100644
--- 
a/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/UserAccessCredentials.scala
+++ 
b/google-common/src/main/scala/org/apache/pekko/stream/connectors/google/auth/UserAccessCredentials.scala
@@ -46,8 +46,9 @@ private[connectors] object UserAccessCredentials {
         projectId = c.getString("project-id"))
     } else {
       val src = Source.fromFile(c.getString("path"))
-      val credentials = 
JsonParser(src.mkString).convertTo[UserAccessCredentialsFile]
-      src.close()
+      val credentials =
+        try JsonParser(src.mkString).convertTo[UserAccessCredentialsFile]
+        finally src.close()
       apply(
         clientId = credentials.client_id,
         clientSecret = credentials.client_secret,
diff --git 
a/google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
 
b/google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
index c6bda3783..23c7ec408 100644
--- 
a/google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
+++ 
b/google-common/src/test/scala/org/apache/pekko/stream/connectors/google/auth/CredentialsSpec.scala
@@ -25,6 +25,9 @@ import org.scalatest.BeforeAndAfterAll
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.wordspec.AnyWordSpecLike
 
+import java.nio.charset.StandardCharsets.UTF_8
+import java.nio.file.{ Files, Path }
+
 class CredentialsSpec
     extends TestKit(ActorSystem("CredentialsSpec"))
     with AnyWordSpecLike
@@ -36,6 +39,78 @@ class CredentialsSpec
     super.afterAll()
   }
 
+  private def withJsonFile(contents: String)(test: String => Unit): Unit = {
+    val file = Files.createTempFile("pekko-connectors-credentials", ".json")
+    Files.write(file, contents.getBytes(UTF_8))
+    try test(hoconPath(file))
+    finally Files.deleteIfExists(file): Unit
+  }
+
+  // backslashes are escapes in a HOCON quoted string, and Java accepts 
forward slashes on Windows too
+  private def hoconPath(file: Path): String = 
file.toAbsolutePath.toString.replace('\\', '/')
+
+  private val serviceAccountJson =
+    
"""{"project_id":"a-project","client_email":"[email protected]","private_key":"a-key"}"""
+
+  private val userAccessJson =
+    
"""{"client_id":"an-id","client_secret":"a-secret","refresh_token":"a-token","quota_project_id":"a-project"}"""
+
+  "ServiceAccountCredentials" should {
+
+    "read a service account file" in withJsonFile(serviceAccountJson) { path =>
+      val config = ConfigFactory.parseString(s"""
+           |project-id = ""
+           |client-email = ""
+           |private-key = ""
+           |path = "$path"
+        """.stripMargin)
+
+      ServiceAccountCredentials(config, Set("a-scope")).projectId shouldBe 
"a-project"
+    }
+
+    "propagate the failure, having closed the file, when the file is not a 
service account" in
+    withJsonFile(userAccessJson) { path =>
+      val config = ConfigFactory.parseString(s"""
+           |project-id = ""
+           |client-email = ""
+           |private-key = ""
+           |path = "$path"
+        """.stripMargin)
+
+      // this is the routine case: `gcloud auth application-default login` 
writes a user access file,
+      // and the application-default provider tries the service account parser 
against it first
+      an[Exception] should be thrownBy ServiceAccountCredentials(config, 
Set("a-scope"))
+    }
+  }
+
+  "UserAccessCredentials" should {
+
+    "read a user access file" in withJsonFile(userAccessJson) { path =>
+      val config = ConfigFactory.parseString(s"""
+           |project-id = ""
+           |client-id = ""
+           |client-secret = ""
+           |refresh-token = ""
+           |path = "$path"
+        """.stripMargin)
+
+      UserAccessCredentials(config).projectId shouldBe "a-project"
+    }
+
+    "propagate the failure, having closed the file, when the file is not a 
user access file" in
+    withJsonFile(serviceAccountJson) { path =>
+      val config = ConfigFactory.parseString(s"""
+           |project-id = ""
+           |client-id = ""
+           |client-secret = ""
+           |refresh-token = ""
+           |path = "$path"
+        """.stripMargin)
+
+      an[Exception] should be thrownBy UserAccessCredentials(config)
+    }
+  }
+
   "Credentials" should {
 
     "parse 'none' provider" in {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to