diff --git a/server/src/main/java/org/opensearch/OpenSearchServerException.java b/server/src/main/java/org/opensearch/OpenSearchServerException.java index 825197c07b977..fd27f1091baae 100644 --- a/server/src/main/java/org/opensearch/OpenSearchServerException.java +++ b/server/src/main/java/org/opensearch/OpenSearchServerException.java @@ -1196,6 +1196,14 @@ public static void registerExceptions() { V_2_7_0 ) ); + registerExceptionHandle( + new OpenSearchExceptionHandle( + org.opensearch.crypto.CryptoClientMissingException.class, + org.opensearch.crypto.CryptoClientMissingException::new, + 171, + V_3_0_0 + ) + ); registerExceptionHandle( new OpenSearchExceptionHandle( org.opensearch.cluster.block.IndexCreateBlockException.class, diff --git a/server/src/main/java/org/opensearch/action/admin/cluster/crypto/CryptoSettings.java b/server/src/main/java/org/opensearch/action/admin/cluster/crypto/CryptoSettings.java new file mode 100644 index 0000000000000..45a0a01b8441d --- /dev/null +++ b/server/src/main/java/org/opensearch/action/admin/cluster/crypto/CryptoSettings.java @@ -0,0 +1,182 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.action.admin.cluster.crypto; + +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; + +import static org.opensearch.action.ValidateActions.addValidationError; +import static org.opensearch.common.settings.Settings.Builder.EMPTY_SETTINGS; +import static org.opensearch.common.settings.Settings.readSettingsFromStream; +import static org.opensearch.common.settings.Settings.writeSettingsToStream; + +/** + * Crypto settings supplied during a put repository request + * + * @opensearch.internal + */ +public class CryptoSettings implements Writeable, ToXContentObject { + + private String keyProviderName; + private String keyProviderType; + private Settings settings = EMPTY_SETTINGS; + + public CryptoSettings(StreamInput in) throws IOException { + keyProviderName = in.readString(); + keyProviderType = in.readString(); + settings = readSettingsFromStream(in); + } + + public CryptoSettings(String keyProviderName) { + this.keyProviderName = keyProviderName; + } + + /** + * Validate settings supplied in put repository request. + * @return Exception in case validation fails. + */ + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (keyProviderName == null) { + validationException = addValidationError("key_provider_name is missing", validationException); + } + if (keyProviderType == null) { + validationException = addValidationError("key_provider_type is missing", validationException); + } + return validationException; + } + + /** + * Returns key provider name + * @return keyProviderName + */ + public String getKeyProviderName() { + return keyProviderName; + } + + /** + * Returns key provider type + * @return keyProviderType + */ + public String getKeyProviderType() { + return keyProviderType; + } + + /** + * Returns crypto settings + * @return settings + */ + public Settings getSettings() { + return settings; + } + + /** + * Constructs a new crypto settings with provided key provider name. + * @param keyProviderName Name of the key provider + */ + public CryptoSettings keyProviderName(String keyProviderName) { + this.keyProviderName = keyProviderName; + return this; + } + + /** + * Constructs a new crypto settings with provided key provider type. + * @param keyProviderType Type of key provider to be used in encryption. + */ + public CryptoSettings keyProviderType(String keyProviderType) { + this.keyProviderType = keyProviderType; + return this; + } + + /** + * Sets the encryption settings + * + * @param settings for encryption + * @return this request + */ + public CryptoSettings settings(Settings.Builder settings) { + this.settings = settings.build(); + return this; + } + + /** + * Sets the encryption settings. + * + * @param source encryption settings in json or yaml format + * @param xContentType the content type of the source + * @return this request + */ + public CryptoSettings settings(String source, XContentType xContentType) { + this.settings = Settings.builder().loadFromSource(source, xContentType).build(); + return this; + } + + /** + * Sets the encryption settings. + * + * @param source encryption settings + * @return this request + */ + public CryptoSettings settings(Map source) { + this.settings = Settings.builder().loadFromMap(source).build(); + return this; + } + + /** + * Parses crypto settings definition. + * + * @param cryptoDefinition crypto settings definition + */ + public CryptoSettings(Map cryptoDefinition) { + for (Map.Entry entry : cryptoDefinition.entrySet()) { + if (entry.getKey().equals("key_provider_name")) { + keyProviderName(entry.getValue().toString()); + } else if (entry.getKey().equals("key_provider_type")) { + keyProviderType(entry.getValue().toString()); + } else if (entry.getKey().equals("settings")) { + if (!(entry.getValue() instanceof Map)) { + throw new IllegalArgumentException("Malformed settings section in crypto settings, should include an inner object"); + } + @SuppressWarnings("unchecked") + Map sub = (Map) entry.getValue(); + settings(sub); + } + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(keyProviderName); + out.writeString(keyProviderType); + writeSettingsToStream(settings, out); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("key_provider_name", keyProviderName); + builder.field("key_provider_type", keyProviderType); + + builder.startObject("settings"); + settings.toXContent(builder, params); + builder.endObject(); + + builder.endObject(); + return builder; + } +} diff --git a/server/src/main/java/org/opensearch/action/admin/cluster/crypto/package-info.java b/server/src/main/java/org/opensearch/action/admin/cluster/crypto/package-info.java new file mode 100644 index 0000000000000..bb9375c20c87e --- /dev/null +++ b/server/src/main/java/org/opensearch/action/admin/cluster/crypto/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Crypto client request and settings handlers. + */ +package org.opensearch.action.admin.cluster.crypto; diff --git a/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequest.java b/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequest.java index d57fd04b30eaa..96c218cacc4db 100644 --- a/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequest.java +++ b/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequest.java @@ -32,7 +32,9 @@ package org.opensearch.action.admin.cluster.repositories.put; +import org.opensearch.Version; import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.action.admin.cluster.crypto.CryptoSettings; import org.opensearch.action.support.master.AcknowledgedRequest; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.io.stream.StreamOutput; @@ -48,6 +50,7 @@ import static org.opensearch.common.settings.Settings.readSettingsFromStream; import static org.opensearch.common.settings.Settings.writeSettingsToStream; import static org.opensearch.common.settings.Settings.Builder.EMPTY_SETTINGS; +import static org.opensearch.common.xcontent.support.XContentMapValues.nodeBooleanValue; /** * Register repository request. @@ -67,12 +70,22 @@ public class PutRepositoryRequest extends AcknowledgedRequest repositoryDefinition) { @SuppressWarnings("unchecked") Map sub = (Map) entry.getValue(); settings(sub); + } else if (name.equals("encrypted")) { + encrypted(nodeBooleanValue(entry.getValue(), "encrypted")); + } else if (name.equals("crypto_settings")) { + if (!(entry.getValue() instanceof Map)) { + throw new IllegalArgumentException("Malformed encryption_settings section, should include an inner object"); + } + @SuppressWarnings("unchecked") + Map sub = (Map) entry.getValue(); + CryptoSettings cryptoSettings = new CryptoSettings(sub); + cryptoSettings(cryptoSettings); } } return this; @@ -236,6 +301,12 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(type); writeSettingsToStream(settings, out); out.writeBoolean(verify); + if (out.getVersion().onOrAfter(Version.V_3_0_0)) { + out.writeOptionalBoolean(encrypted); + if (Boolean.TRUE.equals(encrypted)) { + cryptoSettings.writeTo(out); + } + } } @Override @@ -249,6 +320,16 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.endObject(); builder.field("verify", verify); + + if (null != encrypted) { + builder.field("encrypted", encrypted); + if (encrypted == true) { + builder.startObject("crypto_settings"); + cryptoSettings.toXContent(builder, params); + builder.endObject(); + } + } + builder.endObject(); return builder; } diff --git a/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequestBuilder.java b/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequestBuilder.java index 6e1b2795b6375..1dafbf9681bc9 100644 --- a/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequestBuilder.java +++ b/server/src/main/java/org/opensearch/action/admin/cluster/repositories/put/PutRepositoryRequestBuilder.java @@ -32,6 +32,7 @@ package org.opensearch.action.admin.cluster.repositories.put; +import org.opensearch.action.admin.cluster.crypto.CryptoSettings; import org.opensearch.action.support.master.AcknowledgedRequestBuilder; import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.client.OpenSearchClient; @@ -141,4 +142,26 @@ public PutRepositoryRequestBuilder setVerify(boolean verify) { request.verify(verify); return this; } + + /** + * Sets whether repository data should be encrypted and stored. + * + * @param encrypted true if repository data should be encrypted and stored, false otherwise + * @return this builder + */ + public PutRepositoryRequestBuilder setEncrypted(Boolean encrypted) { + request.encrypted(encrypted); + return this; + } + + /** + * Sets the repository encryption settings + * + * @param cryptoSettings repository crypto settings builder + * @return this builder + */ + public PutRepositoryRequestBuilder setEncryptionSettings(CryptoSettings cryptoSettings) { + request.cryptoSettings(cryptoSettings); + return this; + } } diff --git a/server/src/main/java/org/opensearch/cluster/metadata/CryptoMetadata.java b/server/src/main/java/org/opensearch/cluster/metadata/CryptoMetadata.java new file mode 100644 index 0000000000000..51e50e135525e --- /dev/null +++ b/server/src/main/java/org/opensearch/cluster/metadata/CryptoMetadata.java @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.cluster.metadata; + +import org.opensearch.OpenSearchParseException; +import org.opensearch.action.admin.cluster.crypto.CryptoSettings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +/** + * Metadata about encryption and decryption + * + * @opensearch.internal + */ +public class CryptoMetadata implements Writeable { + private final String keyProviderName; + private final String keyProviderType; + private final Settings settings; + + /** + * Constructs new crypto metadata + * + * @param keyProviderName key provider name + * @param keyProviderType key provider type + * @param settings crypto settings + */ + public CryptoMetadata(String keyProviderName, String keyProviderType, Settings settings) { + this.keyProviderName = keyProviderName; + this.keyProviderType = keyProviderType; + this.settings = settings; + } + + /** + * Returns key provider name + * + * @return Key provider name + */ + public String keyProviderName() { + return this.keyProviderName; + } + + /** + * Returns key provider type + * + * @return key provider type + */ + public String keyProviderType() { + return this.keyProviderType; + } + + /** + * Returns crypto settings + * + * @return crypto settings + */ + public Settings settings() { + return this.settings; + } + + public CryptoMetadata(StreamInput in) throws IOException { + keyProviderName = in.readString(); + keyProviderType = in.readString(); + settings = Settings.readSettingsFromStream(in); + } + + public static CryptoMetadata fromRequest(CryptoSettings cryptoSettings) { + if (cryptoSettings == null) { + return null; + } + return new CryptoMetadata(cryptoSettings.getKeyProviderName(), cryptoSettings.getKeyProviderType(), cryptoSettings.getSettings()); + } + + /** + * Writes crypto metadata to stream output + * + * @param out stream output + */ + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(keyProviderName); + out.writeString(keyProviderType); + Settings.writeSettingsToStream(settings, out); + } + + public static CryptoMetadata fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + String keyProviderType = null; + Settings settings = null; + String keyProviderName = parser.currentName(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String currentFieldName = parser.currentName(); + if ("key_provider_name".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { + throw new OpenSearchParseException("failed to parse crypto metadata [{}], unknown type"); + } + keyProviderName = parser.text(); + } else if ("key_provider_type".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_STRING) { + throw new OpenSearchParseException("failed to parse crypto metadata [{}], unknown type"); + } + keyProviderType = parser.text(); + } else if ("settings".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new OpenSearchParseException("failed to parse crypto metadata [{}], unknown type"); + } + settings = Settings.fromXContent(parser); + } else { + throw new OpenSearchParseException("failed to parse crypto metadata, unknown field [{}]", currentFieldName); + } + } else { + throw new OpenSearchParseException("failed to parse repositories"); + } + } + return new CryptoMetadata(keyProviderName, keyProviderType, settings); + } + + public void toXContent(CryptoMetadata cryptoMetadata, XContentBuilder builder, ToXContent.Params params) throws IOException { + builder.startObject("crypto_metadata"); + builder.field("key_provider_name", cryptoMetadata.keyProviderName()); + builder.field("key_provider_type", cryptoMetadata.keyProviderType()); + builder.startObject("settings"); + cryptoMetadata.settings().toXContent(builder, params); + builder.endObject(); + builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CryptoMetadata that = (CryptoMetadata) o; + + if (!keyProviderName.equals(that.keyProviderName)) return false; + if (!keyProviderType.equals(that.keyProviderType)) return false; + return settings.equals(that.settings); + } + + @Override + public int hashCode() { + return Objects.hash(keyProviderName, keyProviderType, settings); + } + + @Override + public String toString() { + return "CryptoMetadata{" + keyProviderName + "}{" + keyProviderType + "}{" + settings + "}"; + } +} diff --git a/server/src/main/java/org/opensearch/cluster/metadata/RepositoriesMetadata.java b/server/src/main/java/org/opensearch/cluster/metadata/RepositoriesMetadata.java index 06b97fdd31848..1ec1343c1d1ce 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/RepositoriesMetadata.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/RepositoriesMetadata.java @@ -208,6 +208,8 @@ public static RepositoriesMetadata fromXContent(XContentParser parser) throws IO Settings settings = Settings.EMPTY; long generation = RepositoryData.UNKNOWN_REPO_GEN; long pendingGeneration = RepositoryData.EMPTY_REPO_GEN; + Boolean encrypted = null; + CryptoMetadata cryptoMetadata = null; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { String currentFieldName = parser.currentName(); @@ -231,6 +233,16 @@ public static RepositoriesMetadata fromXContent(XContentParser parser) throws IO throw new OpenSearchParseException("failed to parse repository [{}], unknown type", name); } pendingGeneration = parser.longValue(); + } else if ("encrypted".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.VALUE_BOOLEAN) { + throw new OpenSearchParseException("failed to parse repository [{}], unknown type", name); + } + encrypted = parser.booleanValue(); + } else if ("crypto_metadata".equals(currentFieldName)) { + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new OpenSearchParseException("failed to parse repository [{}], unknown type", name); + } + cryptoMetadata = CryptoMetadata.fromXContent(parser); } else { throw new OpenSearchParseException( "failed to parse repository [{}], unknown field [{}]", @@ -245,7 +257,7 @@ public static RepositoriesMetadata fromXContent(XContentParser parser) throws IO if (type == null) { throw new OpenSearchParseException("failed to parse repository [{}], missing repository type", name); } - repository.add(new RepositoryMetadata(name, type, settings, generation, pendingGeneration)); + repository.add(new RepositoryMetadata(name, type, settings, generation, pendingGeneration, encrypted, cryptoMetadata)); } else { throw new OpenSearchParseException("failed to parse repositories"); } @@ -279,6 +291,10 @@ public EnumSet context() { public static void toXContent(RepositoryMetadata repository, XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(repository.name()); builder.field("type", repository.type()); + if (Boolean.TRUE.equals(repository.encrypted())) { + builder.field("encrypted", true); + repository.cryptoMetadata().toXContent(repository.cryptoMetadata(), builder, params); + } builder.startObject("settings"); repository.settings().toXContent(builder, params); builder.endObject(); diff --git a/server/src/main/java/org/opensearch/cluster/metadata/RepositoryMetadata.java b/server/src/main/java/org/opensearch/cluster/metadata/RepositoryMetadata.java index db4e5d8137a20..00321b83f1d3a 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/RepositoryMetadata.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/RepositoryMetadata.java @@ -31,6 +31,7 @@ package org.opensearch.cluster.metadata; +import org.opensearch.Version; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.io.stream.StreamOutput; import org.opensearch.common.io.stream.Writeable; @@ -50,6 +51,8 @@ public class RepositoryMetadata implements Writeable { private final String name; private final String type; private final Settings settings; + private final Boolean encrypted; + private final CryptoMetadata cryptoMetadata; /** * Safe repository generation. @@ -69,14 +72,30 @@ public class RepositoryMetadata implements Writeable { * @param settings repository settings */ public RepositoryMetadata(String name, String type, Settings settings) { - this(name, type, settings, RepositoryData.UNKNOWN_REPO_GEN, RepositoryData.EMPTY_REPO_GEN); + this(name, type, settings, RepositoryData.UNKNOWN_REPO_GEN, RepositoryData.EMPTY_REPO_GEN, false, null); + } + + public RepositoryMetadata(String name, String type, Settings settings, Boolean encrypted, CryptoMetadata cryptoMetadata) { + this(name, type, settings, RepositoryData.UNKNOWN_REPO_GEN, RepositoryData.EMPTY_REPO_GEN, encrypted, cryptoMetadata); } public RepositoryMetadata(RepositoryMetadata metadata, long generation, long pendingGeneration) { - this(metadata.name, metadata.type, metadata.settings, generation, pendingGeneration); + this(metadata.name, metadata.type, metadata.settings, generation, pendingGeneration, metadata.encrypted, metadata.cryptoMetadata); } public RepositoryMetadata(String name, String type, Settings settings, long generation, long pendingGeneration) { + this(name, type, settings, generation, pendingGeneration, null, null); + } + + public RepositoryMetadata( + String name, + String type, + Settings settings, + long generation, + long pendingGeneration, + Boolean encrypted, + CryptoMetadata cryptoMetadata + ) { this.name = name; this.type = type; this.settings = settings; @@ -87,6 +106,8 @@ public RepositoryMetadata(String name, String type, Settings settings, long gene + "] must be greater or equal to generation [" + generation + "]"; + this.encrypted = encrypted; + this.cryptoMetadata = cryptoMetadata; } /** @@ -116,6 +137,24 @@ public Settings settings() { return this.settings; } + /** + * Returns whether repository is encrypted + * + * @return whether repository is encrypted + */ + public Boolean encrypted() { + return encrypted; + } + + /** + * Returns crypto metadata of repository + * + * @return crypto metadata of repository + */ + public CryptoMetadata cryptoMetadata() { + return this.cryptoMetadata; + } + /** * Returns the safe repository generation. {@link RepositoryData} for this generation is assumed to exist in the repository. * All operations on the repository must be based on the {@link RepositoryData} at this generation. @@ -146,6 +185,17 @@ public RepositoryMetadata(StreamInput in) throws IOException { settings = Settings.readSettingsFromStream(in); generation = in.readLong(); pendingGeneration = in.readLong(); + if (in.getVersion().onOrAfter(Version.V_3_0_0)) { + encrypted = in.readOptionalBoolean(); + if (Boolean.TRUE.equals(encrypted)) { + cryptoMetadata = new CryptoMetadata(in); + } else { + cryptoMetadata = null; + } + } else { + encrypted = null; + cryptoMetadata = null; + } } /** @@ -160,6 +210,12 @@ public void writeTo(StreamOutput out) throws IOException { Settings.writeSettingsToStream(settings, out); out.writeLong(generation); out.writeLong(pendingGeneration); + if (out.getVersion().onOrAfter(Version.V_3_0_0)) { + out.writeOptionalBoolean(encrypted); + if (Boolean.TRUE.equals(encrypted)) { + cryptoMetadata.writeTo(out); + } + } } /** @@ -169,7 +225,11 @@ public void writeTo(StreamOutput out) throws IOException { * @return {@code true} if both instances equal in all fields but the generation fields */ public boolean equalsIgnoreGenerations(RepositoryMetadata other) { - return name.equals(other.name) && type.equals(other.type()) && settings.equals(other.settings()); + return name.equals(other.name) + && type.equals(other.type()) + && settings.equals(other.settings()) + && encrypted == other.encrypted() + && Objects.equals(cryptoMetadata, other.cryptoMetadata()); } @Override @@ -183,16 +243,34 @@ public boolean equals(Object o) { if (!type.equals(that.type)) return false; if (generation != that.generation) return false; if (pendingGeneration != that.pendingGeneration) return false; - return settings.equals(that.settings); + if (!settings.equals(that.settings)) return false; + if (encrypted != that.encrypted) return false; + return Objects.equals(cryptoMetadata, that.cryptoMetadata); } @Override public int hashCode() { - return Objects.hash(name, type, settings, generation, pendingGeneration); + return Objects.hash(name, type, settings, generation, pendingGeneration, encrypted, cryptoMetadata); } @Override public String toString() { - return "RepositoryMetadata{" + name + "}{" + type + "}{" + settings + "}{" + generation + "}{" + pendingGeneration + "}"; + String toStr = "RepositoryMetadata{" + + name + + "}{" + + type + + "}{" + + settings + + "}{" + + generation + + "}{" + + pendingGeneration + + "}{" + + encrypted + + "}"; + if (Boolean.TRUE.equals(encrypted)) { + return toStr + "{" + cryptoMetadata + "}"; + } + return toStr; } } diff --git a/server/src/main/java/org/opensearch/crypto/CryptoClient.java b/server/src/main/java/org/opensearch/crypto/CryptoClient.java new file mode 100644 index 0000000000000..c088a0aa3130e --- /dev/null +++ b/server/src/main/java/org/opensearch/crypto/CryptoClient.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.crypto; + +import org.opensearch.common.io.InputStreamContainer; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.RefCounted; + +import java.io.InputStream; + +/** + * Crypto plugin interface used for encryption and decryption. + */ +public interface CryptoClient extends RefCounted { + + /** + * A factory interface for constructing crypto client. + * + * See {@link org.opensearch.plugins.CryptoPlugin}. + */ + interface Factory { + + /** + * Constructs a crypto client used for encryption and decryption + * + * @param cryptoSettings Settings needed for creating crypto client. + * @param keyProviderName Name of the key provider. + * @return instance of CryptoClient + */ + CryptoClient create(Settings cryptoSettings, String keyProviderName); + } + + /** + * @return key provider type + */ + String type(); + + /** + * @return key provider name + */ + String name(); + + /** + * To Initialise a crypto context used in encryption. This might be needed to set the context before beginning + * encryption. + * + * @return crypto context instance + */ + Object initCryptoContext(); + + /** + * In scenarios where content is divided into multiple parts and streams are emitted against each part, + * it is sometimes required to adjust the size of a part. For e.g. in case of frame encryption, content should + * line up exactly along frame boundaries. + * + * @param cryptoContextObj crypto context instance + * @param streamSize Size of the raw stream + * @return Adjusted size of the stream. + */ + long adjustEncryptedStreamSize(Object cryptoContextObj, long streamSize); + + /** + * Used where length of the encrypted content is required before actual encryption begins. + * + * @param cryptoContextObj crypto context instance + * @param contentLength Size of the raw content + * @return Calculated size of the encrypted stream for the provided raw stream. + */ + long estimateEncryptedLength(Object cryptoContextObj, long contentLength); + + /** + * Wraps a raw InputStream with encrypting stream + * + * @param cryptoContext created earlier to set the crypto context. + * @param streamContainer consisting of raw InputStream to encrypt + * @return stream container consisting of encrypting stream wrapped around raw InputStream. + */ + InputStreamContainer createEncryptingStream(Object cryptoContext, InputStreamContainer streamContainer); + + /** + * Provides encrypted stream for a raw stream emitted for a part of content. + * + * @param cryptoContextObj crypto context instance. + * @param streamContainer raw stream container for which encrypted stream has to be created. + * @param totalStreams Number of streams being used for the entire content. + * @param streamIdx Index of the current stream. + * @return Encrypted stream for the provided raw stream. + */ + InputStreamContainer createEncryptingStreamOfPart( + Object cryptoContextObj, + InputStreamContainer streamContainer, + int totalStreams, + int streamIdx + ); + + /** + * This method accepts an encrypted stream and provides a decrypting wrapper. + * @param encryptingStream to be decrypted. + * @return Decrypting wrapper stream + */ + InputStream createDecryptingStream(InputStream encryptingStream); +} diff --git a/server/src/main/java/org/opensearch/crypto/CryptoClientMissingException.java b/server/src/main/java/org/opensearch/crypto/CryptoClientMissingException.java new file mode 100644 index 0000000000000..0d55d18056354 --- /dev/null +++ b/server/src/main/java/org/opensearch/crypto/CryptoClientMissingException.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.crypto; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.rest.RestStatus; + +import java.io.IOException; + +/** + * Thrown when expected crypto client is not found. + * + * @opensearch.internal + */ +public class CryptoClientMissingException extends OpenSearchException { + private final String name; + private final String type; + + public CryptoClientMissingException(String clientName, String clientType) { + super("[Crypto client : " + clientName + " of type " + clientType + " ] is missing"); + this.name = clientName; + this.type = clientType; + } + + @Override + public RestStatus status() { + return RestStatus.NOT_FOUND; + } + + public CryptoClientMissingException(StreamInput in) throws IOException { + super(in); + this.name = in.readOptionalString(); + this.type = in.readOptionalString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(name); + out.writeOptionalString(type); + } +} diff --git a/server/src/main/java/org/opensearch/crypto/package-info.java b/server/src/main/java/org/opensearch/crypto/package-info.java new file mode 100644 index 0000000000000..742960ac1cf97 --- /dev/null +++ b/server/src/main/java/org/opensearch/crypto/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** + * Package for crypto client abstractions and exceptions. + */ +package org.opensearch.crypto; diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index 808c054de4969..95ca374161db8 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -56,6 +56,7 @@ import org.opensearch.monitor.fs.FsProbe; import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.SearchPipelinePlugin; +import org.opensearch.plugins.CryptoPlugin; import org.opensearch.search.backpressure.SearchBackpressureService; import org.opensearch.search.backpressure.settings.SearchBackpressureSettings; import org.opensearch.search.pipeline.SearchPipelineService; @@ -896,6 +897,7 @@ protected Node( RepositoriesModule repositoriesModule = new RepositoriesModule( this.environment, pluginsService.filterPlugins(RepositoryPlugin.class), + pluginsService.filterPlugins(CryptoPlugin.class), transportService, clusterService, threadPool, diff --git a/server/src/main/java/org/opensearch/plugins/CryptoPlugin.java b/server/src/main/java/org/opensearch/plugins/CryptoPlugin.java new file mode 100644 index 0000000000000..e45c30812f5e7 --- /dev/null +++ b/server/src/main/java/org/opensearch/plugins/CryptoPlugin.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.plugins; + +import org.opensearch.crypto.CryptoClient; + +import java.util.Set; + +/** + * An extension point for {@link Plugin} implementations to provide client side encryption/decryption support. + * + * @opensearch.api + */ +public interface CryptoPlugin { + + /** + * Crypto plugin name + * @return crypto plugin name + */ + String name(); + + /** + * @return Collection of registered key provider implementations + */ + Set getKeyProviderTypes(); + + /** + * Creates factory for the creation of crypto client. + * @param keyProviderType Key provider type/implementation for which factory needs to be created. + * @return Crypto client factory. + */ + CryptoClient.Factory createClientFactory(String keyProviderType); +} diff --git a/server/src/main/java/org/opensearch/repositories/RepositoriesModule.java b/server/src/main/java/org/opensearch/repositories/RepositoriesModule.java index cc4d3c006d84c..c56d54b8a3709 100644 --- a/server/src/main/java/org/opensearch/repositories/RepositoriesModule.java +++ b/server/src/main/java/org/opensearch/repositories/RepositoriesModule.java @@ -35,8 +35,10 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.crypto.CryptoClient; import org.opensearch.env.Environment; import org.opensearch.indices.recovery.RecoverySettings; +import org.opensearch.plugins.CryptoPlugin; import org.opensearch.plugins.RepositoryPlugin; import org.opensearch.repositories.fs.FsRepository; import org.opensearch.threadpool.ThreadPool; @@ -59,6 +61,7 @@ public final class RepositoriesModule { public RepositoriesModule( Environment env, List repoPlugins, + List cryptoPlugins, TransportService transportService, ClusterService clusterService, ThreadPool threadPool, @@ -105,15 +108,27 @@ public RepositoriesModule( } } + Map cryptoClientFactoriesMap = new HashMap<>(); + for (CryptoPlugin cryptoPlugin : cryptoPlugins) { + for (String keyProviderType : cryptoPlugin.getKeyProviderTypes()) { + if (cryptoClientFactoriesMap.containsKey(keyProviderType)) { + throw new IllegalArgumentException("Crypto plugin key provider type [" + keyProviderType + "] is already registered"); + } + cryptoClientFactoriesMap.put(keyProviderType, cryptoPlugin.createClientFactory(keyProviderType)); + } + } + Settings settings = env.settings(); Map repositoryTypes = Collections.unmodifiableMap(factories); Map internalRepositoryTypes = Collections.unmodifiableMap(internalFactories); + Map cryptoClientRegistry = Collections.unmodifiableMap(cryptoClientFactoriesMap); repositoriesService = new RepositoriesService( settings, clusterService, transportService, repositoryTypes, internalRepositoryTypes, + cryptoClientRegistry, threadPool ); } diff --git a/server/src/main/java/org/opensearch/repositories/RepositoriesService.java b/server/src/main/java/org/opensearch/repositories/RepositoriesService.java index 9c56d172f2ea1..e0b6a6bcfa0a8 100644 --- a/server/src/main/java/org/opensearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/opensearch/repositories/RepositoriesService.java @@ -38,6 +38,7 @@ import org.opensearch.Version; import org.opensearch.action.ActionListener; import org.opensearch.action.ActionRunnable; +import org.opensearch.action.admin.cluster.crypto.CryptoSettings; import org.opensearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; import org.opensearch.action.admin.cluster.repositories.put.PutRepositoryRequest; import org.opensearch.cluster.AckedClusterStateUpdateTask; @@ -49,6 +50,7 @@ import org.opensearch.cluster.SnapshotDeletionsInProgress; import org.opensearch.cluster.SnapshotsInProgress; import org.opensearch.cluster.ack.ClusterStateUpdateResponse; +import org.opensearch.cluster.metadata.CryptoMetadata; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.cluster.metadata.RepositoriesMetadata; import org.opensearch.cluster.metadata.RepositoryMetadata; @@ -65,6 +67,8 @@ import org.opensearch.common.unit.TimeValue; import org.opensearch.common.util.concurrent.ConcurrentCollections; import org.opensearch.common.util.io.IOUtils; +import org.opensearch.crypto.CryptoClient; +import org.opensearch.crypto.CryptoClientMissingException; import org.opensearch.repositories.blobstore.MeteredBlobStoreRepository; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -106,6 +110,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C private final Map typesRegistry; private final Map internalTypesRegistry; + private final Map cryptoClientRegistry; private final ClusterService clusterService; @@ -115,6 +120,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C private final Map internalRepositories = ConcurrentCollections.newConcurrentMap(); private volatile Map repositories = Collections.emptyMap(); + private volatile Map cryptoClients = Collections.emptyMap(); private final RepositoriesStatsArchive repositoriesStatsArchive; private final ClusterManagerTaskThrottler.ThrottlingKey putRepositoryTaskKey; private final ClusterManagerTaskThrottler.ThrottlingKey deleteRepositoryTaskKey; @@ -125,10 +131,12 @@ public RepositoriesService( TransportService transportService, Map typesRegistry, Map internalTypesRegistry, + Map cryptoClientRegistry, ThreadPool threadPool ) { this.typesRegistry = typesRegistry; this.internalTypesRegistry = internalTypesRegistry; + this.cryptoClientRegistry = cryptoClientRegistry; this.clusterService = clusterService; this.threadPool = threadPool; // Doesn't make sense to maintain repositories on non-master and non-data nodes @@ -161,9 +169,18 @@ public RepositoriesService( public void registerRepository(final PutRepositoryRequest request, final ActionListener listener) { assert lifecycle.started() : "Trying to register new repository but service is in state [" + lifecycle.state() + "]"; - final RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata(request.name(), request.type(), request.settings()); + final RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata( + request.name(), + request.type(), + request.settings(), + request.encrypted(), + CryptoMetadata.fromRequest(request.cryptoSettings()) + ); validate(request.name()); validateRepositoryMetadataSettings(clusterService, request.name(), request.settings()); + if (Boolean.TRUE.equals(newRepositoryMetadata.encrypted())) { + validate(newRepositoryMetadata.cryptoMetadata().keyProviderName()); + } final ActionListener registrationListener; if (request.verify()) { @@ -193,6 +210,16 @@ public void registerRepository(final PutRepositoryRequest request, final ActionL return; } + if (Boolean.TRUE.equals(newRepositoryMetadata.encrypted())) { + // Trying to create the new crypto client on cluster-manager to make sure it works + try { + closeCryptoClient(createCryptoClient(newRepositoryMetadata, cryptoClientRegistry)); + } catch (Exception e) { + registrationListener.onFailure(e); + return; + } + } + clusterService.submitStateUpdateTask( "put_repository [" + request.name() + "]", new AckedClusterStateUpdateTask(request, registrationListener) { @@ -210,7 +237,15 @@ public ClusterState execute(ClusterState currentState) { if (repositories == null) { logger.info("put repository [{}]", request.name()); repositories = new RepositoriesMetadata( - Collections.singletonList(new RepositoryMetadata(request.name(), request.type(), request.settings())) + Collections.singletonList( + new RepositoryMetadata( + request.name(), + request.type(), + request.settings(), + request.encrypted(), + CryptoMetadata.fromRequest(request.cryptoSettings()) + ) + ) ); } else { boolean found = false; @@ -222,6 +257,7 @@ public ClusterState execute(ClusterState currentState) { // Previous version is the same as this one no update is needed. return currentState; } + ensureCryptoSettingsAreSame(repositoryMetadata, request); found = true; repositoriesMetadata.add(newRepositoryMetadata); } else { @@ -230,7 +266,15 @@ public ClusterState execute(ClusterState currentState) { } if (!found) { logger.info("put repository [{}]", request.name()); - repositoriesMetadata.add(new RepositoryMetadata(request.name(), request.type(), request.settings())); + repositoriesMetadata.add( + new RepositoryMetadata( + request.name(), + request.type(), + request.settings(), + request.encrypted(), + CryptoMetadata.fromRequest(request.cryptoSettings()) + ) + ); } else { logger.info("update repository [{}]", request.name()); } @@ -406,16 +450,21 @@ public void applyClusterState(ClusterChangedEvent event) { logger.debug("unregistering repository [{}]", entry.getKey()); Repository repository = entry.getValue(); closeRepository(repository); + if (Boolean.TRUE.equals(repository.getMetadata().encrypted())) { + closeCryptoClient(cryptoClients.get(getCryptoClientKey(repository.getMetadata()))); + } archiveRepositoryStats(repository, state.version()); } else { survivors.put(entry.getKey(), entry.getValue()); } } + Map cryptoClientBuilder = new HashMap<>(); Map builder = new HashMap<>(); if (newMetadata != null) { // Now go through all repositories and update existing or create missing for (RepositoryMetadata repositoryMetadata : newMetadata.repositories()) { + CryptoClient cryptoClient = null; Repository repository = survivors.get(repositoryMetadata.name()); if (repository != null) { // Found previous version of this repository @@ -425,10 +474,16 @@ public void applyClusterState(ClusterChangedEvent event) { // Previous version is different from the version in settings logger.debug("updating repository [{}]", repositoryMetadata.name()); closeRepository(repository); + if (Boolean.TRUE.equals(repositoryMetadata.encrypted())) { + closeCryptoClient(cryptoClients.get(getCryptoClientKey(repositoryMetadata))); + } archiveRepositoryStats(repository, state.version()); repository = null; try { repository = createRepository(repositoryMetadata, typesRegistry); + if (Boolean.TRUE.equals(repositoryMetadata.encrypted())) { + cryptoClient = createCryptoClient(repositoryMetadata, cryptoClientRegistry); + } } catch (RepositoryException ex) { // TODO: this catch is bogus, it means the old repo is already closed, // but we have nothing to replace it @@ -437,10 +492,15 @@ public void applyClusterState(ClusterChangedEvent event) { ex ); } + } else if (Boolean.TRUE.equals(previousMetadata.encrypted())) { + cryptoClient = cryptoClients.get(getCryptoClientKey(previousMetadata)); } } else { try { repository = createRepository(repositoryMetadata, typesRegistry); + if (Boolean.TRUE.equals(repository.getMetadata().encrypted())) { + cryptoClient = createCryptoClient(repositoryMetadata, cryptoClientRegistry); + } } catch (RepositoryException ex) { logger.warn(() -> new ParameterizedMessage("failed to create repository [{}]", repositoryMetadata.name()), ex); } @@ -448,6 +508,9 @@ public void applyClusterState(ClusterChangedEvent event) { if (repository != null) { logger.debug("registering repository [{}]", repositoryMetadata.name()); builder.put(repositoryMetadata.name(), repository); + if (cryptoClient != null) { + cryptoClientBuilder.put(getCryptoClientKey(repositoryMetadata), cryptoClient); + } } } } @@ -455,12 +518,17 @@ public void applyClusterState(ClusterChangedEvent event) { repo.updateState(state); } repositories = Collections.unmodifiableMap(builder); + cryptoClients = Collections.unmodifiableMap(cryptoClientBuilder); } catch (Exception ex) { assert false : new AssertionError(ex); logger.warn("failure updating cluster state ", ex); } } + private String getCryptoClientKey(RepositoryMetadata repositoryMetadata) { + return repositoryMetadata.cryptoMetadata().keyProviderName() + "#" + repositoryMetadata.cryptoMetadata().keyProviderType(); + } + /** * Gets the {@link RepositoryData} for the given repository. * @@ -498,6 +566,25 @@ public Repository repository(String repositoryName) { throw new RepositoryMissingException(repositoryName); } + /** + * Returns registered crypto client + * @param repositoryMetadata repository metadata for which crypto client needs to be returned. + * @return crypto client + */ + public CryptoClient cryptoClient(RepositoryMetadata repositoryMetadata) { + return cryptoClients.get(getCryptoClientKey(repositoryMetadata)); + } + + // For tests + public int totalCryptoClients() { + return cryptoClients.size(); + } + + // Used in tests + CryptoClient getCryptoClient(String clientKey) { + return cryptoClients.get(clientKey); + } + public List repositoriesStats() { List archivedRepoStats = repositoriesStatsArchive.getArchivedStats(); List activeRepoStats = getRepositoryStatsForActiveRepositories(); @@ -555,6 +642,9 @@ public void unregisterInternalRepository(String name) { RepositoryMetadata metadata = repository.getMetadata(); logger.debug(() -> new ParameterizedMessage("delete internal repository [{}][{}].", metadata.type(), name)); closeRepository(repository); + if (Boolean.TRUE.equals(repository.getMetadata().encrypted())) { + closeCryptoClient(cryptoClients.get(getCryptoClientKey(metadata))); + } } } @@ -564,6 +654,15 @@ private void closeRepository(Repository repository) { repository.close(); } + /** Closes the given crypto client. */ + private void closeCryptoClient(CryptoClient cryptoClient) { + if (cryptoClient == null) { + return; + } + logger.debug("closing crypto client [{}][{}]", cryptoClient.type(), cryptoClient.name()); + cryptoClient.decRef(); + } + private void archiveRepositoryStats(Repository repository, long clusterStateVersion) { if (repository instanceof MeteredBlobStoreRepository) { RepositoryStatsSnapshot stats = ((MeteredBlobStoreRepository) repository).statsSnapshotForArchival(clusterStateVersion); @@ -629,12 +728,64 @@ public static void validateRepositoryMetadataSettings( } } + // package private for tests + CryptoClient createCryptoClient(RepositoryMetadata repositoryMetadata, Map cryptoClientRegistry) { + CryptoMetadata cryptoMetadata = repositoryMetadata.cryptoMetadata(); + logger.debug("creating crypto client [{}][{}]", cryptoMetadata.keyProviderType(), cryptoMetadata.keyProviderName()); + CryptoClient.Factory factory = cryptoClientRegistry.get(cryptoMetadata.keyProviderType()); + if (factory == null) { + throw new CryptoClientMissingException( + repositoryMetadata.name(), + "Crypto client couldn't be created of type [" + cryptoMetadata.keyProviderType() + "]" + ); + } + + CryptoClient cryptoClient; + try { + cryptoClient = factory.create(cryptoMetadata.settings(), cryptoMetadata.keyProviderName()); + return cryptoClient; + } catch (Exception e) { + logger.warn( + new ParameterizedMessage( + "failed to create crypto client [{}][{}]", + cryptoMetadata.keyProviderType(), + cryptoMetadata.keyProviderName() + ), + e + ); + throw new RepositoryException(repositoryMetadata.name(), "failed to create crypto client", e); + } + } + private static void ensureRepositoryNotInUse(ClusterState clusterState, String repository) { if (isRepositoryInUse(clusterState, repository)) { throw new IllegalStateException("trying to modify or unregister repository that is currently used"); } } + private static void ensureCryptoSettingsAreSame(RepositoryMetadata repositoryMetadata, PutRepositoryRequest request) { + if (repositoryMetadata.encrypted() == null && request.encrypted() == null) { + return; + } + if (repositoryMetadata.encrypted() == null || request.encrypted() == null) { + throw new IllegalArgumentException("Crypto settings changes found in the repository update request. This is not allowed"); + } + boolean existingEncrypted = Boolean.TRUE.equals(repositoryMetadata.encrypted()); + boolean changeEncrypted = Boolean.TRUE.equals(request.encrypted()); + if (existingEncrypted == false && changeEncrypted == false) { + return; + } + + CryptoMetadata cryptoMetadata = repositoryMetadata.cryptoMetadata(); + CryptoSettings cryptoSettings = request.cryptoSettings(); + if (existingEncrypted != changeEncrypted + || !cryptoMetadata.keyProviderName().equals(cryptoSettings.getKeyProviderName()) + || !cryptoMetadata.keyProviderType().equals(cryptoSettings.getKeyProviderType()) + || !cryptoMetadata.settings().toString().equals(cryptoSettings.getSettings().toString())) { + throw new IllegalArgumentException("Changes in crypto settings found in the repository update request. This is not allowed"); + } + } + /** * Checks if a repository is currently in use by one of the snapshots * diff --git a/server/src/main/java/org/opensearch/repositories/UnSupportedRepositoryTypeException.java b/server/src/main/java/org/opensearch/repositories/UnSupportedRepositoryTypeException.java new file mode 100644 index 0000000000000..37decb7950560 --- /dev/null +++ b/server/src/main/java/org/opensearch/repositories/UnSupportedRepositoryTypeException.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.repositories; + +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.rest.RestStatus; + +import java.io.IOException; + +/** + * Unsupported repo type exception + * + * @opensearch.internal + */ +public class UnSupportedRepositoryTypeException extends RepositoryException { + public UnSupportedRepositoryTypeException(String repository, String msg) { + super(repository, msg); + } + + @Override + public RestStatus status() { + return RestStatus.BAD_REQUEST; + } + + public UnSupportedRepositoryTypeException(StreamInput in) throws IOException { + super(in); + } +} diff --git a/server/src/main/java/org/opensearch/snapshots/SnapshotsService.java b/server/src/main/java/org/opensearch/snapshots/SnapshotsService.java index b523c1ba12b05..8ef69ace766d4 100644 --- a/server/src/main/java/org/opensearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/opensearch/snapshots/SnapshotsService.java @@ -98,6 +98,7 @@ import org.opensearch.repositories.RepositoryMissingException; import org.opensearch.repositories.RepositoryShardId; import org.opensearch.repositories.ShardGenerations; +import org.opensearch.repositories.UnSupportedRepositoryTypeException; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -267,6 +268,16 @@ public void createSnapshot(final CreateSnapshotRequest request, final ActionList listener.onFailure(new RepositoryException(repository.getMetadata().name(), "cannot create snapshot in a readonly repository")); return; } + if (repository.getMetadata() != null + && (repository.getMetadata().encrypted() != null || repository.getMetadata().cryptoMetadata() != null)) { + listener.onFailure( + new UnSupportedRepositoryTypeException( + repository.getMetadata().name(), + "Snapshot creation for an encrypted repository is not supported" + ) + ); + return; + } final Snapshot snapshot = new Snapshot(repositoryName, snapshotId); final Map userMeta = repository.adaptUserMetadata(request.userMetadata()); repository.executeConsistentStateUpdate(repositoryData -> new ClusterStateUpdateTask() { diff --git a/server/src/test/java/org/opensearch/ExceptionSerializationTests.java b/server/src/test/java/org/opensearch/ExceptionSerializationTests.java index 8b0d6faeb1e61..f9486eb749770 100644 --- a/server/src/test/java/org/opensearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/opensearch/ExceptionSerializationTests.java @@ -78,6 +78,7 @@ import org.opensearch.common.util.set.Sets; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.XContentLocation; +import org.opensearch.crypto.CryptoClientMissingException; import org.opensearch.discovery.MasterNotDiscoveredException; import org.opensearch.env.ShardLockObtainFailedException; import org.opensearch.index.Index; @@ -888,6 +889,7 @@ public void testIds() { ids.put(168, PreferenceBasedSearchNotAllowedException.class); ids.put(169, NodeWeighedAwayException.class); ids.put(170, SearchPipelineProcessingException.class); + ids.put(171, CryptoClientMissingException.class); ids.put(10001, IndexCreateBlockException.class); Map, Integer> reverse = new HashMap<>(); diff --git a/server/src/test/java/org/opensearch/index/IndexModuleTests.java b/server/src/test/java/org/opensearch/index/IndexModuleTests.java index d9d87196ca289..888a77c3b55f2 100644 --- a/server/src/test/java/org/opensearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/opensearch/index/IndexModuleTests.java @@ -210,6 +210,7 @@ public void setUp() throws Exception { transportService, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap(), threadPool ); diff --git a/server/src/test/java/org/opensearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java b/server/src/test/java/org/opensearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java index be64d89130950..4d21ce4562023 100644 --- a/server/src/test/java/org/opensearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java +++ b/server/src/test/java/org/opensearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java @@ -554,6 +554,7 @@ private IndicesClusterStateService createIndicesClusterStateService( transportService, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap(), threadPool ); final PeerRecoveryTargetService recoveryTargetService = new PeerRecoveryTargetService( diff --git a/server/src/test/java/org/opensearch/repositories/RepositoriesModuleTests.java b/server/src/test/java/org/opensearch/repositories/RepositoriesModuleTests.java index 21ca81bb5df92..aa75ac585a115 100644 --- a/server/src/test/java/org/opensearch/repositories/RepositoriesModuleTests.java +++ b/server/src/test/java/org/opensearch/repositories/RepositoriesModuleTests.java @@ -34,9 +34,12 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.set.Sets; import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.crypto.CryptoClient; import org.opensearch.env.Environment; import org.opensearch.indices.recovery.RecoverySettings; +import org.opensearch.plugins.CryptoPlugin; import org.opensearch.plugins.RepositoryPlugin; import org.opensearch.test.OpenSearchTestCase; import org.opensearch.threadpool.ThreadPool; @@ -54,9 +57,13 @@ public class RepositoriesModuleTests extends OpenSearchTestCase { private Environment environment; private NamedXContentRegistry contentRegistry; private List repoPlugins = new ArrayList<>(); + private List cryptoPlugins = new ArrayList<>(); private RepositoryPlugin plugin1; private RepositoryPlugin plugin2; private Repository.Factory factory; + private CryptoPlugin cryptoPlugin1; + private CryptoPlugin cryptoPlugin2; + private CryptoClient.Factory cryptoFactory; private ThreadPool threadPool; private ClusterService clusterService; private RecoverySettings recoverySettings; @@ -74,6 +81,12 @@ public void setUp() throws Exception { factory = mock(Repository.Factory.class); repoPlugins.add(plugin1); repoPlugins.add(plugin2); + cryptoPlugin1 = mock(CryptoPlugin.class); + cryptoPlugin2 = mock(CryptoPlugin.class); + cryptoPlugins.add(cryptoPlugin1); + cryptoPlugins.add(cryptoPlugin2); + cryptoFactory = mock(CryptoClient.Factory.class); + when(environment.settings()).thenReturn(Settings.EMPTY); } @@ -89,6 +102,30 @@ public void testCanRegisterTwoRepositoriesWithDifferentTypes() { new RepositoriesModule( environment, repoPlugins, + cryptoPlugins, + mock(TransportService.class), + mock(ClusterService.class), + threadPool, + contentRegistry, + recoverySettings + ); + } + + public void testCanRegisterTwoRepositoriesWithDifferentKeyProviderTypes() { + when(plugin1.getRepositories(environment, contentRegistry, clusterService, recoverySettings)).thenReturn( + Collections.singletonMap("type1", factory) + ); + when(plugin2.getRepositories(environment, contentRegistry, clusterService, recoverySettings)).thenReturn( + Collections.singletonMap("type2", factory) + ); + + when(cryptoPlugin1.getKeyProviderTypes()).thenReturn(Sets.newHashSet("type1")); + when(cryptoPlugin2.getKeyProviderTypes()).thenReturn(Sets.newHashSet("type2")); + + new RepositoriesModule( + environment, + repoPlugins, + cryptoPlugins, mock(TransportService.class), mock(ClusterService.class), threadPool, @@ -110,6 +147,7 @@ public void testCannotRegisterTwoRepositoriesWithSameTypes() { () -> new RepositoriesModule( environment, repoPlugins, + cryptoPlugins, mock(TransportService.class), clusterService, threadPool, @@ -121,6 +159,32 @@ public void testCannotRegisterTwoRepositoriesWithSameTypes() { assertEquals("Repository type [type1] is already registered", ex.getMessage()); } + public void testCanRegisterTwoRepositoriesWithSameKeyProviderTypes() { + when(plugin1.getRepositories(environment, contentRegistry, clusterService, recoverySettings)).thenReturn( + Collections.singletonMap("type1", factory) + ); + when(plugin2.getRepositories(environment, contentRegistry, clusterService, recoverySettings)).thenReturn( + Collections.singletonMap("type2", factory) + ); + + when(cryptoPlugin1.getKeyProviderTypes()).thenReturn(Sets.newHashSet("type1")); + when(cryptoPlugin2.getKeyProviderTypes()).thenReturn(Sets.newHashSet("type1")); + + // Would throw + IllegalArgumentException ex = expectThrows( + IllegalArgumentException.class, + () -> new RepositoriesModule( + environment, + repoPlugins, + cryptoPlugins, + mock(TransportService.class), + mock(ClusterService.class), + threadPool, + contentRegistry, + recoverySettings + )); + } + public void testCannotRegisterTwoInternalRepositoriesWithSameTypes() { when(plugin1.getInternalRepositories(environment, contentRegistry, clusterService, recoverySettings)).thenReturn( Collections.singletonMap("type1", factory) @@ -134,6 +198,7 @@ public void testCannotRegisterTwoInternalRepositoriesWithSameTypes() { () -> new RepositoriesModule( environment, repoPlugins, + cryptoPlugins, mock(TransportService.class), clusterService, threadPool, @@ -158,6 +223,7 @@ public void testCannotRegisterNormalAndInternalRepositoriesWithSameTypes() { () -> new RepositoriesModule( environment, repoPlugins, + cryptoPlugins, mock(TransportService.class), clusterService, threadPool, diff --git a/server/src/test/java/org/opensearch/repositories/RepositoriesServiceTests.java b/server/src/test/java/org/opensearch/repositories/RepositoriesServiceTests.java index 43d371bf5a187..b0cee2761e18c 100644 --- a/server/src/test/java/org/opensearch/repositories/RepositoriesServiceTests.java +++ b/server/src/test/java/org/opensearch/repositories/RepositoriesServiceTests.java @@ -35,11 +35,15 @@ import org.apache.lucene.index.IndexCommit; import org.opensearch.Version; import org.opensearch.action.ActionListener; +import org.opensearch.action.admin.cluster.crypto.CryptoSettings; import org.opensearch.action.admin.cluster.repositories.put.PutRepositoryRequest; +import org.opensearch.cluster.AckedClusterStateUpdateTask; import org.opensearch.cluster.ClusterChangedEvent; import org.opensearch.cluster.ClusterName; import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.ClusterStateUpdateTask; +import org.opensearch.cluster.ack.ClusterStateUpdateResponse; +import org.opensearch.cluster.metadata.CryptoMetadata; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.cluster.metadata.RepositoriesMetadata; @@ -53,8 +57,11 @@ import org.opensearch.common.blobstore.BlobStore; import org.opensearch.common.component.Lifecycle; import org.opensearch.common.component.LifecycleListener; +import org.opensearch.common.io.InputStreamContainer; import org.opensearch.common.settings.Settings; import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.crypto.CryptoClient; +import org.opensearch.crypto.CryptoClientMissingException; import org.opensearch.index.mapper.MapperService; import org.opensearch.index.shard.ShardId; import org.opensearch.index.snapshots.IndexShardSnapshotStatus; @@ -69,24 +76,40 @@ import org.opensearch.transport.Transport; import org.opensearch.transport.TransportService; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doAnswer; public class RepositoriesServiceTests extends OpenSearchTestCase { private RepositoriesService repositoriesService; + private Map cryptoRegistry; @Override public void setUp() throws Exception { super.setUp(); + ThreadPool threadPool = mock(ThreadPool.class); + final ClusterApplierService clusterApplierService = mock(ClusterApplierService.class); + when(clusterApplierService.threadPool()).thenReturn(threadPool); + final ClusterService clusterService = mock(ClusterService.class); + repositoriesService = createRepositoriesServiceWithMockedClusterService(clusterService); + } + + private RepositoriesService createRepositoriesServiceWithMockedClusterService(ClusterService clusterService) { ThreadPool threadPool = mock(ThreadPool.class); final TransportService transportService = new TransportService( Settings.EMPTY, @@ -99,7 +122,6 @@ public void setUp() throws Exception { ); final ClusterApplierService clusterApplierService = mock(ClusterApplierService.class); when(clusterApplierService.threadPool()).thenReturn(threadPool); - final ClusterService clusterService = mock(ClusterService.class); when(clusterService.getClusterApplierService()).thenReturn(clusterApplierService); Map typesRegistry = Map.of( TestRepository.TYPE, @@ -109,15 +131,55 @@ public void setUp() throws Exception { MeteredRepositoryTypeB.TYPE, metadata -> new MeteredRepositoryTypeB(metadata, clusterService) ); - repositoriesService = new RepositoriesService( + + Map cryptoRegistry = Map.of( + TestCryptoClientTypeA.TYPE, + new CryptoCreator(TestCryptoClientTypeA.TYPE), + TestCryptoClientTypeB.TYPE, + new CryptoCreator(TestCryptoClientTypeB.TYPE) + ); + this.cryptoRegistry = cryptoRegistry; + RepositoriesService repositoriesService = new RepositoriesService( Settings.EMPTY, - mock(ClusterService.class), + clusterService, transportService, typesRegistry, typesRegistry, + cryptoRegistry, threadPool ); repositoriesService.start(); + return repositoriesService; + } + + class CryptoCreator implements CryptoClient.Factory { + private final Map cryptoClients = new HashMap<>(); + private final String type; + + public CryptoCreator(String type) { + this.type = type; + } + + @Override + public CryptoClient create(Settings cryptoSettings, String keyProviderName) { + if (cryptoClients.containsKey(keyProviderName)) { + cryptoClients.get(keyProviderName).incRef(); + return cryptoClients.get(keyProviderName); + } + + CryptoClient cryptoClient; + switch (type) { + case TestCryptoClientTypeA.TYPE: + cryptoClient = new TestCryptoClientTypeA(cryptoSettings, keyProviderName); + cryptoClients.put(keyProviderName, cryptoClient); + return cryptoClient; + case TestCryptoClientTypeB.TYPE: + cryptoClient = new TestCryptoClientTypeB(cryptoSettings, keyProviderName); + cryptoClients.put(keyProviderName, cryptoClient); + return cryptoClient; + } + return null; + } } public void testRegisterInternalRepository() { @@ -188,6 +250,264 @@ public void testRepositoriesStatsCanHaveTheSameNameAndDifferentTypeOverTime() { assertThat(repositoryStatsTypeB.getRepositoryStats(), equalTo(MeteredRepositoryTypeB.STATS)); } + public void testWithSameKeyProviderNames() { + CryptoMetadata cryptoMetadata = new CryptoMetadata("kp-name", "kp-type", Settings.EMPTY); + RepositoryMetadata repositoryMetadata = new RepositoryMetadata("repoName", "repoType", Settings.EMPTY, true, cryptoMetadata); + expectThrows(CryptoClientMissingException.class, () -> repositoriesService.createCryptoClient(repositoryMetadata, cryptoRegistry)); + + String keyProviderName = "kp-name"; + ClusterState clusterStateWithRepoTypeA = createClusterStateWithKeyProvider( + "repoName", + MeteredRepositoryTypeA.TYPE, + keyProviderName, + TestCryptoClientTypeA.TYPE + ); + + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeA, emptyState())); + assertThat(repositoriesService.repositoriesStats().size(), equalTo(1)); + assertEquals(1, repositoriesService.totalCryptoClients()); + + ClusterState clusterStateWithRepoTypeB = createClusterStateWithKeyProvider( + "repoName", + MeteredRepositoryTypeB.TYPE, + keyProviderName, + TestCryptoClientTypeB.TYPE + ); + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeB, emptyState())); + assertThat(repositoriesService.repositoriesStats().size(), equalTo(2)); + assertEquals(1, repositoriesService.totalCryptoClients()); + } + + public void testCryptoClientsUnchangedWithSameCryptoMetadata() { + String keyProviderName = "kp-name"; + ClusterState clusterStateWithRepoTypeA = createClusterStateWithKeyProvider( + "repoName", + MeteredRepositoryTypeA.TYPE, + keyProviderName, + TestCryptoClientTypeA.TYPE + ); + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeA, emptyState())); + assertThat(repositoriesService.repositoriesStats().size(), equalTo(1)); + assertEquals(1, repositoriesService.totalCryptoClients()); + + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeA, emptyState())); + assertThat(repositoriesService.repositoriesStats().size(), equalTo(1)); + assertEquals(1, repositoriesService.totalCryptoClients()); + } + + public void testRepositoryUpdateWithDifferentCryptoMetadata() { + String keyProviderName = "kp-name"; + + ClusterState clusterStateWithRepoTypeA = createClusterStateWithKeyProvider( + "repoName", + MeteredRepositoryTypeA.TYPE, + keyProviderName, + TestCryptoClientTypeA.TYPE + ); + ClusterService clusterService = mock(ClusterService.class); + + PutRepositoryRequest request = new PutRepositoryRequest("repoName"); + request.type(MeteredRepositoryTypeA.TYPE); + request.settings(Settings.EMPTY); + + doAnswer((invocation) -> { + AckedClusterStateUpdateTask task = (AckedClusterStateUpdateTask< + ClusterStateUpdateResponse>) invocation.getArguments()[1]; + task.execute(clusterStateWithRepoTypeA); + return null; + }).when(clusterService).submitStateUpdateTask(any(), any()); + + RepositoriesService repositoriesService = createRepositoriesServiceWithMockedClusterService(clusterService); + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeA, emptyState())); + assertThat(repositoriesService.repositoriesStats().size(), equalTo(1)); + assertEquals(1, repositoriesService.totalCryptoClients()); + + expectThrows(IllegalArgumentException.class, () -> repositoriesService.registerRepository(request, null)); + + request.encrypted(true); + CryptoSettings cryptoSettings = new CryptoSettings(keyProviderName); + cryptoSettings.keyProviderType(TestCryptoClientTypeA.TYPE); + cryptoSettings.settings(Settings.builder().put("key-1", "val-1")); + request.cryptoSettings(cryptoSettings); + expectThrows(IllegalArgumentException.class, () -> repositoriesService.registerRepository(request, null)); + + cryptoSettings.settings(Settings.builder()); + cryptoSettings.keyProviderName("random"); + expectThrows(IllegalArgumentException.class, () -> repositoriesService.registerRepository(request, null)); + + cryptoSettings.keyProviderName(keyProviderName); + cryptoSettings.keyProviderType("random"); + AtomicBoolean expectedExceptionFound = new AtomicBoolean(); + repositoriesService.registerRepository(request, new ActionListener<>() { + @Override + public void onResponse(ClusterStateUpdateResponse clusterStateUpdateResponse) {} + + @Override + public void onFailure(Exception e) { + if (e instanceof CryptoClientMissingException) { + expectedExceptionFound.set(true); + } + } + }); + assertTrue(expectedExceptionFound.get()); + + cryptoSettings.keyProviderType(TestCryptoClientTypeA.TYPE); + repositoriesService.registerRepository(request, null); + } + + public void testCryptoClientClusterStateChanges() { + + ClusterService clusterService = mock(ClusterService.class); + AtomicBoolean verified = new AtomicBoolean(); + List repositoryMetadata = new ArrayList<>(); + + String keyProviderName = "kp-name-1"; + String repoName = "repoName"; + String keyProviderType = TestCryptoClientTypeA.TYPE; + Settings.Builder settings = Settings.builder(); + PutRepositoryRequest request = createPutRepositoryEncryptedRequest( + repoName, + MeteredRepositoryTypeA.TYPE, + keyProviderName, + settings, + keyProviderType + ); + verified.set(false); + RepositoriesService repositoriesService = createRepositoriesServiceAndMockCryptoClusterState( + clusterService, + repoName, + keyProviderName, + keyProviderType, + settings.build(), + verified, + repositoryMetadata + ); + repositoriesService.registerRepository(request, null); + TestCryptoClientTypeA cryptoClientTypeA = (TestCryptoClientTypeA) repositoriesService.getCryptoClient( + keyProviderName + "#" + TestCryptoClientTypeA.TYPE + ); + assertNotNull(cryptoClientTypeA); + assertEquals(1, cryptoClientTypeA.getReferenceCount()); + assertTrue(verified.get()); + + // No change + keyProviderType = TestCryptoClientTypeA.TYPE; + settings = Settings.builder(); + request = createPutRepositoryEncryptedRequest(repoName, MeteredRepositoryTypeA.TYPE, keyProviderName, settings, keyProviderType); + verified.set(false); + repositoriesService = createRepositoriesServiceAndMockCryptoClusterState( + clusterService, + repoName, + keyProviderName, + keyProviderType, + settings.build(), + verified, + repositoryMetadata + ); + repositoriesService.registerRepository(request, null); + cryptoClientTypeA = (TestCryptoClientTypeA) repositoriesService.getCryptoClient(keyProviderName + "#" + keyProviderType); + assertNotNull(cryptoClientTypeA); + assertEquals(1, cryptoClientTypeA.getReferenceCount()); + assertTrue(verified.get()); + + // Same crypto client in new repo + repoName = "repoName-2"; + keyProviderType = TestCryptoClientTypeA.TYPE; + settings = Settings.builder(); + request = createPutRepositoryEncryptedRequest(repoName, MeteredRepositoryTypeA.TYPE, keyProviderName, settings, keyProviderType); + verified.set(false); + repositoriesService = createRepositoriesServiceAndMockCryptoClusterState( + clusterService, + repoName, + keyProviderName, + keyProviderType, + settings.build(), + verified, + repositoryMetadata + ); + repositoriesService.registerRepository(request, null); + cryptoClientTypeA = (TestCryptoClientTypeA) repositoriesService.getCryptoClient(keyProviderName + "#" + keyProviderType); + assertNotNull(cryptoClientTypeA); + assertEquals(2, cryptoClientTypeA.getReferenceCount()); + assertTrue(verified.get()); + + // Different crypto client in new repo + repoName = "repoName-3"; + keyProviderType = TestCryptoClientTypeB.TYPE; + settings = Settings.builder(); + request = createPutRepositoryEncryptedRequest(repoName, MeteredRepositoryTypeA.TYPE, keyProviderName, settings, keyProviderType); + verified.set(false); + repositoriesService = createRepositoriesServiceAndMockCryptoClusterState( + clusterService, + repoName, + keyProviderName, + keyProviderType, + settings.build(), + verified, + repositoryMetadata + ); + repositoriesService.registerRepository(request, null); + TestCryptoClientTypeB cryptoClientTypeB = (TestCryptoClientTypeB) repositoriesService.getCryptoClient( + keyProviderName + "#" + keyProviderType + ); + assertNotNull(cryptoClientTypeB); + assertEquals(1, cryptoClientTypeB.getReferenceCount()); + assertEquals(2, cryptoClientTypeA.getReferenceCount()); + assertTrue(verified.get()); + + } + + private RepositoriesService createRepositoriesServiceAndMockCryptoClusterState( + ClusterService clusterService, + String repoName, + String keyProviderName, + String keyProviderType, + Settings settings, + AtomicBoolean verified, + List repositoryMetadataList + ) { + + ClusterState.Builder state = ClusterState.builder(new ClusterName("test")); + CryptoMetadata newCryptoMetadata = new CryptoMetadata(keyProviderName, keyProviderType, Settings.EMPTY); + Metadata.Builder mdBuilder = Metadata.builder(); + + RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata( + repoName, + MeteredRepositoryTypeA.TYPE, + Settings.EMPTY, + true, + newCryptoMetadata + ); + if (!repositoryMetadataList.contains(newRepositoryMetadata)) { + repositoryMetadataList.add(newRepositoryMetadata); + } + RepositoriesMetadata newRepositoriesMetadata = new RepositoriesMetadata(repositoryMetadataList); + mdBuilder.putCustom(RepositoriesMetadata.TYPE, newRepositoriesMetadata); + state.metadata(mdBuilder); + ClusterState clusterStateWithRepoTypeA = state.build(); + + RepositoriesService repositoriesService = createRepositoriesServiceWithMockedClusterService(clusterService); + + doAnswer((invocation) -> { + AckedClusterStateUpdateTask task = (AckedClusterStateUpdateTask< + ClusterStateUpdateResponse>) invocation.getArguments()[1]; + ClusterState clusterState = task.execute(clusterStateWithRepoTypeA); + RepositoriesMetadata repositories = clusterState.metadata().custom(RepositoriesMetadata.TYPE); + RepositoryMetadata repositoryMetadata = repositories.repositories().get(repositoryMetadataList.size() - 1); + assertTrue(repositoryMetadata.encrypted()); + CryptoMetadata cryptoMetadata = repositoryMetadata.cryptoMetadata(); + assertNotNull(cryptoMetadata); + assertEquals(keyProviderName, cryptoMetadata.keyProviderName()); + assertEquals(keyProviderType, cryptoMetadata.keyProviderType()); + assertEquals(cryptoMetadata.settings(), settings); + verified.set(true); + repositoriesService.applyClusterState(new ClusterChangedEvent("new repo", clusterStateWithRepoTypeA, emptyState())); + return null; + }).when(clusterService).submitStateUpdateTask(any(), any()); + + return repositoriesService; + } + private ClusterState createClusterStateWithRepo(String repoName, String repoType) { ClusterState.Builder state = ClusterState.builder(new ClusterName("test")); Metadata.Builder mdBuilder = Metadata.builder(); @@ -200,6 +520,46 @@ private ClusterState createClusterStateWithRepo(String repoName, String repoType return state.build(); } + private ClusterState createClusterStateWithKeyProvider( + String repoName, + String repoType, + String keyProviderName, + String keyProviderType + ) { + ClusterState.Builder state = ClusterState.builder(new ClusterName("test")); + Metadata.Builder mdBuilder = Metadata.builder(); + CryptoMetadata cryptoMetadata = new CryptoMetadata(keyProviderName, keyProviderType, Settings.EMPTY); + mdBuilder.putCustom( + RepositoriesMetadata.TYPE, + new RepositoriesMetadata( + Collections.singletonList(new RepositoryMetadata(repoName, repoType, Settings.EMPTY, true, cryptoMetadata)) + ) + ); + state.metadata(mdBuilder); + + return state.build(); + } + + private PutRepositoryRequest createPutRepositoryEncryptedRequest( + String repoName, + String repoType, + String keyProviderName, + Settings.Builder settings, + String keyProviderType + ) { + PutRepositoryRequest repositoryRequest = new PutRepositoryRequest(repoName); + repositoryRequest.type(repoType); + repositoryRequest.settings(Settings.EMPTY); + repositoryRequest.encrypted(true); + CryptoSettings cryptoSettings = new CryptoSettings(keyProviderName); + cryptoSettings.keyProviderName(keyProviderName); + cryptoSettings.keyProviderType(keyProviderType); + cryptoSettings.settings(settings); + repositoryRequest.cryptoSettings(cryptoSettings); + + return repositoryRequest; + } + private ClusterState emptyState() { return ClusterState.builder(new ClusterName("test")).build(); } @@ -209,6 +569,103 @@ private void assertThrowsOnRegister(String repoName) { expectThrows(RepositoryException.class, () -> repositoriesService.registerRepository(request, null)); } + private static abstract class TestCryptoClient implements CryptoClient { + private final String name; + private final AtomicInteger ref; + + public TestCryptoClient(Settings settings, String keyProviderName) { + this.name = keyProviderName; + this.ref = new AtomicInteger(1); + } + + @Override + public void incRef() { + ref.incrementAndGet(); + } + + @Override + public boolean tryIncRef() { + ref.incrementAndGet(); + return true; + } + + @Override + public boolean decRef() { + ref.decrementAndGet(); + return true; + } + + public int getReferenceCount() { + return ref.get(); + } + + @Override + public String name() { + return name; + } + + @Override + public Object initCryptoContext() { + return new Object(); + } + + @Override + public long adjustEncryptedStreamSize(Object cryptoContextObj, long streamSize) { + return 0; + } + + @Override + public long estimateEncryptedLength(Object cryptoContextObj, long contentLength) { + return 0; + } + + @Override + public InputStreamContainer createEncryptingStream(Object cryptoContext, InputStreamContainer streamContainer) { + return null; + } + + @Override + public InputStreamContainer createEncryptingStreamOfPart( + Object cryptoContextObj, + InputStreamContainer stream, + int totalStreams, + int streamIdx + ) { + return null; + } + + @Override + public InputStream createDecryptingStream(InputStream encryptingStream) { + return null; + } + } + + private static class TestCryptoClientTypeA extends TestCryptoClient { + public static final String TYPE = "type-A"; + + public TestCryptoClientTypeA(Settings settings, String keyProviderName) { + super(settings, keyProviderName); + } + + @Override + public String type() { + return TYPE; + } + } + + private static class TestCryptoClientTypeB extends TestCryptoClient { + public static final String TYPE = "type-B"; + + public TestCryptoClientTypeB(Settings settings, String keyProviderName) { + super(settings, keyProviderName); + } + + @Override + public String type() { + return TYPE; + } + } + private static class TestRepository implements Repository { private static final String TYPE = "internal"; @@ -315,21 +772,6 @@ public void snapshotShard( } - @Override - public void snapshotRemoteStoreIndexShard( - Store store, - SnapshotId snapshotId, - IndexId indexId, - IndexCommit snapshotIndexCommit, - String shardStateIdentifier, - IndexShardSnapshotStatus snapshotStatus, - long primaryTerm, - long startTime, - ActionListener listener - ) { - - } - @Override public void restoreShard( Store store, diff --git a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java index 0bb2b604e8f1a..bbbd2359ebbc9 100644 --- a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java @@ -1769,6 +1769,7 @@ public void onFailure(final Exception e) { transportService, Collections.singletonMap(FsRepository.TYPE, getRepoFactory(environment)), emptyMap(), + emptyMap(), threadPool ); final ActionFilters actionFilters = new ActionFilters(emptySet());