From 00b72197f6dded83ac50a31c43db6e973c0a5ccb Mon Sep 17 00:00:00 2001 From: David Han Date: Wed, 10 Jun 2026 17:01:01 -0500 Subject: [PATCH 1/4] KNOX-3340: Add Control for LDAPRolesLookupInterceptor This commit adds a RolesLookupBypassControl for use with the LDAPRolesLookupInterceptor. The LDAPRolesLookupInterceptor will skip role mapping if this control is present and true in the request. --- gateway-server/pom.xml | 10 ++ .../services/ldap/KnoxLDAPServerManager.java | 11 ++ .../control/RolesLookupBypassControl.java | 30 ++++ .../RolesLookupBypassControlDecorator.java | 74 ++++++++++ .../RolesLookupBypassControlFactory.java | 67 +++++++++ .../control/RolesLookupBypassControlImpl.java | 38 ++++++ .../LDAPRolesLookupInterceptor.java | 13 ++ .../interceptor/UserSearchInterceptor.java | 6 - .../ldap/KnoxLDAPServerManagerTest.java | 19 ++- ...RolesLookupBypassControlDecoratorTest.java | 129 ++++++++++++++++++ .../RolesLookupBypassControlFactoryTest.java | 104 ++++++++++++++ .../ConfigurableEntriesTestInterceptor.java | 55 ++++++++ ...DuplicateUserFilteringInterceptorTest.java | 27 ---- .../LDAPRolesLookupInterceptorTest.java | 108 ++++++++++++++- knox-site/docs/service_ldap_server.md | 25 +++- pom.xml | 10 ++ 16 files changed, 688 insertions(+), 38 deletions(-) create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecorator.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/ConfigurableEntriesTestInterceptor.java diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml index 41341bbfe1..95bd6b24d3 100644 --- a/gateway-server/pom.xml +++ b/gateway-server/pom.xml @@ -519,6 +519,16 @@ api-ldap-client-api provided + + org.apache.directory.api + api-ldap-codec-core + provided + + + org.apache.directory.api + api-asn1-api + provided + org.apache.mina diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java index 2cee216260..197a45a410 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java @@ -19,6 +19,8 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; +import org.apache.directory.api.ldap.codec.api.LdapApiService; +import org.apache.directory.api.ldap.codec.api.LdapApiServiceFactory; import org.apache.directory.api.ldap.model.cursor.Cursor; import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultEntry; @@ -42,6 +44,7 @@ import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory; import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory; import java.io.File; @@ -135,6 +138,14 @@ public void start() throws Exception { directoryService = new DefaultDirectoryService(); directoryService.setInstanceLayout(new InstanceLayout(workDir)); + // Add RolesLookupBypassControlFactory + LdapApiService apiService = directoryService.getLdapCodecService(); + if (apiService == null) { + apiService = LdapApiServiceFactory.getSingleton(); + } + RolesLookupBypassControlFactory rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(apiService); + apiService.registerRequestControl(rolesLookupBypassControlFactory); + // Create SchemaManager SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager(); directoryService.setSchemaManager(schemaManager); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java new file mode 100644 index 0000000000..2a0c8b2c4a --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java @@ -0,0 +1,30 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import org.apache.directory.api.ldap.model.message.Control; + +public interface RolesLookupBypassControl extends Control { + // OID created from a UUID to ensure no collisions: + // Apache Root OID for core object classes 1.3.6.1.4.1.18060.2 + // UUID "5236bee0-8a22-4419-9f8e-f1de43312ce1" + String OID = "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953"; + + boolean isBypassRolesLookup(); + void setBypassRolesLookup(boolean bypassRolesLookup); +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecorator.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecorator.java new file mode 100644 index 0000000000..4821efc57a --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecorator.java @@ -0,0 +1,74 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import org.apache.directory.api.asn1.Asn1Object; +import org.apache.directory.api.asn1.DecoderException; +import org.apache.directory.api.asn1.EncoderException; +import org.apache.directory.api.asn1.util.Asn1Buffer; +import org.apache.directory.api.ldap.codec.api.ControlDecorator; +import org.apache.directory.api.ldap.codec.api.LdapApiService; + +import java.nio.ByteBuffer; + +public class RolesLookupBypassControlDecorator extends ControlDecorator implements RolesLookupBypassControl { + + private final RolesLookupBypassControlFactory rolesLookupBypassControlFactory; + + public RolesLookupBypassControlDecorator(LdapApiService codec, RolesLookupBypassControl decoratedControl, RolesLookupBypassControlFactory rolesLookupBypassControlFactory) { + super(codec, decoratedControl); + this.rolesLookupBypassControlFactory = rolesLookupBypassControlFactory; + } + + @Override + public Asn1Object decode(byte[] bytes) throws DecoderException { + rolesLookupBypassControlFactory.decodeValue(getDecorated(), bytes); + return this; + } + + @Override + public int computeLength() { + return 3; // Tag, Length, Value + } + + @Override + public ByteBuffer encode(ByteBuffer byteBuffer) throws EncoderException { + Asn1Buffer asn1Buffer = new Asn1Buffer(); + rolesLookupBypassControlFactory.encodeValue(asn1Buffer, getDecorated()); + + // reverse the byte ordering because Asn1Buffers store bytes in reverse + ByteBuffer factoryBuffer = asn1Buffer.getBytes(); + int totalBytes = factoryBuffer.remaining(); + byte[] factoryBytes = factoryBuffer.array(); + for (int i = totalBytes - 1; i >= 0; i-- ) { + byteBuffer.put(factoryBytes[i]); + } + + return byteBuffer; + } + + @Override + public boolean isBypassRolesLookup() { + return getDecorated().isBypassRolesLookup(); + } + + @Override + public void setBypassRolesLookup(boolean bypassRolesLookup) { + getDecorated().setBypassRolesLookup(bypassRolesLookup); + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java new file mode 100644 index 0000000000..05e1aefbf7 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java @@ -0,0 +1,67 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import org.apache.directory.api.asn1.DecoderException; +import org.apache.directory.api.asn1.util.Asn1Buffer; +import org.apache.directory.api.ldap.codec.api.AbstractControlFactory; +import org.apache.directory.api.ldap.codec.api.LdapApiService; +import org.apache.directory.api.ldap.model.message.Control; + +public class RolesLookupBypassControlFactory extends AbstractControlFactory { + public static final byte BOOLEAN_TAG_BYTE = 0x01; + + public RolesLookupBypassControlFactory(LdapApiService codec) { + super(codec, RolesLookupBypassControl.OID); + } + + @Override + public Control newControl() { + return new RolesLookupBypassControlDecorator(codec, new RolesLookupBypassControlImpl(), this); + } + + @Override + public void decodeValue(Control control, byte[] controlBytes) throws DecoderException { + if (control instanceof RolesLookupBypassControl) { + RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; + if (controlBytes == null || controlBytes.length < 3) { + throw new DecoderException("Invalid BER encoding for Boolean Control"); + } + + if (controlBytes[0] != BOOLEAN_TAG_BYTE) { + throw new DecoderException("Expected Boolean Tag (0x01), found: " + controlBytes[0]); + } + + boolean value = (controlBytes[2] != 0x00); + rolesLookupBypassControl.setBypassRolesLookup(value); + } else { + throw new DecoderException("Cannot decode into " + control.getClass().getSimpleName() + ". Control must be instance of RolesLookupBypassControl."); + } + } + + @Override + public void encodeValue(Asn1Buffer buffer, Control control) { + if (control instanceof RolesLookupBypassControl) { + RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; + + buffer.put(BOOLEAN_TAG_BYTE); + buffer.put((byte) 3); + buffer.put((byte) (rolesLookupBypassControl.isBypassRolesLookup() ? 0xFF : 0x00)); + } + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java new file mode 100644 index 0000000000..c401f1fadf --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java @@ -0,0 +1,38 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import org.apache.directory.api.ldap.model.message.controls.AbstractControl; + +public class RolesLookupBypassControlImpl extends AbstractControl implements RolesLookupBypassControl { + private boolean bypassRolesLookup; + + public RolesLookupBypassControlImpl() { + super(OID); + } + + @Override + public boolean isBypassRolesLookup() { + return bypassRolesLookup; + } + + @Override + public void setBypassRolesLookup(boolean bypassRolesLookup) { + this.bypassRolesLookup = bypassRolesLookup; + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java index a860c2a4e0..2fc4af36fe 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java @@ -23,6 +23,7 @@ import org.apache.directory.api.ldap.model.entry.Value; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; +import org.apache.directory.api.ldap.model.message.Control; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.api.ldap.model.name.Rdn; import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; @@ -33,6 +34,7 @@ import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService; import org.apache.knox.gateway.services.ldap.LdapMessages; import org.apache.knox.gateway.services.ldap.LdapUtils; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControl; import java.util.ArrayList; import java.util.Collection; @@ -46,6 +48,7 @@ */ public class LDAPRolesLookupInterceptor extends BaseInterceptor { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + private final LDAPRolesLookupService rolesLookupService; public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService) { @@ -54,6 +57,16 @@ public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService) { @Override public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapException { + if (ctx.hasRequestControl(RolesLookupBypassControl.OID)) { + Control control = ctx.getRequestControl(RolesLookupBypassControl.OID); + if (control instanceof RolesLookupBypassControl) { + RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; + if (rolesLookupBypassControl.isBypassRolesLookup()) { + return next(ctx); + } + } + } + final List entries = new ArrayList<>(); try (EntryFilteringCursor cursor = next(ctx)) { while (cursor.next()) { diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java index 0f67f25753..03743d959c 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java @@ -22,7 +22,6 @@ import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.server.core.api.CoreSession; -import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.api.LdapPrincipal; import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; import org.apache.directory.server.core.api.filtering.EntryFilteringCursorImpl; @@ -63,11 +62,6 @@ public LdapBackend getBackend() { return backend; } - @Override - public void init(DirectoryService directoryService) throws LdapException { - super.init(directoryService); - } - @Override public Entry lookup(LookupOperationContext ctx) throws LdapException { Entry entry = next(ctx); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java index b9ffdcda16..bd1d93a10d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java @@ -17,8 +17,12 @@ */ package org.apache.knox.gateway.services.ldap; +import org.apache.directory.api.ldap.codec.api.ControlFactory; +import org.apache.directory.api.ldap.model.message.Control; import org.apache.directory.server.core.api.interceptor.Interceptor; import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControl; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory; import org.easymock.EasyMock; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptor; @@ -374,8 +378,21 @@ public void testStartWithMultipleBackendsIdCollision() throws Exception { } @Test - public void testGetUserGroups() { + public void testStartRegistersRolesLookupBypassControl() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + serverManager.start(); + Map> controlFactoryMap = serverManager.directoryService.getLdapCodecService().getRequestControlFactories(); + assertTrue(controlFactoryMap.containsKey(RolesLookupBypassControl.OID)); + assertTrue(controlFactoryMap.get(RolesLookupBypassControl.OID) instanceof RolesLookupBypassControlFactory); } private Map createFileBackendInterceptorConfig() { diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java new file mode 100644 index 0000000000..7435f0db38 --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java @@ -0,0 +1,129 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.directory.api.asn1.DecoderException; +import org.apache.directory.api.ldap.codec.api.LdapApiService; +import org.junit.Before; +import org.junit.Test; + +import java.nio.ByteBuffer; + +public class RolesLookupBypassControlDecoratorTest { + + private RolesLookupBypassControl rolesLookupBypassControl; + private LdapApiService mockLdapApiService; + private RolesLookupBypassControlFactory rolesLookupBypassControlFactory; + + private RolesLookupBypassControlDecorator rolesLookupBypassControlDecorator; + + @Before + public void setUp() throws Exception { + mockLdapApiService = mock(LdapApiService.class); + replay(mockLdapApiService); + + rolesLookupBypassControl = new RolesLookupBypassControlImpl(); + rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService); + + rolesLookupBypassControlDecorator = new RolesLookupBypassControlDecorator(mockLdapApiService, rolesLookupBypassControl, rolesLookupBypassControlFactory); + } + + @Test + public void decodeFalseValue() throws Exception { + byte[] bytes = new byte[]{0x01, 0x03, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + + assertFalse(rolesLookupBypassControl.isBypassRolesLookup()); + } + + @Test + public void decodeTrueValue() throws Exception { + byte[] bytes = new byte[]{0x01, 0x03, (byte) 0xff}; + rolesLookupBypassControlDecorator.decode(bytes); + + assertTrue(rolesLookupBypassControl.isBypassRolesLookup()); + } + + @Test(expected = DecoderException.class) + public void decodeWrongTag() throws Exception { + byte[] bytes = new byte[]{0x02, 0x03, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + } + + @Test(expected = DecoderException.class) + public void decodeWrongLength() throws Exception { + byte[] bytes = new byte[]{0x02, 0x04, 0x00, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + } + + + @Test + public void computeLength() { + assertEquals("Length must always be 3", 3, rolesLookupBypassControlDecorator.computeLength()); + } + + @Test + public void encodeTrueValue() throws Exception { + rolesLookupBypassControl.setBypassRolesLookup(true); + byte[] expectedBytes = new byte[]{0x01, 0x03, (byte) 0xff}; + + ByteBuffer byteBuffer = ByteBuffer.allocate(3); + ByteBuffer encodedBuffer = rolesLookupBypassControlDecorator.encode(byteBuffer); + // transition from write mode to read mode + encodedBuffer.flip(); + byte[] encodedBytes = new byte[encodedBuffer.remaining()]; + encodedBuffer.get(encodedBytes); + assertArrayEquals(expectedBytes, encodedBytes); + } + + @Test + public void encodeFalseValue() throws Exception { + byte[] expectedBytes = new byte[]{0x01, 0x03, 0x00}; + + ByteBuffer byteBuffer = ByteBuffer.allocate(3); + ByteBuffer encodedBuffer = rolesLookupBypassControlDecorator.encode(byteBuffer); + // transition from write mode to read mode + encodedBuffer.flip(); + byte[] encodedBytes = new byte[encodedBuffer.remaining()]; + encodedBuffer.get(encodedBytes); + assertArrayEquals(expectedBytes, encodedBytes); + } + + @Test + public void isBypassRolesLookup() { + rolesLookupBypassControl.setBypassRolesLookup(true); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", rolesLookupBypassControl.isBypassRolesLookup(), rolesLookupBypassControlDecorator.isBypassRolesLookup()); + rolesLookupBypassControl.setBypassRolesLookup(false); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", rolesLookupBypassControl.isBypassRolesLookup(), rolesLookupBypassControlDecorator.isBypassRolesLookup()); + } + + @Test + public void setBypassRolesLookup() { + rolesLookupBypassControlDecorator.setBypassRolesLookup(true); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", rolesLookupBypassControl.isBypassRolesLookup(), rolesLookupBypassControlDecorator.isBypassRolesLookup()); + rolesLookupBypassControlDecorator.setBypassRolesLookup(false); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", rolesLookupBypassControl.isBypassRolesLookup(), rolesLookupBypassControlDecorator.isBypassRolesLookup()); + } +} \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java new file mode 100644 index 0000000000..4e8738cfee --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java @@ -0,0 +1,104 @@ +/* + * 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.knox.gateway.services.ldap.control; + +import static org.easymock.EasyMock.mock; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.directory.api.asn1.util.Asn1Buffer; +import org.apache.directory.api.ldap.codec.api.LdapApiService; +import org.apache.directory.api.ldap.model.message.Control; +import org.junit.Before; +import org.junit.Test; + +import java.nio.ByteBuffer; + +public class RolesLookupBypassControlFactoryTest { + + private LdapApiService mockLdapApiService; + private RolesLookupBypassControlFactory rolesLookupBypassControlFactory; + + @Before + public void setUp() throws Exception { + mockLdapApiService = mock(LdapApiService.class); + replay(mockLdapApiService); + rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService); + } + + @Test + public void newControl() { + Control control = rolesLookupBypassControlFactory.newControl(); + assertTrue("Control must be a RolesLookupBypassControlDecorator", control instanceof RolesLookupBypassControlDecorator); + } + + @Test + public void decodeFalseValue() throws Exception { + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + byte[] bytes = new byte[]{0x01, 0x03, 0x00}; + + rolesLookupBypassControlFactory.decodeValue(control, bytes); + + assertFalse(control.isBypassRolesLookup()); + } + + @Test + public void decodeTrueValue() throws Exception { + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + byte[] bytes = new byte[]{0x01, 0x03, (byte) 0xff}; + + rolesLookupBypassControlFactory.decodeValue(control, bytes); + + assertTrue(control.isBypassRolesLookup()); + } + + @Test + public void encodeTrueValue() { + Asn1Buffer asn1Buffer = new Asn1Buffer(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + control.setBypassRolesLookup(true); + + rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); + + // expectedBytes in reverse because Asn1Buffer stores bytes in reverse order + byte[] expectedBytes = new byte[]{(byte) 0xff, 0x03, 0x01}; + System.out.println(asn1Buffer.toString()); + ByteBuffer encodedBuffer = asn1Buffer.getBytes(); + byte[] encodedBytes = new byte[encodedBuffer.remaining()]; + encodedBuffer.get(encodedBytes); + assertArrayEquals(expectedBytes, encodedBytes); + } + + @Test + public void encodeFalseValue() { + Asn1Buffer asn1Buffer = new Asn1Buffer(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + control.setBypassRolesLookup(false); + + rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); + + // expectedBytes in reverse because Asn1Buffer stores bytes in reverse order + byte[] expectedBytes = new byte[]{0x00, 0x03, 0x01}; + ByteBuffer encodedBuffer = asn1Buffer.getBytes(); + byte[] encodedBytes = new byte[encodedBuffer.remaining()]; + encodedBuffer.get(encodedBytes); + assertArrayEquals(expectedBytes, encodedBytes); + } +} \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/ConfigurableEntriesTestInterceptor.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/ConfigurableEntriesTestInterceptor.java new file mode 100644 index 0000000000..27ac17f86b --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/ConfigurableEntriesTestInterceptor.java @@ -0,0 +1,55 @@ +/* + * 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.knox.gateway.services.ldap.interceptor; + +import org.apache.directory.api.ldap.model.cursor.ListCursor; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; +import org.apache.directory.server.core.api.filtering.EntryFilteringCursorImpl; +import org.apache.directory.server.core.api.interceptor.BaseInterceptor; +import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; + +import java.util.List; + +/** + * Interceptor for testing. This interceptor will return a Cursor of a List + * of configured Entries. + */ +public class ConfigurableEntriesTestInterceptor extends BaseInterceptor { + private List entries; + private EntryFilteringCursor cursor; + + ConfigurableEntriesTestInterceptor(String name) { + super(name); + } + + public void setEntries(List entries) { + this.entries = entries; + } + + public EntryFilteringCursor getCursor() { + return cursor; + } + + @Override + public EntryFilteringCursor search(SearchOperationContext searchContext) throws LdapException { + cursor = new EntryFilteringCursorImpl(new ListCursor<>(entries), searchContext, schemaManager); + return cursor; + } +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java index 828937074e..d4088a15b1 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java @@ -21,18 +21,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import org.apache.directory.api.ldap.model.cursor.ListCursor; import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultEntry; import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.entry.Value; -import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.schema.SchemaManager; import org.apache.directory.server.core.api.CoreSession; import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; -import org.apache.directory.server.core.api.filtering.EntryFilteringCursorImpl; -import org.apache.directory.server.core.api.interceptor.BaseInterceptor; import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; import org.apache.knox.gateway.security.ldap.SimpleDirectoryService; import org.apache.knox.gateway.services.ldap.SchemaManagerFactory; @@ -140,27 +136,4 @@ private void assertNextEntryUid(EntryFilteringCursor cursor, String uid) throws Value value = uidAttr.get(); assertEquals("Uid should match " + uid, uid, value.getString()); } - - private static class ConfigurableEntriesTestInterceptor extends BaseInterceptor { - private List entries; - private EntryFilteringCursor cursor; - - ConfigurableEntriesTestInterceptor(String name) { - super(name); - } - - public void setEntries(List entries) { - this.entries = entries; - } - - public EntryFilteringCursor getCursor() { - return cursor; - } - - @Override - public EntryFilteringCursor search(SearchOperationContext searchContext) throws LdapException { - cursor = new EntryFilteringCursorImpl(new ListCursor<>(entries), searchContext, schemaManager); - return cursor; - } - } } \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java index 637e1fc0f9..a28b6635af 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java @@ -20,20 +20,41 @@ import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultEntry; import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.server.core.api.CoreSession; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; +import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; +import org.apache.knox.gateway.security.ldap.SimpleDirectoryService; import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService; +import org.apache.knox.gateway.services.ldap.SchemaManagerFactory; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControl; +import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlImpl; import org.easymock.EasyMock; +import org.junit.Before; import org.junit.Test; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class LDAPRolesLookupInterceptorTest { + private SchemaManager schemaManager; + + @Before + public void setUp() throws Exception { + schemaManager = SchemaManagerFactory.createSchemaManager(); + } @Test public void testModifyEntryWithRoles() throws Exception { @@ -68,14 +89,95 @@ public void testModifyEntryNoMemberOfNoRoles() throws Exception { assertNull(modifiedEntry.get("memberOf")); } - private LDAPRolesLookupInterceptor createInterceptor() { + @Test + public void testRolesLookupNoBypass() throws Exception { + final DirectoryService directoryService = new SimpleDirectoryService(); + directoryService.setShutdownHookEnabled(false); + SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager(); + directoryService.setSchemaManager(schemaManager); + final LDAPRolesLookupService mockRolesService = EasyMock.createMock(LDAPRolesLookupService.class); + + final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(mockRolesService); + rolesLookupInterceptor.init(directoryService); + directoryService.addLast(rolesLookupInterceptor); + + final ConfigurableEntriesTestInterceptor nextInterceptor = new ConfigurableEntriesTestInterceptor("NEXT"); + nextInterceptor.init(directoryService); + directoryService.addLast(nextInterceptor); + + final CoreSession session = directoryService.getSession(); + final SearchOperationContext ctx = new SearchOperationContext(session); + ctx.setInterceptors(List.of(rolesLookupInterceptor.getName(), "NEXT")); + final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(); + rolesLookupBypassControl.setBypassRolesLookup(false); + ctx.addRequestControl(rolesLookupBypassControl); + + // Set up test to with group and role mapping + final Entry userEntry = createUserEntry("alice", "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + nextInterceptor.setEntries(List.of(userEntry)); + + final Collection roles = Arrays.asList("roleA", "roleG"); + expect(mockRolesService.lookupRoles(anyString(), anyObject())).andReturn(roles).atLeastOnce(); replay(mockRolesService); - return new LDAPRolesLookupInterceptor(mockRolesService); + + final EntryFilteringCursor entries = rolesLookupInterceptor.search(ctx); + + assertTrue(entries.next()); + Entry modifiedEntry = entries.get(); + assertMemberOf(modifiedEntry, + "cn=roleA,ou=groups,dc=hadoop,dc=apache,dc=org", + "cn=roleG,ou=groups,dc=hadoop,dc=apache,dc=org"); + assertFalse(entries.next()); + } + + @Test + public void testRolesLookupWithBypass() throws Exception { + final DirectoryService directoryService = new SimpleDirectoryService(); + directoryService.setShutdownHookEnabled(false); + SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager(); + directoryService.setSchemaManager(schemaManager); + + final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(createMockRolesService()); + rolesLookupInterceptor.init(directoryService); + directoryService.addLast(rolesLookupInterceptor); + + final ConfigurableEntriesTestInterceptor nextInterceptor = new ConfigurableEntriesTestInterceptor("NEXT"); + nextInterceptor.init(directoryService); + directoryService.addLast(nextInterceptor); + + final CoreSession session = directoryService.getSession(); + final SearchOperationContext ctx = new SearchOperationContext(session); + ctx.setInterceptors(List.of(rolesLookupInterceptor.getName(), "NEXT")); + + final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(); + rolesLookupBypassControl.setBypassRolesLookup(true); + ctx.addRequestControl(rolesLookupBypassControl); + + // Set up test to with group and role mapping + final Entry userEntry = createUserEntry("alice", "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + nextInterceptor.setEntries(List.of(userEntry)); + + final EntryFilteringCursor entries = rolesLookupInterceptor.search(ctx); + + assertTrue(entries.next()); + Entry modifiedEntry = entries.get(); + assertMemberOf(modifiedEntry, "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + assertFalse(entries.next()); + } + + private LDAPRolesLookupService createMockRolesService() throws Exception { + final LDAPRolesLookupService mockRolesService = EasyMock.createMock(LDAPRolesLookupService.class); + replay(mockRolesService); + return mockRolesService; + } + + private LDAPRolesLookupInterceptor createInterceptor() throws Exception { + return new LDAPRolesLookupInterceptor(createMockRolesService()); } private Entry createUserEntry(final String username, final String... memberOfDns) throws Exception { - final Entry entry = new DefaultEntry(); + final Entry entry = new DefaultEntry(schemaManager); entry.add("uid", username); for (final String dn : memberOfDns) { entry.add("memberOf", dn); diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md index 6e0d1a56be..288cf52398 100644 --- a/knox-site/docs/service_ldap_server.md +++ b/knox-site/docs/service_ldap_server.md @@ -60,6 +60,24 @@ The duplicate user filter interceptor ensures that each `Entry` has a unique `ui The user search interceptor is created if the `interceptorType` configuration is set to `backend`. This interceptor forwards search queries to its configured backend. +#### Roles Lookup Interceptor (`rolesLookup`) + +The rolesLookup interceptor is created if the `interceptorType` configuration is set to `rolesLookup`. This interceptor transforms the response entities based on the mappings provided by the Role Lookup Service. For each entity, a request will be made to lookup roles based on the user's name and group membership. These roles will replace the values in the `memberOf` attribute. + +The interceptor will skip role mapping for a search request if the RolesLookupBypassControl is set to true. + +For example, the control can be added to the `ldapsearch` cli using the `-e` option. +```shell script +ldapsearch -v -x -H ldap://localhost:3890 -b 'ou=people,DC=proxy,DC=com' -e "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953=AQP/" '(uid=sam*)' '*' +``` +The control is specified using it's OID, `1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953`. The value is a 3 byte array. This value must be base64 encoded for `ldapsearch`. + +| Byte | Value | Description | +| :--- | :--- | :--- | +| Tag | 0x01 | The Boolean Tag value | +| Length | 0x03 | The length of the value in bytes | +| Bypass | 0x00 or Oxff | 0x00 corresponds to `false` and 0xff corresponds to `true | + ### Backend Types #### Common Backend Properties @@ -259,13 +277,18 @@ Alternative: Use host and port instead of URL --> - gateway.ldap.interceptor.duplicatefilter.interceptorType duplicateuserfilter + + + gateway.ldap.interceptor.rolesLookup.interceptorType + rolesLookup + + ``` diff --git a/pom.xml b/pom.xml index 3e89796af0..fad6fc3dcd 100644 --- a/pom.xml +++ b/pom.xml @@ -2227,6 +2227,16 @@ api-util ${apacheds.directory.api.version} + + org.apache.directory.api + api-ldap-codec-core + ${apacheds.directory.api.version} + + + org.apache.directory.api + api-asn1-api + ${apacheds.directory.api.version} + org.apache.mina From b3d0b1fd5077cac1b74c4976689f47e87f5fe5ca Mon Sep 17 00:00:00 2001 From: David Han Date: Thu, 11 Jun 2026 09:25:53 -0500 Subject: [PATCH 2/4] fix BER so that length reflects length of value only --- .../control/RolesLookupBypassControlFactory.java | 2 +- .../RolesLookupBypassControlDecoratorTest.java | 12 ++++++------ .../control/RolesLookupBypassControlFactoryTest.java | 8 ++++---- knox-site/docs/service_ldap_server.md | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java index 05e1aefbf7..02a6cf3924 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java @@ -60,7 +60,7 @@ public void encodeValue(Asn1Buffer buffer, Control control) { RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; buffer.put(BOOLEAN_TAG_BYTE); - buffer.put((byte) 3); + buffer.put((byte) 1); // Value is one byte long buffer.put((byte) (rolesLookupBypassControl.isBypassRolesLookup() ? 0xFF : 0x00)); } } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java index 7435f0db38..6543b9259d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java @@ -52,7 +52,7 @@ public void setUp() throws Exception { @Test public void decodeFalseValue() throws Exception { - byte[] bytes = new byte[]{0x01, 0x03, 0x00}; + byte[] bytes = new byte[]{0x01, 0x01, 0x00}; rolesLookupBypassControlDecorator.decode(bytes); assertFalse(rolesLookupBypassControl.isBypassRolesLookup()); @@ -60,7 +60,7 @@ public void decodeFalseValue() throws Exception { @Test public void decodeTrueValue() throws Exception { - byte[] bytes = new byte[]{0x01, 0x03, (byte) 0xff}; + byte[] bytes = new byte[]{0x01, 0x01, (byte) 0xff}; rolesLookupBypassControlDecorator.decode(bytes); assertTrue(rolesLookupBypassControl.isBypassRolesLookup()); @@ -68,13 +68,13 @@ public void decodeTrueValue() throws Exception { @Test(expected = DecoderException.class) public void decodeWrongTag() throws Exception { - byte[] bytes = new byte[]{0x02, 0x03, 0x00}; + byte[] bytes = new byte[]{0x02, 0x01, 0x00}; rolesLookupBypassControlDecorator.decode(bytes); } @Test(expected = DecoderException.class) public void decodeWrongLength() throws Exception { - byte[] bytes = new byte[]{0x02, 0x04, 0x00, 0x00}; + byte[] bytes = new byte[]{0x02, 0x02, 0x00, 0x00}; rolesLookupBypassControlDecorator.decode(bytes); } @@ -87,7 +87,7 @@ public void computeLength() { @Test public void encodeTrueValue() throws Exception { rolesLookupBypassControl.setBypassRolesLookup(true); - byte[] expectedBytes = new byte[]{0x01, 0x03, (byte) 0xff}; + byte[] expectedBytes = new byte[]{0x01, 0x01, (byte) 0xff}; ByteBuffer byteBuffer = ByteBuffer.allocate(3); ByteBuffer encodedBuffer = rolesLookupBypassControlDecorator.encode(byteBuffer); @@ -100,7 +100,7 @@ public void encodeTrueValue() throws Exception { @Test public void encodeFalseValue() throws Exception { - byte[] expectedBytes = new byte[]{0x01, 0x03, 0x00}; + byte[] expectedBytes = new byte[]{0x01, 0x01, 0x00}; ByteBuffer byteBuffer = ByteBuffer.allocate(3); ByteBuffer encodedBuffer = rolesLookupBypassControlDecorator.encode(byteBuffer); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java index 4e8738cfee..5c9af2d109 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java @@ -52,7 +52,7 @@ public void newControl() { @Test public void decodeFalseValue() throws Exception { RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); - byte[] bytes = new byte[]{0x01, 0x03, 0x00}; + byte[] bytes = new byte[]{0x01, 0x01, 0x00}; rolesLookupBypassControlFactory.decodeValue(control, bytes); @@ -62,7 +62,7 @@ public void decodeFalseValue() throws Exception { @Test public void decodeTrueValue() throws Exception { RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); - byte[] bytes = new byte[]{0x01, 0x03, (byte) 0xff}; + byte[] bytes = new byte[]{0x01, 0x01, (byte) 0xff}; rolesLookupBypassControlFactory.decodeValue(control, bytes); @@ -78,7 +78,7 @@ public void encodeTrueValue() { rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); // expectedBytes in reverse because Asn1Buffer stores bytes in reverse order - byte[] expectedBytes = new byte[]{(byte) 0xff, 0x03, 0x01}; + byte[] expectedBytes = new byte[]{(byte) 0xff, 0x01, 0x01}; System.out.println(asn1Buffer.toString()); ByteBuffer encodedBuffer = asn1Buffer.getBytes(); byte[] encodedBytes = new byte[encodedBuffer.remaining()]; @@ -95,7 +95,7 @@ public void encodeFalseValue() { rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); // expectedBytes in reverse because Asn1Buffer stores bytes in reverse order - byte[] expectedBytes = new byte[]{0x00, 0x03, 0x01}; + byte[] expectedBytes = new byte[]{0x00, 0x01, 0x01}; ByteBuffer encodedBuffer = asn1Buffer.getBytes(); byte[] encodedBytes = new byte[encodedBuffer.remaining()]; encodedBuffer.get(encodedBytes); diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md index 288cf52398..521facbffb 100644 --- a/knox-site/docs/service_ldap_server.md +++ b/knox-site/docs/service_ldap_server.md @@ -68,7 +68,7 @@ The interceptor will skip role mapping for a search request if the RolesLookupBy For example, the control can be added to the `ldapsearch` cli using the `-e` option. ```shell script -ldapsearch -v -x -H ldap://localhost:3890 -b 'ou=people,DC=proxy,DC=com' -e "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953=AQP/" '(uid=sam*)' '*' +ldapsearch -v -x -H ldap://localhost:3890 -b 'ou=people,DC=proxy,DC=com' -e "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953=AQH/" '(uid=sam*)' '*' ``` The control is specified using it's OID, `1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953`. The value is a 3 byte array. This value must be base64 encoded for `ldapsearch`. From 523ff62d9cbc618986b727eba67fce53e41428e2 Mon Sep 17 00:00:00 2001 From: David Han Date: Thu, 11 Jun 2026 15:24:28 -0500 Subject: [PATCH 3/4] move OID to configuration --- .../config/impl/GatewayConfigImpl.java | 5 +++ .../services/ldap/KnoxLDAPServerManager.java | 23 +++++++--- .../control/RolesLookupBypassControl.java | 5 --- .../RolesLookupBypassControlFactory.java | 6 +-- .../control/RolesLookupBypassControlImpl.java | 4 +- .../LDAPRolesLookupInterceptor.java | 20 +++++---- .../LDAPRolesLookupInterceptorFactory.java | 13 +++++- .../ldap/KnoxLDAPServerManagerTest.java | 42 +++++++++++++++++-- .../services/ldap/KnoxLDAPServiceTest.java | 2 + ...RolesLookupBypassControlDecoratorTest.java | 5 ++- .../RolesLookupBypassControlFactoryTest.java | 11 ++--- .../control/RolesLookupTestConstants.java | 25 +++++++++++ ...LDAPRolesLookupInterceptorFactoryTest.java | 3 ++ .../LDAPRolesLookupInterceptorTest.java | 11 ++--- .../knox/gateway/GatewayTestConfig.java | 5 +++ .../knox/gateway/config/GatewayConfig.java | 6 +++ knox-site/docs/service_ldap_server.md | 20 +++++++-- 17 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupTestConstants.java diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java index a8aa19fcb9..99c3377371 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java @@ -1823,6 +1823,11 @@ public String getLdapRolesLookupFilePath() { return get(LDAP_ROLES_LOOKUP_FILE_PATH); } + @Override + public String getLdapRolesLookupBypassControlOid() { + return get(LDAP_ROLES_LOOKUP_BYPASS_CONTROL_OID); + } + @Override public boolean getGroupUIServicesOnHomepage() { return getBoolean(KNOX_HOMEPAGE_GROUP_UI_SERVICES, DEFAULT_GROUP_UI_SERVICES); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java index 197a45a410..6b6a9db328 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java @@ -19,6 +19,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; +import org.apache.directory.api.asn1.util.Oid; import org.apache.directory.api.ldap.codec.api.LdapApiService; import org.apache.directory.api.ldap.codec.api.LdapApiServiceFactory; import org.apache.directory.api.ldap.model.cursor.Cursor; @@ -42,6 +43,7 @@ import org.apache.directory.server.core.partition.ldif.LdifPartition; import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.knox.gateway.config.ConfigurationException; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory; @@ -74,6 +76,7 @@ public class KnoxLDAPServerManager { private String baseDn; // Collection of DNs for the proxied backend LDAP servers private Set baseDns; + private String rolesLookupBypassControlOid; /** * Initialize the LDAP server with the given configuration @@ -91,6 +94,14 @@ public void initialize(GatewayConfig config) throws Exception { this.port = config.getLDAPPort(); this.baseDn = config.getLDAPBaseDN(); + // Get OID for roles lookup bypass control + this.rolesLookupBypassControlOid = gatewayConfig.getLdapRolesLookupBypassControlOid(); + if (StringUtils.isNotBlank(this.rolesLookupBypassControlOid)) { + if (!Oid.isOid(this.rolesLookupBypassControlOid)) { + throw new ConfigurationException("Roles Lookup Bypass Control OID is not valid"); + } + } + createInterceptors(config); // Clean up previous run if it didn't shut down cleanly @@ -139,12 +150,14 @@ public void start() throws Exception { directoryService.setInstanceLayout(new InstanceLayout(workDir)); // Add RolesLookupBypassControlFactory - LdapApiService apiService = directoryService.getLdapCodecService(); - if (apiService == null) { - apiService = LdapApiServiceFactory.getSingleton(); + if (StringUtils.isNotBlank(rolesLookupBypassControlOid)) { + LdapApiService apiService = directoryService.getLdapCodecService(); + if (apiService == null) { + apiService = LdapApiServiceFactory.getSingleton(); + } + RolesLookupBypassControlFactory rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(apiService, rolesLookupBypassControlOid); + apiService.registerRequestControl(rolesLookupBypassControlFactory); } - RolesLookupBypassControlFactory rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(apiService); - apiService.registerRequestControl(rolesLookupBypassControlFactory); // Create SchemaManager SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager(); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java index 2a0c8b2c4a..78712fc38f 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.java @@ -20,11 +20,6 @@ import org.apache.directory.api.ldap.model.message.Control; public interface RolesLookupBypassControl extends Control { - // OID created from a UUID to ensure no collisions: - // Apache Root OID for core object classes 1.3.6.1.4.1.18060.2 - // UUID "5236bee0-8a22-4419-9f8e-f1de43312ce1" - String OID = "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953"; - boolean isBypassRolesLookup(); void setBypassRolesLookup(boolean bypassRolesLookup); } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java index 02a6cf3924..a3bfc98ca1 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java @@ -26,13 +26,13 @@ public class RolesLookupBypassControlFactory extends AbstractControlFactory { public static final byte BOOLEAN_TAG_BYTE = 0x01; - public RolesLookupBypassControlFactory(LdapApiService codec) { - super(codec, RolesLookupBypassControl.OID); + public RolesLookupBypassControlFactory(LdapApiService codec, String oid) { + super(codec, oid); } @Override public Control newControl() { - return new RolesLookupBypassControlDecorator(codec, new RolesLookupBypassControlImpl(), this); + return new RolesLookupBypassControlDecorator(codec, new RolesLookupBypassControlImpl(oid), this); } @Override diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java index c401f1fadf..382d9aacc9 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java @@ -22,8 +22,8 @@ public class RolesLookupBypassControlImpl extends AbstractControl implements RolesLookupBypassControl { private boolean bypassRolesLookup; - public RolesLookupBypassControlImpl() { - super(OID); + public RolesLookupBypassControlImpl(String oid) { + super(oid); } @Override diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java index 2fc4af36fe..93c7e6ef3c 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.services.ldap.interceptor; +import org.apache.commons.lang3.StringUtils; import org.apache.directory.api.ldap.model.cursor.ListCursor; import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.Entry; @@ -50,19 +51,24 @@ public class LDAPRolesLookupInterceptor extends BaseInterceptor { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); private final LDAPRolesLookupService rolesLookupService; + private final String rolesLookupBypassControlOid; - public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService) { + // tODO take in config + public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService, String rolesLookupBypassControlOid) { this.rolesLookupService = rolesLookupService; + this.rolesLookupBypassControlOid = rolesLookupBypassControlOid; } @Override public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapException { - if (ctx.hasRequestControl(RolesLookupBypassControl.OID)) { - Control control = ctx.getRequestControl(RolesLookupBypassControl.OID); - if (control instanceof RolesLookupBypassControl) { - RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; - if (rolesLookupBypassControl.isBypassRolesLookup()) { - return next(ctx); + if (StringUtils.isNotBlank(rolesLookupBypassControlOid)) { + if (ctx.hasRequestControl(rolesLookupBypassControlOid)) { + Control control = ctx.getRequestControl(rolesLookupBypassControlOid); + if (control instanceof RolesLookupBypassControl) { + RolesLookupBypassControl rolesLookupBypassControl = (RolesLookupBypassControl) control; + if (rolesLookupBypassControl.isBypassRolesLookup()) { + return next(ctx); + } } } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java index 4ab8d903ed..42c1de1531 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java @@ -16,6 +16,8 @@ */ package org.apache.knox.gateway.services.ldap.interceptor; +import org.apache.commons.lang3.StringUtils; +import org.apache.directory.api.asn1.util.Oid; import org.apache.directory.server.core.api.interceptor.Interceptor; import org.apache.knox.gateway.GatewayServer; import org.apache.knox.gateway.config.GatewayConfig; @@ -35,7 +37,16 @@ public Interceptor create(GatewayConfig gatewayConfig, String name, Map> controlFactoryMap = serverManager.directoryService.getLdapCodecService().getRequestControlFactories(); - assertTrue(controlFactoryMap.containsKey(RolesLookupBypassControl.OID)); - assertTrue(controlFactoryMap.get(RolesLookupBypassControl.OID) instanceof RolesLookupBypassControlFactory); + assertTrue(controlFactoryMap.containsKey(ROLES_LOOKUP_BYPASS_CONTROL_OID)); + assertTrue(controlFactoryMap.get(ROLES_LOOKUP_BYPASS_CONTROL_OID) instanceof RolesLookupBypassControlFactory); } + @Test + public void testStartDontRegistersRolesLookupBypassControl() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes(); + expect(mockConfig.getLdapRolesLookupBypassControlOid()).andReturn("").anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + serverManager.start(); + + Map> controlFactoryMap = serverManager.directoryService.getLdapCodecService().getRequestControlFactories(); + assertFalse(controlFactoryMap.values().stream() + .filter(control -> control instanceof RolesLookupBypassControlFactory) + .count() > 0); + } + + @Test(expected = ConfigurationException.class) + public void testInitializeRolesLookupBypassControlBadOid() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes(); + expect(mockConfig.getLdapRolesLookupBypassControlOid()).andReturn("notanoid").anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + } + + private Map createFileBackendInterceptorConfig() { Map config = new HashMap<>(); config.put("interceptorType", "backend"); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java index 530cdb1022..e5c3f37f6e 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; +import static org.apache.knox.gateway.services.ldap.control.RolesLookupTestConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; @@ -172,6 +173,7 @@ private void setupMockConfig(String backendType) throws Exception { expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ? "dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce(); expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce(); expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce(); + expect(mockConfig.getLdapRolesLookupBypassControlOid()).andReturn(ROLES_LOOKUP_BYPASS_CONTROL_OID).anyTimes(); replay(mockConfig); } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java index 6543b9259d..5fa34d554d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.services.ldap.control; +import static org.apache.knox.gateway.services.ldap.control.RolesLookupTestConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID; import static org.easymock.EasyMock.mock; import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertArrayEquals; @@ -44,8 +45,8 @@ public void setUp() throws Exception { mockLdapApiService = mock(LdapApiService.class); replay(mockLdapApiService); - rolesLookupBypassControl = new RolesLookupBypassControlImpl(); - rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService); + rolesLookupBypassControl = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); + rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService, ROLES_LOOKUP_BYPASS_CONTROL_OID); rolesLookupBypassControlDecorator = new RolesLookupBypassControlDecorator(mockLdapApiService, rolesLookupBypassControl, rolesLookupBypassControlFactory); } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java index 5c9af2d109..58e25d7f4c 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.services.ldap.control; +import static org.apache.knox.gateway.services.ldap.control.RolesLookupTestConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID; import static org.easymock.EasyMock.mock; import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertArrayEquals; @@ -40,7 +41,7 @@ public class RolesLookupBypassControlFactoryTest { public void setUp() throws Exception { mockLdapApiService = mock(LdapApiService.class); replay(mockLdapApiService); - rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService); + rolesLookupBypassControlFactory = new RolesLookupBypassControlFactory(mockLdapApiService, ROLES_LOOKUP_BYPASS_CONTROL_OID); } @Test @@ -51,7 +52,7 @@ public void newControl() { @Test public void decodeFalseValue() throws Exception { - RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); byte[] bytes = new byte[]{0x01, 0x01, 0x00}; rolesLookupBypassControlFactory.decodeValue(control, bytes); @@ -61,7 +62,7 @@ public void decodeFalseValue() throws Exception { @Test public void decodeTrueValue() throws Exception { - RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); byte[] bytes = new byte[]{0x01, 0x01, (byte) 0xff}; rolesLookupBypassControlFactory.decodeValue(control, bytes); @@ -72,7 +73,7 @@ public void decodeTrueValue() throws Exception { @Test public void encodeTrueValue() { Asn1Buffer asn1Buffer = new Asn1Buffer(); - RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); control.setBypassRolesLookup(true); rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); @@ -89,7 +90,7 @@ public void encodeTrueValue() { @Test public void encodeFalseValue() { Asn1Buffer asn1Buffer = new Asn1Buffer(); - RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); control.setBypassRolesLookup(false); rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupTestConstants.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupTestConstants.java new file mode 100644 index 0000000000..542110f2bb --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupTestConstants.java @@ -0,0 +1,25 @@ +/* + * 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.knox.gateway.services.ldap.control; + +public class RolesLookupTestConstants { + // Test OID created from a UUID to ensure no collisions until an official OID is obtained: + // Apache Root OID for core object classes 1.3.6.1.4.1.18060.2 + // UUID "5236bee0-8a22-4419-9f8e-f1de43312ce1" + public static final String ROLES_LOOKUP_BYPASS_CONTROL_OID = "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953"; +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java index ca28b7959d..66f1407055 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java @@ -25,6 +25,8 @@ import java.util.Collections; +import static org.apache.knox.gateway.services.ldap.control.RolesLookupTestConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID; +import static org.easymock.EasyMock.expect; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -44,6 +46,7 @@ protected LDAPRolesLookupService getLDAPRolesLookupService() { }; GatewayConfig mockConfig = EasyMock.createMock(GatewayConfig.class); + expect(mockConfig.getLdapRolesLookupBypassControlOid()).andReturn(ROLES_LOOKUP_BYPASS_CONTROL_OID).anyTimes(); EasyMock.replay(mockConfig); Interceptor interceptor = factory.create(mockConfig, "test", Collections.emptyMap()); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java index a28b6635af..3fec3c731d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.List; +import static org.apache.knox.gateway.services.ldap.control.RolesLookupTestConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.anyString; import static org.easymock.EasyMock.expect; @@ -98,7 +99,7 @@ public void testRolesLookupNoBypass() throws Exception { final LDAPRolesLookupService mockRolesService = EasyMock.createMock(LDAPRolesLookupService.class); - final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(mockRolesService); + final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(mockRolesService, ROLES_LOOKUP_BYPASS_CONTROL_OID); rolesLookupInterceptor.init(directoryService); directoryService.addLast(rolesLookupInterceptor); @@ -109,7 +110,7 @@ public void testRolesLookupNoBypass() throws Exception { final CoreSession session = directoryService.getSession(); final SearchOperationContext ctx = new SearchOperationContext(session); ctx.setInterceptors(List.of(rolesLookupInterceptor.getName(), "NEXT")); - final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(); + final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); rolesLookupBypassControl.setBypassRolesLookup(false); ctx.addRequestControl(rolesLookupBypassControl); @@ -138,7 +139,7 @@ public void testRolesLookupWithBypass() throws Exception { SchemaManager schemaManager = SchemaManagerFactory.createSchemaManager(); directoryService.setSchemaManager(schemaManager); - final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(createMockRolesService()); + final LDAPRolesLookupInterceptor rolesLookupInterceptor = new LDAPRolesLookupInterceptor(createMockRolesService(), ROLES_LOOKUP_BYPASS_CONTROL_OID); rolesLookupInterceptor.init(directoryService); directoryService.addLast(rolesLookupInterceptor); @@ -150,7 +151,7 @@ public void testRolesLookupWithBypass() throws Exception { final SearchOperationContext ctx = new SearchOperationContext(session); ctx.setInterceptors(List.of(rolesLookupInterceptor.getName(), "NEXT")); - final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(); + final RolesLookupBypassControl rolesLookupBypassControl = new RolesLookupBypassControlImpl(ROLES_LOOKUP_BYPASS_CONTROL_OID); rolesLookupBypassControl.setBypassRolesLookup(true); ctx.addRequestControl(rolesLookupBypassControl); @@ -173,7 +174,7 @@ private LDAPRolesLookupService createMockRolesService() throws Exception { } private LDAPRolesLookupInterceptor createInterceptor() throws Exception { - return new LDAPRolesLookupInterceptor(createMockRolesService()); + return new LDAPRolesLookupInterceptor(createMockRolesService(), ROLES_LOOKUP_BYPASS_CONTROL_OID); } private Entry createUserEntry(final String username, final String... memberOfDns) throws Exception { diff --git a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java index 5331f2bc22..0c93900440 100644 --- a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java +++ b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java @@ -1290,6 +1290,11 @@ public String getLdapRolesLookupFilePath() { return ""; } + @Override + public String getLdapRolesLookupBypassControlOid() { + return ""; + } + @Override public boolean getGroupUIServicesOnHomepage() { return false; diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java index 0eac12d0d7..6c8d34c252 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java @@ -135,6 +135,7 @@ public interface GatewayConfig { String LDAP_ROLES_LOOKUP_STRATEGY = "gateway.ldap.roles.lookup.strategy"; String LDAP_ROLES_LOOKUP_REST_API_ENDPOINT = "gateway.ldap.roles.lookup.rest.api.endpoint"; String LDAP_ROLES_LOOKUP_FILE_PATH = "gateway.ldap.roles.lookup.file.path"; + String LDAP_ROLES_LOOKUP_BYPASS_CONTROL_OID = "gateway.ldap.roles.lookup.bypass.control.oid"; /** * The location of the gateway configuration. @@ -1117,6 +1118,11 @@ public interface GatewayConfig { */ String getLdapRolesLookupFilePath(); + /** + * @return the OID to register the role lookup bypass control + */ + String getLdapRolesLookupBypassControlOid(); + /** * @return set of all property names in the configuration */ diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md index 521facbffb..732902a8ae 100644 --- a/knox-site/docs/service_ldap_server.md +++ b/knox-site/docs/service_ldap_server.md @@ -68,15 +68,22 @@ The interceptor will skip role mapping for a search request if the RolesLookupBy For example, the control can be added to the `ldapsearch` cli using the `-e` option. ```shell script -ldapsearch -v -x -H ldap://localhost:3890 -b 'ou=people,DC=proxy,DC=com' -e "1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953=AQH/" '(uid=sam*)' '*' +ldapsearch -v -x -H ldap://localhost:3890 -b 'ou=people,DC=proxy,DC=com' -e "=" '(uid=sam*)' '*' ``` -The control is specified using it's OID, `1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953`. The value is a 3 byte array. This value must be base64 encoded for `ldapsearch`. +The control is specified using it's OID. The OID used for this control is configurable until an official OID is generated. An OID generated from a UUID, e.g., `1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953`, is guaranteed not to collide with existing OIDs. + +| Property | Default Value | Description | +| :--- | :--- | :--- | +| `gateway.ldap.roles.lookup.bypass.control.oid` | N/A | The OID to use for the bypass control. The control will not be registered if this value is not provided. The Knox LDAP Service will fail to initialize if the value provided is on an OID. | + + +The value is a 3 byte array. This value must be base64 encoded for `ldapsearch`. | Byte | Value | Description | | :--- | :--- | :--- | | Tag | 0x01 | The Boolean Tag value | -| Length | 0x03 | The length of the value in bytes | -| Bypass | 0x00 or Oxff | 0x00 corresponds to `false` and 0xff corresponds to `true | +| Length | 0x01 | The length of the value in bytes | +| Bypass | 0x00 or Oxff | 0x00 corresponds to `false` and 0xff corresponds to `true` | ### Backend Types @@ -173,6 +180,11 @@ To configure Knox to act as an LDAP proxy for a local file and an Active Directo http://localhost:8080/auth/roles + + gateway.ldap.roles.lookup.bypass.control.oid + 1.3.6.1.4.1.18060.2.1379319520.35362.17433.40846.265936912329953 + + gateway.ldap.interceptor.localfile.interceptorType From 86a29ff001563af89ee55194c364a871d8a43eef Mon Sep 17 00:00:00 2001 From: David Han Date: Thu, 11 Jun 2026 16:00:56 -0500 Subject: [PATCH 4/4] minor fix --- .../knox/gateway/services/ldap/KnoxLDAPServerManager.java | 6 +++--- .../ldap/interceptor/LDAPRolesLookupInterceptor.java | 1 - .../ldap/interceptor/LDAPRolesLookupInterceptorFactory.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java index 6b6a9db328..f1a3b47942 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java @@ -95,9 +95,9 @@ public void initialize(GatewayConfig config) throws Exception { this.baseDn = config.getLDAPBaseDN(); // Get OID for roles lookup bypass control - this.rolesLookupBypassControlOid = gatewayConfig.getLdapRolesLookupBypassControlOid(); - if (StringUtils.isNotBlank(this.rolesLookupBypassControlOid)) { - if (!Oid.isOid(this.rolesLookupBypassControlOid)) { + rolesLookupBypassControlOid = gatewayConfig.getLdapRolesLookupBypassControlOid(); + if (StringUtils.isNotBlank(rolesLookupBypassControlOid)) { + if (!Oid.isOid(rolesLookupBypassControlOid)) { throw new ConfigurationException("Roles Lookup Bypass Control OID is not valid"); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java index 93c7e6ef3c..b3ecb0e4d7 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java @@ -53,7 +53,6 @@ public class LDAPRolesLookupInterceptor extends BaseInterceptor { private final LDAPRolesLookupService rolesLookupService; private final String rolesLookupBypassControlOid; - // tODO take in config public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService, String rolesLookupBypassControlOid) { this.rolesLookupService = rolesLookupService; this.rolesLookupBypassControlOid = rolesLookupBypassControlOid; diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java index 42c1de1531..17908233b0 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java @@ -42,7 +42,7 @@ public Interceptor create(GatewayConfig gatewayConfig, String name, Map