diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/LdapAuthenticator.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/LdapAuthenticator.java
index edad29c4f26d..895c2c5027c0 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/LdapAuthenticator.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/LdapAuthenticator.java
@@ -81,6 +81,7 @@
@Slf4j
public class LdapAuthenticator implements AuthenticatorHandler {
+ static final String AD_RECURSIVE_GROUP_MATCHING_RULE = "1.2.840.113556.1.4.1941";
static final String LDAP_ERR_MSG = "[LDAP] Issue in creating a LookUp Connection ";
private static final int MAX_RETRIES = 3;
private static final int BASE_DELAY_MS = 500;
@@ -397,8 +398,7 @@ private void getRoleForLdap(String userDn, User user, Boolean reAssign)
Filter.createEqualityFilter(
ldapConfiguration.getGroupAttributeName(),
ldapConfiguration.getGroupAttributeValue());
- Filter groupMemberAttr =
- Filter.createEqualityFilter(ldapConfiguration.getGroupMemberAttributeName(), userDn);
+ Filter groupMemberAttr = buildGroupMemberFilter(ldapConfiguration, userDn);
Filter groupAndMemberFilter = Filter.createANDFilter(groupFilter, groupMemberAttr);
SearchRequest searchRequest =
new SearchRequest(
@@ -474,6 +474,18 @@ private void getRoleForLdap(String userDn, User user, Boolean reAssign)
}
}
+ static Filter buildGroupMemberFilter(LdapConfiguration ldapConfiguration, String userDn) {
+ if (Boolean.TRUE.equals(ldapConfiguration.getRecursiveGroupMembership())) {
+ return Filter.createExtensibleMatchFilter(
+ ldapConfiguration.getGroupMemberAttributeName(),
+ AD_RECURSIVE_GROUP_MATCHING_RULE,
+ false,
+ userDn);
+ }
+
+ return Filter.createEqualityFilter(ldapConfiguration.getGroupMemberAttributeName(), userDn);
+ }
+
/**
* Check if user should be admin based on adminPrincipals configuration
*/
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/jdbi3/SystemRepositoryLdapConfigTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/jdbi3/SystemRepositoryLdapConfigTest.java
index 944fd264f484..15ca41f9f937 100644
--- a/openmetadata-service/src/test/java/org/openmetadata/service/jdbi3/SystemRepositoryLdapConfigTest.java
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/jdbi3/SystemRepositoryLdapConfigTest.java
@@ -1,6 +1,7 @@
package org.openmetadata.service.jdbi3;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
@@ -68,6 +69,8 @@ void testEnsureLdapConfigDefaultValues_AllFieldsNull() {
assertNotNull(ldapConfig.getAuthRolesMapping());
assertEquals("", ldapConfig.getAuthRolesMapping());
+
+ assertFalse(Boolean.TRUE.equals(ldapConfig.getRecursiveGroupMembership()));
}
@Test
@@ -86,6 +89,7 @@ void testEnsureLdapConfigDefaultValues_SomeFieldsNull() {
assertEquals("", ldapConfig.getGroupBaseDN());
assertEquals("", ldapConfig.getAllAttributeName());
assertEquals("", ldapConfig.getAuthRolesMapping());
+ assertFalse(Boolean.TRUE.equals(ldapConfig.getRecursiveGroupMembership()));
}
@Test
@@ -143,5 +147,6 @@ void testEnsureLdapConfigDefaultValues_OtherOptionalFieldsDefaults() {
assertEquals("", ldapConfig.getRoleAdminName());
assertEquals("", ldapConfig.getAllAttributeName());
assertEquals("", ldapConfig.getAuthRolesMapping());
+ assertFalse(Boolean.TRUE.equals(ldapConfig.getRecursiveGroupMembership()));
}
}
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/security/auth/LdapAuthenticatorTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/security/auth/LdapAuthenticatorTest.java
new file mode 100644
index 000000000000..555bebbe0d77
--- /dev/null
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/security/auth/LdapAuthenticatorTest.java
@@ -0,0 +1,46 @@
+package org.openmetadata.service.security.auth;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.unboundid.ldap.sdk.Filter;
+import org.junit.jupiter.api.Test;
+import org.openmetadata.schema.auth.LdapConfiguration;
+
+class LdapAuthenticatorTest {
+
+ @Test
+ void buildGroupMemberFilterUsesEqualityWhenRecursiveMembershipIsDisabled() {
+ LdapConfiguration ldapConfiguration = new LdapConfiguration();
+ ldapConfiguration.setGroupMemberAttributeName("member");
+ ldapConfiguration.setRecursiveGroupMembership(false);
+
+ Filter filter =
+ LdapAuthenticator.buildGroupMemberFilter(
+ ldapConfiguration, "cn=john,ou=users,dc=example,dc=com");
+
+ assertEquals(Filter.FILTER_TYPE_EQUALITY, filter.getFilterType());
+ assertEquals("member", filter.getAttributeName());
+ assertEquals("cn=john,ou=users,dc=example,dc=com", filter.getAssertionValue());
+ assertFalse(filter.getDNAttributes());
+ assertNull(filter.getMatchingRuleID());
+ }
+
+ @Test
+ void buildGroupMemberFilterUsesRecursiveMatchRuleWhenEnabled() {
+ LdapConfiguration ldapConfiguration = new LdapConfiguration();
+ ldapConfiguration.setGroupMemberAttributeName("member");
+ ldapConfiguration.setRecursiveGroupMembership(true);
+
+ Filter filter =
+ LdapAuthenticator.buildGroupMemberFilter(
+ ldapConfiguration, "cn=john,ou=users,dc=example,dc=com");
+
+ assertEquals(Filter.FILTER_TYPE_EXTENSIBLE_MATCH, filter.getFilterType());
+ assertEquals("member", filter.getAttributeName());
+ assertEquals("1.2.840.113556.1.4.1941", filter.getMatchingRuleID());
+ assertEquals("cn=john,ou=users,dc=example,dc=com", filter.getAssertionValue());
+ assertFalse(filter.getDNAttributes());
+ }
+}
diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/ldapConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/ldapConfiguration.json
index ef75ef52cdb5..698ab50ac99c 100644
--- a/openmetadata-spec/src/main/resources/json/schema/configuration/ldapConfiguration.json
+++ b/openmetadata-spec/src/main/resources/json/schema/configuration/ldapConfiguration.json
@@ -73,6 +73,11 @@
"description": "Group Member Name attribute name",
"type": "string"
},
+ "recursiveGroupMembership": {
+ "description": "Enable transitive group membership resolution for Active Directory nested groups using LDAP_MATCHING_RULE_IN_CHAIN.",
+ "type": "boolean",
+ "default": false
+ },
"authRolesMapping": {
"description": "Json string of roles mapping between LDAP roles and Ranger roles",
"type": "string"
diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md
index f90aa18ac0ba..959070e163a1 100644
--- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md
+++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md
@@ -174,6 +174,13 @@ LDAP authentication enables users to log in with their LDAP directory credential
4. Example: `member: cn=john,ou=users,dc=company,dc=com` → use `member`
- **Validation:** OpenMetadata checks this attribute exists on actual group objects
+## Recursive Group Membership
+
+- **Definition:** Enables nested group resolution for Active Directory.
+- **Why it matters:** If an AD group contains another group, users in the nested group will still match the mapped parent group.
+- **How it works:** Uses Active Directory's transitive membership matching rule instead of a direct member equality filter.
+- **When to enable:** Turn this on only when your LDAP directory is Active Directory and you rely on nested groups for access control.
+
## Auth Roles Mapping
- **Definition:** Mapping between LDAP groups and OpenMetadata roles.
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/SSO.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/SSO.constant.ts
index fee7ec59cba7..61b63c47a8b2 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/SSO.constant.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/SSO.constant.ts
@@ -198,6 +198,11 @@ export const LDAP_UI_SCHEMA = {
groupAttributeName: { 'ui:title': 'Group Attribute Name' },
groupAttributeValue: { 'ui:title': 'Group Attribute Value' },
groupMemberAttributeName: { 'ui:title': 'Group Member Attribute Name' },
+ recursiveGroupMembership: {
+ 'ui:title': 'Recursive Group Membership',
+ 'ui:help':
+ 'Enable this for Active Directory nested groups. OpenMetadata will use AD transitive membership matching when checking role mappings.',
+ },
authRolesMapping: {
'ui:title': 'Auth Roles Mapping',
'ui:widget': 'LdapRoleMappingWidget',
diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/authenticationConfiguration.ts b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/authenticationConfiguration.ts
index 5627e025bb74..826cecacb0cd 100644
--- a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/authenticationConfiguration.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/authenticationConfiguration.ts
@@ -160,6 +160,11 @@ export interface LDAPConfiguration {
* Port of the server
*/
port: number;
+ /**
+ * Enable transitive group membership resolution for Active Directory nested groups using
+ * LDAP_MATCHING_RULE_IN_CHAIN.
+ */
+ recursiveGroupMembership?: boolean;
/**
* Admin role name
*/
diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/ldapConfiguration.ts b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/ldapConfiguration.ts
index 778157816a2d..9c54df6cb213 100644
--- a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/ldapConfiguration.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/ldapConfiguration.ts
@@ -70,6 +70,11 @@ export interface LDAPConfiguration {
* Port of the server
*/
port: number;
+ /**
+ * Enable transitive group membership resolution for Active Directory nested groups using
+ * LDAP_MATCHING_RULE_IN_CHAIN.
+ */
+ recursiveGroupMembership?: boolean;
/**
* Admin role name
*/
diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/securityConfiguration.ts b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/securityConfiguration.ts
index 58e6edcd13c7..6c135000a15f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/generated/configuration/securityConfiguration.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/generated/configuration/securityConfiguration.ts
@@ -176,6 +176,11 @@ export interface LDAPConfiguration {
* Port of the server
*/
port: number;
+ /**
+ * Enable transitive group membership resolution for Active Directory nested groups using
+ * LDAP_MATCHING_RULE_IN_CHAIN.
+ */
+ recursiveGroupMembership?: boolean;
/**
* Admin role name
*/
diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/settings/settings.ts b/openmetadata-ui/src/main/resources/ui/src/generated/settings/settings.ts
index e4e729e96a15..2aaf3a095843 100644
--- a/openmetadata-ui/src/main/resources/ui/src/generated/settings/settings.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/generated/settings/settings.ts
@@ -1202,6 +1202,11 @@ export interface LDAPConfiguration {
* Port of the server
*/
port: number;
+ /**
+ * Enable transitive group membership resolution for Active Directory nested groups using
+ * LDAP_MATCHING_RULE_IN_CHAIN.
+ */
+ recursiveGroupMembership?: boolean;
/**
* Admin role name
*/