Skip to content

Commit

Permalink
Do not require MongoDB privileges for fetching metadata
Browse files Browse the repository at this point in the history
Send `authorizedDatabases=true` parameter while listing databases/schemas
Send `authorizedCollections=true` parameter while listing collections/tables
Without those listing databases & collections require `listDatabases` & `listCollections`
actions.
  • Loading branch information
huberty89 authored and ebyhr committed Dec 5, 2022
1 parent 34efd53 commit f695af0
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 7 deletions.
6 changes: 6 additions & 0 deletions plugin/trino-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
Expand Down Expand Up @@ -141,6 +142,11 @@ public class MongoSession
// The 'simple' locale is the default collection in MongoDB. The locale doesn't allow specifying other fields (e.g. numericOrdering)
// https://www.mongodb.com/docs/manual/reference/collation/
private static final Collation SIMPLE_COLLATION = Collation.builder().locale("simple").build();
private static final Map<String, Object> AUTHORIZED_LIST_COLLECTIONS_COMMAND = ImmutableMap.<String, Object>builder()
.put("listCollections", 1.0)
.put("nameOnly", true)
.put("authorizedCollections", true)
.buildOrThrow();

private final TypeManager typeManager;
private final MongoClient client;
Expand Down Expand Up @@ -180,9 +186,7 @@ public List<HostAddress> getAddresses()

public List<String> getAllSchemas()
{
return ImmutableList.copyOf(client.listDatabaseNames()).stream()
.map(name -> name.toLowerCase(ENGLISH))
.collect(toImmutableList());
return ImmutableList.copyOf(listDatabaseNames().map(name -> name.toLowerCase(ENGLISH)));
}

public void createSchema(String schemaName)
Expand All @@ -202,7 +206,7 @@ public Set<String> getAllTables(String schema)
String schemaName = toRemoteSchemaName(schema);
ImmutableSet.Builder<String> builder = ImmutableSet.builder();

