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());
+    }
+}

Reply via email to