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