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..78712fc38f --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControl.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; + +import org.apache.directory.api.ldap.model.message.Control; + +public interface RolesLookupBypassControl extends Control { + 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..b700664bbf --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactory.java @@ -0,0 +1,68 @@ +/* + * 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; +import org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants; + +public class RolesLookupBypassControlFactory extends AbstractControlFactory { + public static final byte BOOLEAN_TAG_BYTE = 0x01; + + public RolesLookupBypassControlFactory(LdapApiService codec) { + super(codec, SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_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) 1); // Value is one byte long + 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..403ad85d0a --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlImpl.java @@ -0,0 +1,39 @@ +/* + * 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; +import org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants; + +public class RolesLookupBypassControlImpl extends AbstractControl implements RolesLookupBypassControl { + private boolean bypassRolesLookup; + + public RolesLookupBypassControlImpl() { + super(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_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..c5a5a6abe8 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,8 @@ 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 org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants; import java.util.ArrayList; import java.util.Collection; @@ -46,6 +49,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 +58,16 @@ public LDAPRolesLookupInterceptor(LDAPRolesLookupService rolesLookupService) { @Override public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapException { + if (ctx.hasRequestControl(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID)) { + Control control = ctx.getRequestControl(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_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..77229054e6 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.RolesLookupBypassControlFactory; +import org.apache.knox.gateway.services.ldap.model.constants.SchemaConstants; 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(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_OID)); + assertTrue(controlFactoryMap.get(SchemaConstants.ROLES_LOOKUP_BYPASS_CONTROL_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..2b6c3f44b3 --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlDecoratorTest.java @@ -0,0 +1,128 @@ +/* + * 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 testDecodeFalseValue() throws Exception { + byte[] bytes = new byte[]{0x01, 0x01, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + + assertFalse(rolesLookupBypassControl.isBypassRolesLookup()); + } + + @Test + public void testDecodeTrueValue() throws Exception { + byte[] bytes = new byte[]{0x01, 0x01, (byte) 0xff}; + rolesLookupBypassControlDecorator.decode(bytes); + + assertTrue(rolesLookupBypassControl.isBypassRolesLookup()); + } + + @Test(expected = DecoderException.class) + public void testDecodeWrongTag() throws Exception { + byte[] bytes = new byte[]{0x02, 0x01, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + } + + @Test(expected = DecoderException.class) + public void testDecodeWrongLength() throws Exception { + byte[] bytes = new byte[]{0x02, 0x02, 0x00, 0x00}; + rolesLookupBypassControlDecorator.decode(bytes); + } + + + @Test + public void testComputeLength() { + assertEquals("Length must always be 3", 3, rolesLookupBypassControlDecorator.computeLength()); + } + + @Test + public void testEncodeTrueValue() throws Exception { + testEncode(true); + } + + @Test + public void testEncodeFalseValue() throws Exception { + testEncode(false); + } + + private void testEncode(boolean encodeValue) throws Exception { + byte byteValue = encodeValue ? (byte) 0xff : 0x00; + rolesLookupBypassControlDecorator.setBypassRolesLookup(encodeValue); + + byte[] expectedBytes = new byte[] {0x01, 0x01, byteValue}; + 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 testIsBypassRolesLookup() { + // Set value on the decorated instance and check that the decorator matches. + rolesLookupBypassControl.setBypassRolesLookup(true); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", true, rolesLookupBypassControlDecorator.isBypassRolesLookup()); + rolesLookupBypassControl.setBypassRolesLookup(false); + assertEquals("isBypassRolesLookup should match the value from the decorated Impl", false, rolesLookupBypassControlDecorator.isBypassRolesLookup()); + } + + @Test + public void testSetBypassRolesLookup() { + // Set value on the decorator and check that the decorated instance matches. + rolesLookupBypassControlDecorator.setBypassRolesLookup(true); + assertEquals("Decorated instance value should be updated by the decorator", true, rolesLookupBypassControl.isBypassRolesLookup()); + rolesLookupBypassControlDecorator.setBypassRolesLookup(false); + assertEquals("Decorated instance value should be updated by the decorator", false, rolesLookupBypassControl.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..7e26224dcf --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/control/RolesLookupBypassControlFactoryTest.java @@ -0,0 +1,99 @@ +/* + * 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 testNewControl() { + Control control = rolesLookupBypassControlFactory.newControl(); + assertTrue("Control must be a RolesLookupBypassControlDecorator", control instanceof RolesLookupBypassControlDecorator); + } + + @Test + public void testDecodeFalseValue() throws Exception { + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + byte[] bytes = new byte[]{0x01, 0x01, 0x00}; + + rolesLookupBypassControlFactory.decodeValue(control, bytes); + + assertFalse(control.isBypassRolesLookup()); + } + + @Test + public void testDecodeTrueValue() throws Exception { + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + byte[] bytes = new byte[]{0x01, 0x01, (byte) 0xff}; + + rolesLookupBypassControlFactory.decodeValue(control, bytes); + + assertTrue(control.isBypassRolesLookup()); + } + + @Test + public void testEncodeTrueValue() { + testEncode(true); + } + + @Test + public void testEncodeFalseValue() { + testEncode(false); + } + + private void testEncode(boolean encodeValue) { + byte byteValue = encodeValue ? (byte) 0xff : 0x00; + + Asn1Buffer asn1Buffer = new Asn1Buffer(); + RolesLookupBypassControl control = new RolesLookupBypassControlImpl(); + control.setBypassRolesLookup(encodeValue); + + rolesLookupBypassControlFactory.encodeValue(asn1Buffer, control); + + // expectedBytes in reverse because Asn1Buffer stores bytes in reverse order + byte[] expectedBytes = new byte[]{byteValue, 0x01, 0x01}; + System.out.println(asn1Buffer.toString()); + 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..52b37d99ce 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,40 @@ 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.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 +88,89 @@ public void testModifyEntryNoMemberOfNoRoles() throws Exception { assertNull(modifiedEntry.get("memberOf")); } - private LDAPRolesLookupInterceptor createInterceptor() { + @Test + public void testRolesLookupNoBypass() throws Exception { final LDAPRolesLookupService mockRolesService = EasyMock.createMock(LDAPRolesLookupService.class); + + final Collection roles = Arrays.asList("roleA", "roleG"); + expect(mockRolesService.lookupRoles(anyString(), anyObject())) + .andReturn(roles) + .atLeastOnce(); replay(mockRolesService); - return new LDAPRolesLookupInterceptor(mockRolesService); + + TestContext testContext = createTestContext(false, mockRolesService); + + // Set up test to with group and role mapping + final Entry userEntry = createUserEntry("alice", "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + testContext.nextInterceptor.setEntries(List.of(userEntry)); + + final EntryFilteringCursor entries = testContext.interceptor.search(testContext.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 LDAPRolesLookupService mockRolesService = EasyMock.createMock(LDAPRolesLookupService.class); + + TestContext testContext = createTestContext(true, mockRolesService); + + // Set up test to with group and role mapping + final Entry userEntry = createUserEntry("alice", "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + testContext.nextInterceptor.setEntries(List.of(userEntry)); + + final EntryFilteringCursor entries = testContext.interceptor.search(testContext.ctx); + + assertTrue(entries.next()); + Entry modifiedEntry = entries.get(); + assertMemberOf(modifiedEntry, "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org"); + assertFalse(entries.next()); + } + + private TestContext createTestContext(boolean bypass, LDAPRolesLookupService rolesService) throws Exception { + DirectoryService directoryService = new SimpleDirectoryService(); + directoryService.setShutdownHookEnabled(false); + directoryService.setSchemaManager(SchemaManagerFactory.createSchemaManager()); + + LDAPRolesLookupInterceptor interceptor = + new LDAPRolesLookupInterceptor(rolesService); + interceptor.init(directoryService); + directoryService.addLast(interceptor); + + ConfigurableEntriesTestInterceptor nextInterceptor = + new ConfigurableEntriesTestInterceptor("NEXT"); + nextInterceptor.init(directoryService); + directoryService.addLast(nextInterceptor); + + SearchOperationContext ctx = + new SearchOperationContext(directoryService.getSession()); + ctx.setInterceptors(List.of(interceptor.getName(), "NEXT")); + + RolesLookupBypassControl control = + new RolesLookupBypassControlImpl(); + control.setBypassRolesLookup(bypass); + ctx.addRequestControl(control); + + return new TestContext(interceptor, nextInterceptor, ctx); + } + + 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); @@ -90,4 +185,10 @@ private void assertMemberOf(final Entry entry, final String... expectedDns) { assertTrue("Missing expected role DN: " + expected, memberOf.contains(expected)); } } + + private record TestContext( + LDAPRolesLookupInterceptor interceptor, + ConfigurableEntriesTestInterceptor nextInterceptor, + SearchOperationContext ctx) { + } } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/model/constants/SchemaConstants.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/model/constants/SchemaConstants.java new file mode 100644 index 0000000000..ef34744d76 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/model/constants/SchemaConstants.java @@ -0,0 +1,29 @@ +/* + * 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.model.constants; + +/** + * A utility class where we declare the schema objects being used by the Knox LDAP Server. + * Apache Knox OID Base = 1.3.6.1.4.1.18060.18 + */ +public final class SchemaConstants { + + // Apache Knox LDAP Controls 1.3.6.1.4.1.18060.18.0 + public static final String ROLES_LOOKUP_BYPASS_CONTROL_OID = "1.3.6.1.4.1.18060.18.0.1"; + +} diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md index 6e0d1a56be..c6e6e91285 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. The control is specified using it's OID, `1.3.6.1.4.1.18060.18.0.1`. 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 | + + +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.18.0.1=AQH/" '(uid=sam*)' '*' +``` + ### 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