diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java index 68ba6617e0b..bb211d7b7e3 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java @@ -23,6 +23,7 @@ import com.mongodb.ServerCursor; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerDescription; +import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.binding.AsyncConnectionSource; @@ -57,6 +58,7 @@ import static com.mongodb.ReadPreference.primary; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.connection.ServerType.SHARD_ROUTER; +import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import static com.mongodb.internal.operation.CommandOperationHelper.createReadCommandAndExecute; @@ -83,6 +85,8 @@ * An operation that provides a cursor allowing iteration through the metadata of all the collections in a database. This operation * ensures that the value of the {@code name} field of each returned document is the simple name of the collection rather than the full * namespace. + *

+ * See {@code listCollections}

. * * @param the document type * @since 3.0 @@ -95,6 +99,7 @@ public class ListCollectionsOperation implements AsyncReadOperation nameOnly(final boolean nameOnly) { return this; } + /** + * Ignored unless {@link #nameOnly(boolean)} is {@code true}. + * + * @since 4.5 + * @mongodb.server.release 4.0 + */ + public ListCollectionsOperation authorizedCollections(final boolean authorizedCollections) { + this.authorizedCollections = authorizedCollections; + return this; + } + + /** + * This method is used by tests via the reflection API. For example, see {@code TestHelper.assertOperationIsTheSameAs}. + */ + @VisibleForTesting(otherwise = PRIVATE) + public boolean isAuthorizedCollections() { + return authorizedCollections; + } + /** * Gets the number of documents to return per batch. * @@ -351,6 +375,9 @@ private BsonDocument getCommand() { } if (nameOnly) { command.append("nameOnly", BsonBoolean.TRUE); + if (authorizedCollections) { + command.append("authorizedCollections", BsonBoolean.TRUE); + } } if (maxTimeMS > 0) { command.put("maxTimeMS", new BsonInt64(maxTimeMS)); diff --git a/driver-core/src/main/com/mongodb/internal/operation/Operations.java b/driver-core/src/main/com/mongodb/internal/operation/Operations.java index b094b4fe74c..13b37f7a7f9 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/Operations.java +++ b/driver-core/src/main/com/mongodb/internal/operation/Operations.java @@ -542,11 +542,13 @@ public DropIndexOperation dropIndex(final Bson keys, final DropIndexOptions drop public ListCollectionsOperation listCollections(final String databaseName, final Class resultClass, final Bson filter, final boolean collectionNamesOnly, + final boolean authorizedCollections, final Integer batchSize, final long maxTimeMS) { return new ListCollectionsOperation(databaseName, codecRegistry.get(resultClass)) .retryReads(retryReads) .filter(toBsonDocumentOrNull(filter)) .nameOnly(collectionNamesOnly) + .authorizedCollections(authorizedCollections) .batchSize(batchSize == null ? 0 : batchSize) .maxTime(maxTimeMS, MILLISECONDS); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java b/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java index c9a53d3a33d..f04ea3a15fd 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java @@ -240,8 +240,10 @@ public WriteOperation dropIndex(final Bson keys, final DropIndexOptions op public ReadOperation> listCollections(final String databaseName, final Class resultClass, final Bson filter, final boolean collectionNamesOnly, + final boolean authorizedCollections, final Integer batchSize, final long maxTimeMS) { - return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, batchSize, maxTimeMS); + return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, authorizedCollections, + batchSize, maxTimeMS); } public ReadOperation> listDatabases(final Class resultClass, final Bson filter, diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/ListCollectionsPublisher.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/ListCollectionsPublisher.java index 2efcaf65acd..9019190cc78 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/ListCollectionsPublisher.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/ListCollectionsPublisher.java @@ -27,6 +27,7 @@ * * @param The type of the result. * @since 1.0 + * @mongodb.driver.manual reference/command/listCollections/ listCollections */ public interface ListCollectionsPublisher extends Publisher { @@ -39,6 +40,21 @@ public interface ListCollectionsPublisher extends Publisher { */ ListCollectionsPublisher filter(@Nullable Bson filter); + /** + * Sets the {@code authorizedCollections} field of the {@code listCollections} command. + * This method is ignored if called on a {@link ListCollectionsPublisher} obtained not via any of the + * {@link MongoDatabase#listCollectionNames() MongoDatabase.listCollectionNames} methods. + * + * @param authorizedCollections If {@code true}, allows executing the {@code listCollections} command, + * which has the {@code nameOnly} field set to {@code true}, without having the + * + * {@code listCollections} privilege on the corresponding database resource. + * @return {@code this}. + * @since 4.5 + * @mongodb.server.release 4.0 + */ + ListCollectionsPublisher authorizedCollections(boolean authorizedCollections); + /** * Sets the maximum execution time on the server for this operation. * diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoDatabase.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoDatabase.java index dabdb86ff9a..5f7fc912bbc 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoDatabase.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/MongoDatabase.java @@ -237,18 +237,20 @@ public interface MongoDatabase { * Gets the names of all the collections in this database. * * @return a publisher with all the names of all the collections in this database + * @mongodb.driver.manual reference/command/listCollections listCollections */ - Publisher listCollectionNames(); + ListCollectionsPublisher listCollectionNames(); /** * Gets the names of all the collections in this database. * * @param clientSession the client session with which to associate this operation * @return a publisher with all the names of all the collections in this database + * @mongodb.driver.manual reference/command/listCollections listCollections * @mongodb.server.release 3.6 * @since 1.7 */ - Publisher listCollectionNames(ClientSession clientSession); + ListCollectionsPublisher listCollectionNames(ClientSession clientSession); /** * Finds all the collections in this database. diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImpl.java index fb852f46cb9..cf8aee2230e 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImpl.java @@ -17,21 +17,29 @@ package com.mongodb.reactivestreams.client.internal; import com.mongodb.ReadConcern; +import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.operation.AsyncReadOperation; import com.mongodb.lang.Nullable; import com.mongodb.reactivestreams.client.ClientSession; import com.mongodb.reactivestreams.client.ListCollectionsPublisher; import org.bson.conversions.Bson; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import static com.mongodb.assertions.Assertions.notNull; +import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; import static java.util.concurrent.TimeUnit.MILLISECONDS; final class ListCollectionsPublisherImpl extends BatchCursorPublisher implements ListCollectionsPublisher { private final boolean collectionNamesOnly; + private boolean authorizedCollections; private Bson filter; private long maxTimeMS; @@ -59,8 +67,72 @@ public ListCollectionsPublisherImpl filter(@Nullable final Bson filter) { return this; } + @Override + public ListCollectionsPublisherImpl authorizedCollections(final boolean authorizedCollections) { + this.authorizedCollections = authorizedCollections; + return this; + } + AsyncReadOperation> asAsyncReadOperation(final int initialBatchSize) { return getOperations().listCollections(getNamespace().getDatabaseName(), getDocumentClass(), filter, collectionNamesOnly, - initialBatchSize, maxTimeMS); + authorizedCollections, initialBatchSize, maxTimeMS); + } + + ListCollectionsPublisher map(final Function mapper) { + return new Mapping<>(this, mapper); + } + + private static final class Mapping implements ListCollectionsPublisher { + private final ListCollectionsPublisher wrapped; + private final Publisher mappingPublisher; + private final Function mapper; + + Mapping(final ListCollectionsPublisher publisher, final Function mapper) { + this.wrapped = publisher; + mappingPublisher = Flux.from(publisher).map(mapper); + this.mapper = mapper; + } + + @Override + public ListCollectionsPublisher filter(@Nullable final Bson filter) { + wrapped.filter(filter); + return this; + } + + @Override + public ListCollectionsPublisher authorizedCollections(final boolean authorizedCollections) { + wrapped.authorizedCollections(authorizedCollections); + return this; + } + + @Override + public ListCollectionsPublisher maxTime(final long maxTime, final TimeUnit timeUnit) { + wrapped.maxTime(maxTime, timeUnit); + return this; + } + + @Override + public ListCollectionsPublisher batchSize(final int batchSize) { + wrapped.batchSize(batchSize); + return this; + } + + @Override + public Publisher first() { + return Mono.from(wrapped.first()).map(mapper); + } + + @Override + public void subscribe(final Subscriber s) { + mappingPublisher.subscribe(s); + } + + /** + * This method is used in tests via the reflection API. + */ + @VisibleForTesting(otherwise = PRIVATE) + ListCollectionsPublisher getMapped() { + return wrapped; + } } } diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MongoDatabaseImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MongoDatabaseImpl.java index ece57b2bbdd..a6d6ae98bcf 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MongoDatabaseImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/MongoDatabaseImpl.java @@ -34,7 +34,6 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import java.util.Collections; import java.util.List; @@ -168,14 +167,14 @@ public Publisher drop(final ClientSession clientSession) { } @Override - public Publisher listCollectionNames() { - return Flux.from(new ListCollectionsPublisherImpl<>(null, mongoOperationPublisher, true)) + public ListCollectionsPublisher listCollectionNames() { + return new ListCollectionsPublisherImpl<>(null, mongoOperationPublisher, true) .map(d -> d.getString("name")); } @Override - public Publisher listCollectionNames(final ClientSession clientSession) { - return Flux.from(new ListCollectionsPublisherImpl<>(notNull("clientSession", clientSession), mongoOperationPublisher, true)) + public ListCollectionsPublisher listCollectionNames(final ClientSession clientSession) { + return new ListCollectionsPublisherImpl<>(notNull("clientSession", clientSession), mongoOperationPublisher, true) .map(d -> d.getString("name")); } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncListCollectionsIterable.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncListCollectionsIterable.java index 1cba7ff5fa8..0fe0f0ce20d 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncListCollectionsIterable.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncListCollectionsIterable.java @@ -37,6 +37,12 @@ public ListCollectionsIterable filter(@Nullable final Bson filter) { return this; } + @Override + public ListCollectionsIterable authorizedCollections(final boolean authorizedCollections) { + wrapped.authorizedCollections(authorizedCollections); + return this; + } + @Override public ListCollectionsIterable maxTime(final long maxTime, final TimeUnit timeUnit) { wrapped.maxTime(maxTime, timeUnit); diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncMongoDatabase.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncMongoDatabase.java index 080a664fa3c..197b6a5bbbc 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncMongoDatabase.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/syncadapter/SyncMongoDatabase.java @@ -25,7 +25,6 @@ import com.mongodb.client.ListCollectionsIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.CreateViewOptions; import org.bson.Document; @@ -157,7 +156,7 @@ public void drop(final ClientSession clientSession) { } @Override - public MongoIterable listCollectionNames() { + public ListCollectionsIterable listCollectionNames() { throw new UnsupportedOperationException(); } @@ -172,7 +171,7 @@ public ListCollectionsIterable listCollections(final Class listCollectionNames(final ClientSession clientSession) { + public ListCollectionsIterable listCollectionNames(final ClientSession clientSession) { throw new UnsupportedOperationException(); } diff --git a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImplTest.java b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImplTest.java index c875ab7973c..390c6100785 100644 --- a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImplTest.java +++ b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/ListCollectionsPublisherImplTest.java @@ -40,12 +40,15 @@ public class ListCollectionsPublisherImplTest extends TestHelper { void shouldBuildTheExpectedOperation() { TestOperationExecutor executor = createOperationExecutor(asList(getBatchCursor(), getBatchCursor())); ListCollectionsPublisher publisher = new ListCollectionsPublisherImpl<>(null, createMongoOperationPublisher(executor) - .withDocumentClass(String.class), true); + .withDocumentClass(String.class), true) + .authorizedCollections(true); ListCollectionsOperation expectedOperation = new ListCollectionsOperation<>(DATABASE_NAME, getDefaultCodecRegistry().get(String.class)) .batchSize(Integer.MAX_VALUE) - .nameOnly(true).retryReads(true); + .nameOnly(true) + .authorizedCollections(true) + .retryReads(true); // default input should be as expected Flux.from(publisher).blockFirst(); diff --git a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/MongoDatabaseImplTest.java b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/MongoDatabaseImplTest.java index ced5b201b29..f5ffbc6a2a7 100644 --- a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/MongoDatabaseImplTest.java +++ b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/MongoDatabaseImplTest.java @@ -134,6 +134,13 @@ void testListCollectionNames() { new ListCollectionsPublisherImpl<>(null, mongoOperationPublisher, true); assertPublisherIsTheSameAs(expected, database.listCollectionNames(), "Default"); }, + () -> { + ListCollectionsPublisher expected = + new ListCollectionsPublisherImpl<>(null, mongoOperationPublisher, true) + .authorizedCollections(true); + assertPublisherIsTheSameAs(expected, database.listCollectionNames().authorizedCollections(true), + "nameOnly & authorizedCollections"); + }, () -> { ListCollectionsPublisher expected = new ListCollectionsPublisherImpl<>(clientSession, mongoOperationPublisher, true); diff --git a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/TestHelper.java b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/TestHelper.java index 4e122979ce5..c1de66f81c8 100644 --- a/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/TestHelper.java +++ b/driver-reactive-streams/src/test/unit/com/mongodb/reactivestreams/client/internal/TestHelper.java @@ -44,6 +44,7 @@ import reactor.core.publisher.Mono; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -52,6 +53,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; +import java.util.stream.Stream; import static com.mongodb.reactivestreams.client.MongoClients.getDefaultCodecRegistry; import static java.util.stream.Collectors.toList; @@ -190,21 +192,23 @@ private static Object checkValueTypes(final Object instance) { } private static Publisher getRootSource(final Publisher publisher) { - Optional> sourcePublisher = Optional.of(publisher); + Publisher sourcePublisher = publisher; // Uses reflection to find the root / source publisher if (publisher instanceof Scannable) { Scannable scannable = (Scannable) publisher; List parents = scannable.parents().collect(toList()); if (parents.isEmpty()) { - sourcePublisher = getSource(scannable); + sourcePublisher = getSource(scannable).orElse(publisher); } else { sourcePublisher = parents.stream().map(TestHelper::getSource) .filter(Optional::isPresent) .reduce((first, second) -> second) - .orElse(Optional.empty()); + .flatMap(Function.identity()) + .orElse(publisher); } } - return sourcePublisher.orElse(publisher); + sourcePublisher = getMapped(sourcePublisher).orElse(sourcePublisher); + return sourcePublisher; } private static Optional> getSource(final Scannable scannable) { @@ -216,6 +220,19 @@ private static Optional> getSource(final Scannable scannable) { } } + private static Optional> getMapped(final Publisher maybeMappingPublisher) { + return Stream.of(maybeMappingPublisher.getClass().getDeclaredMethods()) + .filter(m -> m.getName().equals("getMapped")) + .findFirst() + .map(m -> { + try { + return (Publisher) m.invoke(maybeMappingPublisher); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }); + } + private static Optional> getScannableSource(final Scannable scannable) { return (Optional>) getScannableFieldValue(scannable, "source"); } diff --git a/driver-scala/src/it/scala/org/mongodb/scala/syncadapter/SyncListCollectionsIterable.scala b/driver-scala/src/it/scala/org/mongodb/scala/syncadapter/SyncListCollectionsIterable.scala index 033b51b9205..b1486724dc4 100644 --- a/driver-scala/src/it/scala/org/mongodb/scala/syncadapter/SyncListCollectionsIterable.scala +++ b/driver-scala/src/it/scala/org/mongodb/scala/syncadapter/SyncListCollectionsIterable.scala @@ -29,6 +29,11 @@ case class SyncListCollectionsIterable[T](wrapped: ListCollectionsObservable[T]) this } + override def authorizedCollections(authorizedCollections: Boolean): ListCollectionsIterable[T] = { + wrapped.authorizedCollections(authorizedCollections) + this + } + override def maxTime(maxTime: Long, timeUnit: TimeUnit): ListCollectionsIterable[T] = { wrapped.maxTime(maxTime, timeUnit) this diff --git a/driver-scala/src/main/scala/org/mongodb/scala/ListCollectionsObservable.scala b/driver-scala/src/main/scala/org/mongodb/scala/ListCollectionsObservable.scala index 06df5a5a374..4f33dd78023 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/ListCollectionsObservable.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/ListCollectionsObservable.scala @@ -44,6 +44,23 @@ case class ListCollectionsObservable[TResult](wrapped: ListCollectionsPublisher[ this } + /** + * Sets the `authorizedCollections` field of the `istCollections` command. + * This method is ignored if called on a [[ListCollectionsObservable]] obtained not via any of the + * `MongoDatabase.listCollectionNames` methods. + * + * @param authorizedCollections If `true`, allows executing the `listCollections` command, + * which has the `nameOnly` field set to `true`, without having the + * + * `listCollections` privilege on the corresponding database resource. + * @return `this`. + * @since 4.5 + */ + def authorizedCollections(authorizedCollections: Boolean): ListCollectionsObservable[TResult] = { + wrapped.authorizedCollections(authorizedCollections) + this + } + /** * Sets the maximum execution time on the server for this operation. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/MongoDatabase.scala b/driver-scala/src/main/scala/org/mongodb/scala/MongoDatabase.scala index cde98735769..c283ac3e956 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/MongoDatabase.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/MongoDatabase.scala @@ -203,7 +203,8 @@ case class MongoDatabase(private[scala] val wrapped: JMongoDatabase) { * * @return a Observable with all the names of all the collections in this database */ - def listCollectionNames(): Observable[String] = wrapped.listCollectionNames() + def listCollectionNames(): ListCollectionsObservable[String] = + ListCollectionsObservable(wrapped.listCollectionNames()) /** * Finds all the collections in this database. @@ -226,7 +227,8 @@ case class MongoDatabase(private[scala] val wrapped: JMongoDatabase) { * @since 2.2 * @note Requires MongoDB 3.6 or greater */ - def listCollectionNames(clientSession: ClientSession): Observable[String] = wrapped.listCollectionNames(clientSession) + def listCollectionNames(clientSession: ClientSession): ListCollectionsObservable[String] = + ListCollectionsObservable(wrapped.listCollectionNames(clientSession)) /** * Finds all the collections in this database. diff --git a/driver-sync/src/main/com/mongodb/client/ListCollectionsIterable.java b/driver-sync/src/main/com/mongodb/client/ListCollectionsIterable.java index 4865e4171be..82566a0eb0b 100644 --- a/driver-sync/src/main/com/mongodb/client/ListCollectionsIterable.java +++ b/driver-sync/src/main/com/mongodb/client/ListCollectionsIterable.java @@ -26,6 +26,7 @@ * * @param The type of the result. * @since 3.0 + * @mongodb.driver.manual reference/command/listCollections/ listCollections */ public interface ListCollectionsIterable extends MongoIterable { @@ -38,6 +39,21 @@ public interface ListCollectionsIterable extends MongoIterable */ ListCollectionsIterable filter(@Nullable Bson filter); + /** + * Sets the {@code authorizedCollections} field of the {@code listCollections} command. + * This method is ignored if called on a {@link ListCollectionsIterable} obtained not via any of the + * {@link MongoDatabase#listCollectionNames() MongoDatabase.listCollectionNames} methods. + * + * @param authorizedCollections If {@code true}, allows executing the {@code listCollections} command, + * which has the {@code nameOnly} field set to {@code true}, without having the + * + * {@code listCollections} privilege on the corresponding database resource. + * @return {@code this}. + * @since 4.5 + * @mongodb.server.release 4.0 + */ + ListCollectionsIterable authorizedCollections(boolean authorizedCollections); + /** * Sets the maximum execution time on the server for this operation. * diff --git a/driver-sync/src/main/com/mongodb/client/MongoDatabase.java b/driver-sync/src/main/com/mongodb/client/MongoDatabase.java index e1812bc6d3d..1aae0d39437 100644 --- a/driver-sync/src/main/com/mongodb/client/MongoDatabase.java +++ b/driver-sync/src/main/com/mongodb/client/MongoDatabase.java @@ -240,8 +240,9 @@ public interface MongoDatabase { * Gets the names of all the collections in this database. * * @return an iterable containing all the names of all the collections in this database + * @mongodb.driver.manual reference/command/listCollections listCollections */ - MongoIterable listCollectionNames(); + ListCollectionsIterable listCollectionNames(); /** * Finds all the collections in this database. @@ -268,8 +269,9 @@ public interface MongoDatabase { * @return an iterable containing all the names of all the collections in this database * @since 3.6 * @mongodb.server.release 3.6 + * @mongodb.driver.manual reference/command/listCollections listCollections */ - MongoIterable listCollectionNames(ClientSession clientSession); + ListCollectionsIterable listCollectionNames(ClientSession clientSession); /** * Finds all the collections in this database. diff --git a/driver-sync/src/main/com/mongodb/client/internal/ListCollectionsIterableImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ListCollectionsIterableImpl.java index 38c2afe1216..ec89d176340 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ListCollectionsIterableImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ListCollectionsIterableImpl.java @@ -16,10 +16,13 @@ package com.mongodb.client.internal; +import com.mongodb.Function; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.client.ClientSession; import com.mongodb.client.ListCollectionsIterable; +import com.mongodb.client.MongoCursor; +import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.operation.BatchCursor; import com.mongodb.internal.operation.ReadOperation; import com.mongodb.internal.operation.SyncOperations; @@ -28,9 +31,11 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import java.util.Collection; import java.util.concurrent.TimeUnit; import static com.mongodb.assertions.Assertions.notNull; +import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; import static java.util.concurrent.TimeUnit.MILLISECONDS; class ListCollectionsIterableImpl extends MongoIterableImpl implements ListCollectionsIterable { @@ -40,14 +45,9 @@ class ListCollectionsIterableImpl extends MongoIterableImpl im private Bson filter; private final boolean collectionNamesOnly; + private boolean authorizedCollections; private long maxTimeMS; - ListCollectionsIterableImpl(@Nullable final ClientSession clientSession, final String databaseName, final boolean collectionNamesOnly, - final Class resultClass, final CodecRegistry codecRegistry, final ReadPreference readPreference, - final OperationExecutor executor) { - this(clientSession, databaseName, collectionNamesOnly, resultClass, codecRegistry, readPreference, executor, true); - } - ListCollectionsIterableImpl(@Nullable final ClientSession clientSession, final String databaseName, final boolean collectionNamesOnly, final Class resultClass, final CodecRegistry codecRegistry, final ReadPreference readPreference, final OperationExecutor executor, final boolean retryReads) { @@ -64,6 +64,12 @@ public ListCollectionsIterable filter(@Nullable final Bson filter) { return this; } + @Override + public ListCollectionsIterableImpl authorizedCollections(final boolean authorizedCollections) { + this.authorizedCollections = authorizedCollections; + return this; + } + @Override public ListCollectionsIterable maxTime(final long maxTime, final TimeUnit timeUnit) { notNull("timeUnit", timeUnit); @@ -79,6 +85,82 @@ public ListCollectionsIterable batchSize(final int batchSize) { @Override public ReadOperation> asReadOperation() { - return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, getBatchSize(), maxTimeMS); + return operations.listCollections(databaseName, resultClass, filter, collectionNamesOnly, authorizedCollections, + getBatchSize(), maxTimeMS); + } + + @Override + public ListCollectionsIterable map(final Function mapper) { + return new Mapping<>(this, mapper); + } + + private static final class Mapping implements ListCollectionsIterable { + private final ListCollectionsIterable wrapped; + private final Function mapper; + + Mapping(final ListCollectionsIterable iterable, final Function mapper) { + this.wrapped = iterable; + this.mapper = mapper; + } + + @Override + public Mapping filter(@Nullable final Bson filter) { + wrapped.filter(filter); + return this; + } + + @Override + public ListCollectionsIterable authorizedCollections(final boolean authorizedCollections) { + wrapped.authorizedCollections(authorizedCollections); + return this; + } + + @Override + public ListCollectionsIterable maxTime(final long maxTime, final TimeUnit timeUnit) { + wrapped.maxTime(maxTime, timeUnit); + return this; + } + + @Override + public ListCollectionsIterable batchSize(final int batchSize) { + wrapped.batchSize(batchSize); + return this; + } + + @Override + public MongoCursor iterator() { + return new MongoMappingCursor<>(wrapped.iterator(), mapper); + } + + @Override + public MongoCursor cursor() { + return iterator(); + } + + @Nullable + @Override + public U first() { + T first = wrapped.first(); + return first == null ? null : mapper.apply(first); + } + + @Override + public Mapping map(final Function mapper) { + return new Mapping<>(this, mapper); + } + + @Override + public > A into(final A target) { + forEach(target::add); + return target; + } + + /** + * This method is used in tests written in Groovy. + */ + @VisibleForTesting(otherwise = PRIVATE) + ListCollectionsIterable getMapped() { + return wrapped; + } } } diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoDatabaseImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoDatabaseImpl.java index bae604935a3..63f5807ab26 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoDatabaseImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoDatabaseImpl.java @@ -28,7 +28,6 @@ import com.mongodb.client.ListCollectionsIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.CreateViewOptions; import com.mongodb.client.model.IndexOptionDefaults; @@ -212,17 +211,17 @@ private void executeDrop(@Nullable final ClientSession clientSession) { } @Override - public MongoIterable listCollectionNames() { + public ListCollectionsIterable listCollectionNames() { return createListCollectionNamesIterable(null); } @Override - public MongoIterable listCollectionNames(final ClientSession clientSession) { + public ListCollectionsIterable listCollectionNames(final ClientSession clientSession) { notNull("clientSession", clientSession); return createListCollectionNamesIterable(clientSession); } - private MongoIterable createListCollectionNamesIterable(@Nullable final ClientSession clientSession) { + private ListCollectionsIterable createListCollectionNamesIterable(@Nullable final ClientSession clientSession) { return createListCollectionsIterable(clientSession, BsonDocument.class, true) .map(new Function() { @Override @@ -253,7 +252,7 @@ public ListCollectionsIterable listCollections(final ClientSe return createListCollectionsIterable(clientSession, resultClass, false); } - private ListCollectionsIterable createListCollectionsIterable(@Nullable final ClientSession clientSession, + private ListCollectionsIterableImpl createListCollectionsIterable(@Nullable final ClientSession clientSession, final Class resultClass, final boolean collectionNamesOnly) { return new ListCollectionsIterableImpl<>(clientSession, name, collectionNamesOnly, resultClass, codecRegistry, diff --git a/driver-sync/src/test/unit/com/mongodb/client/internal/ListCollectionsIterableSpecification.groovy b/driver-sync/src/test/unit/com/mongodb/client/internal/ListCollectionsIterableSpecification.groovy index 3bc72838b10..5cd4182afe6 100644 --- a/driver-sync/src/test/unit/com/mongodb/client/internal/ListCollectionsIterableSpecification.groovy +++ b/driver-sync/src/test/unit/com/mongodb/client/internal/ListCollectionsIterableSpecification.groovy @@ -46,14 +46,17 @@ class ListCollectionsIterableSpecification extends Specification { def 'should build the expected listCollectionOperation'() { given: - def executor = new TestOperationExecutor([null, null, null]); + def executor = new TestOperationExecutor([null, null, null, null]); def listCollectionIterable = new ListCollectionsIterableImpl(null, 'db', false, Document, codecRegistry, - readPreference, executor) + readPreference, executor, true) .filter(new Document('filter', 1)) .batchSize(100) .maxTime(1000, MILLISECONDS) def listCollectionNamesIterable = new ListCollectionsIterableImpl(null, 'db', true, Document, codecRegistry, - readPreference, executor) + readPreference, executor, true) + def listAuthorizedCollectionNamesIterable = new ListCollectionsIterableImpl(null, 'db', true, Document, + codecRegistry, readPreference, executor, true) + .authorizedCollections(true) when: 'default input should be as expected' listCollectionIterable.iterator() @@ -85,6 +88,16 @@ class ListCollectionsIterableSpecification extends Specification { then: 'should create operation with nameOnly' expect operation, isTheSameAs(new ListCollectionsOperation('db', new DocumentCodec()).nameOnly(true) .retryReads(true)) + + when: 'requesting authorized collection names only' + listAuthorizedCollectionNamesIterable.iterator() + operation = executor.getReadOperation() as ListCollectionsOperation + + then: 'should create operation with `nameOnly` and `authorizedCollections`' + expect operation, isTheSameAs(new ListCollectionsOperation('db', new DocumentCodec()) + .nameOnly(true) + .authorizedCollections(true) + .retryReads(true)) } def 'should use ClientSession'() { @@ -94,7 +107,7 @@ class ListCollectionsIterableSpecification extends Specification { } def executor = new TestOperationExecutor([batchCursor, batchCursor]); def listCollectionIterable = new ListCollectionsIterableImpl(clientSession, 'db', false, Document, codecRegistry, - readPreference, executor) + readPreference, executor, true) when: listCollectionIterable.first() @@ -134,7 +147,7 @@ class ListCollectionsIterableSpecification extends Specification { } def executor = new TestOperationExecutor([cursor(), cursor(), cursor(), cursor()]); def mongoIterable = new ListCollectionsIterableImpl(null, 'db', false, Document, codecRegistry, readPreference, - executor) + executor, true) when: def results = mongoIterable.first() @@ -178,7 +191,7 @@ class ListCollectionsIterableSpecification extends Specification { when: def batchSize = 5 def mongoIterable = new ListCollectionsIterableImpl(null, 'db', false, Document, codecRegistry, readPreference, - Stub(OperationExecutor)) + Stub(OperationExecutor), true) then: mongoIterable.getBatchSize() == null