Skip to content

Commit 59585de

Browse files
authored
Add the ClientEncryption.createEncryptedCollection helper method (#1079)
Add the `ClientEncryption.createEncryptedCollection` helper method JAVA-4679
1 parent 6aebed7 commit 59585de

File tree

16 files changed

+842
-3
lines changed

16 files changed

+842
-3
lines changed

config/spotbugs/exclude.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,18 @@
240240
<Method name="acquirePermitOrGetAvailableOpenedConnection"/>
241241
<Bug pattern="NS_NON_SHORT_CIRCUIT"/>
242242
</Match>
243+
244+
<!-- Can actually be null, but is not annotated as `@Nullable`. Annotating it as such causes warnings
245+
in other places where `null` is not handled, see https://jira.mongodb.org/browse/JAVA-4861.
246+
When the aforementioned ticket is done, it will be clear what to do with the warnings suppressed here. -->
247+
<Match>
248+
<Class name="com.mongodb.client.internal.ClientEncryptionImpl"/>
249+
<Method name="createEncryptedCollection"/>
250+
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
251+
</Match>
252+
<Match>
253+
<Class name="com.mongodb.reactivestreams.client.internal.vault.ClientEncryptionImpl"/>
254+
<Method name="~.*createEncryptedCollection.*"/>
255+
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
256+
</Match>
243257
</FindBugsFilter>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb;
17+
18+
import com.mongodb.annotations.Beta;
19+
import org.bson.BsonDocument;
20+
21+
import static com.mongodb.assertions.Assertions.assertNotNull;
22+
23+
/**
24+
* An exception thrown by methods that may automatically create data encryption keys
25+
* where needed based on the {@code encryptedFields} configuration.
26+
*
27+
* @since 4.9
28+
*/
29+
@Beta(Beta.Reason.SERVER)
30+
public final class MongoUpdatedEncryptedFieldsException extends MongoClientException {
31+
private static final long serialVersionUID = 1;
32+
33+
private final BsonDocument encryptedFields;
34+
35+
/**
36+
* Not part of the public API.
37+
*
38+
* @param encryptedFields The (partially) updated {@code encryptedFields} document,
39+
* which allows users to infer which data keys are known to be created before the exception happened
40+
* (see {@link #getEncryptedFields()} for more details).
41+
* Reporting this back to a user may be helpful because creation of a data key includes persisting it in the key vault.
42+
* @param msg The message.
43+
* @param cause The cause.
44+
*/
45+
public MongoUpdatedEncryptedFieldsException(final BsonDocument encryptedFields, final String msg, final Throwable cause) {
46+
super(msg, assertNotNull(cause));
47+
this.encryptedFields = assertNotNull(encryptedFields);
48+
}
49+
50+
/**
51+
* The {@code encryptedFields} document that allows inferring which data keys are <strong>known to be created</strong>
52+
* before {@code this} exception happened by comparing this document with the original {@code encryptedFields} configuration.
53+
* Creation of a data key includes persisting it in the key vault.
54+
* <p>
55+
* Note that the returned {@code encryptedFields} document is not guaranteed to contain information about all the data keys that
56+
* may be created, only about those that the driver is certain about. For example, if persisting a data key times out,
57+
* the driver does not know whether it can be considered created or not, and does not include the information about the key in
58+
* the {@code encryptedFields} document. You can analyze whether the {@linkplain #getCause() cause} is a definite or indefinite
59+
* error, and rely on the returned {@code encryptedFields} to be containing information on all created keys
60+
* only if the error is definite.</p>
61+
*
62+
* @return The updated {@code encryptedFields} document.
63+
*/
64+
public BsonDocument getEncryptedFields() {
65+
return encryptedFields;
66+
}
67+
}

driver-core/src/main/com/mongodb/client/model/CreateCollectionOptions.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,32 @@ public class CreateCollectionOptions {
4545
private ClusteredIndexOptions clusteredIndexOptions;
4646
private Bson encryptedFields;
4747

48+
public CreateCollectionOptions() {
49+
}
50+
51+
/**
52+
* A shallow copy constructor.
53+
*
54+
* @param options The options to copy.
55+
*
56+
* @since 4.9
57+
*/
58+
public CreateCollectionOptions(final CreateCollectionOptions options) {
59+
notNull("options", options);
60+
maxDocuments = options.maxDocuments;
61+
capped = options.capped;
62+
sizeInBytes = options.sizeInBytes;
63+
storageEngineOptions = options.storageEngineOptions;
64+
indexOptionDefaults = options.indexOptionDefaults;
65+
validationOptions = options.validationOptions;
66+
collation = options.collation;
67+
expireAfterSeconds = options.expireAfterSeconds;
68+
timeSeriesOptions = options.timeSeriesOptions;
69+
changeStreamPreAndPostImagesOptions = options.changeStreamPreAndPostImagesOptions;
70+
clusteredIndexOptions = options.clusteredIndexOptions;
71+
encryptedFields = options.encryptedFields;
72+
}
73+
4874
/**
4975
* Gets the maximum number of documents allowed in a capped collection.
5076
*
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model;
18+
19+
import com.mongodb.annotations.Beta;
20+
import com.mongodb.client.model.vault.DataKeyOptions;
21+
import com.mongodb.lang.Nullable;
22+
import org.bson.BsonDocument;
23+
24+
import static com.mongodb.assertions.Assertions.notNull;
25+
26+
/**
27+
* Auxiliary parameters for creating an encrypted collection.
28+
*
29+
* @since 4.9
30+
*/
31+
@Beta(Beta.Reason.SERVER)
32+
public final class CreateEncryptedCollectionParams {
33+
private final String kmsProvider;
34+
@Nullable
35+
private BsonDocument masterKey;
36+
37+
/**
38+
* @param kmsProvider The name of the KMS provider.
39+
*/
40+
public CreateEncryptedCollectionParams(final String kmsProvider) {
41+
this.kmsProvider = notNull("kmsProvider", kmsProvider);
42+
masterKey = null;
43+
}
44+
45+
/**
46+
* The name of the KMS provider.
47+
*
48+
* @return The name of the KMS provider.
49+
*/
50+
public String getKmsProvider() {
51+
return kmsProvider;
52+
}
53+
54+
/**
55+
* Sets the {@linkplain DataKeyOptions#getMasterKey() master key} for creating a data key.
56+
*
57+
* @param masterKey The master key for creating a data key.
58+
* @return {@code this}.
59+
*/
60+
public CreateEncryptedCollectionParams masterKey(@Nullable final BsonDocument masterKey) {
61+
this.masterKey = masterKey;
62+
return this;
63+
}
64+
65+
/**
66+
* The {@linkplain DataKeyOptions#getMasterKey() master key} for creating a data key.
67+
* The default is {@code null}.
68+
*
69+
* @return The master key for creating a data key.
70+
*/
71+
@Nullable
72+
public BsonDocument getMasterKey() {
73+
return masterKey;
74+
}
75+
76+
@Override
77+
public String toString() {
78+
return "CreateEncryptedCollectionParams{"
79+
+ ", kmsProvider=" + kmsProvider
80+
+ ", masterKey=" + masterKey
81+
+ '}';
82+
}
83+
}

driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/vault/ClientEncryptionImpl.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
package com.mongodb.reactivestreams.client.internal.vault;
1818

1919
import com.mongodb.ClientEncryptionSettings;
20+
import com.mongodb.MongoClientSettings;
21+
import com.mongodb.MongoConfigurationException;
2022
import com.mongodb.MongoNamespace;
23+
import com.mongodb.MongoUpdatedEncryptedFieldsException;
2124
import com.mongodb.ReadConcern;
2225
import com.mongodb.WriteConcern;
26+
import com.mongodb.client.model.CreateCollectionOptions;
27+
import com.mongodb.client.model.CreateEncryptedCollectionParams;
2328
import com.mongodb.client.model.Filters;
2429
import com.mongodb.client.model.UpdateOneModel;
2530
import com.mongodb.client.model.Updates;
@@ -32,22 +37,30 @@
3237
import com.mongodb.reactivestreams.client.MongoClient;
3338
import com.mongodb.reactivestreams.client.MongoClients;
3439
import com.mongodb.reactivestreams.client.MongoCollection;
40+
import com.mongodb.reactivestreams.client.MongoDatabase;
3541
import com.mongodb.reactivestreams.client.internal.crypt.Crypt;
3642
import com.mongodb.reactivestreams.client.internal.crypt.Crypts;
3743
import com.mongodb.reactivestreams.client.vault.ClientEncryption;
3844
import org.bson.BsonArray;
3945
import org.bson.BsonBinary;
4046
import org.bson.BsonDocument;
47+
import org.bson.BsonNull;
4148
import org.bson.BsonString;
4249
import org.bson.BsonValue;
50+
import org.bson.codecs.configuration.CodecRegistry;
4351
import org.bson.conversions.Bson;
4452
import org.reactivestreams.Publisher;
53+
import reactor.core.publisher.Flux;
4554
import reactor.core.publisher.Mono;
4655

4756
import java.util.List;
57+
import java.util.Objects;
58+
import java.util.concurrent.atomic.AtomicBoolean;
4859
import java.util.stream.Collectors;
4960

61+
import static com.mongodb.assertions.Assertions.notNull;
5062
import static com.mongodb.internal.capi.MongoCryptHelper.validateRewrapManyDataKeyOptions;
63+
import static java.lang.String.format;
5164
import static java.util.Arrays.asList;
5265
import static java.util.Collections.singletonList;
5366

@@ -183,6 +196,79 @@ public Publisher<RewrapManyDataKeyResult> rewrapManyDataKey(final Bson filter, f
183196
}));
184197
}
185198

