This is an automated email from the ASF dual-hosted git repository.
nvazquez pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new 50869a45b8c allow creating ldap on existing linked domain (#13945)
50869a45b8c is described below
commit 50869a45b8c2a066f8f0d4f4e16b77fff4a02b82
Author: dahn <[email protected]>
AuthorDate: Mon Nov 9 21:43:37 2026 +0100
allow creating ldap on existing linked domain (#13945)
* allow creating ldap accounts after linking accounts to the same the domain
* sonarqube
* tests
* null guard on mock
---
.../cloudstack/ldap/OpenLdapUserManagerImpl.java | 62 +++++---
.../ldap/OpenLdapUserManagerImplTest.java | 172 +++++++++++++++++++++
2 files changed, 212 insertions(+), 22 deletions(-)
diff --git
a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java
b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java
index 98d3be2f10c..06a9adcc69b 100644
---
a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java
+++
b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java
@@ -74,7 +74,12 @@ public class OpenLdapUserManagerImpl implements
LdapUserManager {
return new LdapUser(username, email, firstname, lastname, principal,
domain, disabled, memberships);
}
- private String generateSearchFilter(final String username, Long domainId) {
+ /**
+ * @param restrictToLinkedGroups scope to groups already linked in this
domain; only valid for
+ * browsing/importing. Applied to a single
known username, it wrongly
+ * blocks creating one ldap account once
another is linked to a group.
+ */
+ private String generateSearchFilter(final String username, Long domainId,
final boolean restrictToLinkedGroups) {
final StringBuilder userObjectFilter = new StringBuilder();
userObjectFilter.append("(objectClass=");
userObjectFilter.append(_ldapConfiguration.getUserObject(domainId));
@@ -89,14 +94,16 @@ public class OpenLdapUserManagerImpl implements
LdapUserManager {
String memberOfAttribute = getMemberOfAttribute(domainId);
StringBuilder ldapGroupsFilter = new StringBuilder();
- // this should get the trustmaps for this domain
- List<String> ldapGroups = getMappedLdapGroups(domainId);
- if (null != ldapGroups && ldapGroups.size() > 0) {
- ldapGroupsFilter.append("(|");
- for (String ldapGroup : ldapGroups) {
- ldapGroupsFilter.append(getMemberOfGroupString(ldapGroup,
memberOfAttribute));
+ if (restrictToLinkedGroups) {
+ // this should get the trustmaps for this domain
+ List<String> ldapGroups = getMappedLdapGroups(domainId);
+ if (null != ldapGroups && ldapGroups.size() > 0) {
+ ldapGroupsFilter.append("(|");
+ for (String ldapGroup : ldapGroups) {
+ ldapGroupsFilter.append(getMemberOfGroupString(ldapGroup,
memberOfAttribute));
+ }
+ ldapGroupsFilter.append(')');
}
- ldapGroupsFilter.append(')');
}
// make sure only users in the principle group are retrieved
String pricipleGroup =
_ldapConfiguration.getSearchGroupPrinciple(domainId);
@@ -166,10 +173,14 @@ public class OpenLdapUserManagerImpl implements
LdapUserManager {
return result.toString();
}
+ /**
+ * Looks up one known username, unscoped by linked groups, so an existing
group link
+ * elsewhere in the domain can't block finding this user.
+ */
@Override
public LdapUser getUser(final String username, final LdapContext context,
Long domainId) throws NamingException, IOException {
- List<LdapUser> result = searchUsers(username, context, domainId);
- if (result!= null && result.size() == 1) {
+ List<LdapUser> result = searchUsers(username, context, domainId,
false);
+ if (result.size() == 1) {
return result.get(0);
} else {
throw new NamingException("No user found for username " +
username);
@@ -314,6 +325,10 @@ public class OpenLdapUserManagerImpl implements
LdapUserManager {
@Override
@SuppressWarnings("BanJNDI")
public List<LdapUser> searchUsers(final String username, final LdapContext
context, Long domainId) throws NamingException, IOException {
+ return searchUsers(username, context, domainId, true);
+ }
+
+ private List<LdapUser> searchUsers(final String username, final
LdapContext context, Long domainId, final boolean restrictToLinkedGroups)
throws NamingException, IOException {
final SearchControls searchControls = new SearchControls();
@@ -330,27 +345,30 @@ public class OpenLdapUserManagerImpl implements
LdapUserManager {
final List<LdapUser> users = new ArrayList<LdapUser>();
NamingEnumeration<SearchResult> results;
do {
- results = context.search(basedn, generateSearchFilter(username,
domainId), searchControls);
+ results = context.search(basedn, generateSearchFilter(username,
domainId, restrictToLinkedGroups), searchControls);
while (results.hasMoreElements()) {
final SearchResult result = results.nextElement();
if (!isUserDisabled(result)) {
users.add(createUser(result, domainId));
}
}
- Control[] contextControls = context.getResponseControls();
- if (contextControls != null) {
- for (Control control : contextControls) {
- if (control instanceof PagedResultsResponseControl) {
- PagedResultsResponseControl prrc =
(PagedResultsResponseControl) control;
- cookie = prrc.getCookie();
- }
- }
- } else {
- logger.info("No controls were sent from the ldap server");
- }
+ cookie = extractPagedResultsCookie(context.getResponseControls());
context.setRequestControls(new Control[] {new
PagedResultsControl(pageSize, cookie, Control.CRITICAL)});
} while (cookie != null);
return users;
}
+
+ private byte[] extractPagedResultsCookie(Control[] contextControls) {
+ if (contextControls == null) {
+ logger.info("No controls were sent from the ldap server");
+ return null;
+ }
+ for (Control control : contextControls) {
+ if (control instanceof PagedResultsResponseControl) {
+ return ((PagedResultsResponseControl) control).getCookie();
+ }
+ }
+ return null;
+ }
}
diff --git
a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImplTest.java
b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImplTest.java
new file mode 100644
index 00000000000..5e9895e2f18
--- /dev/null
+++
b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImplTest.java
@@ -0,0 +1,172 @@
+// 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.cloudstack.ldap;
+
+import com.cloud.user.Account;
+import org.apache.cloudstack.ldap.dao.LdapTrustMapDao;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.Control;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.PagedResultsResponseControl;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Regression tests: creating an ldap account must not fail just because
another
+ * account is already linked to an ldap group in the domain; browsing/importing
+ * still honours that group scope.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class OpenLdapUserManagerImplTest {
+
+ private static final Long DOMAIN_ID = 1L;
+ private static final String LINKED_GROUP = "cn=test
admins,ou=groups,dc=my,dc=domain,dc=com";
+
+ private OpenLdapUserManagerImpl openLdapUserManager;
+
+ private MockedStatic<LdapConfiguration> ldapConfigurationMockedStatic;
+
+ @Mock
+ private LdapConfiguration ldapConfigurationMock;
+
+ @Mock
+ private LdapTrustMapDao ldapTrustMapDaoMock;
+
+ @Mock
+ private LdapContext ldapContextMock;
+
+ @Before
+ public void setup() throws Exception {
+ // getUserMemberOfAttribute is static, unlike its LdapConfiguration
siblings; mock statically.
+ ldapConfigurationMockedStatic =
Mockito.mockStatic(LdapConfiguration.class, Mockito.CALLS_REAL_METHODS);
+
when(LdapConfiguration.getUserMemberOfAttribute(any())).thenReturn("memberOf");
+
+ openLdapUserManager = new
OpenLdapUserManagerImpl(ldapConfigurationMock);
+ openLdapUserManager._ldapTrustMapDao = ldapTrustMapDaoMock;
+
+
when(ldapConfigurationMock.getScope()).thenReturn(SearchControls.SUBTREE_SCOPE);
+ when(ldapConfigurationMock.getReturnAttributes(any())).thenReturn(new
String[]{"uid"});
+
when(ldapConfigurationMock.getSearchGroupPrinciple(any())).thenReturn(null);
+
when(ldapConfigurationMock.getBaseDn(any())).thenReturn("dc=my,dc=domain,dc=com");
+
when(ldapConfigurationMock.getUsernameAttribute(any())).thenReturn("uid");
+
when(ldapConfigurationMock.getUserObject(any())).thenReturn("inetOrgPerson");
+ when(ldapConfigurationMock.getLdapPageSize(any())).thenReturn(1000);
+
+ LdapTrustMapVO linkedGroup = new LdapTrustMapVO(DOMAIN_ID,
LdapManager.LinkType.GROUP, LINKED_GROUP, Account.Type.NORMAL, 5L);
+
when(ldapTrustMapDaoMock.searchByDomainId(anyLong())).thenReturn(Collections.singletonList(linkedGroup));
+
+ NamingEnumeration<SearchResult> noResults =
mock(NamingEnumeration.class);
+ when(noResults.hasMoreElements()).thenReturn(false);
+ when(ldapContextMock.search(any(String.class), any(String.class),
any(SearchControls.class))).thenReturn(noResults);
+ when(ldapContextMock.getResponseControls()).thenReturn(null);
+ }
+
+ @After
+ public void tearDown() {
+ if (ldapConfigurationMockedStatic != null) {
+ ldapConfigurationMockedStatic.close();
+ }
+ }
+
+ @Test
+ public void getUserDoesNotRestrictToAlreadyLinkedGroups() throws Exception
{
+ try {
+ openLdapUserManager.getUser("test_user", ldapContextMock,
DOMAIN_ID);
+ } catch (NamingException expected) {
+ // no results stubbed; only the filter sent matters here
+ }
+
+ String filter = capturedSearchFilter();
+ assertFalse("a lookup for one specific username must not be scoped to
already-linked groups: " + filter,
+ filter.contains("memberOf=" + LINKED_GROUP));
+ }
+
+ @Test
+ public void getUsersStillRestrictsToAlreadyLinkedGroups() throws Exception
{
+ openLdapUserManager.getUsers("test_user", ldapContextMock, DOMAIN_ID);
+
+ String filter = capturedSearchFilter();
+ assertTrue("browsing/importing users should still be scoped to
already-linked groups: " + filter,
+ filter.contains("memberOf=" + LINKED_GROUP));
+ }
+
+ @Test
+ public void getUsersDoesNotAddGroupScopeWhenDomainHasNoLinkedGroups()
throws Exception {
+
when(ldapTrustMapDaoMock.searchByDomainId(anyLong())).thenReturn(Collections.emptyList());
+
+ openLdapUserManager.getUsers("test_user", ldapContextMock, DOMAIN_ID);
+
+ String filter = capturedSearchFilter();
+ assertFalse("no linked groups in the domain means no group scoping
should be applied: " + filter,
+ filter.contains("memberOf="));
+ }
+
+ @Test
+ public void searchStopsAfterOnePageWhenNoPagedResultsControlIsReturned()
throws Exception {
+ Control unrelatedControl = mock(Control.class);
+ when(ldapContextMock.getResponseControls()).thenReturn(new
Control[]{unrelatedControl});
+
+ List<LdapUser> result = openLdapUserManager.searchUsers("test_user",
ldapContextMock, DOMAIN_ID);
+
+ assertTrue(result.isEmpty());
+ verify(ldapContextMock, times(1)).search(any(String.class),
any(String.class), any(SearchControls.class));
+ }
+
+ @Test
+ public void searchFollowsCookieFromPagedResultsControlToNextPage() throws
Exception {
+ PagedResultsResponseControl pagedControl =
mock(PagedResultsResponseControl.class);
+ when(pagedControl.getCookie()).thenReturn(new byte[]{1, 2, 3});
+ when(ldapContextMock.getResponseControls())
+ .thenReturn(new Control[]{pagedControl})
+ .thenReturn(null);
+
+ List<LdapUser> result = openLdapUserManager.searchUsers("test_user",
ldapContextMock, DOMAIN_ID);
+
+ assertTrue(result.isEmpty());
+ verify(ldapContextMock, times(2)).search(any(String.class),
any(String.class), any(SearchControls.class));
+ }
+
+ private String capturedSearchFilter() throws Exception {
+ ArgumentCaptor<String> filterCaptor =
ArgumentCaptor.forClass(String.class);
+ verify(ldapContextMock, atLeastOnce()).search(any(String.class),
filterCaptor.capture(), any(SearchControls.class));
+ return filterCaptor.getValue();
+ }
+}