From 27f202820572924c6a7cab3693df1340f140debb Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 17 Sep 2026 10:58:57 +0100 Subject: [PATCH] Look up Podman credentials in the config home and honour DOCKER_CONFIG podman login on macOS writes to ~/.config/containers/auth.json and never sets XDG_RUNTIME_DIR, so those credentials were never found. Fixes https://github.com/roastedroot/inlay/issues/4 --- src/main/java/land/oras/auth/AuthStore.java | 32 +++++- .../java/land/oras/auth/AuthStoreTest.java | 104 ++++++++++++++++++ 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/main/java/land/oras/auth/AuthStore.java b/src/main/java/land/oras/auth/AuthStore.java index d764e7cb..f4fd67fc 100644 --- a/src/main/java/land/oras/auth/AuthStore.java +++ b/src/main/java/land/oras/auth/AuthStore.java @@ -109,7 +109,7 @@ public static AuthStore newStore(List configPaths) { /** * Creates a new FileStore from default location. * If the {@code REGISTRY_AUTH_FILE} environment variable is set it is used exclusively. - * Otherwise, the Docker config and (when {@code XDG_RUNTIME_DIR} is set) the Podman auth file are searched. + * Otherwise, the Docker config and the Podman auth files are searched. * * @return FileStore instance. */ @@ -123,19 +123,39 @@ public static AuthStore newStore() { } /** - * Returns the ordered list of auth file paths to search when no {@code REGISTRY_AUTH_FILE} is set. - * Docker config is always included; the Podman auth file is added when {@code XDG_RUNTIME_DIR} is set. + * Returns the ordered list of auth file paths to search when no {@code REGISTRY_AUTH_FILE} is set: + * the Docker config, the Podman config home auth file and, when {@code XDG_RUNTIME_DIR} is set, + * the Podman runtime auth file. Later files take precedence over earlier ones. * * @return list of candidate paths. */ private static List defaultAuthPaths() { - Path dockerPath = Path.of(System.getProperty("user.home"), ".docker", "config.json"); + List paths = new ArrayList<>(); + paths.add(dockerConfigDir().resolve("config.json")); + // https://github.com/containers/image/blob/main/pkg/docker/config/config.go + paths.add(xdgConfigHome().resolve("containers").resolve("auth.json")); String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); if (xdgRuntimeDir != null) { // https://docs.podman.io/en/stable/markdown/podman-login.1.html#description - return List.of(dockerPath, Path.of(xdgRuntimeDir, "containers", "auth.json")); + paths.add(Path.of(xdgRuntimeDir, "containers", "auth.json")); } - return List.of(dockerPath); + return paths; + } + + private static Path dockerConfigDir() { + String dockerConfig = System.getenv("DOCKER_CONFIG"); + if (dockerConfig != null && !dockerConfig.isEmpty()) { + return Path.of(dockerConfig); + } + return Path.of(System.getProperty("user.home"), ".docker"); + } + + private static Path xdgConfigHome() { + String xdgConfigHome = System.getenv("XDG_CONFIG_HOME"); + if (xdgConfigHome != null && !xdgConfigHome.isEmpty()) { + return Path.of(xdgConfigHome); + } + return Path.of(System.getProperty("user.home"), ".config"); } /** diff --git a/src/test/java/land/oras/auth/AuthStoreTest.java b/src/test/java/land/oras/auth/AuthStoreTest.java index 3b46fcf6..ebada222 100644 --- a/src/test/java/land/oras/auth/AuthStoreTest.java +++ b/src/test/java/land/oras/auth/AuthStoreTest.java @@ -51,6 +51,15 @@ class AuthStoreTest { @TempDir private static Path xdgRuntimeDir; + @TempDir + private static Path xdgConfigHome; + + @TempDir + private static Path podmanHomeDir; + + @TempDir + private static Path dockerConfigDir; + private AuthStore authStore; private AuthStore.Config mockConfig; private AuthStore.Credential mockCredential; @@ -110,6 +119,18 @@ static void init() throws Exception { Files.createDirectory(xdgRuntimeDir.resolve("containers")); Files.writeString(xdgRuntimeDir.resolve("containers").resolve("auth.json"), SAMPLE_PODMAN_CONFIG); + // Write a sample Podman config file in the XDG config home + Files.createDirectory(xdgConfigHome.resolve("containers")); + Files.writeString(xdgConfigHome.resolve("containers").resolve("auth.json"), SAMPLE_PODMAN_CONFIG); + + // Write a sample Podman config file below a home directory, as podman does on macOS + Files.createDirectories(podmanHomeDir.resolve(".config").resolve("containers")); + Files.writeString( + podmanHomeDir.resolve(".config").resolve("containers").resolve("auth.json"), SAMPLE_PODMAN_CONFIG); + + // Write a sample Docker config file in a DOCKER_CONFIG directory + Files.writeString(dockerConfigDir.resolve("config.json"), SAMPLE_DOCKER_CONFIG); + Path helper = Path.of("docker-credential-fake"); String newPath = helper.toAbsolutePath().getParent() + System.getProperty("path.separator") + System.getenv("PATH"); @@ -129,6 +150,8 @@ void testShouldReadCredentialsFromCredentialHelperNullCheck() throws Exception { new EnvironmentVariables() .set("XDG_RUNTIME_DIR", "not-used") .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -148,6 +171,8 @@ void testShouldReadCredentialsFromCredentialStoreNullCheck() throws Exception { new EnvironmentVariables() .set("XDG_RUNTIME_DIR", "not-used") .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -180,6 +205,8 @@ void testShouldReadCredentialsFromCredentialHelperFake() throws Exception { .set("XDG_RUNTIME_DIR", "not-used") .set("PATH", newPath) .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -212,6 +239,8 @@ void testShouldReadCredentialsFromCredentialHelperHandleNonZeroReturnCode() thro .set("XDG_RUNTIME_DIR", "not-used") .set("PATH", newPath) .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -231,6 +260,8 @@ void testShouldReadCredentialsFromDockerConfig() throws Exception { new EnvironmentVariables() .set("XDG_RUNTIME_DIR", "not-used") .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -262,6 +293,8 @@ void testShouldReadCredentialsFromPodManConfig() throws Exception { new EnvironmentVariables() .set("XDG_RUNTIME_DIR", xdgRuntimeDir.toAbsolutePath().toString()) .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", "not-used").execute(() -> { assertNotNull(System.getenv("XDG_RUNTIME_DIR")); @@ -367,11 +400,78 @@ void testHierarchicalCredentialLookupNoMatch() throws Exception { assertNull(credential); } + @Test + void testShouldReadCredentialsFromPodmanConfigHome() throws Exception { + new EnvironmentVariables() + .set("XDG_CONFIG_HOME", xdgConfigHome.toAbsolutePath().toString()) + .remove("XDG_RUNTIME_DIR") + .remove("REGISTRY_AUTH_FILE") + .remove("DOCKER_CONFIG") + .execute(() -> { + new SystemProperties("user.home", "not-used").execute(() -> { + AuthStore authStoreInstance = AuthStore.newStore(); + assertNotNull(authStoreInstance); + + AuthStore.Credential credential = + authStoreInstance.get(ContainerRef.parse("registry.other.com/foo/bar:latest")); + assertNotNull(credential); + assertEquals(USERNAME, credential.username()); + assertEquals(PASSWORD, credential.password()); + }); + }); + } + + @Test + void testShouldReadCredentialsFromPodmanConfigHomeViaUserHome() throws Exception { + new EnvironmentVariables() + .remove("XDG_CONFIG_HOME") + .remove("XDG_RUNTIME_DIR") + .remove("REGISTRY_AUTH_FILE") + .remove("DOCKER_CONFIG") + .execute(() -> { + new SystemProperties( + "user.home", podmanHomeDir.toAbsolutePath().toString()) + .execute(() -> { + AuthStore authStoreInstance = AuthStore.newStore(); + assertNotNull(authStoreInstance); + + AuthStore.Credential credential = + authStoreInstance.get(ContainerRef.parse("registry.other.com/foo/bar:latest")); + assertNotNull(credential); + assertEquals(USERNAME, credential.username()); + assertEquals(PASSWORD, credential.password()); + }); + }); + } + + @Test + void testDockerConfigEnvOverridesUserHome() throws Exception { + new EnvironmentVariables() + .set("DOCKER_CONFIG", dockerConfigDir.toAbsolutePath().toString()) + .remove("XDG_CONFIG_HOME") + .remove("XDG_RUNTIME_DIR") + .remove("REGISTRY_AUTH_FILE") + .execute(() -> { + new SystemProperties("user.home", "not-used").execute(() -> { + AuthStore authStoreInstance = AuthStore.newStore(); + assertNotNull(authStoreInstance); + + AuthStore.Credential credential = + authStoreInstance.get(ContainerRef.parse("registry.example.com/foo/bar:latest")); + assertNotNull(credential); + assertEquals(USERNAME, credential.username()); + assertEquals(PASSWORD, credential.password()); + }); + }); + } + @Test void testWithoutXdgRuntimeDir() throws Exception { new EnvironmentVariables() .remove("XDG_RUNTIME_DIR") .remove("REGISTRY_AUTH_FILE") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { assertNull(System.getenv("XDG_RUNTIME_DIR")); AuthStore authStoreInstance = AuthStore.newStore(); @@ -387,6 +487,8 @@ void testRegistryAuthFileIsUsedWhenSet() throws Exception { new EnvironmentVariables() .set("REGISTRY_AUTH_FILE", authFile.toAbsolutePath().toString()) .remove("XDG_RUNTIME_DIR") + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { AuthStore authStoreInstance = AuthStore.newStore(); @@ -416,6 +518,8 @@ void testRegistryAuthFileTakesPrecedenceOverDefaults() throws Exception { new EnvironmentVariables() .set("REGISTRY_AUTH_FILE", authFile.toAbsolutePath().toString()) .set("XDG_RUNTIME_DIR", xdgRuntimeDir.toAbsolutePath().toString()) + .remove("XDG_CONFIG_HOME") + .remove("DOCKER_CONFIG") .execute(() -> { new SystemProperties("user.home", homeDir.toAbsolutePath().toString()).execute(() -> { AuthStore authStoreInstance = AuthStore.newStore();