199+
@Override
200+
public Publisher<BsonDocument> createEncryptedCollection(final MongoDatabase database, final String collectionName,
201+
final CreateCollectionOptions createCollectionOptions, final CreateEncryptedCollectionParams createEncryptedCollectionParams) {
202+
notNull("collectionName", collectionName);
203+
notNull("createCollectionOptions", createCollectionOptions);
204+
notNull("createEncryptedCollectionParams", createEncryptedCollectionParams);
205+
MongoNamespace namespace = new MongoNamespace(database.getName(), collectionName);
206+
Bson rawEncryptedFields = createCollectionOptions.getEncryptedFields();
207+
if (rawEncryptedFields == null) {
208+
throw new MongoConfigurationException(format("`encryptedFields` is not configured for the collection %s.", namespace));
209+
}
210+
CodecRegistry codecRegistry = options.getKeyVaultMongoClientSettings() == null
211+
? MongoClientSettings.getDefaultCodecRegistry()
212+
: options.getKeyVaultMongoClientSettings().getCodecRegistry();
213+
BsonDocument encryptedFields = rawEncryptedFields.toBsonDocument(BsonDocument.class, codecRegistry);
214+
BsonValue fields = encryptedFields.get("fields");
215+
if (fields != null && fields.isArray()) {
216+
String kmsProvider = createEncryptedCollectionParams.getKmsProvider();
217+
DataKeyOptions dataKeyOptions = new DataKeyOptions();
218+
BsonDocument masterKey = createEncryptedCollectionParams.getMasterKey();
219+
if (masterKey != null) {
220+
dataKeyOptions.masterKey(masterKey);
221+
}
222+
String keyIdBsonKey = "keyId";
223+
return Mono.defer(() -> {
224+
// `Mono.defer` results in `maybeUpdatedEncryptedFields` and `dataKeyMightBeCreated` (mutable state)
225+
// being created once per `Subscriber`, which allows the produced `Mono` to support multiple `Subscribers`.
226+
BsonDocument maybeUpdatedEncryptedFields = encryptedFields.clone();
227+
AtomicBoolean dataKeyMightBeCreated = new AtomicBoolean();
228+
Iterable<Mono<BsonDocument>> publishersOfUpdatedFields = () -> maybeUpdatedEncryptedFields.get("fields").asArray()
229+
.stream()
230+
.filter(BsonValue::isDocument)
231+
.map(BsonValue::asDocument)
232+
.filter(field -> field.containsKey(keyIdBsonKey))
233+
.filter(field -> Objects.equals(field.get(keyIdBsonKey), BsonNull.VALUE))
234+
// here we rely on the `createDataKey` publisher being cold, i.e., doing nothing until it is subscribed to
235+
.map(field -> Mono.fromDirect(createDataKey(kmsProvider, dataKeyOptions))
236+
// This is the closest we can do with reactive streams to setting the `dataKeyMightBeCreated` flag
237+
// immediately before calling `createDataKey`.
238+
.doOnSubscribe(subscription -> dataKeyMightBeCreated.set(true))
239+
.doOnNext(dataKeyId -> field.put(keyIdBsonKey, dataKeyId))
240+
.map(dataKeyId -> field)
241+
)
242+
.iterator();
243+
// `Flux.concat` ensures that data keys are created / fields are updated sequentially one by one
244+
Flux<BsonDocument> publisherOfUpdatedFields = Flux.concat(publishersOfUpdatedFields);
245+
return publisherOfUpdatedFields
246+
// All write actions in `doOnNext` above happen-before the completion (`onComplete`/`onError`) signals
247+
// for this publisher, because all signals are serial. `thenEmpty` further guarantees that the completion signal
248+
// for this publisher happens-before the `onSubscribe` signal for the publisher passed to it
249+
// (the next publisher, which creates a collection).
250+
// `defer` defers calling `createCollection` until the next publisher is subscribed to.
251+
// Therefore, all write actions in `doOnNext` above happen-before the invocation of `createCollection`,
252+
// which means `createCollection` is guaranteed to observe all those write actions, i.e.,
253+
// it is guaranteed to observe the updated document via the `maybeUpdatedEncryptedFields` reference.
254+
//
255+
// Similarly, the `Subscriber` of the returned `Publisher` is guaranteed to observe all those write actions
256+
// via the `maybeUpdatedEncryptedFields` reference, which is emitted as a result of `thenReturn`.
257+
.thenEmpty(Mono.defer(() -> Mono.fromDirect(database.createCollection(collectionName,
258+
new CreateCollectionOptions(createCollectionOptions).encryptedFields(maybeUpdatedEncryptedFields))))
259+
)
260+
.onErrorMap(e -> dataKeyMightBeCreated.get(), e ->
261+
new MongoUpdatedEncryptedFieldsException(maybeUpdatedEncryptedFields,
262+
format("Failed to create %s.", namespace), e)
263+
)
264+
.thenReturn(maybeUpdatedEncryptedFields);
265+
});
266+
} else {
267+
return Mono.fromDirect(database.createCollection(collectionName, createCollectionOptions))
268+
.thenReturn(encryptedFields);
269+
}
270+
}
271+
186272
@Override
187273
public void close() {
188274
keyVaultClient.close();

driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/vault/ClientEncryption.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616

1717
package com.mongodb.reactivestreams.client.vault;
1818

19+
import com.mongodb.AutoEncryptionSettings;
20+
import com.mongodb.MongoUpdatedEncryptedFieldsException;
1921
import com.mongodb.annotations.Beta;
22+
import com.mongodb.client.model.CreateCollectionOptions;
23+
import com.mongodb.client.model.CreateEncryptedCollectionParams;
2024
import com.mongodb.client.model.vault.DataKeyOptions;
2125
import com.mongodb.client.model.vault.EncryptOptions;
2226
import com.mongodb.client.model.vault.RewrapManyDataKeyOptions;
2327
import com.mongodb.client.model.vault.RewrapManyDataKeyResult;
2428
import com.mongodb.client.result.DeleteResult;
2529
import com.mongodb.lang.Nullable;
2630
import com.mongodb.reactivestreams.client.FindPublisher;
31+
import com.mongodb.reactivestreams.client.MongoDatabase;
2732
import org.bson.BsonBinary;
2833
import org.bson.BsonDocument;
2934
import org.bson.BsonValue;
@@ -187,6 +192,33 @@ public interface ClientEncryption extends Closeable {
187192
*/
188193
Publisher<RewrapManyDataKeyResult> rewrapManyDataKey(Bson filter, RewrapManyDataKeyOptions options);
189194

195+
/**
196+
* {@linkplain MongoDatabase#createCollection(String, CreateCollectionOptions) Create} a new collection with encrypted fields,
197+
* automatically {@linkplain #createDataKey(String, DataKeyOptions) creating}
198+
* new data encryption keys when needed based on the configured
199+
* {@link CreateCollectionOptions#getEncryptedFields() encryptedFields}, which must be specified.
200+
* This method does not modify the configured {@code encryptedFields} when creating new data keys,
201+
* instead it creates a new configuration if needed.
202+
*
203+
* @param database The database to use for creating the collection.
204+
* @param collectionName The name for the collection to create.
205+
* @param createCollectionOptions Options for creating the collection.
206+
* @param createEncryptedCollectionParams Auxiliary parameters for creating an encrypted collection.
207+
* @return A publisher of the (potentially updated) {@code encryptedFields} configuration that was used to create the
208+
* collection. A user may use this document to configure {@link AutoEncryptionSettings#getEncryptedFieldsMap()}.
209+
* <p>
210+
* {@linkplain org.reactivestreams.Subscriber#onError(Throwable) Signals} {@link MongoUpdatedEncryptedFieldsException}
211+
* if an exception happens after creating at least one data key. This exception makes the updated {@code encryptedFields}
212+
* {@linkplain MongoUpdatedEncryptedFieldsException#getEncryptedFields() available} to the caller.</p>
213+
*
214+
* @since 4.9
215+
* @mongodb.server.release 6.0
216+
* @mongodb.driver.manual reference/command/create Create Command
217+
*/
218+
@Beta(Beta.Reason.SERVER)
219+
Publisher<BsonDocument> createEncryptedCollection(MongoDatabase database, String collectionName,
220+
CreateCollectionOptions createCollectionOptions, CreateEncryptedCollectionParams createEncryptedCollectionParams);
221+
190222
@Override
191223
void close();
192224
}

0 commit comments

Comments
 (0)