diff --git a/.github/workflows/plugins-jdk17-test.0.yaml b/.github/workflows/plugins-jdk17-test.0.yaml index b62166b155..60a230e812 100644 --- a/.github/workflows/plugins-jdk17-test.0.yaml +++ b/.github/workflows/plugins-jdk17-test.0.yaml @@ -81,6 +81,7 @@ jobs: - jetty-10.x-scenario - jetty-12.x-scenario - spring-ai-1.x-scenario + - spring-ldap-3.x-4.x-scenario - spring-rabbitmq-scenario - graphql-20plus-scenario - spring-kafka-3.x-scenario diff --git a/CHANGES.md b/CHANGES.md index 4b492bfd3b..085a52a0ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Release Notes. 9.8.0 ------------------ +* Add Spring LDAP 3.3.x-4.x plugin. All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/263?closed=1) diff --git a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java index a8b57ba7f8..e5d86e1303 100755 --- a/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java +++ b/apm-protocol/apm-network/src/main/java/org/apache/skywalking/apm/network/trace/component/ComponentsDefine.java @@ -295,4 +295,6 @@ public class ComponentsDefine { public static final OfficialComponent SPRING_AI = new OfficialComponent(178, "spring-ai"); + public static final OfficialComponent SPRING_LDAP = new OfficialComponent(179, "spring-ldap"); + } diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml b/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml index 9e11462ac5..2e9d1cb19a 100644 --- a/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/pom.xml @@ -46,6 +46,7 @@ spring-webflux-6.x-webclient-plugin resttemplate-commons spring-ai-1.x-plugin + spring-ldap-3.x-4.x-plugin spring-rabbitmq-plugin pom diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/pom.xml b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/pom.xml new file mode 100644 index 0000000000..58a9591af5 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + + org.apache.skywalking + spring-plugins + 9.8.0-SNAPSHOT + + + apm-spring-ldap-3.x-4.x-plugin + spring-ldap-3.x-4.x-plugin + jar + + + 3.3.8 + + + + + org.springframework.ldap + spring-ldap-core + ${spring-ldap.version} + provided + + + diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecFactoryInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecFactoryInterceptor.java new file mode 100644 index 0000000000..93ef2724d0 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecFactoryInterceptor.java @@ -0,0 +1,48 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.InstanceMethodsAroundInterceptorV2; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.MethodInvocationContext; + +public class LdapClientSpecFactoryInterceptor implements InstanceMethodsAroundInterceptorV2 { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInvocationContext context) { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret, MethodInvocationContext context) { + Object dynamicField = objInst.getSkyWalkingDynamicField(); + if (dynamicField instanceof SpringLdapEnhanceInfo && ret instanceof EnhancedInstance) { + SpringLdapEnhanceInfo info = (SpringLdapEnhanceInfo) dynamicField; + ((EnhancedInstance) ret).setSkyWalkingDynamicField(info.withOperation(method.getName())); + } + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecPropagationInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecPropagationInterceptor.java new file mode 100644 index 0000000000..8badd04f8f --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapClientSpecPropagationInterceptor.java @@ -0,0 +1,46 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.InstanceMethodsAroundInterceptorV2; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.MethodInvocationContext; + +public class LdapClientSpecPropagationInterceptor implements InstanceMethodsAroundInterceptorV2 { + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInvocationContext context) { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret, MethodInvocationContext context) { + if (ret instanceof EnhancedInstance) { + ((EnhancedInstance) ret).setSkyWalkingDynamicField(objInst.getSkyWalkingDynamicField()); + } + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapContextSourceSetterInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapContextSourceSetterInterceptor.java new file mode 100644 index 0000000000..a0684f89e5 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapContextSourceSetterInterceptor.java @@ -0,0 +1,54 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.InstanceMethodsAroundInterceptorV2; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.MethodInvocationContext; + +public class LdapContextSourceSetterInterceptor implements InstanceMethodsAroundInterceptorV2 { + + /** + * The V2 interceptor bridge always invokes {@code afterMethod} in {@code finally}, even when + * the original setter throws. This marker skips the peer update for a rejected setter call. + */ + private static final Object SETTER_FAILED = new Object(); + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInvocationContext context) { + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret, MethodInvocationContext context) { + if (context.getContext() == SETTER_FAILED) { + return ret; + } + objInst.setSkyWalkingDynamicField(LdapEndpointResolver.resolve(allArguments[0])); + return ret; + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { + context.setContext(SETTER_FAILED); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointConstructorInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointConstructorInterceptor.java new file mode 100644 index 0000000000..3f88601afa --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointConstructorInterceptor.java @@ -0,0 +1,31 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceConstructorInterceptor; + +public class LdapEndpointConstructorInterceptor implements InstanceConstructorInterceptor { + + @Override + public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { + Object contextSource = allArguments != null && allArguments.length > 0 ? allArguments[0] : null; + objInst.setSkyWalkingDynamicField(LdapEndpointResolver.resolve(contextSource)); + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolver.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolver.java new file mode 100644 index 0000000000..076f040765 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolver.java @@ -0,0 +1,243 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; + +public final class LdapEndpointResolver { + + private static final int MAX_UNWRAP_DEPTH = 8; + + private static final int PORT_UNSPECIFIED = -1; + + private static final int PORT_INVALID = -2; + + private static final int MIN_PORT = 1; + + private static final int MAX_PORT = 65535; + + private LdapEndpointResolver() { + } + + public static SpringLdapEnhanceInfo resolve(Object contextSource) { + Set visited = Collections.newSetFromMap(new IdentityHashMap()); + Object current = contextSource; + for (int depth = 0; current != null && depth < MAX_UNWRAP_DEPTH && visited.add(current); depth++) { + SpringLdapEnhanceInfo enhancedInfo = enhancedInfo(current); + if (enhancedInfo != null) { + return enhancedInfo; + } + + String peer = peerFromUrls(invokeNoArg(current, "getUrls")); + if (peer != null) { + return new SpringLdapEnhanceInfo(peer); + } + + Object delegate = invokeNoArg(current, "getContextSource"); + if (delegate == null || delegate == current) { + delegate = invokeNoArg(current, "getTarget"); + } + current = delegate; + } + return new SpringLdapEnhanceInfo(SpringLdapConstants.UNKNOWN_PEER); + } + + static String peerFromUrls(Object urls) { + if (urls == null) { + return null; + } + + Set peers = new LinkedHashSet(); + if (urls.getClass().isArray()) { + for (int i = 0; i < Array.getLength(urls); i++) { + addPeer(peers, Array.get(urls, i)); + } + } else if (urls instanceof Iterable) { + for (Object url : (Iterable) urls) { + addPeer(peers, url); + } + } else { + addPeer(peers, urls); + } + + if (peers.isEmpty()) { + return null; + } + StringBuilder result = new StringBuilder(); + for (String peer : peers) { + if (result.length() > 0) { + result.append(','); + } + result.append(peer); + } + return result.toString(); + } + + private static SpringLdapEnhanceInfo enhancedInfo(Object source) { + if (!(source instanceof EnhancedInstance)) { + return null; + } + Object dynamicField = ((EnhancedInstance) source).getSkyWalkingDynamicField(); + return dynamicField instanceof SpringLdapEnhanceInfo ? (SpringLdapEnhanceInfo) dynamicField : null; + } + + private static Object invokeNoArg(Object target, String methodName) { + Method method; + try { + method = target.getClass().getMethod(methodName); + } catch (Exception ignored) { + return null; + } + try { + return method.invoke(target); + } catch (IllegalAccessException ignored) { + try { + method.setAccessible(true); + return method.invoke(target); + } catch (Exception inaccessible) { + return null; + } + } catch (Exception ignored) { + return null; + } + } + + private static void addPeer(Set peers, Object value) { + if (value == null) { + return; + } + String peer = peerFromUrl(String.valueOf(value)); + if (peer != null) { + peers.add(peer); + } + } + + private static String peerFromUrl(String value) { + try { + URI uri = new URI(value.trim()); + String scheme = uri.getScheme(); + if (scheme == null) { + return null; + } + scheme = scheme.toLowerCase(Locale.ROOT); + if (!"ldap".equals(scheme) && !"ldaps".equals(scheme)) { + return null; + } + + HostAndPort hostAndPort = hostAndPort(uri); + if (hostAndPort.host == null || hostAndPort.host.isEmpty()) { + return null; + } + int port = resolvedPort(scheme, hostAndPort.port); + if (port == PORT_INVALID) { + return null; + } + return formatHost(hostAndPort.host) + ':' + port; + } catch (Exception ignored) { + return null; + } + } + + private static int resolvedPort(String scheme, int port) { + if (port == PORT_UNSPECIFIED) { + return "ldaps".equals(scheme) ? 636 : 389; + } + if (port < MIN_PORT || port > MAX_PORT) { + return PORT_INVALID; + } + return port; + } + + private static HostAndPort hostAndPort(URI uri) { + if (uri.getHost() != null) { + int port = uri.getPort() >= 0 ? uri.getPort() : PORT_UNSPECIFIED; + return new HostAndPort(uri.getHost(), port); + } + String authority = uri.getRawAuthority(); + if (authority == null) { + return new HostAndPort(null, PORT_UNSPECIFIED); + } + int userInfoEnd = authority.lastIndexOf('@'); + if (userInfoEnd >= 0) { + authority = authority.substring(userInfoEnd + 1); + } + if (authority.startsWith("[")) { + int bracket = authority.indexOf(']'); + if (bracket < 0) { + return new HostAndPort(null, PORT_INVALID); + } + String host = authority.substring(1, bracket); + return new HostAndPort(host, parsePort(authority.substring(bracket + 1))); + } + int colon = authority.lastIndexOf(':'); + if (colon > 0 && authority.indexOf(':') == colon) { + return new HostAndPort(authority.substring(0, colon), parsePort(authority.substring(colon))); + } + return new HostAndPort(authority, PORT_UNSPECIFIED); + } + + private static int parsePort(String value) { + if (value == null || value.isEmpty()) { + return PORT_UNSPECIFIED; + } + if (value.charAt(0) != ':' || value.length() == 1) { + return PORT_INVALID; + } + for (int i = 1; i < value.length(); i++) { + char character = value.charAt(i); + if (character < '0' || character > '9') { + return PORT_INVALID; + } + } + try { + int port = Integer.parseInt(value.substring(1)); + return port < 0 ? PORT_INVALID : port; + } catch (NumberFormatException ignored) { + return PORT_INVALID; + } + } + + private static String formatHost(String host) { + String normalized = host; + if (normalized.startsWith("[") && normalized.endsWith("]")) { + normalized = normalized.substring(1, normalized.length() - 1); + } + return normalized.indexOf(':') >= 0 ? '[' + normalized + ']' : normalized; + } + + private static final class HostAndPort { + + private final String host; + + private final int port; + + private HostAndPort(String host, int port) { + this.host = host; + this.port = port; + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapConstants.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapConstants.java new file mode 100644 index 0000000000..74b8128e94 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapConstants.java @@ -0,0 +1,36 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import org.apache.skywalking.apm.agent.core.context.tag.AbstractTag; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; + +final class SpringLdapConstants { + + static final String DB_TYPE = "LDAP"; + + static final String OPERATION_PREFIX = "SpringLDAP/"; + + static final String UNKNOWN_PEER = "unknown"; + + static final AbstractTag LDAP_OPERATION = Tags.ofKey("ldap.operation"); + + private SpringLdapConstants() { + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapEnhanceInfo.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapEnhanceInfo.java new file mode 100644 index 0000000000..1238ed5f70 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapEnhanceInfo.java @@ -0,0 +1,47 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +public final class SpringLdapEnhanceInfo { + + private final String peer; + + private final String operation; + + SpringLdapEnhanceInfo(String peer) { + this(peer, null); + } + + SpringLdapEnhanceInfo(String peer, String operation) { + this.peer = peer == null || peer.isEmpty() ? SpringLdapConstants.UNKNOWN_PEER : peer; + this.operation = operation; + } + + SpringLdapEnhanceInfo withOperation(String operation) { + return new SpringLdapEnhanceInfo(peer, operation); + } + + String getPeer() { + return peer; + } + + String getOperation() { + return operation; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapOperationInterceptor.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapOperationInterceptor.java new file mode 100644 index 0000000000..d91dcd2175 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapOperationInterceptor.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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Method; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.tag.Tags; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.InstanceMethodsAroundInterceptorV2; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.MethodInvocationContext; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; + +public class SpringLdapOperationInterceptor implements InstanceMethodsAroundInterceptorV2 { + + private static final String AUTHENTICATE_OPERATION = "authenticate"; + + @Override + public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + MethodInvocationContext context) throws Throwable { + Object dynamicField = objInst.getSkyWalkingDynamicField(); + if (!(dynamicField instanceof SpringLdapEnhanceInfo)) { + return; + } + + SpringLdapEnhanceInfo info = (SpringLdapEnhanceInfo) dynamicField; + // Nested remote calls reuse the active exit span; skip so LDAP tags are not overwritten. + if (ContextManager.isActive() && ContextManager.activeSpan().isExit()) { + return; + } + + String operation = info.getOperation() == null ? method.getName() : info.getOperation(); + AbstractSpan span = ContextManager.createExitSpan( + SpringLdapConstants.OPERATION_PREFIX + operation, info.getPeer()); + try { + span.setComponent(ComponentsDefine.SPRING_LDAP); + SpanLayer.asDB(span); + Tags.DB_TYPE.set(span, SpringLdapConstants.DB_TYPE); + span.tag(SpringLdapConstants.LDAP_OPERATION, operation); + context.setContext(new InvocationState(span, operation)); + } catch (Throwable initializationFailure) { + // The V2 bridge continues the application even when beforeMethod fails. + try { + ContextManager.stopSpan(span); + } catch (Throwable stopFailure) { + initializationFailure.addSuppressed(stopFailure); + } + throw initializationFailure; + } + } + + @Override + public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes, + Object ret, MethodInvocationContext context) { + InvocationState state = invocationState(context); + if (state == null) { + return ret; + } + try { + if (failedPrimitiveBooleanAuthenticate(method, state.operation, ret)) { + state.span.errorOccurred(); + } + return ret; + } finally { + ContextManager.stopSpan(state.span); + } + } + + @Override + public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, + Class[] argumentsTypes, Throwable t, MethodInvocationContext context) { + InvocationState state = invocationState(context); + if (state == null) { + return; + } + // Default to a privacy-safe error flag. Exception messages/stacks can contain DNs, + // filters, or credentials and are collected only when explicitly enabled. + state.span.errorOccurred(); + if (SpringLdapPluginConfig.Plugin.SpringLDAP.COLLECT_EXCEPTION_DETAILS) { + state.span.log(t); + } + } + + /** + * Only primitive {@code boolean authenticate(...)} methods report invalid credentials as + * {@code false}. Mapper overloads may return {@link Boolean#FALSE} as a successful mapped value. + */ + private static boolean failedPrimitiveBooleanAuthenticate(Method method, String operation, Object ret) { + return AUTHENTICATE_OPERATION.equals(operation) + && method != null + && method.getReturnType() == boolean.class + && Boolean.FALSE.equals(ret); + } + + private static InvocationState invocationState(MethodInvocationContext context) { + Object state = context.getContext(); + return state instanceof InvocationState ? (InvocationState) state : null; + } + + private static final class InvocationState { + + private final AbstractSpan span; + + private final String operation; + + private InvocationState(AbstractSpan span, String operation) { + this.span = span; + this.operation = operation; + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapPluginConfig.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapPluginConfig.java new file mode 100644 index 0000000000..a81c1a06f1 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapPluginConfig.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.skywalking.apm.plugin.spring.ldap; + +import org.apache.skywalking.apm.agent.core.boot.PluginConfig; + +public class SpringLdapPluginConfig { + + public static class Plugin { + + @PluginConfig(root = SpringLdapPluginConfig.class) + public static class SpringLDAP { + + /** + * Whether to collect Spring LDAP exception messages and stack traces on spans. + * Enabling this may expose sensitive LDAP data, including DNs, filters, + * attributes, and credentials. + */ + public static boolean COLLECT_EXCEPTION_DETAILS = false; + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/AbstractSpringLdapInstrumentation.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/AbstractSpringLdapInstrumentation.java new file mode 100644 index 0000000000..759ce22a84 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/AbstractSpringLdapInstrumentation.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.skywalking.apm.plugin.spring.ldap.define; + +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.ClassInstanceMethodsEnhancePluginDefineV2; + +/** + * Spring LDAP 3.3+ witness. {@code ObservationContextSource} was added in 3.3.0 and is + * packaged in {@code spring-ldap-core}, so it gates the plugin to the documented range + * and avoids weaving Spring LDAP 2.x {@code LdapTemplate}. + */ +public abstract class AbstractSpringLdapInstrumentation extends ClassInstanceMethodsEnhancePluginDefineV2 { + + static final String OBSERVATION_CONTEXT_SOURCE = + "org.springframework.ldap.core.support.ObservationContextSource"; + + @Override + protected final String[] witnessClasses() { + return new String[] { + OBSERVATION_CONTEXT_SOURCE + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/DefaultLdapClientInstrumentation.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/DefaultLdapClientInstrumentation.java new file mode 100644 index 0000000000..5512bc3314 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/DefaultLdapClientInstrumentation.java @@ -0,0 +1,91 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.DeclaredInstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.InstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +public class DefaultLdapClientInstrumentation extends AbstractSpringLdapInstrumentation { + + private static final String ENHANCE_CLASS = "org.springframework.ldap.core.DefaultLdapClient"; + + private static final String CONTEXT_SOURCE = "org.springframework.ldap.core.ContextSource"; + + private static final String FACTORY_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapClientSpecFactoryInterceptor"; + + private static final String CONSTRUCTOR_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapEndpointConstructorInterceptor"; + + @Override + protected ClassMatch enhanceClass() { + return byName(ENHANCE_CLASS); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return takesArgument(0, named(CONTEXT_SOURCE)); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + } + }; + } + + @Override + public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { + return new InstanceMethodsInterceptV2Point[] { + new DeclaredInstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return isPublic().and(namedOneOf( + "list", "listBindings", "search", "authenticate", "bind", "modify", "unbind" + )); + } + + @Override + public String getMethodsInterceptorV2() { + return FACTORY_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapClientSpecInstrumentation.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapClientSpecInstrumentation.java new file mode 100644 index 0000000000..3bd043e498 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapClientSpecInstrumentation.java @@ -0,0 +1,100 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.DeclaredInstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.InstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static org.apache.skywalking.apm.agent.core.plugin.match.MultiClassNameMatch.byMultiClassMatch; + +public class LdapClientSpecInstrumentation extends AbstractSpringLdapInstrumentation { + + private static final String[] ENHANCE_CLASSES = { + "org.springframework.ldap.core.DefaultLdapClient$DefaultListSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultListBindingsSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultAuthenticateSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultSearchSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultBindSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultModifySpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultUnbindSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultSearchSpec$ContextMapperSearchSpec", + "org.springframework.ldap.core.DefaultLdapClient$DefaultSearchSpec$AttributeMapperSearchSpec" + }; + + private static final String OPERATION_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.SpringLdapOperationInterceptor"; + + private static final String PROPAGATION_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapClientSpecPropagationInterceptor"; + + @Override + protected ClassMatch enhanceClass() { + return byMultiClassMatch(ENHANCE_CLASSES); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[0]; + } + + @Override + public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { + return new InstanceMethodsInterceptV2Point[] { + new DeclaredInstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return isPublic().and(namedOneOf("toList", "toObject", "list", "execute")); + } + + @Override + public String getMethodsInterceptorV2() { + return OPERATION_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + }, + new DeclaredInstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return isPublic().and(named("map")); + } + + @Override + public String getMethodsInterceptorV2() { + return PROPAGATION_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapTemplateInstrumentation.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapTemplateInstrumentation.java new file mode 100644 index 0000000000..85c04ac3b2 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/LdapTemplateInstrumentation.java @@ -0,0 +1,114 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.DeclaredInstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.InstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.any; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +public class LdapTemplateInstrumentation extends AbstractSpringLdapInstrumentation { + + private static final String ENHANCE_CLASS = "org.springframework.ldap.core.LdapTemplate"; + + private static final String OPERATION_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.SpringLdapOperationInterceptor"; + + private static final String SETTER_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapContextSourceSetterInterceptor"; + + private static final String CONSTRUCTOR_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapEndpointConstructorInterceptor"; + + @Override + protected ClassMatch enhanceClass() { + return byName(ENHANCE_CLASS); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return any(); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + } + }; + } + + @Override + public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { + return new InstanceMethodsInterceptV2Point[] { + new DeclaredInstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return isPublic().and(namedOneOf( + "search", "searchForObject", "searchForContext", + "find", "findAll", "findOne", "findByDn", + "lookup", "lookupContext", "list", "listBindings", + "bind", "rebind", "modifyAttributes", "rename", "unbind", + "authenticate", "create", "update", "delete", + "executeReadOnly", "executeReadWrite" + )); + } + + @Override + public String getMethodsInterceptorV2() { + return OPERATION_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + }, + new DeclaredInstanceMethodsInterceptV2Point() { + @Override + public ElementMatcher getMethodsMatcher() { + return named("setContextSource").and(takesArguments(1)); + } + + @Override + public String getMethodsInterceptorV2() { + return SETTER_INTERCEPTOR; + } + + @Override + public boolean isOverrideArgs() { + return false; + } + } + }; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/ObservationContextSourceInstrumentation.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/ObservationContextSourceInstrumentation.java new file mode 100644 index 0000000000..f115b9b962 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/spring/ldap/define/ObservationContextSourceInstrumentation.java @@ -0,0 +1,61 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap.define; + +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.v2.InstanceMethodsInterceptV2Point; +import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch; + +import static net.bytebuddy.matcher.ElementMatchers.any; +import static org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName; + +public class ObservationContextSourceInstrumentation extends AbstractSpringLdapInstrumentation { + + private static final String CONSTRUCTOR_INTERCEPTOR = + "org.apache.skywalking.apm.plugin.spring.ldap.LdapEndpointConstructorInterceptor"; + + @Override + protected ClassMatch enhanceClass() { + return byName(OBSERVATION_CONTEXT_SOURCE); + } + + @Override + public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { + return new ConstructorInterceptPoint[] { + new ConstructorInterceptPoint() { + @Override + public ElementMatcher getConstructorMatcher() { + return any(); + } + + @Override + public String getConstructorInterceptor() { + return CONSTRUCTOR_INTERCEPTOR; + } + } + }; + } + + @Override + public InstanceMethodsInterceptV2Point[] getInstanceMethodsInterceptV2Points() { + return new InstanceMethodsInterceptV2Point[0]; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/resources/skywalking-plugin.def b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/resources/skywalking-plugin.def new file mode 100644 index 0000000000..0a38c779db --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/main/resources/skywalking-plugin.def @@ -0,0 +1,20 @@ +# 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. + +spring-ldap-3.x-4.x=org.apache.skywalking.apm.plugin.spring.ldap.define.LdapTemplateInstrumentation +spring-ldap-3.x-4.x=org.apache.skywalking.apm.plugin.spring.ldap.define.DefaultLdapClientInstrumentation +spring-ldap-3.x-4.x=org.apache.skywalking.apm.plugin.spring.ldap.define.LdapClientSpecInstrumentation +spring-ldap-3.x-4.x=org.apache.skywalking.apm.plugin.spring.ldap.define.ObservationContextSourceInstrumentation diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolverTest.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolverTest.java new file mode 100644 index 0000000000..6553f58446 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/LdapEndpointResolverTest.java @@ -0,0 +1,152 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.util.Arrays; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.plugin.spring.ldap.fixture.PackagePrivateContextSources; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class LdapEndpointResolverTest { + + @Test + public void shouldResolveAndSanitizeLdapUrls() { + UrlContextSource contextSource = new UrlContextSource( + "ldap://admin:secret@ldap-one.example.org/dc=example,dc=org??sub?(uid=alice)", + "ldaps://ldap-two.example.org:1636/dc=example,dc=org", + "ldap://ldap-one.example.org:389/dc=another,dc=org" + ); + + SpringLdapEnhanceInfo info = LdapEndpointResolver.resolve(contextSource); + + assertThat(info.getPeer(), is("ldap-one.example.org:389,ldap-two.example.org:1636")); + } + + @Test + public void shouldUseLdapsDefaultPortAndFormatIpv6() { + assertThat( + LdapEndpointResolver.peerFromUrls(Arrays.asList("ldaps://[2001:db8::1]/dc=example,dc=org")), + is("[2001:db8::1]:636") + ); + } + + @Test + public void shouldUnwrapPublicContextSourceDelegates() { + ContextSourceWrapper wrapper = new ContextSourceWrapper( + new ContextSourceTarget(new UrlContextSource("ldap://wrapped-ldap:1389/dc=example,dc=org"))); + + assertThat(LdapEndpointResolver.resolve(wrapper).getPeer(), is("wrapped-ldap:1389")); + } + + @Test + public void shouldReadEndpointFromEnhancedObservationWrapper() { + EnhancedContextSource contextSource = new EnhancedContextSource(); + contextSource.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("observed-ldap:389")); + + assertThat(LdapEndpointResolver.resolve(contextSource).getPeer(), is("observed-ldap:389")); + } + + @Test + public void shouldReturnUnknownForUnsupportedOrMalformedUrls() { + UrlContextSource contextSource = new UrlContextSource( + "https://example.org/dc=example,dc=org", "not-an-ldap-url"); + + assertThat(LdapEndpointResolver.resolve(contextSource).getPeer(), is(SpringLdapConstants.UNKNOWN_PEER)); + } + + @Test + public void shouldIgnoreInvalidOrOutOfRangePorts() { + UrlContextSource contextSource = new UrlContextSource( + "ldap://host:notaport", + "ldap://host:0", + "ldap://host:-1", + "ldap://host:+389", + "ldap://host:65536", + "ldap://host:99999", + "ldap://valid-ldap:1389" + ); + + assertThat(LdapEndpointResolver.resolve(contextSource).getPeer(), is("valid-ldap:1389")); + } + + @Test + public void shouldResolvePeerFromPackagePrivateContextSource() { + assertThat( + LdapEndpointResolver.resolve(PackagePrivateContextSources.create()).getPeer(), + is("hidden-ldap:1389") + ); + } + + public static class UrlContextSource { + + private final String[] urls; + + public UrlContextSource(String... urls) { + this.urls = urls; + } + + public String[] getUrls() { + return urls; + } + } + + public static class ContextSourceWrapper { + + private final Object contextSource; + + public ContextSourceWrapper(Object contextSource) { + this.contextSource = contextSource; + } + + public Object getContextSource() { + return contextSource; + } + } + + public static class ContextSourceTarget { + + private final Object target; + + public ContextSourceTarget(Object target) { + this.target = target; + } + + public Object getTarget() { + return target; + } + } + + private static class EnhancedContextSource implements EnhancedInstance { + + private Object dynamicField; + + @Override + public Object getSkyWalkingDynamicField() { + return dynamicField; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + dynamicField = value; + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapInterceptorTest.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapInterceptorTest.java new file mode 100644 index 0000000000..0fbd98b794 --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/SpringLdapInterceptorTest.java @@ -0,0 +1,394 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap; + +import java.lang.reflect.Method; +import java.util.List; +import org.apache.skywalking.apm.agent.core.context.ContextManager; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan; +import org.apache.skywalking.apm.agent.core.context.trace.AbstractTracingSpan; +import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer; +import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment; +import org.apache.skywalking.apm.agent.core.context.util.TagValuePair; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance; +import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.v2.MethodInvocationContext; +import org.apache.skywalking.apm.agent.test.helper.SegmentHelper; +import org.apache.skywalking.apm.agent.test.helper.SpanHelper; +import org.apache.skywalking.apm.agent.test.tools.AgentServiceRule; +import org.apache.skywalking.apm.agent.test.tools.SegmentStorage; +import org.apache.skywalking.apm.agent.test.tools.SegmentStoragePoint; +import org.apache.skywalking.apm.agent.test.tools.SpanAssert; +import org.apache.skywalking.apm.agent.test.tools.TracingSegmentRunner; +import org.apache.skywalking.apm.network.trace.component.ComponentsDefine; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.ldap.core.AuthenticatedLdapEntryContextMapper; +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.query.LdapQuery; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@RunWith(TracingSegmentRunner.class) +public class SpringLdapInterceptorTest { + + @SegmentStoragePoint + private SegmentStorage segmentStorage; + + @Rule + public AgentServiceRule serviceRule = new AgentServiceRule(); + + private SpringLdapOperationInterceptor operationInterceptor; + + private TestEnhancedInstance ldapTemplate; + + private Method searchMethod; + + private Method primitiveAuthenticateMethod; + + private Method templateMapperAuthenticateMethod; + + private Method clientMapperExecuteMethod; + + private Method contextSourceSetterMethod; + + @Before + public void setUp() throws Exception { + SpringLdapPluginConfig.Plugin.SpringLDAP.COLLECT_EXCEPTION_DETAILS = false; + operationInterceptor = new SpringLdapOperationInterceptor(); + ldapTemplate = new TestEnhancedInstance(); + ldapTemplate.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("ldap-server:389")); + searchMethod = TestOperations.class.getMethod("search", String.class, String.class); + primitiveAuthenticateMethod = LdapTemplate.class.getMethod( + "authenticate", String.class, String.class, String.class); + templateMapperAuthenticateMethod = LdapTemplate.class.getMethod( + "authenticate", LdapQuery.class, String.class, AuthenticatedLdapEntryContextMapper.class); + clientMapperExecuteMethod = LdapClient.AuthenticateSpec.class.getMethod( + "execute", AuthenticatedLdapEntryContextMapper.class); + contextSourceSetterMethod = LdapTemplate.class.getMethod("setContextSource", ContextSource.class); + } + + @After + public void tearDown() { + SpringLdapPluginConfig.Plugin.SpringLDAP.COLLECT_EXCEPTION_DETAILS = false; + } + + @Test + public void shouldCreatePrivacySafeDatabaseExitSpan() throws Throwable { + Object[] arguments = {"ou=people", "(uid=alice)"}; + MethodInvocationContext context = new MethodInvocationContext(); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, arguments, null, context); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, arguments, null, null, context); + + AbstractTracingSpan span = onlySpan(); + assertThat(span.getOperationName(), is("SpringLDAP/search")); + assertThat(span.isExit(), is(true)); + assertThat(SpanHelper.getPeer(span), is("ldap-server:389")); + SpanAssert.assertComponent(span, ComponentsDefine.SPRING_LDAP); + SpanAssert.assertLayer(span, SpanLayer.DB); + SpanAssert.assertTagSize(span, 2); + assertTag(span, 0, "db.type", "LDAP"); + assertTag(span, 1, "ldap.operation", "search"); + } + + @Test + public void shouldMarkFalsePrimitiveAuthenticationAsError() throws Throwable { + TestEnhancedInstance authentication = new TestEnhancedInstance(); + authentication.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("ldap-server:389", "authenticate")); + MethodInvocationContext context = new MethodInvocationContext(); + Object[] arguments = {"ou=people", "(uid=alice)", "invalid-password"}; + + operationInterceptor.beforeMethod(authentication, primitiveAuthenticateMethod, arguments, null, context); + operationInterceptor.afterMethod(authentication, primitiveAuthenticateMethod, arguments, null, false, context); + + AbstractTracingSpan span = onlySpan(); + assertThat(span.getOperationName(), is("SpringLDAP/authenticate")); + SpanAssert.assertOccurException(span, true); + SpanAssert.assertLogSize(span, 0); + } + + @Test + public void shouldNotMarkSuccessfulTemplateMapperBooleanFalseAsError() throws Throwable { + TestEnhancedInstance authentication = new TestEnhancedInstance(); + authentication.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("ldap-server:389", "authenticate")); + MethodInvocationContext context = new MethodInvocationContext(); + Object[] arguments = {new Object(), "secret", new Object()}; + + operationInterceptor.beforeMethod(authentication, templateMapperAuthenticateMethod, arguments, null, context); + operationInterceptor.afterMethod( + authentication, templateMapperAuthenticateMethod, arguments, null, Boolean.FALSE, context); + + AbstractTracingSpan span = onlySpan(); + assertThat(span.getOperationName(), is("SpringLDAP/authenticate")); + SpanAssert.assertOccurException(span, false); + SpanAssert.assertLogSize(span, 0); + } + + @Test + public void shouldNotMarkSuccessfulClientMapperBooleanFalseAsError() throws Throwable { + TestEnhancedInstance authentication = new TestEnhancedInstance(); + authentication.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("ldap-server:389", "authenticate")); + MethodInvocationContext context = new MethodInvocationContext(); + Object[] arguments = {new Object()}; + + operationInterceptor.beforeMethod(authentication, clientMapperExecuteMethod, arguments, null, context); + operationInterceptor.afterMethod( + authentication, clientMapperExecuteMethod, arguments, null, Boolean.FALSE, context); + + AbstractTracingSpan span = onlySpan(); + assertThat(span.getOperationName(), is("SpringLDAP/authenticate")); + SpanAssert.assertOccurException(span, false); + SpanAssert.assertLogSize(span, 0); + } + + @Test + public void shouldSuppressDelegatingOverloadSpansForSameTemplate() throws Throwable { + MethodInvocationContext outerContext = new MethodInvocationContext(); + MethodInvocationContext innerContext = new MethodInvocationContext(); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, outerContext); + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, innerContext); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, innerContext); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, outerContext); + + assertThat(spans().size(), is(1)); + } + + @Test + public void shouldNotMutateActiveExitSpanForNestedLdapOperations() throws Throwable { + TestEnhancedInstance nestedClient = new TestEnhancedInstance(); + nestedClient.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("second-ldap:636", "lookup")); + MethodInvocationContext outerContext = new MethodInvocationContext(); + MethodInvocationContext nestedContext = new MethodInvocationContext(); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, outerContext); + operationInterceptor.beforeMethod(nestedClient, searchMethod, new Object[0], null, nestedContext); + operationInterceptor.afterMethod(nestedClient, searchMethod, new Object[0], null, null, nestedContext); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, outerContext); + + AbstractTracingSpan span = onlySpan(); + assertThat(span.getOperationName(), is("SpringLDAP/search")); + assertThat(SpanHelper.getPeer(span), is("ldap-server:389")); + SpanAssert.assertTagSize(span, 2); + } + + @Test + public void shouldMarkExceptionWithoutCollectingDetailsByDefault() throws Throwable { + MethodInvocationContext context = new MethodInvocationContext(); + IllegalStateException exception = new IllegalStateException( + "[LDAP: error code 32 - No Such Object]; remaining name 'uid=alice,ou=people'"); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, context); + operationInterceptor.handleMethodException(ldapTemplate, searchMethod, new Object[0], null, exception, context); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, context); + + AbstractTracingSpan span = onlySpan(); + SpanAssert.assertOccurException(span, true); + SpanAssert.assertLogSize(span, 0); + } + + @Test + public void shouldCollectExceptionDetailsWhenExplicitlyEnabled() throws Throwable { + SpringLdapPluginConfig.Plugin.SpringLDAP.COLLECT_EXCEPTION_DETAILS = true; + MethodInvocationContext context = new MethodInvocationContext(); + IllegalStateException exception = new IllegalStateException( + "[LDAP: error code 32 - No Such Object]; remaining name 'uid=alice,ou=people'"); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, context); + operationInterceptor.handleMethodException(ldapTemplate, searchMethod, new Object[0], null, exception, context); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, context); + + AbstractTracingSpan span = onlySpan(); + SpanAssert.assertOccurException(span, true); + SpanAssert.assertLogSize(span, 1); + SpanAssert.assertException( + SpanHelper.getLogs(span).get(0), + IllegalStateException.class, + "[LDAP: error code 32 - No Such Object]; remaining name 'uid=alice,ou=people'"); + } + + @Test + public void shouldRecordExceptionOnLdapSpanWhenCallbackSpanIsActive() throws Throwable { + MethodInvocationContext context = new MethodInvocationContext(); + IllegalStateException exception = new IllegalStateException( + "[LDAP: error code 32 - No Such Object]; remaining name 'uid=alice,ou=people'"); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, context); + AbstractSpan callbackSpan = ContextManager.createLocalSpan("ldap-callback"); + operationInterceptor.handleMethodException( + ldapTemplate, searchMethod, new Object[0], null, exception, context); + ContextManager.stopSpan(callbackSpan); + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, context); + + AbstractTracingSpan ldapSpan = span("SpringLDAP/search"); + AbstractTracingSpan nestedSpan = span("ldap-callback"); + SpanAssert.assertOccurException(ldapSpan, true); + SpanAssert.assertLogSize(ldapSpan, 0); + SpanAssert.assertOccurException(nestedSpan, false); + SpanAssert.assertLogSize(nestedSpan, 0); + } + + @Test + public void shouldNotStopUnexpectedActiveSpan() throws Throwable { + MethodInvocationContext context = new MethodInvocationContext(); + + operationInterceptor.beforeMethod(ldapTemplate, searchMethod, new Object[0], null, context); + AbstractSpan ldapSpan = ContextManager.activeSpan(); + AbstractSpan callbackSpan = ContextManager.createLocalSpan("ldap-callback"); + boolean unexpectedSpanDetected = false; + try { + operationInterceptor.afterMethod(ldapTemplate, searchMethod, new Object[0], null, null, context); + } catch (IllegalStateException expected) { + unexpectedSpanDetected = true; + assertThat(ContextManager.activeSpan() == callbackSpan, is(true)); + } finally { + if (ContextManager.isActive() && ContextManager.activeSpan() == callbackSpan) { + ContextManager.stopSpan(callbackSpan); + } + if (ContextManager.isActive() && ContextManager.activeSpan() == ldapSpan) { + ContextManager.stopSpan(ldapSpan); + } + } + + assertThat(unexpectedSpanDetected, is(true)); + assertThat(spans().size(), is(2)); + } + + @Test + public void shouldSkipInternalClientSpecWithoutPropagationInfo() throws Throwable { + TestEnhancedInstance internalSearchSpec = new TestEnhancedInstance(); + MethodInvocationContext context = new MethodInvocationContext(); + + operationInterceptor.beforeMethod(internalSearchSpec, searchMethod, new Object[0], null, context); + operationInterceptor.afterMethod(internalSearchSpec, searchMethod, new Object[0], null, null, context); + + assertThat(segmentStorage.getTraceSegments().size(), is(0)); + } + + @Test + public void shouldPropagateEndpointAndOperationToClientSpecs() throws Throwable { + TestEnhancedInstance client = new TestEnhancedInstance(); + TestEnhancedInstance searchSpec = new TestEnhancedInstance(); + TestEnhancedInstance mappedSpec = new TestEnhancedInstance(); + client.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("client-ldap:389")); + Method factoryMethod = TestOperations.class.getMethod("search"); + LdapClientSpecFactoryInterceptor factoryInterceptor = new LdapClientSpecFactoryInterceptor(); + LdapClientSpecPropagationInterceptor propagationInterceptor = new LdapClientSpecPropagationInterceptor(); + + factoryInterceptor.afterMethod( + client, factoryMethod, new Object[0], null, searchSpec, new MethodInvocationContext()); + propagationInterceptor.afterMethod( + searchSpec, factoryMethod, new Object[0], null, mappedSpec, new MethodInvocationContext()); + + SpringLdapEnhanceInfo info = (SpringLdapEnhanceInfo) mappedSpec.getSkyWalkingDynamicField(); + assertThat(info.getPeer(), is("client-ldap:389")); + assertThat(info.getOperation(), is("search")); + } + + @Test + public void shouldRefreshEndpointWhenContextSourceChanges() throws Throwable { + LdapEndpointResolverTest.UrlContextSource contextSource = + new LdapEndpointResolverTest.UrlContextSource("ldaps://replacement-ldap/dc=example,dc=org"); + LdapEndpointConstructorInterceptor constructorInterceptor = new LdapEndpointConstructorInterceptor(); + LdapContextSourceSetterInterceptor setterInterceptor = new LdapContextSourceSetterInterceptor(); + constructorInterceptor.onConstruct(ldapTemplate, new Object[0]); + + setterInterceptor.afterMethod( + ldapTemplate, contextSourceSetterMethod, new Object[] {contextSource}, null, null, + new MethodInvocationContext()); + + SpringLdapEnhanceInfo info = (SpringLdapEnhanceInfo) ldapTemplate.getSkyWalkingDynamicField(); + assertThat(info.getPeer(), is("replacement-ldap:636")); + } + + @Test + public void shouldPreservePeerWhenContextSourceSetterFails() throws Throwable { + ldapTemplate.setSkyWalkingDynamicField(new SpringLdapEnhanceInfo("ldap-server:389")); + LdapContextSourceSetterInterceptor setterInterceptor = new LdapContextSourceSetterInterceptor(); + MethodInvocationContext context = new MethodInvocationContext(); + Object[] arguments = {null}; + + setterInterceptor.beforeMethod(ldapTemplate, contextSourceSetterMethod, arguments, null, context); + setterInterceptor.handleMethodException( + ldapTemplate, contextSourceSetterMethod, arguments, null, + new IllegalArgumentException("contextSource must not be null"), context); + setterInterceptor.afterMethod(ldapTemplate, contextSourceSetterMethod, arguments, null, null, context); + + SpringLdapEnhanceInfo info = (SpringLdapEnhanceInfo) ldapTemplate.getSkyWalkingDynamicField(); + assertThat(info.getPeer(), is("ldap-server:389")); + } + + private AbstractTracingSpan onlySpan() { + List spans = spans(); + assertThat(spans.size(), is(1)); + return spans.get(0); + } + + private List spans() { + assertThat(segmentStorage.getTraceSegments().size(), is(1)); + TraceSegment segment = segmentStorage.getTraceSegments().get(0); + return SegmentHelper.getSpans(segment); + } + + private AbstractTracingSpan span(String operationName) { + for (AbstractTracingSpan span : spans()) { + if (operationName.equals(span.getOperationName())) { + return span; + } + } + throw new AssertionError("Span not found: " + operationName); + } + + private static void assertTag(AbstractTracingSpan span, int index, String key, String value) { + TagValuePair tag = SpanHelper.getTags(span).get(index); + assertThat(tag.getKey().key(), is(key)); + assertThat(tag.getValue(), is(value)); + } + + public static class TestOperations { + + public void search(String base, String filter) { + } + + public Object search() { + return null; + } + } + + private static class TestEnhancedInstance implements EnhancedInstance { + + private Object dynamicField; + + @Override + public Object getSkyWalkingDynamicField() { + return dynamicField; + } + + @Override + public void setSkyWalkingDynamicField(Object value) { + dynamicField = value; + } + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSource.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSource.java new file mode 100644 index 0000000000..54928c86ac --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSource.java @@ -0,0 +1,26 @@ +/* + * 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.skywalking.apm.plugin.spring.ldap.fixture; + +class PackagePrivateContextSource { + + public String[] getUrls() { + return new String[] {"ldap://hidden-ldap:1389"}; + } +} diff --git a/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSources.java b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSources.java new file mode 100644 index 0000000000..75cd90d66c --- /dev/null +++ b/apm-sniffer/apm-sdk-plugin/spring-plugins/spring-ldap-3.x-4.x-plugin/src/test/java/org/apache/skywalking/apm/plugin/spring/ldap/fixture/PackagePrivateContextSources.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.skywalking.apm.plugin.spring.ldap.fixture; + +public final class PackagePrivateContextSources { + + private PackagePrivateContextSources() { + } + + public static Object create() { + return new PackagePrivateContextSource(); + } +} diff --git a/apm-sniffer/config/agent.config b/apm-sniffer/config/agent.config index b9101f1f1c..09534a06a9 100755 --- a/apm-sniffer/config/agent.config +++ b/apm-sniffer/config/agent.config @@ -370,4 +370,7 @@ plugin.springai.collect_retrieval_query=${SW_PLUGIN_SPRINGAI_COLLECT_RETRIEVAL_Q # Use a negative value to represent no limit, but be aware this could cause OOM. plugin.springai.retrieval_query_length_limit=${SW_PLUGIN_SPRINGAI_RETRIEVAL_QUERY_LENGTH_LIMIT:1024} # Whether to collect the documents of the rag call. -plugin.springai.collect_retrieval_documents=${SW_PLUGIN_SPRINGAI_COLLECT_RETRIEVAL_DOCUMENTS:false} \ No newline at end of file +plugin.springai.collect_retrieval_documents=${SW_PLUGIN_SPRINGAI_COLLECT_RETRIEVAL_DOCUMENTS:false} +# Whether to collect Spring LDAP exception messages and stack traces on spans. +# Enabling this may expose sensitive LDAP data, including DNs, filters, attributes, and credentials. +plugin.springldap.collect_exception_details=${SW_PLUGIN_SPRINGLDAP_COLLECT_EXCEPTION_DETAILS:false} diff --git a/docs/en/setup/service-agent/java-agent/Plugin-list.md b/docs/en/setup/service-agent/java-agent/Plugin-list.md index ac9ea19eeb..7e325e9edb 100644 --- a/docs/en/setup/service-agent/java-agent/Plugin-list.md +++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md @@ -118,6 +118,7 @@ - spring-core-patch - spring-kafka-1.x - spring-kafka-2.x +- spring-ldap-3.x-4.x - spring-rabbitmq - spring-mvc-annotation - spring-mvc-annotation-3.x diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md b/docs/en/setup/service-agent/java-agent/Supported-list.md index aff0f0d077..d686c4f901 100644 --- a/docs/en/setup/service-agent/java-agent/Supported-list.md +++ b/docs/en/setup/service-agent/java-agent/Supported-list.md @@ -119,6 +119,7 @@ metrics based on the tracing data. * Spring Bean annotations(@Bean, @Service, @Component, @Repository) 3.x and 4.x (Optional²) * Spring Core Async SuccessCallback/FailureCallback/ListenableFutureCallback 4.x * Spring Transaction 4.x and 5.x (Optional²) + * [Spring LDAP](https://github.com/spring-projects/spring-ldap) 3.3.x -> 4.x (non-streaming synchronous `LdapTemplate` and `LdapClient` operations) * [Hystrix: Latency and Fault Tolerance for Distributed Systems](https://github.com/Netflix/Hystrix) 1.4.20 -> 1.5.18 * [Sentinel: The Sentinel of Your Microservices](https://github.com/alibaba/Sentinel) 1.7.0 -> 1.8.1 * Scheduler diff --git a/docs/en/setup/service-agent/java-agent/configurations.md b/docs/en/setup/service-agent/java-agent/configurations.md index ddc14a145e..d6c4fbb3a0 100644 --- a/docs/en/setup/service-agent/java-agent/configurations.md +++ b/docs/en/setup/service-agent/java-agent/configurations.md @@ -138,6 +138,7 @@ This is the properties list supported in `agent/config/agent.config`. | `plugin.solon.http_body_length_threshold` | Define the max length of collected HTTP body. The default value(=0) means not collecting. | SW_PLUGIN_SOLON_HTTP_BODY_LENGTH_THRESHOLD | `0` | | `plugin.caffeine.operation_mapping_write` | Specify which command should be converted to `write` operation | SW_PLUGIN_EHCACHE_OPERATION_MAPPING_WRITE | `put,putAll,remove,clear` | | `plugin.caffeine.operation_mapping_read` | Specify which command should be converted to `read` operation | SW_PLUGIN_EHCACHE_OPERATION_MAPPING_READ | `getIfPresent,getAllPresent,computeIfAbsent` | +| `plugin.springldap.collect_exception_details` | Whether to collect Spring LDAP exception messages and stack traces on spans. Enabling this may expose sensitive LDAP data, including DNs, filters, attributes, and credentials. | SW_PLUGIN_SPRINGLDAP_COLLECT_EXCEPTION_DETAILS | `false` | # Reset Collection/Map type configurations as empty collection. diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/bin/startup.sh b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/bin/startup.sh new file mode 100644 index 0000000000..b01feec79f --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/bin/startup.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# 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. + +home="$(cd "$(dirname "$0")"; pwd)" + +java -jar ${agent_opts} "${home}/../libs/spring-ldap-3.x-4.x-scenario.jar" & diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/config/expectedData.yaml b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/config/expectedData.yaml new file mode 100644 index 0000000000..fff1324e9d --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/config/expectedData.yaml @@ -0,0 +1,342 @@ +# 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. + +segmentItems: + - serviceName: spring-ldap-3.x-4.x-scenario + segmentSize: gt 0 + segments: + - segmentId: not null + spans: + - operationName: GET:/spring-ldap-3.x-4.x-scenario/case/spring-ldap + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: nq 0 + endTime: nq 0 + componentId: 84 + isError: false + spanType: Entry + peer: '' + skipAnalysis: false + tags: + - key: url + value: http://localhost:8080/spring-ldap-3.x-4.x-scenario/case/spring-ldap + - { key: http.method, value: GET } + - { key: http.status_code, value: '200' } + logs: [] + - segmentId: not null + spans: + - operationName: SpringLDAP/bind + parentSpanId: 0 + spanId: 1 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: bind } + logs: [] + - operationName: SpringLDAP/bind + parentSpanId: 0 + spanId: 2 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: bind } + logs: [] + - operationName: SpringLDAP/search + parentSpanId: 0 + spanId: 3 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: search } + logs: [] + - operationName: SpringLDAP/lookup + parentSpanId: 0 + spanId: 4 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: lookup } + logs: [] + - operationName: SpringLDAP/authenticate + parentSpanId: 0 + spanId: 5 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: authenticate } + logs: [] + - operationName: SpringLDAP/authenticate + parentSpanId: 0 + spanId: 6 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: true + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: authenticate } + logs: [] + - operationName: SpringLDAP/modifyAttributes + parentSpanId: 0 + spanId: 7 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: modifyAttributes } + logs: [] + - operationName: SpringLDAP/rename + parentSpanId: 0 + spanId: 8 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: rename } + logs: [] + - operationName: SpringLDAP/list + parentSpanId: 0 + spanId: 9 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: list } + logs: [] + - operationName: SpringLDAP/listBindings + parentSpanId: 0 + spanId: 10 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: listBindings } + logs: [] + - operationName: SpringLDAP/search + parentSpanId: 0 + spanId: 11 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: search } + logs: [] + - operationName: SpringLDAP/authenticate + parentSpanId: 0 + spanId: 12 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: authenticate } + logs: [] + - operationName: SpringLDAP/bind + parentSpanId: 0 + spanId: 13 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: bind } + logs: [] + - operationName: SpringLDAP/modify + parentSpanId: 0 + spanId: 14 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: modify } + logs: [] + - operationName: SpringLDAP/unbind + parentSpanId: 0 + spanId: 15 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: unbind } + logs: [] + - operationName: SpringLDAP/authenticate + parentSpanId: 0 + spanId: 16 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: authenticate } + logs: [] + - operationName: SpringLDAP/authenticate + parentSpanId: 0 + spanId: 17 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: authenticate } + logs: [] + - operationName: SpringLDAP/unbind + parentSpanId: 0 + spanId: 18 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: unbind } + logs: [] + - operationName: SpringLDAP/unbind + parentSpanId: 0 + spanId: 19 + spanLayer: Database + startTime: nq 0 + endTime: nq 0 + componentId: 179 + isError: false + spanType: Exit + peer: openldap:389 + skipAnalysis: false + tags: + - { key: db.type, value: LDAP } + - { key: ldap.operation, value: unbind } + logs: [] + - operationName: UndertowDispatch + parentSpanId: -1 + spanId: 0 + spanLayer: Unknown + startTime: nq 0 + endTime: nq 0 + componentId: 84 + isError: false + spanType: Local + peer: '' + skipAnalysis: false + refs: + - { parentEndpoint: GET:/spring-ldap-3.x-4.x-scenario/case/spring-ldap, + networkAddress: '', refType: CrossThread, parentSpanId: 0, + parentTraceSegmentId: not null, parentServiceInstance: not null, + parentService: spring-ldap-3.x-4.x-scenario, traceId: not null } diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/configuration.yml b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/configuration.yml new file mode 100644 index 0000000000..3e744505cc --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/configuration.yml @@ -0,0 +1,33 @@ +# 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. + +type: jvm +entryService: http://localhost:8080/spring-ldap-3.x-4.x-scenario/case/spring-ldap +healthCheck: http://localhost:8080/spring-ldap-3.x-4.x-scenario/case/healthCheck +startScript: ./bin/startup.sh +depends_on: + - openldap +dependencies: + openldap: + image: osixia/openldap:1.5.0 + hostname: openldap + expose: + - "389" + environment: + - LDAP_ORGANISATION=SkyWalking + - LDAP_DOMAIN=example.org + - LDAP_ADMIN_PASSWORD=admin + - LDAP_TLS=false diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/pom.xml b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/pom.xml new file mode 100644 index 0000000000..a944a9d1bb --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + org.apache.skywalking + spring-ldap-3.x-4.x-scenario + 1.0.0 + + + UTF-8 + 17 + 3.13.0 + 3.7.1 + org.apache.skywalking.apm.testcase.spring.ldap.Application + + spring-ldap + 3.3.8 + 3.5.7 + 2.3.25.Final + + + spring-ldap-3.x-4.x-scenario + + + + org.springframework.ldap + spring-ldap-core + ${test.framework.version} + + + io.undertow + undertow-core + ${undertow.version} + + + + + spring-ldap-3.x-4.x-scenario + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.release} + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + assemble + package + + single + + + + src/main/assembly/assembly.xml + + false + ./target/ + + + + + + + diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/assembly/assembly.xml b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..72485f3f34 --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/assembly/assembly.xml @@ -0,0 +1,41 @@ + + + + bin + + zip + + + + + ./bin + 0775 + + + + + + ./target/spring-ldap-3.x-4.x-scenario.jar + ./libs + 0775 + + + diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/spring/ldap/Application.java b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/spring/ldap/Application.java new file mode 100644 index 0000000000..402daa864a --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/spring/ldap/Application.java @@ -0,0 +1,291 @@ +/* + * 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.skywalking.apm.testcase.spring.ldap; + +import io.micrometer.observation.ObservationRegistry; +import io.undertow.Undertow; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.NameClassPairMapper; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.core.support.ObservationContextSource; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapUtils; + +public final class Application { + + private static final String CASE_PATH = "/spring-ldap-3.x-4.x-scenario/case/spring-ldap"; + + private static final String HEALTH_PATH = "/spring-ldap-3.x-4.x-scenario/case/healthCheck"; + + private static final String ORGANIZATION_UNIT_DN = "ou=skywalking"; + + private static final String USER_DN = "cn=alice," + ORGANIZATION_UNIT_DN; + + private static final String RENAMED_USER_DN = "cn=alice-renamed," + ORGANIZATION_UNIT_DN; + + private static final String TEMP_USER_DN = "cn=temp," + ORGANIZATION_UNIT_DN; + + private static final String USER_PASSWORD = "secret"; + + private final LdapContextSource contextSource; + + private final LdapTemplate ldapTemplate; + + private final LdapClient ldapClient; + + private Application(final LdapContextSource contextSource) { + this.contextSource = contextSource; + this.ldapTemplate = new LdapTemplate(contextSource); + final ObservationContextSource observationContextSource = + new ObservationContextSource(contextSource, ObservationRegistry.NOOP); + this.ldapClient = LdapClient.create(observationContextSource); + } + + public static void main(String[] args) throws Exception { + final LdapContextSource contextSource = createContextSource(); + waitForLdap(contextSource); + final Application application = new Application(contextSource); + + final Undertow server = Undertow.builder() + .addHttpListener(8080, "0.0.0.0") + .setHandler(application::handleRequest) + .build(); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + server.start(); + } + + private static LdapContextSource createContextSource() throws Exception { + LdapContextSource source = new LdapContextSource(); + source.setUrl("ldap://openldap:389"); + source.setBase("dc=example,dc=org"); + source.setUserDn("cn=admin,dc=example,dc=org"); + source.setPassword("admin"); + source.afterPropertiesSet(); + return source; + } + + private static void waitForLdap(ContextSource source) throws InterruptedException { + RuntimeException lastFailure = null; + for (int attempt = 0; attempt < 60; attempt++) { + DirContext context = null; + try { + context = source.getReadOnlyContext(); + return; + } catch (RuntimeException failure) { + lastFailure = failure; + Thread.sleep(500L); + } finally { + LdapUtils.closeContext(context); + } + } + throw new IllegalStateException("OpenLDAP did not become ready", lastFailure); + } + + private void handleRequest(HttpServerExchange exchange) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); + if (HEALTH_PATH.equals(exchange.getRequestPath())) { + exchange.getResponseSender().send("Success"); + return; + } + if (!CASE_PATH.equals(exchange.getRequestPath())) { + exchange.setStatusCode(StatusCodes.NOT_FOUND); + exchange.getResponseSender().send("Not Found"); + return; + } + + exchange.dispatch(() -> { + try { + runScenario(); + exchange.getResponseSender().send("Success"); + } catch (Throwable failure) { + failure.printStackTrace(); + cleanupRaw(); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + exchange.getResponseSender().send("Failure"); + } + }); + } + + private synchronized void runScenario() throws Throwable { + cleanupRaw(); + boolean completed = false; + try { + ldapTemplate.bind(ORGANIZATION_UNIT_DN, null, organizationUnitAttributes()); + ldapTemplate.bind(USER_DN, null, personAttributes("alice", "SkyWalking", "alice")); + + LdapQuery aliceQuery = aliceQuery(); + List templateSearch = ldapTemplate.search(aliceQuery, uidMapper()); + require(templateSearch.size() == 1 && "alice".equals(templateSearch.get(0)), + "LdapTemplate search returned an unexpected result"); + + String lookedUpUid = ldapTemplate.lookup(USER_DN, uidMapper()); + require("alice".equals(lookedUpUid), "LdapTemplate lookup returned an unexpected result"); + + ldapTemplate.authenticate(aliceQuery, USER_PASSWORD); + boolean invalidCredentialsAccepted = ldapTemplate.authenticate( + ORGANIZATION_UNIT_DN, "(uid=alice)", "invalid-password"); + require(!invalidCredentialsAccepted, "LdapTemplate accepted invalid credentials"); + ldapTemplate.modifyAttributes(USER_DN, new ModificationItem[] { + replaceAttribute("sn", "SkyWalkingUpdated") + }); + ldapTemplate.rename(USER_DN, RENAMED_USER_DN); + + List children = ldapClient.list(ORGANIZATION_UNIT_DN).toList(NameClassPair::getName); + require(children.contains("cn=alice-renamed"), "LdapClient list did not return the renamed entry"); + + List bindings = ldapClient.listBindings(ORGANIZATION_UNIT_DN) + .toList((NameClassPairMapper) NameClassPair::getName); + require(bindings.contains("cn=alice-renamed"), + "LdapClient listBindings did not return the renamed entry"); + + List clientSearch = searchWithClient(aliceQuery); + require(clientSearch.size() == 1 && "alice".equals(clientSearch.get(0)), + "LdapClient search returned an unexpected result"); + + ldapClient.authenticate().query(aliceQuery).password(USER_PASSWORD).execute(); + ldapClient.bind(TEMP_USER_DN).attributes(personAttributes("temp", "Temporary", "temp")).execute(); + ldapClient.modify(TEMP_USER_DN).attributes(replaceAttribute("sn", "TemporaryUpdated")).execute(); + ldapClient.unbind(TEMP_USER_DN).execute(); + + Boolean templateMapperResult = ldapTemplate.authenticate( + aliceQuery, USER_PASSWORD, (context, identification) -> Boolean.FALSE); + require(Boolean.FALSE.equals(templateMapperResult), + "LdapTemplate authentication mapper returned an unexpected result"); + + Boolean clientMapperResult = ldapClient.authenticate() + .query(aliceQuery) + .password(USER_PASSWORD) + .execute((context, identification) -> Boolean.FALSE); + require(Boolean.FALSE.equals(clientMapperResult), + "LdapClient authentication mapper returned an unexpected result"); + + ldapTemplate.unbind(RENAMED_USER_DN); + ldapTemplate.unbind(ORGANIZATION_UNIT_DN); + completed = true; + } finally { + if (!completed) { + cleanupRaw(); + } + } + } + + @SuppressWarnings("unchecked") + private List searchWithClient(LdapQuery query) throws Throwable { + LdapClient.SearchSpec search = ldapClient.search().query(query); + AttributesMapper mapper = uidMapper(); + try { + Method map = LdapClient.SearchSpec.class.getMethod("map", AttributesMapper.class); + Object mappedSearch = map.invoke(search, mapper); + Class mappedSearchSpec = Class.forName("org.springframework.ldap.core.LdapClient$MappedSearchSpec"); + Method list = mappedSearchSpec.getMethod("list"); + return (List) list.invoke(mappedSearch); + } catch (NoSuchMethodException ignored) { + return search.toList(mapper); + } catch (InvocationTargetException failure) { + throw failure.getCause(); + } + } + + private static LdapQuery aliceQuery() { + return LdapQueryBuilder.query().base(ORGANIZATION_UNIT_DN).where("uid").is("alice"); + } + + private static AttributesMapper uidMapper() { + return attributes -> String.valueOf(attributes.get("uid").get()); + } + + private static Attributes organizationUnitAttributes() { + BasicAttributes attributes = new BasicAttributes(true); + Attribute objectClass = new BasicAttribute("objectClass"); + objectClass.add("top"); + objectClass.add("organizationalUnit"); + attributes.put(objectClass); + attributes.put("ou", "skywalking"); + return attributes; + } + + private static Attributes personAttributes(String commonName, String surname, String uid) { + BasicAttributes attributes = new BasicAttributes(true); + Attribute objectClass = new BasicAttribute("objectClass"); + objectClass.add("top"); + objectClass.add("person"); + objectClass.add("organizationalPerson"); + objectClass.add("inetOrgPerson"); + attributes.put(objectClass); + attributes.put("cn", commonName); + attributes.put("sn", surname); + attributes.put("uid", uid); + attributes.put("userPassword", USER_PASSWORD); + return attributes; + } + + private static ModificationItem replaceAttribute(String name, String value) { + return new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(name, value)); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } + + private void cleanupRaw() { + DirContext context = null; + try { + context = contextSource.getReadWriteContext(); + unbindQuietly(context, TEMP_USER_DN); + unbindQuietly(context, RENAMED_USER_DN); + unbindQuietly(context, USER_DN); + unbindQuietly(context, ORGANIZATION_UNIT_DN); + } catch (RuntimeException failure) { + failure.printStackTrace(); + } finally { + LdapUtils.closeContext(context); + } + } + + private static void unbindQuietly(DirContext context, String name) { + try { + context.unbind(name); + } catch (NameNotFoundException ignored) { + // The test entry is already absent. + } catch (NamingException failure) { + throw new IllegalStateException("Failed to remove test LDAP entry", failure); + } + } +} diff --git a/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/support-version.list b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/support-version.list new file mode 100644 index 0000000000..fe174ee65d --- /dev/null +++ b/test/plugin/scenarios/spring-ldap-3.x-4.x-scenario/support-version.list @@ -0,0 +1,19 @@ +# 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. + +3.3.8 +4.0.4 +4.1.0