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 */