This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new cd0bae372e NIFI-14933 Improved MongoDB URI Authentication Mechanism
(#10263)
cd0bae372e is described below
commit cd0bae372e7fe140855911a597834cc861d1f2b0
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Sep 5 04:52:30 2025 +0200
NIFI-14933 Improved MongoDB URI Authentication Mechanism (#10263)
Signed-off-by: David Handermann <[email protected]>
---
.../nifi/mongodb/MongoDBControllerService.java | 68 ++++---
.../nifi/mongodb/MongoDBControllerServiceTest.java | 213 +++++++++++++++++++++
2 files changed, 256 insertions(+), 25 deletions(-)
diff --git
a/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/main/java/org/apache/nifi/mongodb/MongoDBControllerService.java
b/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/main/java/org/apache/nifi/mongodb/MongoDBControllerService.java
index bf5196d700..e0326584de 100644
---
a/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/main/java/org/apache/nifi/mongodb/MongoDBControllerService.java
+++
b/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/main/java/org/apache/nifi/mongodb/MongoDBControllerService.java
@@ -54,6 +54,8 @@ import java.util.regex.Pattern;
public class MongoDBControllerService extends AbstractControllerService
implements MongoDBClientService {
// Regex to find authMechanism value (case-insensitive)
private static final Pattern AUTH_MECHANISM_PATTERN =
Pattern.compile("(?i)(?:[?&])authmechanism=([^&]*)");
+ // Regex to find the user from the URI if specified
+ private static final Pattern USER_PATTERN =
Pattern.compile("(?i)^mongodb(?:\\+srv)?://[^/]*@.*");
private String uri;
@OnEnabled
@@ -111,41 +113,57 @@ public class MongoDBControllerService extends
AbstractControllerService implemen
final String authMechanism;
if (authMechanismMatcher.find()) {
authMechanism = authMechanismMatcher.group(1);
- uri = authMechanismMatcher.replaceFirst(uri.contains("?") ?
"?" : "");
- // If trailing & or ? is left, clean up
- uri = uri.replaceAll("[&?]+$", "");
} else {
authMechanism = null;
}
- final ConnectionString cs = new ConnectionString(uri);
+ // When properties specify the user, and the URI includes an
authMechanism but no user in the URI,
+ // the Mongo driver would attempt to build credentials from the
URI and fail. In that case, remove the
+ // authMechanism from the URI to allow property-based credentials.
If the URI already includes user info,
+ // keep the mechanism so the URI remains valid; property
credentials will override later.
+ final boolean hasUserInfoInUri =
USER_PATTERN.matcher(uri).matches();
+ final String effectiveUri;
+ if (authMechanism != null && user != null && !hasUserInfoInUri) {
+ String stripped =
AUTH_MECHANISM_PATTERN.matcher(uri).replaceFirst(uri.contains("?") ? "?" : "");
+ stripped = stripped.replaceAll("[&?]+$", "");
+ effectiveUri = stripped;
+ } else {
+ effectiveUri = uri;
+ }
+
+ final ConnectionString cs = new ConnectionString(effectiveUri);
final String database = cs.getDatabase() == null ? "admin" :
cs.getDatabase();
- if (authMechanism != null && user != null && passw != null) {
- final AuthenticationMechanism mechanism =
AuthenticationMechanism.fromMechanismName(authMechanism.toUpperCase());
+ // Apply connection string first to avoid clearing explicitly set
credentials later
+ builder.applyConnectionString(cs);
- switch (mechanism) {
- case SCRAM_SHA_1 ->
builder.credential(MongoCredential.createScramSha1Credential(user, database,
passw.toCharArray()));
- case SCRAM_SHA_256 ->
builder.credential(MongoCredential.createScramSha256Credential(user, database,
passw.toCharArray()));
- case MONGODB_AWS ->
builder.credential(MongoCredential.createAwsCredential(user,
passw.toCharArray()));
- case PLAIN ->
builder.credential(MongoCredential.createPlainCredential(user, database,
passw.toCharArray()));
- default -> throw new IllegalArgumentException("Unsupported
authentication mechanism with username and password: " + mechanism);
+ // If properties specify a user, apply credentials based on
properties and mechanism
+ if (user != null) {
+ if (authMechanism != null) {
+ final AuthenticationMechanism mechanism =
AuthenticationMechanism.fromMechanismName(authMechanism.toUpperCase());
+
+ if (passw != null) {
+ switch (mechanism) {
+ case SCRAM_SHA_1 ->
builder.credential(MongoCredential.createScramSha1Credential(user, database,
passw.toCharArray()));
+ case SCRAM_SHA_256 ->
builder.credential(MongoCredential.createScramSha256Credential(user, database,
passw.toCharArray()));
+ case MONGODB_AWS ->
builder.credential(MongoCredential.createAwsCredential(user,
passw.toCharArray()));
+ case PLAIN ->
builder.credential(MongoCredential.createPlainCredential(user, database,
passw.toCharArray()));
+ default -> throw new
IllegalArgumentException("Unsupported authentication mechanism with username
and password: " + mechanism);
+ }
+ } else { // user only
+ switch (mechanism) {
+ case MONGODB_X509 ->
builder.credential(MongoCredential.createMongoX509Credential(user));
+ case MONGODB_OIDC ->
builder.credential(MongoCredential.createOidcCredential(user));
+ case GSSAPI ->
builder.credential(MongoCredential.createGSSAPICredential(user));
+ default -> throw new
IllegalArgumentException("Unsupported authentication mechanism with username
only: " + mechanism);
+ }
+ }
+ } else if (passw != null) {
+ final MongoCredential credential =
MongoCredential.createCredential(user, database, passw.toCharArray());
+ builder.credential(credential);
}
- } else if (authMechanism != null) {
- final AuthenticationMechanism mechanism =
AuthenticationMechanism.fromMechanismName(authMechanism.toUpperCase());
- switch (mechanism) {
- case MONGODB_X509 ->
builder.credential(MongoCredential.createMongoX509Credential(user));
- case MONGODB_OIDC ->
builder.credential(MongoCredential.createOidcCredential(user));
- case GSSAPI ->
builder.credential(MongoCredential.createGSSAPICredential(user));
- default -> throw new IllegalArgumentException("Unsupported
authentication mechanism with username only: " + mechanism);
- }
- } else if (user != null && passw != null) {
- final MongoCredential credential =
MongoCredential.createCredential(user, database, passw.toCharArray());
- builder.credential(credential);
}
- builder.applyConnectionString(cs);
-
if (sslContext != null) {
builder.applyToSslSettings(sslBuilder ->
sslBuilder.enabled(true).context(sslContext));
}
diff --git
a/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/test/java/org/apache/nifi/mongodb/MongoDBControllerServiceTest.java
b/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/test/java/org/apache/nifi/mongodb/MongoDBControllerServiceTest.java
new file mode 100644
index 0000000000..6d2ad64370
--- /dev/null
+++
b/nifi-extension-bundles/nifi-mongodb-bundle/nifi-mongodb-services/src/test/java/org/apache/nifi/mongodb/MongoDBControllerServiceTest.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.mongodb;
+
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoCredential;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.internal.MongoClientImpl;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.util.MockConfigurationContext;
+import org.apache.nifi.util.MockControllerServiceLookup;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class MongoDBControllerServiceTest {
+
+ private static final String IDENTIFIER = "mongodb-client";
+
+ private TestRunner runner;
+ private MongoDBControllerService service;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ runner =
TestRunners.newTestRunner(TestControllerServiceProcessor.class);
+ service = new MongoDBControllerService();
+ runner.addControllerService(IDENTIFIER, service);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ try {
+ if (service != null) {
+ service.onDisable();
+ }
+ } catch (final Exception ignored) {
+ // Ignore during cleanup
+ }
+ }
+
+ private MongoClientSettings getClientSettings(final
MongoDBControllerService svc) {
+ final MongoClient client = svc.mongoClient; // protected field,
accessible in-package
+ assertNotNull(client, "MongoClient should have been initialized");
+ final MongoClientImpl impl = (MongoClientImpl) client;
+ return impl.getSettings();
+ }
+
+ private Map<PropertyDescriptor, String> getClientServiceProperties() {
+ return ((MockControllerServiceLookup)
runner.getProcessContext().getControllerServiceLookup())
+ .getControllerServices().get(IDENTIFIER).getProperties();
+ }
+
+ @Test
+ public void testAllInUri_ScramSha256_WithDatabase() throws Exception {
+ final String uri =
"mongodb://user1:pass1@localhost:27017/db1?authMechanism=SCRAM-SHA-256";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential, "Credential should be present from URI");
+ assertEquals("user1", credential.getUserName());
+ assertEquals("db1", credential.getSource());
+ }
+
+ @Test
+ public void testAllInUri_X509_WithUserInUri() throws Exception {
+ final String uri =
"mongodb://CN=uriUser@localhost:27017/?authMechanism=MONGODB-X509";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential, "Credential should be present from URI");
+ assertEquals("CN=uriUser", credential.getUserName());
+ // X.509 credentials use $external authentication database
+ assertEquals("$external", credential.getSource());
+ }
+
+ @Test
+ public void testUriPlusUser_X509() throws Exception {
+ final String uri =
"mongodb://localhost:27017/?authMechanism=MONGODB-X509";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"CN=propertyUser");
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential, "Credential should be present from
properties");
+ assertEquals("CN=propertyUser", credential.getUserName());
+ assertEquals("$external", credential.getSource());
+ }
+
+ @Test
+ public void testUriPlusUser_ScramSha256_ShouldFailWithoutPassword() {
+ final String uri =
"mongodb://localhost:27017/db2?authMechanism=SCRAM-SHA-256";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"userOnly");
+
+ // Build configuration context without enabling to capture exception
from createClient
+ final MockConfigurationContext context = new MockConfigurationContext(
+ service,
+ getClientServiceProperties(),
+ runner.getProcessContext().getControllerServiceLookup(),
+ null
+ );
+
+ assertThrows(IllegalArgumentException.class, () ->
service.createClient(context, null));
+ }
+
+ @Test
+ public void testUriPlusUserPassword_ScramSha256() throws Exception {
+ final String uri =
"mongodb://localhost:27017/db3?authMechanism=SCRAM-SHA-256";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"user256");
+ runner.setProperty(service, MongoDBControllerService.DB_PASSWORD,
"pass256");
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential);
+ assertEquals("user256", credential.getUserName());
+ assertEquals("db3", credential.getSource());
+ }
+
+ @Test
+ public void testUriPlusUserPassword_Plain() throws Exception {
+ final String uri =
"mongodb://localhost:27017/mydb?authMechanism=PLAIN";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"plainUser");
+ runner.setProperty(service, MongoDBControllerService.DB_PASSWORD,
"plainPass");
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential);
+ assertEquals("plainUser", credential.getUserName());
+ assertEquals("mydb", credential.getSource());
+ }
+
+ @Test
+ public void testUriPlusUserPassword_Aws() throws Exception {
+ final String uri =
"mongodb://localhost:27017/?authMechanism=MONGODB-AWS";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"awsUser");
+ runner.setProperty(service, MongoDBControllerService.DB_PASSWORD,
"awsSecret");
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential);
+ assertEquals("awsUser", credential.getUserName());
+ // AWS does not use a database in the same way; source is "$external"
+ assertEquals("$external", credential.getSource());
+ }
+
+ @Test
+ public void testUriPlusUserPassword_X509_ShouldFail() {
+ final String uri =
"mongodb://localhost:27017/?authMechanism=MONGODB-X509";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"CN=propUser");
+ runner.setProperty(service, MongoDBControllerService.DB_PASSWORD,
"ignored");
+
+ final MockConfigurationContext context = new MockConfigurationContext(
+ service,
+ getClientServiceProperties(),
+ runner.getProcessContext().getControllerServiceLookup(),
+ null
+ );
+
+ assertThrows(IllegalArgumentException.class, () ->
service.createClient(context, null));
+ }
+
+ @Test
+ public void testX509_PropertyOverridesUserInUri() throws Exception {
+ final String uri =
"mongodb://CN=uriPreferred@localhost:27017/?authMechanism=MONGODB-X509";
+
+ runner.setProperty(service, MongoDBControllerService.URI, uri);
+ runner.setProperty(service, MongoDBControllerService.DB_USER,
"CN=fromProperty");
+ runner.enableControllerService(service);
+
+ final MongoCredential credential =
getClientSettings(service).getCredential();
+ assertNotNull(credential);
+ // Properties override URI when both provided
+ assertEquals("CN=fromProperty", credential.getUserName());
+ assertEquals("$external", credential.getSource());
+ }
+}