builder.addAll(ImmutableList.copyOf(client.getDatabase(schemaName).listCollectionNames()).stream()
builder.addAll(ImmutableList.copyOf(listCollectionNames(schemaName)).stream()
.filter(name -> !name.equals(schemaCollection))
.filter(name -> !SYSTEM_TABLES.contains(name))
.collect(toSet()));
Expand Down Expand Up @@ -644,7 +648,7 @@ private Document getTableMetadata(String schemaName, String tableName)

public boolean collectionExists(MongoDatabase db, String collectionName)
{
for (String name : db.listCollectionNames()) {
for (String name : listCollectionNames(db.getName())) {
if (name.equalsIgnoreCase(collectionName)) {
return true;
}
Expand Down Expand Up @@ -840,34 +844,54 @@ private String toRemoteSchemaName(String schemaName)
if (!caseInsensitiveNameMatching) {
return schemaName;
}
for (String remoteSchemaName : client.listDatabaseNames()) {
for (String remoteSchemaName : listDatabaseNames()) {
if (schemaName.equals(remoteSchemaName.toLowerCase(ENGLISH))) {
return remoteSchemaName;
}
}
return schemaName;
}

private MongoIterable<String> listDatabaseNames()
{
return client.listDatabases()
.nameOnly(true)
.authorizedDatabasesOnly(true)
.map(result -> result.getString("name"));
}

private String toRemoteTableName(String schemaName, String tableName)
{
verify(tableName.equals(tableName.toLowerCase(ENGLISH)), "tableName not in lower-case: %s", tableName);
if (!caseInsensitiveNameMatching) {
return tableName;
}
for (String remoteTableName : client.getDatabase(schemaName).listCollectionNames()) {
for (String remoteTableName : listCollectionNames(schemaName)) {
if (tableName.equals(remoteTableName.toLowerCase(ENGLISH))) {
return remoteTableName;
}
}
return tableName;
}

private List<String> listCollectionNames(String databaseName)
{
MongoDatabase database = client.getDatabase(databaseName);
Document cursor = database.runCommand(new Document(AUTHORIZED_LIST_COLLECTIONS_COMMAND)).get("cursor", Document.class);

List<Document> firstBatch = cursor.get("firstBatch", List.class);
return firstBatch.stream()
.map(document -> document.getString("name"))
.collect(toImmutableList());
}

private boolean isView(String schemaName, String tableName)
{
Document listCollectionsCommand = new Document(ImmutableMap.<String, Object>builder()
.put("listCollections", 1.0)
.put("filter", documentOf("name", tableName))
.put("nameOnly", true)
.put("authorizedCollections", true)
.buildOrThrow());
Document cursor = client.getDatabase(schemaName).runCommand(listCollectionsCommand).get("cursor", Document.class);
List<Document> firstBatch = cursor.get("firstBatch", List.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.plugin.mongodb;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.mongodb.ConnectionString;
import org.bson.Document;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import java.io.Closeable;

import static java.util.Objects.requireNonNull;

public class AuthenticatedMongoServer
implements Closeable
{
private static final int MONGODB_INTERNAL_PORT = 27017;
private static final String ROOT_USER = "root";
private static final String ROOT_PASSWORD = "password";
private static final String TEST_USER = "testUser";
private static final String TEST_PASSWORD = "pass";
private static final String TEST_ROLE = "testRole";
public static final String TEST_DATABASE = "test";
public static final String TEST_COLLECTION = "testCollection";
private final GenericContainer<?> dockerContainer;

public AuthenticatedMongoServer(String mongoVersion)
{
dockerContainer = new GenericContainer<>("mongo:" + requireNonNull(mongoVersion, "mongoVersion is null"))
.withStartupAttempts(3)
.waitingFor(Wait.forLogMessage(".*Listening on 0\\.0\\.0\\.0.*", 1))
.withEnv("MONGO_INITDB_ROOT_USERNAME", ROOT_USER)
.withEnv("MONGO_INITDB_ROOT_PASSWORD", ROOT_PASSWORD)
.withEnv("MONGO_INITDB_DATABASE", "admin")
.withExposedPorts(MONGODB_INTERNAL_PORT)
.withCommand("--auth --bind_ip 0.0.0.0");
dockerContainer.start();
}

public ConnectionString rootUserConnectionString()
{
return new ConnectionString("mongodb://%s:%s@%s:%d".formatted(
ROOT_USER,
ROOT_PASSWORD,
dockerContainer.getHost(),
dockerContainer.getMappedPort(MONGODB_INTERNAL_PORT)));
}

public ConnectionString testUserConnectionString()
{
return new ConnectionString("mongodb://%s:%s@%s:%d/%s".formatted(
TEST_USER,
TEST_PASSWORD,
dockerContainer.getHost(),
dockerContainer.getMappedPort(MONGODB_INTERNAL_PORT),
TEST_DATABASE));
}

public static Document createTestRole()
{
return new Document(ImmutableMap.of(
"createRole", TEST_ROLE,
"privileges", ImmutableList.of(privilege("_schema"), privilege(TEST_COLLECTION)),
"roles", ImmutableList.of()));
}

private static Document privilege(String collectionName)
{
return new Document(ImmutableMap.of(
"resource", resource(collectionName),
"actions", ImmutableList.of("find")));
}

private static Document resource(String collectionName)
{
return new Document(ImmutableMap.of(
"db", TEST_DATABASE,
"collection", collectionName));
}

public static Document createTestUser()
{
return new Document(ImmutableMap.of(
"createUser", TEST_USER,
"pwd", TEST_PASSWORD,
"roles", ImmutableList.of(role())));
}

private static Document role()
{
return new Document(ImmutableMap.of(
"role", TEST_ROLE,
"db", TEST_DATABASE));
}

@Override
public void close()
{
dockerContainer.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.mongodb.DBRef;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.CreateCollectionOptions;
import io.trino.sql.planner.plan.LimitNode;
Expand Down Expand Up @@ -824,6 +825,17 @@ public void testRenameTableTo120bytesTableName()
assertUpdate("DROP TABLE \"" + sourceTableName + "\"");
}

@Test
public void testListTablesFromSchemaWithBigAmountOfTables()
{
MongoDatabase database = client.getDatabase("huge_schema");
for (int i = 0; i < 10_000; i++) {
database.createCollection("table_" + i);
}

assertThat(getQueryRunner().execute("SHOW TABLES FROM mongodb.huge_schema").getRowCount()).isEqualTo(10_000);
}

@Override
protected OptionalInt maxSchemaNameLength()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.plugin.mongodb;

import com.google.common.collect.ImmutableMap;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import io.trino.testing.AbstractTestQueryFramework;
import io.trino.testing.DistributedQueryRunner;
import io.trino.testing.QueryRunner;
import org.bson.Document;
import org.testng.annotations.Test;

import java.util.Locale;
import java.util.Optional;

import static io.airlift.testing.Closeables.closeAllSuppress;
import static io.trino.plugin.mongodb.AuthenticatedMongoServer.TEST_COLLECTION;
import static io.trino.plugin.mongodb.AuthenticatedMongoServer.TEST_DATABASE;
import static io.trino.plugin.mongodb.AuthenticatedMongoServer.createTestRole;
import static io.trino.plugin.mongodb.AuthenticatedMongoServer.createTestUser;
import static io.trino.testing.TestingSession.testSessionBuilder;
import static org.assertj.core.api.Assertions.assertThat;

public class TestMongoPrivileges
extends AbstractTestQueryFramework
{
@Override
protected QueryRunner createQueryRunner()
throws Exception
{
AuthenticatedMongoServer mongoServer = closeAfterClass(setupMongoServer());
return createMongoQueryRunner(mongoServer.testUserConnectionString().getConnectionString());
}

@Test
public void testTablesVisibility()
{
assertQuery("SHOW TABLES FROM mongodb." + TEST_DATABASE, "VALUES '%s'".formatted(TEST_COLLECTION.toLowerCase(Locale.ENGLISH)));
}

private static AuthenticatedMongoServer setupMongoServer()
{
AuthenticatedMongoServer mongoServer = new AuthenticatedMongoServer("4.2.0");
try (MongoClient client = MongoClients.create(mongoServer.rootUserConnectionString())) {
MongoDatabase testDatabase = client.getDatabase(TEST_DATABASE);
runCommand(testDatabase, createTestRole());
runCommand(testDatabase, createTestUser());
testDatabase.createCollection("_schema");
testDatabase.createCollection(TEST_COLLECTION);
testDatabase.createCollection("anotherCollection"); // this collection/table should not be visible
client.getDatabase("another").createCollection("_schema"); // this database/schema should not be visible
}
return mongoServer;
}

private static void runCommand(MongoDatabase database, Document document)
{
Double commandStatus = database.runCommand(document)
.get("ok", Double.class);
assertThat(commandStatus).isEqualTo(1.0);
}

private static DistributedQueryRunner createMongoQueryRunner(String connectionUrl)
throws Exception
{
DistributedQueryRunner queryRunner = null;
try {
queryRunner = DistributedQueryRunner.builder(testSessionBuilder()
.setCatalog(Optional.empty())
.setSchema(Optional.empty())
.build())
.build();
queryRunner.installPlugin(new MongoPlugin());
queryRunner.createCatalog("mongodb", "mongodb", ImmutableMap.of(
"mongodb.case-insensitive-name-matching", "true",
"mongodb.connection-url", connectionUrl));
return queryRunner;
}
catch (Throwable e) {
closeAllSuppress(e, queryRunner);
throw e;
}
}
}

0 comments on commit f695af0

Please sign in to comment.