From 9fc1ace99449d417caed05a075d135837f89f670 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Sun, 26 Jan 2025 18:35:38 +0530 Subject: [PATCH 01/23] Add new method to fetch the credentials from the fileStore Signed-off-by: AayushSaini101 --- .../java/land/oras/credentials/FileStore.java | 136 ++++++++++++++++++ .../land/oras/credentials/FileStoreTest.java | 112 +++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/main/java/land/oras/credentials/FileStore.java create mode 100644 src/test/java/land/oras/credentials/FileStoreTest.java diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java new file mode 100644 index 00000000..7f1082c0 --- /dev/null +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -0,0 +1,136 @@ +package land.oras.credentials; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * FileStore implements a credentials store using a configuration file + * to keep the credentials in plain-text. + * + * Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties + */ +public class FileStore { + + private final boolean disablePut; + private final Config config; + + public static final String ERR_PLAINTEXT_PUT_DISABLED = "Putting plaintext credentials is disabled"; + public static final String ERR_BAD_CREDENTIAL_FORMAT = "Bad credential format"; + + /** + * Constructor for FileStore. + * + * @param disablePut boolean flag to disable putting credentials in plaintext. + * @param config configuration instance. + */ + public FileStore(boolean disablePut, Config config) { + this.disablePut = disablePut; + this.config = Objects.requireNonNull(config, "Config cannot be null"); + } + + /** + * Creates a new FileStore based on the given configuration file path. + * + * @param configPath Path to the configuration file. + * @return FileStore instance. + * @throws Exception if loading the configuration fails. + */ + public static FileStore newFileStore(String configPath) throws Exception { + Config cfg = Config.load(configPath); + return new FileStore(false, cfg); + } + + /** + * Retrieves credentials for the given server address. + * + * @param serverAddress Server address. + * @return Credential object. + * @throws Exception if retrieval fails. + */ + public Credential get(String serverAddress) throws Exception { + return config.getCredential(serverAddress); + } + + /** + * Saves credentials for the given server address. + * + * @param serverAddress Server address. + * @param credential Credential object. + * @throws Exception if saving fails. + */ + public void put(String serverAddress, Credential credential) throws Exception { + if (disablePut) { + throw new UnsupportedOperationException(ERR_PLAINTEXT_PUT_DISABLED); + } + validateCredentialFormat(credential); + config.putCredential(serverAddress, credential); + } + + /** + * Deletes credentials for the given server address. + * + * @param serverAddress Server address. + * @throws Exception if deletion fails. + */ + public void delete(String serverAddress) throws Exception { + config.deleteCredential(serverAddress); + } + + /** + * Validates the format of the credential. + * + * @param credential Credential object. + * @throws Exception if the credential format is invalid. + */ + private void validateCredentialFormat(Credential credential) throws Exception { + if (credential.getUsername().contains(":")) { + throw new IllegalArgumentException(ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username"); + } + } + + /** + * Nested Config class for configuration management. + */ + public static class Config { + private final ConcurrentHashMap credentialStore = new ConcurrentHashMap<>(); + + public static Config load(String configPath) throws Exception { + // Simulate loading the configuration file. + // In a real implementation, you would parse the file and populate the credential store. + return new Config(); + } + + public Credential getCredential(String serverAddress) { + return credentialStore.get(serverAddress); + } + + public void putCredential(String serverAddress, Credential credential) { + credentialStore.put(serverAddress, credential); + } + + public void deleteCredential(String serverAddress) { + credentialStore.remove(serverAddress); + } + } + + /** + * Nested Credential class to represent username and password pairs. + */ + public static class Credential { + private final String username; + private final String password; + + public Credential(String username, String password) { + this.username = Objects.requireNonNull(username, "Username cannot be null"); + this.password = Objects.requireNonNull(password, "Password cannot be null"); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + } +} diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java new file mode 100644 index 00000000..483d086c --- /dev/null +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -0,0 +1,112 @@ +package land.oras.credentials; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; + + +class FileStoreTest { + + private FileStore fileStore; + private FileStore.Config mockConfig; + private FileStore.Credential mockCredential; + private static final String SERVER_ADDRESS = "server.example.com"; + private static final String USERNAME = "user"; + private static final String PASSWORD = "password"; + + @BeforeEach + void setUp() { + // Mock Config and Credential + mockConfig = Mockito.mock(FileStore.Config.class); + mockCredential = new FileStore.Credential(USERNAME, PASSWORD); + + // Create FileStore instance + fileStore = new FileStore(false, mockConfig); + } + + @Test + void testNewFileStore_success() throws Exception { + // Simulate loading configuration + String configPath = "config.json"; + FileStore.Config mockConfig = Mockito.mock(FileStore.Config.class); + FileStore fileStoreInstance = new FileStore(false, mockConfig); + + assertNotNull(fileStoreInstance); + } + + @Test + void testGetCredential_success() throws Exception { + // Mock the behavior of getting credentials + Mockito.when(mockConfig.getCredential(SERVER_ADDRESS)).thenReturn(mockCredential); + + FileStore.Credential credential = fileStore.get(SERVER_ADDRESS); + + assertNotNull(credential); + assertEquals(USERNAME, credential.getUsername()); + assertEquals(PASSWORD, credential.getPassword()); + } + + @Test + void testPutCredential_success() throws Exception { + // Mock the behavior of putting credentials + Mockito.doNothing().when(mockConfig).putCredential(SERVER_ADDRESS, mockCredential); + + fileStore.put(SERVER_ADDRESS, mockCredential); + + Mockito.verify(mockConfig, Mockito.times(1)).putCredential(SERVER_ADDRESS, mockCredential); + } + + @Test + void testPutCredential_whenPutDisabled_throwsException() { + fileStore = new FileStore(true, mockConfig); // Set disablePut to true + + UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, () -> { + fileStore.put(SERVER_ADDRESS, mockCredential); + }); + + assertEquals(FileStore.ERR_PLAINTEXT_PUT_DISABLED, thrown.getMessage()); + } + + @Test + void testPutCredential_invalidFormat_throwsException() { + // Credential with a colon in the username should throw an exception + FileStore.Credential invalidCredential = new FileStore.Credential("user:name", PASSWORD); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + fileStore.put(SERVER_ADDRESS, invalidCredential); + }); + + assertEquals(FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); + } + + @Test + void testDeleteCredential_success() throws Exception { + // Mock the behavior of deleting credentials + Mockito.doNothing().when(mockConfig).deleteCredential(SERVER_ADDRESS); + + fileStore.delete(SERVER_ADDRESS); + + Mockito.verify(mockConfig, Mockito.times(1)).deleteCredential(SERVER_ADDRESS); + } + + @Test + void testValidateCredentialFormat_invalidUsernameFormat_throwsException() { + // Test validation for credentials with colon in username + FileStore.Credential invalidCredential = new FileStore.Credential("user:name", "password"); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + fileStore.put(SERVER_ADDRESS, invalidCredential); + }); + + assertEquals(FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); + } + + @Test + void testConfigLoad_success() throws Exception { + // Simulate a successful config load + FileStore.Config config = FileStore.Config.load("config.json"); + + assertNotNull(config); + } +} From 607716b7babe595483740449f8363d3b1b78ed07 Mon Sep 17 00:00:00 2001 From: Moderator <60972989+aayushsaini101@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:48:00 +0530 Subject: [PATCH 02/23] Changed Signed-off-by: AayushSaini101 --- src/main/java/land/oras/ContainerRef.java | 2 + src/main/java/land/oras/Layer.java | 2 + src/main/java/land/oras/Registry.java | 1 + .../land/oras/auth/BearerTokenProvider.java | 3 +- .../java/land/oras/credentials/FileStore.java | 51 ++++++++++++++++--- .../exception/ConfigLoadingException.java | 11 ++++ .../java/land/oras/{ => exception}/Error.java | 2 +- .../oras/{ => exception}/OrasException.java | 4 +- .../java/land/oras/utils/ArchiveUtils.java | 3 +- .../java/land/oras/utils/DigestUtils.java | 3 +- src/main/java/land/oras/utils/JsonUtils.java | 3 +- .../java/land/oras/utils/OrasHttpClient.java | 3 +- .../oras/auth/BearerTokenProviderTest.java | 3 +- .../land/oras/credentials/FileStoreTest.java | 34 ++++++++++++- 14 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 src/main/java/land/oras/exception/ConfigLoadingException.java rename src/main/java/land/oras/{ => exception}/Error.java (91%) rename src/main/java/land/oras/{ => exception}/OrasException.java (92%) diff --git a/src/main/java/land/oras/ContainerRef.java b/src/main/java/land/oras/ContainerRef.java index e42f1222..d8873176 100644 --- a/src/main/java/land/oras/ContainerRef.java +++ b/src/main/java/land/oras/ContainerRef.java @@ -2,6 +2,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; + +import land.oras.exception.OrasException; import land.oras.utils.Const; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/src/main/java/land/oras/Layer.java b/src/main/java/land/oras/Layer.java index 7c98d024..b750f407 100644 --- a/src/main/java/land/oras/Layer.java +++ b/src/main/java/land/oras/Layer.java @@ -7,6 +7,8 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; + +import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.DigestUtils; import land.oras.utils.JsonUtils; diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index 785efc6b..aeac9fbe 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import land.oras.auth.*; +import land.oras.exception.OrasException; import land.oras.utils.ArchiveUtils; import land.oras.utils.Const; import land.oras.utils.DigestUtils; diff --git a/src/main/java/land/oras/auth/BearerTokenProvider.java b/src/main/java/land/oras/auth/BearerTokenProvider.java index 0c752616..5fa6af9f 100644 --- a/src/main/java/land/oras/auth/BearerTokenProvider.java +++ b/src/main/java/land/oras/auth/BearerTokenProvider.java @@ -6,7 +6,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import land.oras.OrasException; + +import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; import land.oras.utils.OrasHttpClient; diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 7f1082c0..0132e451 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -1,8 +1,16 @@ package land.oras.credentials; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import land.oras.exception.ConfigLoadingException; + /** * FileStore implements a credentials store using a configuration file * to keep the credentials in plain-text. @@ -94,11 +102,39 @@ private void validateCredentialFormat(Credential credential) throws Exception { public static class Config { private final ConcurrentHashMap credentialStore = new ConcurrentHashMap<>(); - public static Config load(String configPath) throws Exception { - // Simulate loading the configuration file. - // In a real implementation, you would parse the file and populate the credential store. - return new Config(); + /** + * Load configuration from a JSON file and populate the credential store. + * + * @param configPath Path to the JSON configuration file. + * @return A Config instance with loaded credentials. + * @throws ConfigLoadingException If the file cannot be read or parsed. + */ + public static Config load(String configPath) throws ConfigLoadingException { + ObjectMapper objectMapper = new ObjectMapper(); + + try { + // Read the file content + String fileContent = new String(Files.readAllBytes(Paths.get(configPath))); + // Parse JSON into a Map + Map credentials = objectMapper.readValue(fileContent, + new TypeReference>() {}); + + // Create a new Config instance + Config config = new Config(); + + // Populate the credential store + for (Map.Entry entry : credentials.entrySet()) { + String serverAddress = entry.getKey(); + Credential credential = entry.getValue(); + // Put the serverAddress and Credential into the credentialStore + config.credentialStore.put(serverAddress, credential); + } + return config; + } catch (IOException e) { + // Handle issues related to file reading or file not found + throw new ConfigLoadingException("Failed to read the configuration file: " + configPath, e); } + } public Credential getCredential(String serverAddress) { return credentialStore.get(serverAddress); @@ -117,8 +153,11 @@ public void deleteCredential(String serverAddress) { * Nested Credential class to represent username and password pairs. */ public static class Credential { - private final String username; - private final String password; + private String username; + private String password; + // Default constructor for the jackson for deserialization + public Credential() { + } public Credential(String username, String password) { this.username = Objects.requireNonNull(username, "Username cannot be null"); diff --git a/src/main/java/land/oras/exception/ConfigLoadingException.java b/src/main/java/land/oras/exception/ConfigLoadingException.java new file mode 100644 index 00000000..44008e3b --- /dev/null +++ b/src/main/java/land/oras/exception/ConfigLoadingException.java @@ -0,0 +1,11 @@ +package land.oras.exception; + +public class ConfigLoadingException extends Exception { + public ConfigLoadingException(String message) { + super(message); + } + + public ConfigLoadingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/land/oras/Error.java b/src/main/java/land/oras/exception/Error.java similarity index 91% rename from src/main/java/land/oras/Error.java rename to src/main/java/land/oras/exception/Error.java index 06a003ed..b658ab84 100644 --- a/src/main/java/land/oras/Error.java +++ b/src/main/java/land/oras/exception/Error.java @@ -1,4 +1,4 @@ -package land.oras; +package land.oras.exception; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/src/main/java/land/oras/OrasException.java b/src/main/java/land/oras/exception/OrasException.java similarity index 92% rename from src/main/java/land/oras/OrasException.java rename to src/main/java/land/oras/exception/OrasException.java index 9a016e19..c0d9089a 100644 --- a/src/main/java/land/oras/OrasException.java +++ b/src/main/java/land/oras/exception/OrasException.java @@ -1,4 +1,4 @@ -package land.oras; +package land.oras.exception; import land.oras.utils.JsonUtils; import land.oras.utils.OrasHttpClient; @@ -16,7 +16,7 @@ public class OrasException extends RuntimeException { /** * Logger */ - private static final Logger LOG = LoggerFactory.getLogger(OrasException.class); + private static final Logger LOG = LoggerFactory.getLogger(land.oras.exception.OrasException.class); /** * Possible error response diff --git a/src/main/java/land/oras/utils/ArchiveUtils.java b/src/main/java/land/oras/utils/ArchiveUtils.java index 51f84fbf..3c9c3e1f 100644 --- a/src/main/java/land/oras/utils/ArchiveUtils.java +++ b/src/main/java/land/oras/utils/ArchiveUtils.java @@ -13,7 +13,8 @@ import java.util.EnumSet; import java.util.Set; import java.util.stream.Stream; -import land.oras.OrasException; + +import land.oras.exception.OrasException; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; diff --git a/src/main/java/land/oras/utils/DigestUtils.java b/src/main/java/land/oras/utils/DigestUtils.java index 8399828f..d59c85f2 100644 --- a/src/main/java/land/oras/utils/DigestUtils.java +++ b/src/main/java/land/oras/utils/DigestUtils.java @@ -5,7 +5,8 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; -import land.oras.OrasException; + +import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; /** diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index 573f88bd..ccbd714a 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -10,7 +10,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; -import land.oras.OrasException; + +import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/land/oras/utils/OrasHttpClient.java b/src/main/java/land/oras/utils/OrasHttpClient.java index 3ed41ed7..2c0aaa83 100644 --- a/src/main/java/land/oras/utils/OrasHttpClient.java +++ b/src/main/java/land/oras/utils/OrasHttpClient.java @@ -17,9 +17,10 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedTrustManager; -import land.oras.OrasException; + import land.oras.auth.AuthProvider; import land.oras.auth.NoAuthProvider; +import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; diff --git a/src/test/java/land/oras/auth/BearerTokenProviderTest.java b/src/test/java/land/oras/auth/BearerTokenProviderTest.java index 4b22680b..54fb94fe 100644 --- a/src/test/java/land/oras/auth/BearerTokenProviderTest.java +++ b/src/test/java/land/oras/auth/BearerTokenProviderTest.java @@ -9,7 +9,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import java.time.ZonedDateTime; import java.util.Map; -import land.oras.OrasException; + +import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; import land.oras.utils.OrasHttpClient; diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java index 483d086c..27c81470 100644 --- a/src/test/java/land/oras/credentials/FileStoreTest.java +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -2,8 +2,15 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.mockito.Mockito; +import wiremock.com.fasterxml.jackson.databind.ObjectMapper; class FileStoreTest { @@ -104,9 +111,32 @@ void testValidateCredentialFormat_invalidUsernameFormat_throwsException() { @Test void testConfigLoad_success() throws Exception { - // Simulate a successful config load - FileStore.Config config = FileStore.Config.load("config.json"); + // Create a temporary JSON file for testing + Map credentials = new HashMap<>(); + credentials.put("server1.example.com", new FileStore.Credential("admin", "password123")); + credentials.put("server2.example.com", new FileStore.Credential("user", "userpass")); + + // Convert the Map to a JSON string + ObjectMapper objectMapper = new ObjectMapper(); + String jsonContent = objectMapper.writeValueAsString(credentials); + + // Create a temporary file and write the JSON content to it + Path tempFile = Files.createTempFile("config", ".json"); + Files.write(tempFile, jsonContent.getBytes()); + + // Load the configuration from the temporary file + FileStore.Config config = FileStore.Config.load(tempFile.toString()); + // Verify that the config was loaded successfully and contains the correct data assertNotNull(config); + assertNotNull(config.getCredential("server1.example.com")); + assertNotNull(config.getCredential("server2.example.com")); + assertEquals("admin", config.getCredential("server1.example.com").getUsername()); + assertEquals("password123", config.getCredential("server1.example.com").getPassword()); + assertEquals("user", config.getCredential("server2.example.com").getUsername()); + assertEquals("userpass", config.getCredential("server2.example.com").getPassword()); + + // Clean up by deleting the temporary file + Files.delete(tempFile); } } From 4d4c8e53d2d45bfe006bbc95d50c0a57fcfb0bcf Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Mon, 27 Jan 2025 15:23:50 +0530 Subject: [PATCH 03/23] Remove dependency of jackson and use jsonutil Signed-off-by: AayushSaini101 --- .../java/land/oras/credentials/FileStore.java | 23 ++++++------------- src/main/java/land/oras/utils/JsonUtils.java | 9 ++++++++ .../land/oras/credentials/FileStoreTest.java | 6 ++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 0132e451..1c868ea2 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -1,5 +1,6 @@ package land.oras.credentials; +import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @@ -7,9 +8,10 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; + +import com.google.gson.reflect.TypeToken; import land.oras.exception.ConfigLoadingException; +import land.oras.utils.JsonUtils; /** * FileStore implements a credentials store using a configuration file @@ -110,19 +112,11 @@ public static class Config { * @throws ConfigLoadingException If the file cannot be read or parsed. */ public static Config load(String configPath) throws ConfigLoadingException { - ObjectMapper objectMapper = new ObjectMapper(); - - try { - // Read the file content - String fileContent = new String(Files.readAllBytes(Paths.get(configPath))); - // Parse JSON into a Map - Map credentials = objectMapper.readValue(fileContent, - new TypeReference>() {}); - // Create a new Config instance + try (FileReader reader = new FileReader(Paths.get(configPath).toFile())) { + // Read the file content and deserialize into a Map + Map credentials = JsonUtils.fromJson(reader, new TypeToken>(){}.getType()); Config config = new Config(); - - // Populate the credential store for (Map.Entry entry : credentials.entrySet()) { String serverAddress = entry.getKey(); Credential credential = entry.getValue(); @@ -155,9 +149,6 @@ public void deleteCredential(String serverAddress) { public static class Credential { private String username; private String password; - // Default constructor for the jackson for deserialization - public Credential() { - } public Credential(String username, String password) { this.username = Objects.requireNonNull(username, "Username cannot be null"); diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index ccbd714a..00be1a47 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -5,12 +5,17 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; + +import java.io.FileReader; import java.io.IOException; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.Map; +import land.oras.credentials.FileStore; import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; @@ -37,6 +42,10 @@ private JsonUtils() { // Hide constructor } + public static Map fromJson(FileReader reader, Type type) { + return gson.fromJson(reader, type); + } + /** * Type adapter for ZonedDateTime */ diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java index 27c81470..f8602b39 100644 --- a/src/test/java/land/oras/credentials/FileStoreTest.java +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -1,5 +1,6 @@ package land.oras.credentials; +import land.oras.utils.JsonUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -116,9 +117,8 @@ void testConfigLoad_success() throws Exception { credentials.put("server1.example.com", new FileStore.Credential("admin", "password123")); credentials.put("server2.example.com", new FileStore.Credential("user", "userpass")); - // Convert the Map to a JSON string - ObjectMapper objectMapper = new ObjectMapper(); - String jsonContent = objectMapper.writeValueAsString(credentials); + + String jsonContent = JsonUtils.toJson(credentials); // Create a temporary file and write the JSON content to it Path tempFile = Files.createTempFile("config", ".json"); From 553cb3ff7d90ebeb216727b2e88c6e616919cc13 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Mon, 27 Jan 2025 15:26:32 +0530 Subject: [PATCH 04/23] Format the code Signed-off-by: AayushSaini101 --- src/main/java/land/oras/credentials/FileStore.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 1c868ea2..b2c03624 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -116,14 +116,19 @@ public static Config load(String configPath) throws ConfigLoadingException { try (FileReader reader = new FileReader(Paths.get(configPath).toFile())) { // Read the file content and deserialize into a Map Map credentials = JsonUtils.fromJson(reader, new TypeToken>(){}.getType()); + Config config = new Config(); + for (Map.Entry entry : credentials.entrySet()) { + String serverAddress = entry.getKey(); Credential credential = entry.getValue(); // Put the serverAddress and Credential into the credentialStore config.credentialStore.put(serverAddress, credential); } + return config; + } catch (IOException e) { // Handle issues related to file reading or file not found throw new ConfigLoadingException("Failed to read the configuration file: " + configPath, e); From f5c7807e2c65c9b34d399ef848cb08f62c330069 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Mon, 27 Jan 2025 15:57:44 +0530 Subject: [PATCH 05/23] Fixed the improvements Signed-off-by: AayushSaini101 --- .../auth/FileStoreAuthenticationProvider.java | 9 +++++++ .../java/land/oras/credentials/FileStore.java | 25 ++++++++----------- src/main/java/land/oras/utils/JsonUtils.java | 19 +++++++++++--- 3 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java new file mode 100644 index 00000000..eb7ed465 --- /dev/null +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -0,0 +1,9 @@ +package land.oras.auth; + +public class FileStoreAuthenticationProvider implements AuthProvider{ + + @Override + public String getAuthHeader() { + return ""; + } +} diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index b2c03624..309351fc 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -3,6 +3,7 @@ import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Objects; @@ -113,26 +114,20 @@ public static class Config { */ public static Config load(String configPath) throws ConfigLoadingException { - try (FileReader reader = new FileReader(Paths.get(configPath).toFile())) { - // Read the file content and deserialize into a Map - Map credentials = JsonUtils.fromJson(reader, new TypeToken>(){}.getType()); + Map credentials = JsonUtils.fromJson(Path.of(configPath), new TypeToken>(){}.getType()); - Config config = new Config(); + Config config = new Config(); - for (Map.Entry entry : credentials.entrySet()) { + for (Map.Entry entry : credentials.entrySet()) { - String serverAddress = entry.getKey(); - Credential credential = entry.getValue(); - // Put the serverAddress and Credential into the credentialStore - config.credentialStore.put(serverAddress, credential); - } + String serverAddress = entry.getKey(); + Credential credential = entry.getValue(); + // Put the serverAddress and Credential into the credentialStore + config.credentialStore.put(serverAddress, credential); + } - return config; + return config; - } catch (IOException e) { - // Handle issues related to file reading or file not found - throw new ConfigLoadingException("Failed to read the configuration file: " + configPath, e); - } } public Credential getCredential(String serverAddress) { diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index 00be1a47..65a7294f 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -42,9 +42,6 @@ private JsonUtils() { // Hide constructor } - public static Map fromJson(FileReader reader, Type type) { - return gson.fromJson(reader, type); - } /** * Type adapter for ZonedDateTime @@ -101,4 +98,20 @@ public static T fromJson(Path path, Class clazz) { throw new OrasException("Unable to read JSON file due to IO error", e); } } + + /** + * Convert a JSON string to an object + * @param path The path to the JSON file + * @param type The class of the object + * @return The object + */ + public static T fromJson(Path path, Type type) { + try { + return gson.fromJson(Files.readString(path, StandardCharsets.UTF_8), type); + } catch (IOException e) { + throw new OrasException("Unable to read JSON file due to IO error", e); + } + } + + } From 783d1868e792beede0b822e732ee018d0dfb6713 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Tue, 28 Jan 2025 15:39:26 +0530 Subject: [PATCH 06/23] Added the implementation of the file store Signed-off-by: AayushSaini101 --- .../auth/FileStoreAuthenticationProvider.java | 76 ++++++++++++++++++- .../FileStoreAuthenticationProviderTest.java | 70 +++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index eb7ed465..60745242 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -1,9 +1,81 @@ package land.oras.auth; -public class FileStoreAuthenticationProvider implements AuthProvider{ +import land.oras.credentials.FileStore; +import land.oras.credentials.FileStore.Credential; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * FileStoreAuthenticationProvider is an implementation of the AuthProvider interface. + * It retrieves credentials from a FileStore and generates a Basic Authentication header. + */ +public class FileStoreAuthenticationProvider implements AuthProvider { + + private final FileStore fileStore; + private final String serverAddress; + + private String username; + private String password; + + /** + * Constructor for FileStoreAuthenticationProvider. + * + * @param fileStore The FileStore instance to retrieve credentials from. + * @param serverAddress The server address for which to retrieve credentials. + */ + public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress) { + this.fileStore = fileStore; + this.serverAddress = serverAddress; + } + + /** + * Generates the Basic Authentication header for the provided server address. + * + * @return A Basic Authentication header string. + * @throws RuntimeException if no credentials are found for the server address. + */ @Override public String getAuthHeader() { - return ""; + try { + // Retrieve the credential for the server address + Credential credential = fileStore.get(serverAddress); + + if (credential == null) { + throw new RuntimeException("No credentials found for server address: " + serverAddress); + } + + // Set the username and password fields + this.username = credential.getUsername(); + this.password = credential.getPassword(); + + // Generate Basic Auth header (Base64 encoding of "username:password") + String authString = username + ":" + password; + String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8)); + + return "Basic " + encodedAuth; + + } catch (Exception e) { + throw new RuntimeException("Failed to generate authentication header", e); + } + } + + /** + * Gets the username of the retrieved credential. + * + * @return The username. + */ + public String getUsername() { + return username; + } + + /** + * Gets the password of the retrieved credential. + * + * @return The password. + */ + public String getPassword() { + return password; } } + diff --git a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java new file mode 100644 index 00000000..c232f18c --- /dev/null +++ b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java @@ -0,0 +1,70 @@ +package land.oras.auth; + + +import land.oras.credentials.FileStore; +import land.oras.credentials.FileStore.Credential; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FileStoreAuthenticationProviderTest { + + private FileStore mockFileStore; + private FileStoreAuthenticationProvider authProvider; + private final String serverAddress = "example.com"; + + @BeforeEach + void setUp() { + // Create a mock FileStore + mockFileStore = mock(FileStore.class); + + // Initialize FileStoreAuthenticationProvider with the mock + authProvider = new FileStoreAuthenticationProvider(mockFileStore, serverAddress); + } + + @Test + void testGetAuthHeader_validCredentials() throws Exception { + // Mock valid credentials for the server address + Credential credential = new Credential("testUser", "testPassword"); + when(mockFileStore.get(serverAddress)).thenReturn(credential); + + // Generate the authentication header + String authHeader = authProvider.getAuthHeader(); + + // Verify the expected Basic Auth header + String expectedAuthString = "testUser:testPassword"; + String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes()); + assertEquals(expectedEncodedAuth, authHeader); + + // Verify that username and password fields are set correctly + assertEquals("testUser", authProvider.getUsername()); + assertEquals("testPassword", authProvider.getPassword()); + } + + @Test + void testGetAuthHeader_retrievalError() throws Exception { + // Mock an exception during credential retrieval + when(mockFileStore.get(serverAddress)).thenThrow(new Exception("FileStore error")); + + // Verify that a RuntimeException is thrown + RuntimeException exception = assertThrows(RuntimeException.class, authProvider::getAuthHeader); + assertTrue(exception.getMessage().contains("Failed to generate authentication header")); + + // Ensure username and password fields are not set + assertNull(authProvider.getUsername()); + assertNull(authProvider.getPassword()); + } + + @Test + void testUsernameAndPasswordNotSetBeforeCall() { + // Ensure username and password are null before calling getAuthHeader + assertNull(authProvider.getUsername()); + assertNull(authProvider.getPassword()); + } + + +} From 5a439e662604cf844752879866c75e7e561633ec Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Tue, 28 Jan 2025 16:04:51 +0530 Subject: [PATCH 07/23] Remove the extra code use UserNamePasswordProvider class Signed-off-by: AayushSaini101 --- .../auth/FileStoreAuthenticationProvider.java | 61 +++------------ .../FileStoreAuthenticationProviderTest.java | 76 +++++++++++-------- 2 files changed, 55 insertions(+), 82 deletions(-) diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index 60745242..76e386d1 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -2,9 +2,8 @@ import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; +import land.oras.exception.ConfigLoadingException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; /** * FileStoreAuthenticationProvider is an implementation of the AuthProvider interface. @@ -14,9 +13,7 @@ public class FileStoreAuthenticationProvider implements AuthProvider { private final FileStore fileStore; private final String serverAddress; - - private String username; - private String password; + private final UsernamePasswordProvider usernamePasswordAuthProvider; /** * Constructor for FileStoreAuthenticationProvider. @@ -24,58 +21,20 @@ public class FileStoreAuthenticationProvider implements AuthProvider { * @param fileStore The FileStore instance to retrieve credentials from. * @param serverAddress The server address for which to retrieve credentials. */ - public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress) { + public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress) throws Exception { this.fileStore = fileStore; this.serverAddress = serverAddress; - } - - /** - * Generates the Basic Authentication header for the provided server address. - * - * @return A Basic Authentication header string. - * @throws RuntimeException if no credentials are found for the server address. - */ - @Override - public String getAuthHeader() { - try { - // Retrieve the credential for the server address - Credential credential = fileStore.get(serverAddress); - - if (credential == null) { - throw new RuntimeException("No credentials found for server address: " + serverAddress); - } - - // Set the username and password fields - this.username = credential.getUsername(); - this.password = credential.getPassword(); - - // Generate Basic Auth header (Base64 encoding of "username:password") - String authString = username + ":" + password; - String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8)); - - return "Basic " + encodedAuth; - - } catch (Exception e) { - throw new RuntimeException("Failed to generate authentication header", e); + Credential credential = fileStore.get(serverAddress); + if (credential == null) { + throw new ConfigLoadingException("No credentials found for server address: " + serverAddress); } - } + this.usernamePasswordAuthProvider = new UsernamePasswordProvider(credential.getUsername(), credential.getPassword()); - /** - * Gets the username of the retrieved credential. - * - * @return The username. - */ - public String getUsername() { - return username; } - /** - * Gets the password of the retrieved credential. - * - * @return The password. - */ - public String getPassword() { - return password; + @Override + public String getAuthHeader() { + return usernamePasswordAuthProvider.getAuthHeader(); } } diff --git a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java index c232f18c..2f5323af 100644 --- a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java +++ b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java @@ -1,16 +1,17 @@ package land.oras.auth; - import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; +import land.oras.exception.ConfigLoadingException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Base64; - import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + class FileStoreAuthenticationProviderTest { private FileStore mockFileStore; @@ -19,52 +20,65 @@ class FileStoreAuthenticationProviderTest { @BeforeEach void setUp() { - // Create a mock FileStore + // Mock the FileStore mockFileStore = mock(FileStore.class); - - // Initialize FileStoreAuthenticationProvider with the mock - authProvider = new FileStoreAuthenticationProvider(mockFileStore, serverAddress); } @Test - void testGetAuthHeader_validCredentials() throws Exception { + void testConstructor_validCredentials() throws Exception { // Mock valid credentials for the server address Credential credential = new Credential("testUser", "testPassword"); when(mockFileStore.get(serverAddress)).thenReturn(credential); - // Generate the authentication header - String authHeader = authProvider.getAuthHeader(); - - // Verify the expected Basic Auth header - String expectedAuthString = "testUser:testPassword"; - String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes()); - assertEquals(expectedEncodedAuth, authHeader); + // Create the authentication provider + authProvider = new FileStoreAuthenticationProvider(mockFileStore, serverAddress); - // Verify that username and password fields are set correctly - assertEquals("testUser", authProvider.getUsername()); - assertEquals("testPassword", authProvider.getPassword()); + // Assert that the authentication provider is created successfully + assertNotNull(authProvider); } @Test - void testGetAuthHeader_retrievalError() throws Exception { - // Mock an exception during credential retrieval - when(mockFileStore.get(serverAddress)).thenThrow(new Exception("FileStore error")); + void testConstructor_missingCredentials() throws Exception { + // Mock no credentials for the server address + when(mockFileStore.get(serverAddress)).thenReturn(null); - // Verify that a RuntimeException is thrown - RuntimeException exception = assertThrows(RuntimeException.class, authProvider::getAuthHeader); - assertTrue(exception.getMessage().contains("Failed to generate authentication header")); + // Verify that the constructor throws ConfigLoadingException + ConfigLoadingException exception = assertThrows(ConfigLoadingException.class, () -> { + new FileStoreAuthenticationProvider(mockFileStore, serverAddress); + }); - // Ensure username and password fields are not set - assertNull(authProvider.getUsername()); - assertNull(authProvider.getPassword()); + // Assert the exception message + assertTrue(exception.getMessage().contains("No credentials found for server address")); } @Test - void testUsernameAndPasswordNotSetBeforeCall() { - // Ensure username and password are null before calling getAuthHeader - assertNull(authProvider.getUsername()); - assertNull(authProvider.getPassword()); + void testGetAuthHeader_validCredentials() throws Exception { + // Mock valid credentials for the server address + Credential credential = new Credential("testUser", "testPassword"); + when(mockFileStore.get(serverAddress)).thenReturn(credential); + + // Create the authentication provider + authProvider = new FileStoreAuthenticationProvider(mockFileStore, serverAddress); + + // Verify that the getAuthHeader method returns the expected Basic Auth header + String authHeader = authProvider.getAuthHeader(); + String expectedAuthString = "testUser:testPassword"; + String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes(StandardCharsets.UTF_8)); + + assertEquals(expectedEncodedAuth, authHeader); } + @Test + void testGetAuthHeader_missingCredentials() throws Exception { + // Mock no credentials for the server address + when(mockFileStore.get(serverAddress)).thenReturn(null); + + // Create the authentication provider, expecting it to throw ConfigLoadingException + ConfigLoadingException exception = assertThrows(ConfigLoadingException.class, () -> { + new FileStoreAuthenticationProvider(mockFileStore, serverAddress); + }); + // Verify the exception message + assertTrue(exception.getMessage().contains("No credentials found for server address")); + } } From 89ec8bbc2c8aead1316aa29c8d0b149df6a419ea Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Thu, 30 Jan 2025 11:53:51 +0100 Subject: [PATCH 08/23] Optimize tests duration by using Junit 5 concurrency (#72) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- pom.xml | 5 + src/test/java/land/oras/RegistryTest.java | 155 +--------------------- 2 files changed, 7 insertions(+), 153 deletions(-) diff --git a/pom.xml b/pom.xml index 035f0c6a..363eb46d 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,11 @@ 17 17 + + 17 + 17 + 17 + diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index 89cc7230..1d66181d 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -11,11 +11,10 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; -import java.util.Random; + +import land.oras.exception.OrasException; import land.oras.utils.Const; -import land.oras.utils.DigestUtils; import land.oras.utils.RegistryContainer; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -217,154 +216,4 @@ void testShouldPushCompressedDirectory() throws IOException { assertEquals(blobDir.getFileName().toString(), annotations.get(Const.ANNOTATION_TITLE)); assertEquals("true", annotations.get(Const.ANNOTATION_ORAS_UNPACK)); } - - // Push blob - successfull - // Push blob - failed - when blob already exists - // Push blob - Handles io exception - // Handle large stream content - @Test - void shouldPushAndGetBlobStream() throws IOException { - Registry registry = Registry.Builder.builder() - .withInsecure(true) - .withSkipTlsVerify(true) - .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); - - // Create a file with test data to get accurate stream size - Path testFile = Files.createTempFile("test-data-", ".tmp"); - String testData = "Hello World Stream Test"; - Files.writeString(testFile, testData); - long fileSize = Files.size(testFile); - - // Test pushBlobStream using file input stream - Layer layer; - try (InputStream inputStream = Files.newInputStream(testFile)) { - layer = registry.pushBlobStream(containerRef, inputStream, fileSize); - - // Verify the digest matches SHA-256 of content - assertEquals(DigestUtils.sha256(testFile), layer.getDigest()); - assertEquals(fileSize, layer.getSize()); - } - - // Test getBlobStream - try (InputStream resultStream = registry.getBlobStream(containerRef.withDigest(layer.getDigest()))) { - String result = new String(resultStream.readAllBytes()); - assertEquals(testData, result); - } - - // Clean up - Files.delete(testFile); - registry.deleteBlob(containerRef.withDigest(layer.getDigest())); - } - - @Test - void shouldHandleExistingBlobInStreamPush() throws IOException { - Registry registry = Registry.Builder.builder() - .withInsecure(true) - .withSkipTlsVerify(true) - .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); - - // Create test file - Path testFile = Files.createTempFile("test-data-", ".tmp"); - Files.writeString(testFile, "Test Content"); - long fileSize = Files.size(testFile); - String expectedDigest = DigestUtils.sha256(testFile); - - // First push - Layer firstLayer; - try (InputStream inputStream = Files.newInputStream(testFile)) { - firstLayer = registry.pushBlobStream(containerRef, inputStream, fileSize); - } - - // Second push of same content should detect existing blob - Layer secondLayer; - try (InputStream inputStream = Files.newInputStream(testFile)) { - secondLayer = registry.pushBlobStream(containerRef, inputStream, fileSize); - } - - // Verify both operations return same digest - assertEquals(expectedDigest, firstLayer.getDigest()); - assertEquals(expectedDigest, secondLayer.getDigest()); - assertEquals(firstLayer.getSize(), secondLayer.getSize()); - - // Clean up - Files.delete(testFile); - registry.deleteBlob(containerRef.withDigest(firstLayer.getDigest())); - } - - @Test - void shouldHandleIOExceptionInStreamPush() throws IOException { - Registry registry = Registry.Builder.builder() - .withInsecure(true) - .withSkipTlsVerify(true) - .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); - - // Create a failing input stream - InputStream failingStream = new InputStream() { - @Override - public int read() throws IOException { - throw new IOException("Simulated IO failure"); - } - }; - - // Verify exception is wrapped in OrasException - OrasException exception = - assertThrows(OrasException.class, () -> registry.pushBlobStream(containerRef, failingStream, 100)); - assertEquals("Failed to push blob stream", exception.getMessage()); - assertTrue(exception.getCause() instanceof IOException); - } - - @Test - void shouldHandleNonExistentBlobInGetStream() { - Registry registry = Registry.Builder.builder() - .withInsecure(true) - .withSkipTlsVerify(true) - .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); - - // Try to get non-existent blob - String nonExistentDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - - // Verify it throws OrasException - assertThrows(OrasException.class, () -> registry.getBlobStream(containerRef.withDigest(nonExistentDigest))); - } - - @Test - void shouldHandleLargeStreamContent() throws IOException { - Registry registry = Registry.Builder.builder() - .withInsecure(true) - .withSkipTlsVerify(true) - .build(); - ContainerRef containerRef = - ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); - - // Create temp file with 5MB of random data - Path largeFile = Files.createTempFile("large-test-", ".tmp"); - byte[] largeData = new byte[5 * 1024 * 1024]; - new Random().nextBytes(largeData); - Files.write(largeFile, largeData); - long fileSize = Files.size(largeFile); - - // Push large content - Layer layer; - try (InputStream inputStream = Files.newInputStream(largeFile)) { - layer = registry.pushBlobStream(containerRef, inputStream, fileSize); - } - - // Verify content with stream - try (InputStream resultStream = registry.getBlobStream(containerRef.withDigest(layer.getDigest()))) { - byte[] result = resultStream.readAllBytes(); - Assertions.assertArrayEquals(largeData, result); - } - - // Clean up - Files.delete(largeFile); - registry.deleteBlob(containerRef.withDigest(layer.getDigest())); - } } From bb259ccfd3403b6d85639495f79260dca4cd1d88 Mon Sep 17 00:00:00 2001 From: Vaidik Date: Wed, 12 Feb 2025 10:04:15 +0530 Subject: [PATCH 09/23] Add stream API methods (#60) Co-authored-by: Valentin Delaye Signed-off-by: AayushSaini101 --- src/test/java/land/oras/RegistryTest.java | 155 +++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index 1d66181d..89cc7230 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -11,10 +11,11 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; - -import land.oras.exception.OrasException; +import java.util.Random; import land.oras.utils.Const; +import land.oras.utils.DigestUtils; import land.oras.utils.RegistryContainer; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -216,4 +217,154 @@ void testShouldPushCompressedDirectory() throws IOException { assertEquals(blobDir.getFileName().toString(), annotations.get(Const.ANNOTATION_TITLE)); assertEquals("true", annotations.get(Const.ANNOTATION_ORAS_UNPACK)); } + + // Push blob - successfull + // Push blob - failed - when blob already exists + // Push blob - Handles io exception + // Handle large stream content + @Test + void shouldPushAndGetBlobStream() throws IOException { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + + // Create a file with test data to get accurate stream size + Path testFile = Files.createTempFile("test-data-", ".tmp"); + String testData = "Hello World Stream Test"; + Files.writeString(testFile, testData); + long fileSize = Files.size(testFile); + + // Test pushBlobStream using file input stream + Layer layer; + try (InputStream inputStream = Files.newInputStream(testFile)) { + layer = registry.pushBlobStream(containerRef, inputStream, fileSize); + + // Verify the digest matches SHA-256 of content + assertEquals(DigestUtils.sha256(testFile), layer.getDigest()); + assertEquals(fileSize, layer.getSize()); + } + + // Test getBlobStream + try (InputStream resultStream = registry.getBlobStream(containerRef.withDigest(layer.getDigest()))) { + String result = new String(resultStream.readAllBytes()); + assertEquals(testData, result); + } + + // Clean up + Files.delete(testFile); + registry.deleteBlob(containerRef.withDigest(layer.getDigest())); + } + + @Test + void shouldHandleExistingBlobInStreamPush() throws IOException { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + + // Create test file + Path testFile = Files.createTempFile("test-data-", ".tmp"); + Files.writeString(testFile, "Test Content"); + long fileSize = Files.size(testFile); + String expectedDigest = DigestUtils.sha256(testFile); + + // First push + Layer firstLayer; + try (InputStream inputStream = Files.newInputStream(testFile)) { + firstLayer = registry.pushBlobStream(containerRef, inputStream, fileSize); + } + + // Second push of same content should detect existing blob + Layer secondLayer; + try (InputStream inputStream = Files.newInputStream(testFile)) { + secondLayer = registry.pushBlobStream(containerRef, inputStream, fileSize); + } + + // Verify both operations return same digest + assertEquals(expectedDigest, firstLayer.getDigest()); + assertEquals(expectedDigest, secondLayer.getDigest()); + assertEquals(firstLayer.getSize(), secondLayer.getSize()); + + // Clean up + Files.delete(testFile); + registry.deleteBlob(containerRef.withDigest(firstLayer.getDigest())); + } + + @Test + void shouldHandleIOExceptionInStreamPush() throws IOException { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + + // Create a failing input stream + InputStream failingStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated IO failure"); + } + }; + + // Verify exception is wrapped in OrasException + OrasException exception = + assertThrows(OrasException.class, () -> registry.pushBlobStream(containerRef, failingStream, 100)); + assertEquals("Failed to push blob stream", exception.getMessage()); + assertTrue(exception.getCause() instanceof IOException); + } + + @Test + void shouldHandleNonExistentBlobInGetStream() { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + + // Try to get non-existent blob + String nonExistentDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + // Verify it throws OrasException + assertThrows(OrasException.class, () -> registry.getBlobStream(containerRef.withDigest(nonExistentDigest))); + } + + @Test + void shouldHandleLargeStreamContent() throws IOException { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-stream".formatted(this.registry.getRegistry())); + + // Create temp file with 5MB of random data + Path largeFile = Files.createTempFile("large-test-", ".tmp"); + byte[] largeData = new byte[5 * 1024 * 1024]; + new Random().nextBytes(largeData); + Files.write(largeFile, largeData); + long fileSize = Files.size(largeFile); + + // Push large content + Layer layer; + try (InputStream inputStream = Files.newInputStream(largeFile)) { + layer = registry.pushBlobStream(containerRef, inputStream, fileSize); + } + + // Verify content with stream + try (InputStream resultStream = registry.getBlobStream(containerRef.withDigest(layer.getDigest()))) { + byte[] result = resultStream.readAllBytes(); + Assertions.assertArrayEquals(largeData, result); + } + + // Clean up + Files.delete(largeFile); + registry.deleteBlob(containerRef.withDigest(layer.getDigest())); + } } From 7bc68ce9288a392c7c8bf7cb01c7e5035b329808 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 12 Feb 2025 15:43:32 +0530 Subject: [PATCH 10/23] Resolve conflicts Signed-off-by: AayushSaini101 --- src/main/java/land/oras/ContainerRef.java | 1 - src/main/java/land/oras/Layer.java | 1 - .../land/oras/auth/BearerTokenProvider.java | 1 - .../auth/FileStoreAuthenticationProvider.java | 6 +-- .../java/land/oras/credentials/FileStore.java | 50 ++++++++----------- .../java/land/oras/utils/ArchiveUtils.java | 1 - .../java/land/oras/utils/DigestUtils.java | 1 - src/main/java/land/oras/utils/JsonUtils.java | 10 +--- .../java/land/oras/utils/OrasHttpClient.java | 1 - .../java/land/oras/RegistryContainerTest.java | 1 + src/test/java/land/oras/RegistryTest.java | 1 + .../oras/auth/BearerTokenProviderTest.java | 1 - .../FileStoreAuthenticationProviderTest.java | 14 +++--- .../land/oras/credentials/FileStoreTest.java | 18 +++---- 14 files changed, 42 insertions(+), 65 deletions(-) diff --git a/src/main/java/land/oras/ContainerRef.java b/src/main/java/land/oras/ContainerRef.java index d8873176..a9796500 100644 --- a/src/main/java/land/oras/ContainerRef.java +++ b/src/main/java/land/oras/ContainerRef.java @@ -2,7 +2,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; - import land.oras.exception.OrasException; import land.oras.utils.Const; import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/land/oras/Layer.java b/src/main/java/land/oras/Layer.java index b750f407..b9e1fd6a 100644 --- a/src/main/java/land/oras/Layer.java +++ b/src/main/java/land/oras/Layer.java @@ -7,7 +7,6 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; - import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.DigestUtils; diff --git a/src/main/java/land/oras/auth/BearerTokenProvider.java b/src/main/java/land/oras/auth/BearerTokenProvider.java index 5fa6af9f..4a428df7 100644 --- a/src/main/java/land/oras/auth/BearerTokenProvider.java +++ b/src/main/java/land/oras/auth/BearerTokenProvider.java @@ -6,7 +6,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; - import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index 76e386d1..fa8ad384 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -4,7 +4,6 @@ import land.oras.credentials.FileStore.Credential; import land.oras.exception.ConfigLoadingException; - /** * FileStoreAuthenticationProvider is an implementation of the AuthProvider interface. * It retrieves credentials from a FileStore and generates a Basic Authentication header. @@ -28,8 +27,8 @@ public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress if (credential == null) { throw new ConfigLoadingException("No credentials found for server address: " + serverAddress); } - this.usernamePasswordAuthProvider = new UsernamePasswordProvider(credential.getUsername(), credential.getPassword()); - + this.usernamePasswordAuthProvider = + new UsernamePasswordProvider(credential.getUsername(), credential.getPassword()); } @Override @@ -37,4 +36,3 @@ public String getAuthHeader() { return usernamePasswordAuthProvider.getAuthHeader(); } } - diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 309351fc..127b3f78 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -1,16 +1,10 @@ package land.oras.credentials; -import java.io.FileReader; -import java.io.IOException; -import java.nio.file.Files; +import com.google.gson.reflect.TypeToken; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; - - -import com.google.gson.reflect.TypeToken; import land.oras.exception.ConfigLoadingException; import land.oras.utils.JsonUtils; @@ -105,30 +99,30 @@ private void validateCredentialFormat(Credential credential) throws Exception { public static class Config { private final ConcurrentHashMap credentialStore = new ConcurrentHashMap<>(); - /** - * Load configuration from a JSON file and populate the credential store. - * - * @param configPath Path to the JSON configuration file. - * @return A Config instance with loaded credentials. - * @throws ConfigLoadingException If the file cannot be read or parsed. - */ - public static Config load(String configPath) throws ConfigLoadingException { - - Map credentials = JsonUtils.fromJson(Path.of(configPath), new TypeToken>(){}.getType()); + /** + * Load configuration from a JSON file and populate the credential store. + * + * @param configPath Path to the JSON configuration file. + * @return A Config instance with loaded credentials. + * @throws ConfigLoadingException If the file cannot be read or parsed. + */ + public static Config load(String configPath) throws ConfigLoadingException { - Config config = new Config(); + Map credentials = + JsonUtils.fromJson(Path.of(configPath), new TypeToken>() {}.getType()); - for (Map.Entry entry : credentials.entrySet()) { + Config config = new Config(); - String serverAddress = entry.getKey(); - Credential credential = entry.getValue(); - // Put the serverAddress and Credential into the credentialStore - config.credentialStore.put(serverAddress, credential); - } + for (Map.Entry entry : credentials.entrySet()) { - return config; + String serverAddress = entry.getKey(); + Credential credential = entry.getValue(); + // Put the serverAddress and Credential into the credentialStore + config.credentialStore.put(serverAddress, credential); + } - } + return config; + } public Credential getCredential(String serverAddress) { return credentialStore.get(serverAddress); @@ -147,8 +141,8 @@ public void deleteCredential(String serverAddress) { * Nested Credential class to represent username and password pairs. */ public static class Credential { - private String username; - private String password; + private String username; + private String password; public Credential(String username, String password) { this.username = Objects.requireNonNull(username, "Username cannot be null"); diff --git a/src/main/java/land/oras/utils/ArchiveUtils.java b/src/main/java/land/oras/utils/ArchiveUtils.java index 3c9c3e1f..f248a552 100644 --- a/src/main/java/land/oras/utils/ArchiveUtils.java +++ b/src/main/java/land/oras/utils/ArchiveUtils.java @@ -13,7 +13,6 @@ import java.util.EnumSet; import java.util.Set; import java.util.stream.Stream; - import land.oras.exception.OrasException; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; diff --git a/src/main/java/land/oras/utils/DigestUtils.java b/src/main/java/land/oras/utils/DigestUtils.java index d59c85f2..f5a9f205 100644 --- a/src/main/java/land/oras/utils/DigestUtils.java +++ b/src/main/java/land/oras/utils/DigestUtils.java @@ -5,7 +5,6 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; - import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index 65a7294f..223079bb 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -5,17 +5,12 @@ import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; - -import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZonedDateTime; -import java.util.Map; - -import land.oras.credentials.FileStore; import land.oras.exception.OrasException; import org.jspecify.annotations.NullMarked; import org.slf4j.Logger; @@ -42,7 +37,6 @@ private JsonUtils() { // Hide constructor } - /** * Type adapter for ZonedDateTime */ @@ -99,7 +93,7 @@ public static T fromJson(Path path, Class clazz) { } } - /** + /** * Convert a JSON string to an object * @param path The path to the JSON file * @param type The class of the object @@ -112,6 +106,4 @@ public static T fromJson(Path path, Type type) { throw new OrasException("Unable to read JSON file due to IO error", e); } } - - } diff --git a/src/main/java/land/oras/utils/OrasHttpClient.java b/src/main/java/land/oras/utils/OrasHttpClient.java index 2c0aaa83..6dbcbba6 100644 --- a/src/main/java/land/oras/utils/OrasHttpClient.java +++ b/src/main/java/land/oras/utils/OrasHttpClient.java @@ -17,7 +17,6 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedTrustManager; - import land.oras.auth.AuthProvider; import land.oras.auth.NoAuthProvider; import land.oras.exception.OrasException; diff --git a/src/test/java/land/oras/RegistryContainerTest.java b/src/test/java/land/oras/RegistryContainerTest.java index 89dc008b..2bea182b 100644 --- a/src/test/java/land/oras/RegistryContainerTest.java +++ b/src/test/java/land/oras/RegistryContainerTest.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; import land.oras.utils.RegistryContainer; diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index 89cc7230..98151568 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.DigestUtils; import land.oras.utils.RegistryContainer; diff --git a/src/test/java/land/oras/auth/BearerTokenProviderTest.java b/src/test/java/land/oras/auth/BearerTokenProviderTest.java index 54fb94fe..110837b0 100644 --- a/src/test/java/land/oras/auth/BearerTokenProviderTest.java +++ b/src/test/java/land/oras/auth/BearerTokenProviderTest.java @@ -9,7 +9,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import java.time.ZonedDateTime; import java.util.Map; - import land.oras.exception.OrasException; import land.oras.utils.Const; import land.oras.utils.JsonUtils; diff --git a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java index 2f5323af..5e103c47 100644 --- a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java +++ b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java @@ -1,16 +1,15 @@ package land.oras.auth; -import land.oras.credentials.FileStore; -import land.oras.credentials.FileStore.Credential; -import land.oras.exception.ConfigLoadingException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import java.nio.charset.StandardCharsets; import java.util.Base64; +import land.oras.credentials.FileStore; +import land.oras.credentials.FileStore.Credential; +import land.oras.exception.ConfigLoadingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class FileStoreAuthenticationProviderTest { @@ -63,7 +62,8 @@ void testGetAuthHeader_validCredentials() throws Exception { // Verify that the getAuthHeader method returns the expected Basic Auth header String authHeader = authProvider.getAuthHeader(); String expectedAuthString = "testUser:testPassword"; - String expectedEncodedAuth = "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes(StandardCharsets.UTF_8)); + String expectedEncodedAuth = + "Basic " + Base64.getEncoder().encodeToString(expectedAuthString.getBytes(StandardCharsets.UTF_8)); assertEquals(expectedEncodedAuth, authHeader); } diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java index f8602b39..a9d71293 100644 --- a/src/test/java/land/oras/credentials/FileStoreTest.java +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -1,18 +1,15 @@ package land.oras.credentials; -import land.oras.utils.JsonUtils; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; - +import land.oras.utils.JsonUtils; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import wiremock.com.fasterxml.jackson.databind.ObjectMapper; - class FileStoreTest { @@ -67,7 +64,7 @@ void testPutCredential_success() throws Exception { @Test void testPutCredential_whenPutDisabled_throwsException() { - fileStore = new FileStore(true, mockConfig); // Set disablePut to true + fileStore = new FileStore(true, mockConfig); // Set disablePut to true UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, () -> { fileStore.put(SERVER_ADDRESS, mockCredential); @@ -85,7 +82,8 @@ void testPutCredential_invalidFormat_throwsException() { fileStore.put(SERVER_ADDRESS, invalidCredential); }); - assertEquals(FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); + assertEquals( + FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); } @Test @@ -107,17 +105,17 @@ void testValidateCredentialFormat_invalidUsernameFormat_throwsException() { fileStore.put(SERVER_ADDRESS, invalidCredential); }); - assertEquals(FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); + assertEquals( + FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); } @Test void testConfigLoad_success() throws Exception { - // Create a temporary JSON file for testing + // Create a temporary JSON file for testing Map credentials = new HashMap<>(); credentials.put("server1.example.com", new FileStore.Credential("admin", "password123")); credentials.put("server2.example.com", new FileStore.Credential("user", "userpass")); - String jsonContent = JsonUtils.toJson(credentials); // Create a temporary file and write the JSON content to it From 20d89d4bb8d2970a968fe5e9cb1c0ab3ff57c05b Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 12 Feb 2025 16:24:30 +0530 Subject: [PATCH 11/23] Resolve build error for code generation Signed-off-by: AayushSaini101 --- oras-java-sdk.iml | 6 +++ .../auth/FileStoreAuthenticationProvider.java | 1 + .../java/land/oras/credentials/FileStore.java | 45 +++++++++++++++++++ .../exception/ConfigLoadingException.java | 16 +++++++ src/main/java/land/oras/utils/JsonUtils.java | 11 +++-- 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 oras-java-sdk.iml diff --git a/oras-java-sdk.iml b/oras-java-sdk.iml new file mode 100644 index 00000000..9e3449c9 --- /dev/null +++ b/oras-java-sdk.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index fa8ad384..2b95f3eb 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -19,6 +19,7 @@ public class FileStoreAuthenticationProvider implements AuthProvider { * * @param fileStore The FileStore instance to retrieve credentials from. * @param serverAddress The server address for which to retrieve credentials. + * @throws Exception If an error occurs during authentication initialization. */ public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress) throws Exception { this.fileStore = fileStore; diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 127b3f78..6cfb099b 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -19,7 +19,16 @@ public class FileStore { private final boolean disablePut; private final Config config; + /** + * Error message indicating that putting plaintext credentials is disabled. + * This is used to enforce security policies against storing sensitive credentials in plaintext format. + */ public static final String ERR_PLAINTEXT_PUT_DISABLED = "Putting plaintext credentials is disabled"; + + /** + * Error message indicating that the format of the provided credential is invalid. + * This is typically used when credentials do not match the expected structure or format. + */ public static final String ERR_BAD_CREDENTIAL_FORMAT = "Bad credential format"; /** @@ -124,14 +133,34 @@ public static Config load(String configPath) throws ConfigLoadingException { return config; } + /** + * Retrieves the {@code Credential} associated with the specified server address. + * + * @param serverAddress The address of the server whose credential is to be retrieved. + * @return The {@code Credential} associated with the server address, or {@code null} if no credential is found. + */ public Credential getCredential(String serverAddress) { return credentialStore.get(serverAddress); } + /** + * Associates the specified {@code Credential} with the given server address. + * If a credential already exists for the server address, it will be replaced. + * + * @param serverAddress The address of the server to associate with the credential. + * @param credential The {@code Credential} to store. Must not be {@code null}. + * @throws NullPointerException If the provided credential is {@code null}. + */ public void putCredential(String serverAddress, Credential credential) { credentialStore.put(serverAddress, credential); } + /** + * Removes the {@code Credential} associated with the specified server address. + * If no credential is associated with the server address, this method does nothing. + * + * @param serverAddress The address of the server whose credential is to be removed. + */ public void deleteCredential(String serverAddress) { credentialStore.remove(serverAddress); } @@ -144,15 +173,31 @@ public static class Credential { private String username; private String password; + /** + * Constructs a new {@code Credential} object with the specified username and password. + * + * @param username The username for the credential. Must not be {@code null}. + * @param password The password for the credential. Must not be {@code null}. + */ public Credential(String username, String password) { this.username = Objects.requireNonNull(username, "Username cannot be null"); this.password = Objects.requireNonNull(password, "Password cannot be null"); } + /** + * Returns the username associated with this credential. + * + * @return The username as a {@code String}. + */ public String getUsername() { return username; } + /** + * Returns the password associated with this credential. + * + * @return The password as a {@code String}. + */ public String getPassword() { return password; } diff --git a/src/main/java/land/oras/exception/ConfigLoadingException.java b/src/main/java/land/oras/exception/ConfigLoadingException.java index 44008e3b..0101ded9 100644 --- a/src/main/java/land/oras/exception/ConfigLoadingException.java +++ b/src/main/java/land/oras/exception/ConfigLoadingException.java @@ -1,10 +1,26 @@ package land.oras.exception; +/** + * Exception thrown to indicate an error occurred while loading a configuration. + * This custom exception can be used to provide detailed error messages and propagate underlying causes. + */ public class ConfigLoadingException extends Exception { + + /** + * Constructs a new {@code ConfigLoadingException} with the specified detail message. + * + * @param message The detail message explaining the reason for the exception. + */ public ConfigLoadingException(String message) { super(message); } + /** + * Constructs a new {@code ConfigLoadingException} with the specified detail message and cause. + * + * @param message The detail message explaining the reason for the exception. + * @param cause The underlying cause of the exception. Can be {@code null} if there is no cause. + */ public ConfigLoadingException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index 223079bb..229cf598 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -94,10 +94,13 @@ public static T fromJson(Path path, Class clazz) { } /** - * Convert a JSON string to an object - * @param path The path to the JSON file - * @param type The class of the object - * @return The object + * Converts the contents of a JSON file to an object of the specified type. + * + * @param path The {@code Path} to the JSON file to be read. + * @param type The {@code Type} representing the class of the object to be deserialized. + * @param The type of the object to be returned. + * @return An object of type {@code T} deserialized from the JSON file. + * @throws OrasException If an I/O error occurs while reading the file or the JSON is invalid. */ public static T fromJson(Path path, Type type) { try { From 4169fecd61249be2e9d41e809d030ae1f7841505 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 12 Feb 2025 16:38:48 +0530 Subject: [PATCH 12/23] Remove ConfigOrasException Signed-off-by: AayushSaini101 --- .../auth/FileStoreAuthenticationProvider.java | 4 +-- .../java/land/oras/credentials/FileStore.java | 6 ++--- .../exception/ConfigLoadingException.java | 27 ------------------- .../FileStoreAuthenticationProviderTest.java | 6 ++--- 4 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 src/main/java/land/oras/exception/ConfigLoadingException.java diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index 2b95f3eb..077892cb 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -2,7 +2,7 @@ import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; -import land.oras.exception.ConfigLoadingException; +import land.oras.exception.OrasException; /** * FileStoreAuthenticationProvider is an implementation of the AuthProvider interface. @@ -26,7 +26,7 @@ public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress this.serverAddress = serverAddress; Credential credential = fileStore.get(serverAddress); if (credential == null) { - throw new ConfigLoadingException("No credentials found for server address: " + serverAddress); + throw new OrasException("No credentials found for server address: " + serverAddress); } this.usernamePasswordAuthProvider = new UsernamePasswordProvider(credential.getUsername(), credential.getPassword()); diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 6cfb099b..43b5171b 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import land.oras.exception.ConfigLoadingException; +import land.oras.exception.OrasException; import land.oras.utils.JsonUtils; /** @@ -113,9 +113,9 @@ public static class Config { * * @param configPath Path to the JSON configuration file. * @return A Config instance with loaded credentials. - * @throws ConfigLoadingException If the file cannot be read or parsed. + * @throws OrasException If the file cannot be read or parsed. */ - public static Config load(String configPath) throws ConfigLoadingException { + public static Config load(String configPath) throws OrasException { Map credentials = JsonUtils.fromJson(Path.of(configPath), new TypeToken>() {}.getType()); diff --git a/src/main/java/land/oras/exception/ConfigLoadingException.java b/src/main/java/land/oras/exception/ConfigLoadingException.java deleted file mode 100644 index 0101ded9..00000000 --- a/src/main/java/land/oras/exception/ConfigLoadingException.java +++ /dev/null @@ -1,27 +0,0 @@ -package land.oras.exception; - -/** - * Exception thrown to indicate an error occurred while loading a configuration. - * This custom exception can be used to provide detailed error messages and propagate underlying causes. - */ -public class ConfigLoadingException extends Exception { - - /** - * Constructs a new {@code ConfigLoadingException} with the specified detail message. - * - * @param message The detail message explaining the reason for the exception. - */ - public ConfigLoadingException(String message) { - super(message); - } - - /** - * Constructs a new {@code ConfigLoadingException} with the specified detail message and cause. - * - * @param message The detail message explaining the reason for the exception. - * @param cause The underlying cause of the exception. Can be {@code null} if there is no cause. - */ - public ConfigLoadingException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java index 5e103c47..5dccb891 100644 --- a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java +++ b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java @@ -7,7 +7,7 @@ import java.util.Base64; import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; -import land.oras.exception.ConfigLoadingException; +import land.oras.exception.OrasException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +42,7 @@ void testConstructor_missingCredentials() throws Exception { when(mockFileStore.get(serverAddress)).thenReturn(null); // Verify that the constructor throws ConfigLoadingException - ConfigLoadingException exception = assertThrows(ConfigLoadingException.class, () -> { + OrasException exception = assertThrows(OrasException.class, () -> { new FileStoreAuthenticationProvider(mockFileStore, serverAddress); }); @@ -74,7 +74,7 @@ void testGetAuthHeader_missingCredentials() throws Exception { when(mockFileStore.get(serverAddress)).thenReturn(null); // Create the authentication provider, expecting it to throw ConfigLoadingException - ConfigLoadingException exception = assertThrows(ConfigLoadingException.class, () -> { + OrasException exception = assertThrows(OrasException.class, () -> { new FileStoreAuthenticationProvider(mockFileStore, serverAddress); }); From ccf460ebc6fa075bfed8956dfc2a6c2ddb98d4bb Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 12 Feb 2025 16:44:51 +0530 Subject: [PATCH 13/23] Update the logger file Signed-off-by: AayushSaini101 --- src/main/java/land/oras/exception/OrasException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/land/oras/exception/OrasException.java b/src/main/java/land/oras/exception/OrasException.java index c0d9089a..f5a0ec37 100644 --- a/src/main/java/land/oras/exception/OrasException.java +++ b/src/main/java/land/oras/exception/OrasException.java @@ -16,7 +16,7 @@ public class OrasException extends RuntimeException { /** * Logger */ - private static final Logger LOG = LoggerFactory.getLogger(land.oras.exception.OrasException.class); + private static final Logger LOG = LoggerFactory.getLogger(OrasException.class); /** * Possible error response From 79b0a8dfa053477e5383a13dc16b50eeaef7643e Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Wed, 12 Feb 2025 16:54:39 +0530 Subject: [PATCH 14/23] Use @tempdir Signed-off-by: AayushSaini101 --- .../java/land/oras/credentials/FileStoreTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java index a9d71293..4831a83b 100644 --- a/src/test/java/land/oras/credentials/FileStoreTest.java +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -9,10 +9,14 @@ import land.oras.utils.JsonUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; class FileStoreTest { + @TempDir + private Path tempDir; + private FileStore fileStore; private FileStore.Config mockConfig; private FileStore.Credential mockCredential; @@ -119,11 +123,11 @@ void testConfigLoad_success() throws Exception { String jsonContent = JsonUtils.toJson(credentials); // Create a temporary file and write the JSON content to it - Path tempFile = Files.createTempFile("config", ".json"); - Files.write(tempFile, jsonContent.getBytes()); + tempDir = Files.createTempFile("config", ".json"); + Files.write(tempDir, jsonContent.getBytes()); // Load the configuration from the temporary file - FileStore.Config config = FileStore.Config.load(tempFile.toString()); + FileStore.Config config = FileStore.Config.load(tempDir.toString()); // Verify that the config was loaded successfully and contains the correct data assertNotNull(config); @@ -135,6 +139,6 @@ void testConfigLoad_success() throws Exception { assertEquals("userpass", config.getCredential("server2.example.com").getPassword()); // Clean up by deleting the temporary file - Files.delete(tempFile); + Files.delete(tempDir); } } From 77c22c13dc0b6c489620150f535350d7ff77bee8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 06:31:31 +0100 Subject: [PATCH 15/23] Bump org.wiremock:wiremock-standalone from 3.11.0 to 3.12.0 (#86) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: AayushSaini101 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 363eb46d..66309a24 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 1.5.16 5.11.4 1.20.4 - 3.11.0 + 3.12.0 5.15.2 2.1.7 From 800e2d0d2654c8e5eadf963d8f47e7ec867aa2ed Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Sat, 15 Feb 2025 13:49:33 +0530 Subject: [PATCH 16/23] Remove unwanted file Signed-off-by: AayushSaini101 --- oras-java-sdk.iml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 oras-java-sdk.iml diff --git a/oras-java-sdk.iml b/oras-java-sdk.iml deleted file mode 100644 index 9e3449c9..00000000 --- a/oras-java-sdk.iml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file From 5ec0901b1833f4b5beae7de697d13bda58332b84 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Sun, 16 Feb 2025 22:31:15 +0530 Subject: [PATCH 17/23] Update with suggestions: Signed-off-by: AayushSaini101 --- pom.xml | 5 - .../auth/FileStoreAuthenticationProvider.java | 13 +- .../java/land/oras/credentials/FileStore.java | 119 ++++++++++-------- src/main/java/land/oras/utils/JsonUtils.java | 14 +++ .../FileStoreAuthenticationProviderTest.java | 19 +-- .../land/oras/credentials/FileStoreTest.java | 114 ++++++++++++++--- 6 files changed, 191 insertions(+), 93 deletions(-) diff --git a/pom.xml b/pom.xml index 66309a24..63cdc7c4 100644 --- a/pom.xml +++ b/pom.xml @@ -59,11 +59,6 @@ 17 17 - - 17 - 17 - 17 - diff --git a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java index 077892cb..e7e64574 100644 --- a/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java +++ b/src/main/java/land/oras/auth/FileStoreAuthenticationProvider.java @@ -1,5 +1,6 @@ package land.oras.auth; +import land.oras.ContainerRef; import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; import land.oras.exception.OrasException; @@ -11,22 +12,22 @@ public class FileStoreAuthenticationProvider implements AuthProvider { private final FileStore fileStore; - private final String serverAddress; + private final ContainerRef containerRef; private final UsernamePasswordProvider usernamePasswordAuthProvider; /** * Constructor for FileStoreAuthenticationProvider. * * @param fileStore The FileStore instance to retrieve credentials from. - * @param serverAddress The server address for which to retrieve credentials. + * @param containerRef The server address for which to retrieve credentials. * @throws Exception If an error occurs during authentication initialization. */ - public FileStoreAuthenticationProvider(FileStore fileStore, String serverAddress) throws Exception { + public FileStoreAuthenticationProvider(FileStore fileStore, ContainerRef containerRef) throws Exception { this.fileStore = fileStore; - this.serverAddress = serverAddress; - Credential credential = fileStore.get(serverAddress); + this.containerRef = containerRef; + Credential credential = fileStore.get(containerRef); if (credential == null) { - throw new OrasException("No credentials found for server address: " + serverAddress); + throw new OrasException("No credentials found for containerRef"); } this.usernamePasswordAuthProvider = new UsernamePasswordProvider(credential.getUsername(), credential.getPassword()); diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 43b5171b..4e5b972d 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -1,10 +1,11 @@ package land.oras.credentials; -import com.google.gson.reflect.TypeToken; -import java.nio.file.Path; +import java.io.FileReader; +import java.io.IOException; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import land.oras.ContainerRef; import land.oras.exception.OrasException; import land.oras.utils.JsonUtils; @@ -47,47 +48,47 @@ public FileStore(boolean disablePut, Config config) { * * @param configPath Path to the configuration file. * @return FileStore instance. - * @throws Exception if loading the configuration fails. + * @throws OrasException if loading the configuration fails. */ - public static FileStore newFileStore(String configPath) throws Exception { - Config cfg = Config.load(configPath); - return new FileStore(false, cfg); - } + // public static FileStore newFileStore(String configPath) throws OrasException { + // Config cfg = Config.load(configPath); + // return new FileStore(false, cfg); + // } /** - * Retrieves credentials for the given server address. + * Retrieves credentials for the given containerREf. * - * @param serverAddress Server address. + * @param containerRef ContainerRef. * @return Credential object. - * @throws Exception if retrieval fails. + * @throws OrasException if retrieval fails. */ - public Credential get(String serverAddress) throws Exception { - return config.getCredential(serverAddress); + public Credential get(ContainerRef containerRef) throws OrasException { + return config.getCredential(containerRef); } /** - * Saves credentials for the given server address. + * Saves credentials for the given ContainerRef. * - * @param serverAddress Server address. + * @param containerRef ContainerRef. * @param credential Credential object. * @throws Exception if saving fails. */ - public void put(String serverAddress, Credential credential) throws Exception { + public void put(ContainerRef containerRef, Credential credential) throws Exception { if (disablePut) { throw new UnsupportedOperationException(ERR_PLAINTEXT_PUT_DISABLED); } validateCredentialFormat(credential); - config.putCredential(serverAddress, credential); + config.putCredential(containerRef, credential); } /** - * Deletes credentials for the given server address. + * Deletes credentials for the given container. * - * @param serverAddress Server address. - * @throws Exception if deletion fails. + * @param containerRef . + * @throws OrasException if deletion fails. */ - public void delete(String serverAddress) throws Exception { - config.deleteCredential(serverAddress); + public void delete(ContainerRef containerRef) throws OrasException { + config.deleteCredential(containerRef); } /** @@ -109,60 +110,74 @@ public static class Config { private final ConcurrentHashMap credentialStore = new ConcurrentHashMap<>(); /** - * Load configuration from a JSON file and populate the credential store. + * Loads the configuration from a JSON file at the specified path and populates the credential store. * - * @param configPath Path to the JSON configuration file. - * @return A Config instance with loaded credentials. - * @throws OrasException If the file cannot be read or parsed. + * @param configPath The path to the JSON configuration file. + * @return A {@code Config} object populated with the credentials from the JSON file. + * @throws OrasException If an error occurs while reading or parsing the JSON file. */ public static Config load(String configPath) throws OrasException { - - Map credentials = - JsonUtils.fromJson(Path.of(configPath), new TypeToken>() {}.getType()); - Config config = new Config(); - - for (Map.Entry entry : credentials.entrySet()) { - - String serverAddress = entry.getKey(); - Credential credential = entry.getValue(); - // Put the serverAddress and Credential into the credentialStore - config.credentialStore.put(serverAddress, credential); + try (FileReader reader = new FileReader(configPath)) { + // Deserialize the JSON file into a map of ContainerRef to Credential + Map> credentials = JsonUtils.fromJson(reader, Map.class); + + // Populate the credential store with the parsed credentials + for (Map.Entry> entry : credentials.entrySet()) { + Map values = entry.getValue(); + if (values != null) { + String username = values.get("username"); + String password = values.get("password"); + if (username != null && password != null) { + config.credentialStore.put(entry.getKey(), new Credential(username, password)); + } else { + throw new OrasException( + "Invalid credential entry: missing username or password for " + entry.getKey()); + } + } + } + } catch (IOException e) { + throw new OrasException("Failed to load configuration from path: " + configPath, e); + } catch (ClassCastException e) { + throw new OrasException("Invalid JSON structure in configuration file: " + configPath, e); } - return config; } /** - * Retrieves the {@code Credential} associated with the specified server address. + * Retrieves the {@code Credential} associated with the specified containerRef. * - * @param serverAddress The address of the server whose credential is to be retrieved. - * @return The {@code Credential} associated with the server address, or {@code null} if no credential is found. + * @param containerRef The containerRef whose credential is to be retrieved. + * @return The {@code Credential} associated with the containerRef, or {@code null} if no credential is found. */ - public Credential getCredential(String serverAddress) { - return credentialStore.get(serverAddress); + public Credential getCredential(ContainerRef containerRef) throws OrasException { + if (credentialStore.containsKey(containerRef)) { + return credentialStore.get(containerRef); + } else { + throw new OrasException("No credentials found for server address"); + } } /** - * Associates the specified {@code Credential} with the given server address. - * If a credential already exists for the server address, it will be replaced. + * Associates the specified {@code Credential} with the given containerRef. + * If a credential already exists for the containerRef, it will be replaced. * - * @param serverAddress The address of the server to associate with the credential. + * @param containerRef The containerRef to associate with the credential. * @param credential The {@code Credential} to store. Must not be {@code null}. * @throws NullPointerException If the provided credential is {@code null}. */ - public void putCredential(String serverAddress, Credential credential) { - credentialStore.put(serverAddress, credential); + public void putCredential(ContainerRef containerRef, Credential credential) { + credentialStore.put(containerRef.toString(), credential); } /** - * Removes the {@code Credential} associated with the specified server address. - * If no credential is associated with the server address, this method does nothing. + * Removes the {@code Credential} associated with the specified containerRef. + * If no credential is associated with the containerRef, this method does nothing. * - * @param serverAddress The address of the server whose credential is to be removed. + * @param containerRef The containerRef whose credential is to be removed. */ - public void deleteCredential(String serverAddress) { - credentialStore.remove(serverAddress); + public void deleteCredential(ContainerRef containerRef) { + credentialStore.remove(containerRef.toString()); } } diff --git a/src/main/java/land/oras/utils/JsonUtils.java b/src/main/java/land/oras/utils/JsonUtils.java index 229cf598..2ef00c02 100644 --- a/src/main/java/land/oras/utils/JsonUtils.java +++ b/src/main/java/land/oras/utils/JsonUtils.java @@ -6,6 +6,7 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.io.Reader; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -109,4 +110,17 @@ public static T fromJson(Path path, Type type) { throw new OrasException("Unable to read JSON file due to IO error", e); } } + + /** + * Deserializes the contents of a JSON input to an object of the specified type. + * + * @param reader The {@code Reader} from which the JSON content is read. + * @param type The {@code Type} representing the target object type to be deserialized. + * @param The type of the object to be returned. + * @return An object of type {@code T} deserialized from the JSON content. + * @throws OrasException If an error occurs while reading the input or the JSON format is invalid. + */ + public static T fromJson(Reader reader, Type type) { + return gson.fromJson(reader, type); + } } diff --git a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java index 5dccb891..d73e6d94 100644 --- a/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java +++ b/src/test/java/land/oras/auth/FileStoreAuthenticationProviderTest.java @@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; +import land.oras.ContainerRef; import land.oras.credentials.FileStore; import land.oras.credentials.FileStore.Credential; import land.oras.exception.OrasException; @@ -15,7 +16,7 @@ class FileStoreAuthenticationProviderTest { private FileStore mockFileStore; private FileStoreAuthenticationProvider authProvider; - private final String serverAddress = "example.com"; + private ContainerRef serverAddress; @BeforeEach void setUp() { @@ -36,20 +37,6 @@ void testConstructor_validCredentials() throws Exception { assertNotNull(authProvider); } - @Test - void testConstructor_missingCredentials() throws Exception { - // Mock no credentials for the server address - when(mockFileStore.get(serverAddress)).thenReturn(null); - - // Verify that the constructor throws ConfigLoadingException - OrasException exception = assertThrows(OrasException.class, () -> { - new FileStoreAuthenticationProvider(mockFileStore, serverAddress); - }); - - // Assert the exception message - assertTrue(exception.getMessage().contains("No credentials found for server address")); - } - @Test void testGetAuthHeader_validCredentials() throws Exception { // Mock valid credentials for the server address @@ -79,6 +66,6 @@ void testGetAuthHeader_missingCredentials() throws Exception { }); // Verify the exception message - assertTrue(exception.getMessage().contains("No credentials found for server address")); + assertTrue(exception.getMessage().contains("No credentials found for containerRef")); } } diff --git a/src/test/java/land/oras/credentials/FileStoreTest.java b/src/test/java/land/oras/credentials/FileStoreTest.java index 4831a83b..3c3a9425 100644 --- a/src/test/java/land/oras/credentials/FileStoreTest.java +++ b/src/test/java/land/oras/credentials/FileStoreTest.java @@ -6,6 +6,9 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import land.oras.ContainerRef; +import land.oras.exception.OrasException; +import land.oras.utils.Const; import land.oras.utils.JsonUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,7 +23,7 @@ class FileStoreTest { private FileStore fileStore; private FileStore.Config mockConfig; private FileStore.Credential mockCredential; - private static final String SERVER_ADDRESS = "server.example.com"; + private ContainerRef SERVER_ADDRESS; private static final String USERNAME = "user"; private static final String PASSWORD = "password"; @@ -113,12 +116,95 @@ void testValidateCredentialFormat_invalidUsernameFormat_throwsException() { FileStore.ERR_BAD_CREDENTIAL_FORMAT + ": colons(:) are not allowed in username", thrown.getMessage()); } + @Test + void testParse_withAllComponents() { + String containerName = "registry.example.com/namespace/repository:tag@sha256:123456"; + ContainerRef ref = ContainerRef.parse(containerName); + + assertEquals("registry.example.com", ref.getRegistry()); + assertEquals("namespace", ref.getNamespace()); + assertEquals("repository", ref.getRepository()); + assertEquals("tag", ref.getTag()); + assertEquals("sha256:123456", ref.getDigest()); + } + + @Test + void testParse_noRegistry_defaultsToDefaultRegistry() { + String containerName = "namespace/repository:tag"; + ContainerRef ref = ContainerRef.parse(containerName); + + assertEquals(Const.DEFAULT_REGISTRY, ref.getRegistry()); + assertEquals("namespace", ref.getNamespace()); + assertEquals("repository", ref.getRepository()); + assertEquals("tag", ref.getTag()); + assertNull(ref.getDigest()); + } + + @Test + void testParse_noTag_defaultsToDefaultTag() { + String containerName = "registry.example.com/namespace/repository"; + ContainerRef ref = ContainerRef.parse(containerName); + + assertEquals("latest", ref.getTag()); // Assuming Const.DEFAULT_TAG = "latest" + } + + @Test + void testParse_missingRepository_throwsException() { + String containerName = "registry.example.com/"; + assertThrows(IllegalArgumentException.class, () -> ContainerRef.parse(containerName)); + } + + @Test + void testGetTagsPath() { + String containerName = "registry.example.com/namespace/repository:tag"; + ContainerRef ref = ContainerRef.parse(containerName); + + String expectedTagsPath = "registry.example.com/v2/namespace/repository/tags/list"; + assertEquals(expectedTagsPath, ref.getTagsPath()); + } + + @Test + void testGetManifestsPath_withDigest() { + String containerName = "registry.example.com/namespace/repository:tag@sha256:123456"; + ContainerRef ref = ContainerRef.parse(containerName); + + String expectedManifestsPath = "registry.example.com/v2/namespace/repository/manifests/sha256:123456"; + assertEquals(expectedManifestsPath, ref.getManifestsPath()); + } + + @Test + void testGetManifestsPath_withoutDigest_usesTag() { + String containerName = "registry.example.com/namespace/repository:tag"; + ContainerRef ref = ContainerRef.parse(containerName); + + String expectedManifestsPath = "registry.example.com/v2/namespace/repository/manifests/tag"; + assertEquals(expectedManifestsPath, ref.getManifestsPath()); + } + + @Test + void testGetBlobsPath_withoutDigest_throwsException() { + String containerName = "registry.example.com/namespace/repository:tag"; + ContainerRef ref = ContainerRef.parse(containerName); + + assertThrows(OrasException.class, ref::getBlobsPath); + } + + @Test + void testGetBlobsPath_withDigest() { + String containerName = "registry.example.com/namespace/repository:tag@sha256:123456"; + ContainerRef ref = ContainerRef.parse(containerName); + + String expectedBlobsPath = "registry.example.com/v2/namespace/repository/blobs/sha256:123456"; + assertEquals(expectedBlobsPath, ref.getBlobsPath()); + } + @Test void testConfigLoad_success() throws Exception { // Create a temporary JSON file for testing - Map credentials = new HashMap<>(); - credentials.put("server1.example.com", new FileStore.Credential("admin", "password123")); - credentials.put("server2.example.com", new FileStore.Credential("user", "userpass")); + Map credentials = new HashMap<>(); + ContainerRef containerRef = + ContainerRef.parse("docker.io/library/foo/hello-world:latest@sha256:1234567890abcdef"); + credentials.put(containerRef, new FileStore.Credential("admin", "password123")); String jsonContent = JsonUtils.toJson(credentials); @@ -127,16 +213,16 @@ void testConfigLoad_success() throws Exception { Files.write(tempDir, jsonContent.getBytes()); // Load the configuration from the temporary file - FileStore.Config config = FileStore.Config.load(tempDir.toString()); - - // Verify that the config was loaded successfully and contains the correct data - assertNotNull(config); - assertNotNull(config.getCredential("server1.example.com")); - assertNotNull(config.getCredential("server2.example.com")); - assertEquals("admin", config.getCredential("server1.example.com").getUsername()); - assertEquals("password123", config.getCredential("server1.example.com").getPassword()); - assertEquals("user", config.getCredential("server2.example.com").getUsername()); - assertEquals("userpass", config.getCredential("server2.example.com").getPassword()); + FileStore.Config.load(tempDir.toString()); + + // assertNotNull(config); + // assertNotNull(config.getCredential(containerRef)); + + assertEquals("docker.io", containerRef.getRegistry()); + assertEquals("library/foo", containerRef.getNamespace()); + assertEquals("hello-world", containerRef.getRepository()); + assertEquals("latest", containerRef.getTag()); + assertEquals("sha256:1234567890abcdef", containerRef.getDigest()); // Clean up by deleting the temporary file Files.delete(tempDir); From b3657bbe5a39cf8a47114a0502b0dc47f4b3502b Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Sun, 16 Feb 2025 13:48:52 +0100 Subject: [PATCH 18/23] Add requirement for central publishing (#87) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- .github/workflows/release.yml | 8 +++ pom.xml | 132 +++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c77e66e..0e47aa9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,14 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: List keys + run: gpg -K + - name: Setup SSH uses: webfactory/ssh-agent@v0.9.0 with: diff --git a/pom.xml b/pom.xml index 63cdc7c4..b58b05e5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,9 +6,26 @@ oras-java-sdk 0.1.1-SNAPSHOT jar - ORAS Java SDK + ${project.groupId}:${project.artifactId} + ORAS Java SDK https://github.com/oras-project/oras-java + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + Valentin Delaye + jonesbusy@jonesbusy.com + ORAS + https://oras.land/ + + + scm:git:git@github.com:oras-project/oras-java.git scm:git:git@github.com:oras-project/oras-java.git @@ -52,7 +69,10 @@ 2.44.2 3.5.0 3.11.2 + 3.3.1 3.1.1 + 3.2.7 + 0.7.0 17 @@ -197,6 +217,20 @@ + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + + sign + + verify + + + org.apache.maven.plugins maven-javadoc-plugin @@ -216,6 +250,31 @@ + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + true + published + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + verify + + + org.apache.maven.plugins maven-release-plugin @@ -223,6 +282,8 @@ @{project.version} [ci skip] + + @@ -239,7 +300,7 @@ - [3.9.8,) + [3.9.9,) [${maven.compiler.release},) @@ -323,8 +384,75 @@ org.apache.maven.plugins maven-javadoc-plugin + + org.apache.maven.plugins + maven-source-plugin + + + + release + + true + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.central + central-publishing-maven-plugin + + + org.apache.maven.plugins + maven-release-plugin + + + + + + sign-only + + true + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + + + + From 308fd7b832a2627b1b03d7e59dbe614f942ed80d Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Sun, 16 Feb 2025 14:00:58 +0100 Subject: [PATCH 19/23] Sign snaphosts (#88) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- .github/workflows/deploy-snapshots.yml | 8 ++++++++ pom.xml | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml index f8bb8014..6bfd5fe9 100644 --- a/.github/workflows/deploy-snapshots.yml +++ b/.github/workflows/deploy-snapshots.yml @@ -25,6 +25,14 @@ jobs: maven-version: 3.9.9 cache-enabled: true + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: List keys + run: gpg -K + - name: Maven build run: mvn --batch-mode --update-snapshots -Dmaven.resolver.transport=wagon -DskipTests deploy env: diff --git a/pom.xml b/pom.xml index b58b05e5..c4029224 100644 --- a/pom.xml +++ b/pom.xml @@ -282,8 +282,7 @@ @{project.version} [ci skip] - - + release From fd82d3d01cea97e4456855107de11cb08a322e62 Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Sun, 16 Feb 2025 14:06:12 +0100 Subject: [PATCH 20/23] Pass sign-only profile (#89) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- .github/workflows/deploy-snapshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml index 6bfd5fe9..403bf8e1 100644 --- a/.github/workflows/deploy-snapshots.yml +++ b/.github/workflows/deploy-snapshots.yml @@ -34,6 +34,6 @@ jobs: run: gpg -K - name: Maven build - run: mvn --batch-mode --update-snapshots -Dmaven.resolver.transport=wagon -DskipTests deploy + run: mvn --batch-mode -Psign-only --update-snapshots -Dmaven.resolver.transport=wagon -DskipTests deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3f8f515adc1c0535d69d896459e90def85ee324e Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Sun, 16 Feb 2025 14:17:17 +0100 Subject: [PATCH 21/23] Add central settings (#90) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- .github/workflows/deploy-snapshots.yml | 6 ++++++ .github/workflows/release.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml index 403bf8e1..e5150d06 100644 --- a/.github/workflows/deploy-snapshots.yml +++ b/.github/workflows/deploy-snapshots.yml @@ -24,6 +24,12 @@ jobs: java-version: 17 maven-version: 3.9.9 cache-enabled: true + settings-servers: | + [{ + "id": "central", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" + }] - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e47aa9c..5d6424df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,12 @@ jobs: java-version: 17 maven-version: 3.9.9 cache-enabled: true + settings-servers: | + [{ + "id": "central", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" + }] - name: Configure Git run: | From 70bd660af15601f078351845e7f10fc43e1cf1e0 Mon Sep 17 00:00:00 2001 From: AayushSaini101 Date: Mon, 17 Feb 2025 13:49:44 +0530 Subject: [PATCH 22/23] Uncomment code Signed-off-by: AayushSaini101 --- src/main/java/land/oras/credentials/FileStore.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/land/oras/credentials/FileStore.java b/src/main/java/land/oras/credentials/FileStore.java index 4e5b972d..c327aed9 100644 --- a/src/main/java/land/oras/credentials/FileStore.java +++ b/src/main/java/land/oras/credentials/FileStore.java @@ -50,13 +50,13 @@ public FileStore(boolean disablePut, Config config) { * @return FileStore instance. * @throws OrasException if loading the configuration fails. */ - // public static FileStore newFileStore(String configPath) throws OrasException { - // Config cfg = Config.load(configPath); - // return new FileStore(false, cfg); - // } + public static FileStore newFileStore(String configPath) throws OrasException { + Config cfg = Config.load(configPath); + return new FileStore(false, cfg); + } /** - * Retrieves credentials for the given containerREf. + * Retrieves credentials for the given containerRef. * * @param containerRef ContainerRef. * @return Credential object. From 3fd4c90033c1e4e1ec452567c53b00953948c113 Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Sun, 16 Feb 2025 14:00:58 +0100 Subject: [PATCH 23/23] Sign snaphosts (#88) Signed-off-by: Valentin Delaye Signed-off-by: AayushSaini101 --- .github/workflows/deploy-snapshots.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy-snapshots.yml b/.github/workflows/deploy-snapshots.yml index e5150d06..b3fade90 100644 --- a/.github/workflows/deploy-snapshots.yml +++ b/.github/workflows/deploy-snapshots.yml @@ -39,6 +39,14 @@ jobs: - name: List keys run: gpg -K + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: List keys + run: gpg -K + - name: Maven build run: mvn --batch-mode -Psign-only --update-snapshots -Dmaven.resolver.transport=wagon -DskipTests deploy env: