Repository: tapdata/tapdata (language: python)

Issue:
fix:TAP-4711 User settings cannot set tags created by other users



Discussion / hints:

Fix patch:
diff --git a/RELEASE b/RELEASE
index 8139c6f18e..4715efa8ec 100644
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-LST: v3.12.0
-DEV: v3.13.0-dev
\ No newline at end of file
+LST: v3.13.0
+DEV: v3.14.0-dev
\ No newline at end of file
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
index bbc16d7339..f488295017 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
@@ -874,7 +874,8 @@ protected void handleTapdataAdjustMemoryEvent(TapdataAdjustMemoryEvent tapdataEv
 						this.tapEventProcessQueue = new LinkedBlockingQueue<>(newQueueSize);
 						this.queueConsumerThreadPool.shutdownNow();
 						if(this.queueConsumerThreadPool.isShutdown()){
-							initQueueConsumerThreadPool();
+							ThreadGroup connectorOnTaskThreadGroup = Thread.currentThread().getThreadGroup();
+							queueConsumerThreadPool = AsyncUtils.createThreadPoolExecutor(String.format("Target-Queue-Consumer-%s[%s]@task-%s", getNode().getName(), getNode().getId(), dataProcessorContext.getTaskDto().getName()), 2, connectorOnTaskThreadGroup, TAG);
 							initTargetQueueConsumer();
 						}
 						obsLogger.info("{}Target queue size adjusted, old size: {}, new size: {}", DynamicAdjustMemoryConstant.LOG_PREFIX, this.writeQueueCapacity, newQueueSize);
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/DownLoadConnectorHandler.java b/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/DownLoadConnectorHandler.java
index d29b1f2190..d58faf9b88 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/DownLoadConnectorHandler.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/DownLoadConnectorHandler.java
@@ -1,5 +1,6 @@
 package io.tapdata.websocket.handler;
 
+import com.alibaba.fastjson.JSON;
 import com.tapdata.constant.ConnectionUtil;
 import com.tapdata.constant.ConnectorConstant;
 import com.tapdata.entity.DatabaseTypeEnum;
@@ -44,7 +45,7 @@ public void initialize(ClientMongoOperator clientMongoOperator, SettingService s
 
     @Override
     public Object handle(Map event, SendMessage sendMessage) {
-        logger.info(String.format("downLoad connector, event: %s", event));
+        logger.debug(() -> String.format("downLoad connector, event: %s", event));
         String pskHash = (String) event.getOrDefault("pdkHash", "");
         String connName = (String) event.getOrDefault("name", "");
         String connectionId = String.valueOf(event.get("id"));
@@ -58,6 +59,8 @@ public Object handle(Map event, SendMessage sendMessage) {
                 .pdkHash(pskHash)
                 .schemaVersion(String.valueOf(event.get("schemaVersion")))
                 .databaseType(String.valueOf(event.get("database_type")));
+        logger.info(() -> String.format("downLoad connector, entity: %s", JSON.toJSONString(entity)));
+
         String threadName = String.format("DOWNLOAD-CONNECTOR-%s", Optional.ofNullable(event.get("name")).orElse(""));
         DisposableThreadGroup threadGroup = new DisposableThreadGroup(DisposableType.DOWNLOAD_CONNECTOR, threadName);
         DatabaseTypeEnum.DatabaseType databaseDefinition = ConnectionUtil.getDatabaseType(clientMongoOperator, pskHash);
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/TestConnectionHandler.java b/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/TestConnectionHandler.java
index 32c148f78c..bdec4db999 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/TestConnectionHandler.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/websocket/handler/TestConnectionHandler.java
@@ -1,5 +1,6 @@
 package io.tapdata.websocket.handler;
 
+import com.alibaba.fastjson.JSON;
 import com.google.common.collect.Maps;
 import com.tapdata.constant.ConnectionUtil;
 import com.tapdata.constant.ConnectorConstant;
@@ -97,10 +98,10 @@ public void initialize(ClientMongoOperator clientMongoOperator, SettingService s
 	@Override
 	public Object handle(Map eventData, SendMessage sendMessage) {
 		Map<String, Object> event = (Map<String, Object>) eventData;
-		getLogger().info("Test connection, event: {}", event);
 		if (MapUtils.isEmpty(event)) {
 			return WebSocketEventResult.handleFailed(WebSocketEventResult.Type.TEST_CONNECTION_RESULT, "Event data cannot be empty");
 		}
+        getLogger().debug(() -> String.format("Test connection, event: %s", event));
 
 		Connections connection = parseConnection(event);
 		String connectionId = ConnectionUpdateOperation.ID.getString(event);
@@ -117,6 +118,8 @@ public Object handle(Map eventData, SendMessage sendMessage) {
 			.pdkHash(pdkHash)
 			.schemaVersion(ConnectionUpdateOperation.SCHEMA_VERSION.getString(event))
 			.databaseType(ConnectionUpdateOperation.DATABASE_TYPE.getString(event));
+        getLogger().info(() -> String.format("Test connection '%s', entity: %s", event.get("connectionUrl"), JSON.toJSONString(entity)));
+
 		String threadName = String.format("TEST-CONNECTION-%s", connName);
 		DisposableThreadGroup threadGroup = new DisposableThreadGroup(DisposableType.CONNECTION_TEST, threadName);
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/CategoryEnum.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/CategoryEnum.java
index 8b8b79faf0..c87af6aeff 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/CategoryEnum.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/CategoryEnum.java
@@ -6,7 +6,8 @@ public enum CategoryEnum {
     SMTP("SMTP"),
     FRONTEND("Frontend"),
     JOB("Job"),
-    WORKER("Worker")
+    WORKER("Worker"),
+    LDAP("LDAP")
     ;
 
     private String value;
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/KeyEnum.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/KeyEnum.java
index 7ce0126313..fcea7e1cf4 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/KeyEnum.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/KeyEnum.java
@@ -12,7 +12,9 @@ public enum KeyEnum {
     BUILD_PROFILE("buildProfile"),
     SHARE_AGENT_CREATE_USER("shareAgentCreateUser"),
     SUPPORT_CDC_CONNECTION("supportCdcConnection"),
-    SHARE_AGENT_EXPRIRE_DAYS("shareAgentExprireDays")
+    SHARE_AGENT_EXPRIRE_DAYS("shareAgentExprireDays"),
+    LDAP_LOGIN_ENABLE("ldap.login.enable"),
+    LDAP_SSL_ENABLE("ldap.ssl.enable")
     ;
 
     private String value;
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/SettingsEnum.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/SettingsEnum.java
index 5abf763874..0250d0b65b 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/SettingsEnum.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/constant/SettingsEnum.java
@@ -32,7 +32,8 @@ public enum SettingsEnum {
     WORKER_HEART_OVERTIME(CategoryEnum.WORKER.getValue(), KeyEnum.WORKER_HEART_TIMEOUT.getValue()),
     SMTP_PASSWORD("SMTP", "smtp.server.password"),
 
-    SCHEDULED_LOAD_SCHEMA_COUNT_THRESHOLD("schema", "scheduled.load.schema.count.threshold")
+    SCHEDULED_LOAD_SCHEMA_COUNT_THRESHOLD("schema", "scheduled.load.schema.count.threshold"),
+    AD_PASSWORD("Active_Directory", "ad.bind.password")
     ;
 
     @Getter
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SettingsDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SettingsDto.java
index 04d8dd81d5..6638dd3bcd 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SettingsDto.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SettingsDto.java
@@ -38,5 +38,6 @@ public class SettingsDto{
     private List<String> enums;
     private Integer min;
     private Integer max;
+    private Boolean open;
 
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/TestResponseDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/TestResponseDto.java
new file mode 100644
index 0000000000..ca270a43b6
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/TestResponseDto.java
@@ -0,0 +1,21 @@
+package com.tapdata.tm.Settings.dto;
+
+import java.io.Serializable;
+
+public class TestResponseDto implements Serializable {
+    private boolean result;
+    private String stack;
+
+    public boolean isResult() {
+        return result;
+    }
+
+    public String getStack() {
+        return stack;
+    }
+
+    public TestResponseDto(boolean result, String stack) {
+        this.result = result;
+        this.stack = stack;
+    }
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/entity/Settings.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/entity/Settings.java
index b139dc0279..96bff668db 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/entity/Settings.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/entity/Settings.java
@@ -36,5 +36,6 @@ public class Settings {
     private List<String> enums;
     private Integer min;
     private Integer max;
+    private Boolean open;
 
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
index 96796bd590..ad3c0afc0b 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
@@ -4,7 +4,7 @@
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
-import com.tapdata.tm.Settings.dto.SendMailResponseDto;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -51,7 +51,7 @@ public interface SettingsService {
 
     List<Settings> findAll(Query query);
 
-    SendMailResponseDto testSendMail(TestMailDto testMailDto);
+    TestResponseDto testSendMail(TestMailDto testMailDto);
 
     void setSettingsRepository(com.tapdata.tm.Settings.repository.SettingsRepository settingsRepository);
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/inspect/service/InspectDetailsService.java b/manager/tm-api/src/main/java/com/tapdata/tm/inspect/service/InspectDetailsService.java
index 69be21c123..c89ed94da4 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/inspect/service/InspectDetailsService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/inspect/service/InspectDetailsService.java
@@ -14,6 +14,7 @@
 import com.tapdata.tm.inspect.repository.InspectDetailsRepository;
 import com.tapdata.tm.inspect.vo.FailTableAndRowsVo;
 import lombok.NonNull;
+import org.apache.commons.lang3.StringUtils;
 import org.bson.types.ObjectId;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -110,6 +111,9 @@ protected void compareDifferenceFields(InspectDetailsDto inspectDetailsDtoTmp,In
         Map<String,Object> resultSource = new HashMap<>();
         Map<String,Object> resultTarget = new HashMap<>();
         String message = inspectDetailsDtoTmp.getMessage();
+        if (StringUtils.isBlank(message)) {
+            return;
+        }
         if (message.contains("Different fields")) {
             List<String> diffFields = Arrays.stream(message.split(":")[1].split(",")).collect(Collectors.toList());
             diffFields.forEach(diffField -> {
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/CreateUserRequest.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/CreateUserRequest.java
index 59c3a29ad1..d8c03729f9 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/CreateUserRequest.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/CreateUserRequest.java
@@ -24,6 +24,8 @@ public class CreateUserRequest {
 	@NotBlank
 	private String email;
 
+	private String ldapAccount;
+
 	private Boolean emailVerified;
 
 	@JsonProperty("emailVerified_from_frontend")
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/LdapLoginDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/LdapLoginDto.java
new file mode 100644
index 0000000000..83642ead30
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/LdapLoginDto.java
@@ -0,0 +1,15 @@
+package com.tapdata.tm.user.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+public class LdapLoginDto {
+    private String ldapUrl; // host+端口
+    private String bindDN; //用户名+域
+    private String password; //密码
+    private String baseDN; //根目录
+    private boolean sslEnable; //是否启用ssl
+    private String cert; //cert文件
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/TestLdapDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/TestLdapDto.java
new file mode 100644
index 0000000000..6a2cef5bdb
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/TestLdapDto.java
@@ -0,0 +1,22 @@
+package com.tapdata.tm.user.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class TestLdapDto {
+    @JsonProperty("Ldap_Server_Host")
+    private String Ldap_Server_Host;
+    @JsonProperty("Ldap_Server_Port")
+    private String Ldap_Server_Port;
+    @JsonProperty("Ldap_Base_DN")
+    private String Ldap_Base_DN;
+    @JsonProperty("Ldap_Bind_DN")
+    private String Ldap_Bind_DN;
+    @JsonProperty("Ldap_Bind_Password")
+    private String Ldap_Bind_Password;
+    @JsonProperty("Ldap_SSL_Enable")
+    private Boolean Ldap_SSL_Enable;
+    @JsonProperty("Ldap_SSL_Cert")
+    private String Ldap_SSL_Cert;
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/UserDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/UserDto.java
index 27d9430edb..7cdd27b831 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/UserDto.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/dto/UserDto.java
@@ -26,6 +26,7 @@ public class UserDto extends BaseDto {
     private String accessCode;
     private String username;
     private String email;
+    private String ldapAccount;
     private Integer role;
     private String phone;
     private String photo;
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/entity/User.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/entity/User.java
index 04dca409ea..e7fefc579f 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/user/entity/User.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/entity/User.java
@@ -30,6 +30,7 @@ public class User extends BaseEntity {
     private String password;
     @Indexed(unique = true)
     private String email;
+    private String ldapAccount;
     private String phone;
     private boolean emailVerified;
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/user/service/UserService.java b/manager/tm-api/src/main/java/com/tapdata/tm/user/service/UserService.java
index 2b4d1241f1..829ef8ef78 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/user/service/UserService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/user/service/UserService.java
@@ -1,6 +1,7 @@
 package com.tapdata.tm.user.service;
 
 import com.mongodb.client.result.UpdateResult;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.base.service.BaseService;
 import com.tapdata.tm.commons.base.dto.BaseDto;
 import com.tapdata.tm.config.security.UserDetail;
@@ -61,5 +62,11 @@ public UserService(@NonNull UserRepository repository) {
     public abstract String getServerPort();
 
     public abstract void updatePermissionRoleMapping(UpdatePermissionRoleMappingDto dto, UserDetail userDetail);
+
+    public abstract boolean checkLdapLoginEnable();
+
+    public abstract TestResponseDto testLoginByLdap(TestLdapDto testAdDto);
+
+    public abstract boolean loginByLdap(String username, String password);
 }
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java b/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
index dacd0f3d9d..aa804d8758 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
@@ -6,7 +6,7 @@
 import com.tapdata.tm.Settings.constant.CategoryEnum;
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
-import com.tapdata.tm.Settings.dto.SendMailResponseDto;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.Settings.service.SettingsService;
 import com.tapdata.tm.base.exception.BizException;
 import com.tapdata.tm.message.constant.MsgTypeEnum;
@@ -454,14 +454,14 @@ public static String getMailTitle(SystemEnum systemEnum, MsgTypeEnum msgTypeEnum
     /**
      * 发送HTML邮件
      */
-    public static SendMailResponseDto sendHtmlEmail(MailAccountDto parms, List<String> adressees, String title, String content) {
+    public static TestResponseDto sendHtmlEmail(MailAccountDto parms, List<String> adressees, String title, String content) {
         adressees = filterBlackList(adressees);
-        if (adressees == null) return new SendMailResponseDto(false,"Please check your configuration, receivers cannot be empty.");
+        if (adressees == null) return new TestResponseDto(false,"Please check your configuration, receivers cannot be empty.");
 
         boolean flag = true;
         if (StringUtils.isAnyBlank(parms.getHost(), parms.getFrom(),parms.getUser(), parms.getPass()) || CollectionUtils.isEmpty(adressees)) {
             log.error("mail account info empty, params:{}", JSON.toJSONString(parms));
-            return new SendMailResponseDto(false,"Please check your configuration, mail account information cannot be empty.");
+            return new TestResponseDto(false,"Please check your configuration, mail account information cannot be empty.");
         } else {
             if (StringUtils.isNotBlank(parms.getProxyHost()) && 0 != parms.getProxyPort()) {
                 return sendEmailForProxy(parms, adressees, title, content, flag);
@@ -498,11 +498,11 @@ public static SendMailResponseDto sendHtmlEmail(MailAccountDto parms, List<Strin
             } catch (Exception e) {
                 log.error("mail send error：{}", e.getMessage(), e);
                 flag = false;
-                return new SendMailResponseDto(flag, TapSimplify.getStackTrace(e));
+                return new TestResponseDto(flag, TapSimplify.getStackTrace(e));
             }
         }
         log.debug("mail send status：{}", flag ? "suc" : "error");
-        return new SendMailResponseDto(flag, null);
+        return new TestResponseDto(flag, null);
     }
 
     @Nullable
@@ -526,7 +526,7 @@ protected static List<String> filterBlackList(List<String> adressees) {
         return adressees;
     }
 
-    protected static SendMailResponseDto sendEmailForProxy(MailAccountDto parms, List<String> adressees, String title, String content, boolean flag) {
+    protected static TestResponseDto sendEmailForProxy(MailAccountDto parms, List<String> adressees, String title, String content, boolean flag) {
         final String username = parms.getUser();
         final String password = parms.getPass();
 
@@ -577,10 +577,10 @@ protected PasswordAuthentication getPasswordAuthentication() {
         } catch (Exception e) {
             flag = false;
             log.error("mail send error：{}", e.getMessage(), e);
-            return new SendMailResponseDto(flag, TapSimplify.getStackTrace(e));
+            return new TestResponseDto(flag, TapSimplify.getStackTrace(e));
         }
         log.debug("mail send status：{}", flag ? "suc" : "error");
-        return new SendMailResponseDto(flag,null);
+        return new TestResponseDto(flag,null);
     }
 
     protected static String assemblyMessageBody(String message) {
diff --git a/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java b/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
index 10472ba0b5..3b5c0f468c 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
@@ -1,6 +1,6 @@
 package com.tapdata.tm.Settings.controller;
 
-import com.tapdata.tm.Settings.dto.SendMailResponseDto;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -117,8 +117,8 @@ public ResponseMessage<Void> updateAlarm(@RequestBody UpdateRuleDto ruleDto) {
 
     @Operation(summary = "test send mail")
     @PostMapping("testEmail")
-    public ResponseMessage<SendMailResponseDto> testSendMail(@RequestBody TestMailDto testMailDto) {
-        SendMailResponseDto res = settingsService.testSendMail(testMailDto);
+    public ResponseMessage<TestResponseDto> testSendMail(@RequestBody TestMailDto testMailDto) {
+        TestResponseDto res = settingsService.testSendMail(testMailDto);
         return success(res);
     }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
index e2672b6d8e..22cc5d7c56 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
@@ -6,7 +6,7 @@
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
-import com.tapdata.tm.Settings.dto.SendMailResponseDto;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -207,7 +207,7 @@ public List<SettingsDto> findALl(String decode, Filter filter) {
             //}).collect(Collectors.toList());
         } else {
             settingsList.stream().filter(settings -> {
-                if ("smtp.server.password".equals(settings.getKey()))
+                if ("smtp.server.password".equals(settings.getKey()) || "ad.bind.password".equals(settings.getKey()))
                     settings.setValue("*****");
                 return true;
             }).collect(Collectors.toList());
@@ -309,7 +309,7 @@ public List<Settings> findAll(Query query) {
         return mongoTemplate.find(query, Settings.class);
     }
 
-    public SendMailResponseDto testSendMail(TestMailDto testMailDto) {
+    public TestResponseDto testSendMail(TestMailDto testMailDto) {
         MailAccountDto mailAccount = getMailAccount(testMailDto);
 
         if ("*****".equals(mailAccount.getPass())) {
diff --git a/manager/tm/src/main/java/com/tapdata/tm/base/aop/AuthAop.java b/manager/tm/src/main/java/com/tapdata/tm/base/aop/AuthAop.java
index 928cf39830..d0a166b23a 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/base/aop/AuthAop.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/base/aop/AuthAop.java
@@ -58,34 +58,6 @@ public Object dataSourceRepository_buildUpdateSet(ProceedingJoinPoint pjp, UserD
     return ignoreUserIdWhenUpdate(pjp, userDetail);
   }
 
-  @Around("execution(public * com.tapdata.tm.base.service.BaseService.*(..)) && target(com.tapdata.tm.metadatadefinition.service.MetadataDefinitionService) && args(.., filter, user)")
-  public Object metadataDefinitionService_filter(ProceedingJoinPoint pjp, Filter filter, UserDetail user) throws Throwable {
-    if (productComponent.isCloud()) {
-      return pjp.proceed();
-    }
-    boolean isChange = false;
-    try {
-      if (user != null && !user.isRoot() && !user.isFreeAuth()) {
-        user.setFreeAuth();
-        filter.setWhere(new Where()
-                .and("and", new ArrayList<Map<String, Object>>() {{
-                  add(filter.getWhere());
-                  add(new HashMap<String, Object>() {{
-                    put("user_id", new HashMap<String, Object>() {{
-                      put("$in", Arrays.asList(user.getUserId(), null, ""));
-                    }});
-
-                  }});
-                }}));
-        isChange = true;
-      }
-      return pjp.proceed();
-    } finally {
-      if (isChange) {
-        user.setAuth();
-      }
-    }
-  }
 
   @Around("execution(public * com.tapdata.tm.base.service.BaseService.*(..)) && target(com.tapdata.tm.metadatadefinition.service.MetadataDefinitionService) && args(.., query, user)")
   public Object metadataDefinitionService_query(ProceedingJoinPoint pjp, Query query, UserDetail user) throws Throwable {
diff --git a/manager/tm/src/main/java/com/tapdata/tm/inspect/controller/InspectDetailsController.java b/manager/tm/src/main/java/com/tapdata/tm/inspect/controller/InspectDetailsController.java
index 0dd42ca93a..894f33b7ae 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/inspect/controller/InspectDetailsController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/inspect/controller/InspectDetailsController.java
@@ -1,5 +1,6 @@
 package com.tapdata.tm.inspect.controller;
 
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.base.controller.BaseController;
 import com.tapdata.tm.base.dto.*;
 import com.tapdata.tm.base.exception.BizException;
@@ -11,11 +12,13 @@
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.enums.ParameterIn;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import io.tapdata.entity.simplify.TapSimplify;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -231,19 +234,23 @@ public void export(@RequestBody InspectDetailsDto inspectDetails,
         String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
         String filename = inspectDetails.getInspectResultId() + "-" + date;
 
-        response.setContentType("application/zip");
-        response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + ".zip\"");
-        ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
-        zipOutputStream.putNextEntry(new ZipEntry(filename + ".json"));
-        try {
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
+            zipOutputStream.putNextEntry(new ZipEntry(filename + ".json"));
             inspectDetailsService.export(inspectDetails, zipOutputStream, getLoginUser(), inspectResultService);
+
+            zipOutputStream.closeEntry();
+            zipOutputStream.flush();
+            zipOutputStream.close();
+
+            response.setContentType("application/zip");
+            response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + ".zip\"");
+            response.getOutputStream().write(byteArrayOutputStream.toByteArray());
         } catch (Exception e) {
             log.error("Export inspectDetails failed", e);
             throw new BizException("export.inspectDetails.failed",e.getMessage());
         } finally {
-            zipOutputStream.closeEntry();
-            zipOutputStream.flush();
-            zipOutputStream.close();
+            byteArrayOutputStream.close();
         }
     }
 
diff --git a/manager/tm/src/main/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionService.java b/manager/tm/src/main/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionService.java
index cc63e38e8d..102ab86daa 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionService.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionService.java
@@ -3,6 +3,7 @@
 import com.google.common.collect.Lists;
 import com.mongodb.client.result.UpdateResult;
 import com.tapdata.manager.common.utils.StringUtils;
+import com.tapdata.tm.Settings.service.SettingsService;
 import com.tapdata.tm.base.dto.Field;
 import com.tapdata.tm.base.dto.Filter;
 import com.tapdata.tm.base.dto.Page;
@@ -74,6 +75,9 @@ public class MetadataDefinitionService extends BaseService<MetadataDefinitionDto
     @Autowired
     private LdpService ldpService;
 
+    @Autowired
+    private SettingsService settingsService;
+
 
     public MetadataDefinitionService(@NonNull MetadataDefinitionRepository repository) {
         super(repository, MetadataDefinitionDto.class, MetadataDefinitionEntity.class);
@@ -316,7 +320,7 @@ public Page<MetadataDefinitionDto> findAndChildAccount(Filter filter, boolean se
 
     @Override
     public Page<MetadataDefinitionDto> find(Filter filter, UserDetail user) {
-        Page<MetadataDefinitionDto> dtoPage = super.find(filter, user);
+        Page<MetadataDefinitionDto> dtoPage = settingsService.isCloud()? super.find(filter, user) : super.find(filter);
         if (filter.getOrder() == null) {
             dtoPage.getItems().sort(Comparator.comparing(MetadataDefinitionDto::getValue));
             dtoPage.getItems().sort(Comparator.comparing(s -> {
diff --git a/manager/tm/src/main/java/com/tapdata/tm/user/controller/UserController.java b/manager/tm/src/main/java/com/tapdata/tm/user/controller/UserController.java
index 41701d64d6..a1bdb21b29 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/user/controller/UserController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/user/controller/UserController.java
@@ -4,6 +4,7 @@
 import com.mongodb.BasicDBObject;
 import com.tapdata.tm.Permission.dto.PermissionDto;
 import com.tapdata.tm.Permission.service.PermissionService;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.accessToken.dto.AccessTokenDto;
 import com.tapdata.tm.accessToken.service.AccessTokenService;
 import com.tapdata.tm.base.controller.BaseController;
@@ -22,6 +23,7 @@
 import com.tapdata.tm.user.entity.User;
 import com.tapdata.tm.user.param.ResetPasswordParam;
 import com.tapdata.tm.user.service.UserService;
+import com.tapdata.tm.user.dto.TestLdapDto;
 import com.tapdata.tm.userLog.constant.Modular;
 import com.tapdata.tm.userLog.service.UserLogService;
 import com.tapdata.tm.utils.RC4Util;
@@ -300,7 +302,45 @@ public ResponseMessage<AccessTokenDto> login(@RequestBody @Validated LoginReques
         } catch (Exception e) {
             return failed(e);
         }
-        User user = userService.findOneByEmail(loginRequest.getEmail());
+        User user;
+        // 从setting中获取是否开启AD
+        boolean adLoginEnable = userService.checkLdapLoginEnable();
+        //若开启，走AD校验，校验通过创建用户，未通过登录失败
+        if (adLoginEnable && !"admin@admin.com".equals(loginRequest.getEmail())) {
+            boolean login = userService.loginByLdap(loginRequest.getEmail(), password);
+            if (!login) {
+                throw new BizException("Incorrect.Password");
+            }
+            //登录成功查询用户是否存在没有就创建用户
+            if (StringUtils.isNotBlank(loginRequest.getEmail())) {
+                String[] split = loginRequest.getEmail().split("@");
+                if (split != null && split.length > 1) {
+                    loginRequest.setEmail(split[0]);
+                }
+            }
+            UserDto adUser = userService.findOne(new Query(Criteria.where("ldapAccount").regex(loginRequest.getEmail(), "i").and("source").is("createLdap")
+                    .orOperator(Criteria.where("isDeleted").is(false), Criteria.where("isDeleted").exists(false))));
+            if (null == adUser) {
+                CreateUserRequest request = new CreateUserRequest();
+                request.setLdapAccount(loginRequest.getEmail());
+                request.setUsername(loginRequest.getEmail());
+                request.setAccesscode("");
+                request.setAccountStatus(1);
+                request.setPassword(password);
+                request.setSource("createLdap");
+                request.setEmailVerified(true);
+                List<Object> roleusers = new ArrayList<>();
+                roleusers.add("5d31ae1ab953565ded04badd");
+                request.setRoleusers(roleusers);
+                UserDetail userDetail = userService.loadUserByUsername("admin@admin.com");
+                adUser = userService.save(request, userDetail);
+            }
+            user = userService.convertToEntity(User.class, adUser);
+            user.setPassword(BCrypt.hashpw(password));
+        } else {
+            //未开启走原来的逻辑
+            user = userService.findOneByEmail(loginRequest.getEmail());
+        }
         if (user.getAccountStatus() == 2) {
             return failed("110500", user.isEmailVerified() ? "WAITING_APPROVE" : "EMAIL_NON_VERIFLED");
         } else if (user.getAccountStatus() == 0) {
@@ -482,4 +522,18 @@ public ResponseMessage<Long> delete(@PathVariable("id") String id) {
         userService.delete(id);
         return success();
     }
+
+    @Operation(summary = "test ldap login")
+    @PostMapping("testLdapLogin")
+    public ResponseMessage<TestResponseDto> testLoginByLdap(@RequestBody TestLdapDto testldapDto) {
+        TestResponseDto res = userService.testLoginByLdap(testldapDto);
+        return success(res);
+    }
+
+    @Operation(summary = "check ldap login enable")
+    @GetMapping("checkLdapLoginEnable")
+    public ResponseMessage<Boolean> checkADLoginEnable() {
+        Boolean res = userService.checkLdapLoginEnable();
+        return success(res);
+    }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
index 0d41bf5615..76ded0c7f8 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
@@ -9,11 +9,16 @@
 import com.mongodb.client.result.UpdateResult;
 import com.tapdata.tm.Permission.entity.PermissionEntity;
 import com.tapdata.tm.Permission.service.PermissionService;
+import com.tapdata.tm.Settings.constant.CategoryEnum;
+import com.tapdata.tm.Settings.constant.KeyEnum;
+import com.tapdata.tm.Settings.constant.SettingsEnum;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
+import com.tapdata.tm.Settings.entity.Settings;
+import com.tapdata.tm.Settings.service.SettingsService;
 import com.tapdata.tm.base.dto.Field;
 import com.tapdata.tm.base.dto.Filter;
 import com.tapdata.tm.base.dto.Where;
 import com.tapdata.tm.base.exception.BizException;
-import com.tapdata.tm.base.service.BaseService;
 import com.tapdata.tm.commons.base.dto.BaseDto;
 import com.tapdata.tm.commons.function.Bi3Consumer;
 import com.tapdata.tm.config.security.SimpleGrantedAuthority;
@@ -38,6 +43,7 @@
 import com.tapdata.tm.utils.MailUtils;
 import com.tapdata.tm.utils.SendStatus;
 import com.tapdata.tm.utils.UUIDUtil;
+import io.tapdata.entity.simplify.TapSimplify;
 import io.tapdata.pdk.core.utils.CommonUtils;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
@@ -51,9 +57,17 @@
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
-import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.stereotype.Component;
 
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.*;
+import javax.net.ssl.*;
+import java.io.*;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
 import java.util.*;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
@@ -102,6 +116,8 @@ public UserServiceImpl(@NonNull UserRepository repository) {
 
     @Autowired
     private LdpService ldpService;
+    @Autowired
+    private SettingsService settingsService;
 
     @Override
     protected void beforeSave(UserDto dto, UserDetail userDetail) {
@@ -355,7 +371,7 @@ public User findOneByEmail(String email) {
 
     public <T extends BaseDto> UserDto save(CreateUserRequest request, UserDetail userDetail) {
 
-        UserDto userDto = findOne(Query.query(Criteria.where("email").is(request.getEmail()).orOperator(Criteria.where("isDeleted").is(false), Criteria.where("isDeleted").exists(false))));
+        UserDto userDto = findOne(Query.query(Criteria.where("email").is(request.getEmail()).and("ldapAccount").is(request.getLdapAccount()).orOperator(Criteria.where("isDeleted").is(false), Criteria.where("isDeleted").exists(false))));
         if (userDto != null) {
             throw new BizException("User.Already.Exists");
         }
@@ -757,4 +773,174 @@ private List<PermissionEntity> getPermissionsByCodes(Set<String> permissionCodes
 			add("v2_user_management_menu");
 			add("v2_role_management");
 		}};
+
+    @Override
+    public boolean checkLdapLoginEnable() {
+        Settings settings = settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_LOGIN_ENABLE);
+        if (settings != null) {
+            return settings.getOpen();
+        }
+        return false;
+    }
+
+    @Override
+    public TestResponseDto testLoginByLdap(TestLdapDto testldapDto) {
+        String ldapUrl = testldapDto.getLdap_Server_Host() + ":" + testldapDto.getLdap_Server_Port();
+        String bindDN = testldapDto.getLdap_Bind_DN();
+        String bindPassword = testldapDto.getLdap_Bind_Password();
+        Boolean sslEnable = testldapDto.getLdap_SSL_Enable();
+        String ldapSslCert = testldapDto.getLdap_SSL_Cert();
+        LdapLoginDto ldapLoginDto = LdapLoginDto.builder().ldapUrl(ldapUrl).bindDN(bindDN).password(bindPassword).sslEnable(sslEnable).cert(ldapSslCert).build();
+        if ("*****".equals(ldapLoginDto.getPassword())) {
+            String value = SettingsEnum.AD_PASSWORD.getValue();
+            ldapLoginDto.setPassword(value);
+        }
+        DirContext dirContext = null;
+        try {
+            dirContext = buildDirContext(ldapLoginDto);
+            if (null != dirContext) {
+                return new TestResponseDto(true, null);
+            } else {
+                return new TestResponseDto(false, "connect to active directory server failed");
+            }
+        } catch (NamingException e) {
+            return new TestResponseDto(false, TapSimplify.getStackTrace(e));
+        } finally {
+            if (null != dirContext) {
+                try {
+                    dirContext.close();
+                } catch (NamingException e) {
+                    // do nothing
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean loginByLdap(String username, String password) {
+        List<Settings> all = settingsService.findAll();
+        Map<String, Object> collect = all.stream().collect(Collectors.toMap(Settings::getKey, Settings::getValue, (e1, e2) -> e1));
+
+        String host = (String) collect.get("ldap.server.host");
+        String port = (String) collect.get("ldap.server.port");
+        String bindDN = (String) collect.get("ldap.bind.dn");
+        String pwd = (String) collect.get("ldap.bind.password");
+        String baseDN = (String) collect.get("ldap.base.dn");
+        String cert = (String) collect.get("ldap.ssl.cert");
+        Boolean ssl = false;
+        Settings settings = settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_SSL_ENABLE);
+        if (settings != null) {
+            ssl = settings.getOpen();
+        }
+        String ldapUrl = host + ":" + port;
+        LdapLoginDto ldapLoginDto = LdapLoginDto.builder().ldapUrl(ldapUrl).bindDN(bindDN).password(pwd).baseDN(baseDN).sslEnable(ssl).cert(cert).build();
+        DirContext dirContext = null;
+        try {
+            boolean exists = searchUser(ldapLoginDto, username);
+            if (!exists) {
+                throw new BizException("AD.Account.Not.Exists");
+            }
+            ldapLoginDto.setBindDN(username);
+            ldapLoginDto.setPassword(password);
+            dirContext = buildDirContext(ldapLoginDto);
+            if (null != dirContext) {
+                return true;
+            }
+        } catch (NamingException e) {
+            throw new BizException("AD.Login.Fail", e);
+        } finally {
+            if (null != dirContext) {
+                try {
+                    dirContext.close();
+                } catch (NamingException e) {
+                    // do nothing
+                }
+            }
+        }
+        return false;
+    }
+
+    protected boolean searchUser(LdapLoginDto ldapLoginDto, String username) throws NamingException {
+        String sAMAccountNameFilter = String.format("(sAMAccountName=%s)", username);
+        String userPrincipalNameFilter = String.format("(userPrincipalName=%s)", username);
+        DirContext ctx = buildDirContext(ldapLoginDto);
+        String searchBases = ldapLoginDto.getBaseDN();
+        if (StringUtils.isBlank(searchBases)) return false;
+        try {
+            SearchControls searchControls = new SearchControls();
+            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+            searchControls.setReturningAttributes(new String[]{"sAMAccountName", "userPrincipalName", "displayName"});
+            String[] searchBase = searchBases.split(";");
+            for (String base : searchBase) {
+                boolean isExist = searchWithFilter(ctx, base, sAMAccountNameFilter, searchControls) || searchWithFilter(ctx, base, userPrincipalNameFilter, searchControls);
+                if (isExist) return true;
+            }
+            return false;
+        } catch (NamingException e) {
+            throw new BizException("AD.Search.Fail", e);
+        } finally {
+            ctx.close();
+        }
+    }
+
+    protected boolean searchWithFilter(DirContext ctx, String searchBase, String filter, SearchControls searchControls) throws NamingException {
+        NamingEnumeration<SearchResult> sAMAccountNameResult = ctx.search(searchBase, filter, searchControls);
+
+        String userPrincipalName = null;
+        String displayName = null;
+        while (sAMAccountNameResult.hasMore()) {
+            SearchResult searchResult = sAMAccountNameResult.next();
+            Attributes attributes = searchResult.getAttributes();
+            userPrincipalName = attributes.get("userPrincipalName") != null ? attributes.get("userPrincipalName").get().toString() : null;
+            displayName = attributes.get("displayName") != null ? attributes.get("displayName").get().toString() : null;
+        }
+        return StringUtils.isNotBlank(userPrincipalName) || StringUtils.isNotBlank(displayName);
+    }
+
+    protected DirContext buildDirContext(LdapLoginDto ldapLoginDto) throws NamingException {
+        String ldapUrl = ldapLoginDto.getLdapUrl();
+        String bindDn = ldapLoginDto.getBindDN();
+        String password = ldapLoginDto.getPassword();
+        Boolean ssl = ldapLoginDto.isSslEnable();
+        String certFile = ldapLoginDto.getCert();
+        Hashtable<String, String> env = new Hashtable<>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+        env.put(Context.PROVIDER_URL, ldapUrl);
+        env.put(Context.SECURITY_AUTHENTICATION, "simple");
+        env.put(Context.SECURITY_PRINCIPAL, bindDn);
+        env.put(Context.SECURITY_CREDENTIALS, password);
+        if (ssl) {
+            if (null == certFile) throw new BizException("AD.Login.Fail");
+            try (InputStream certificates = new ByteArrayInputStream(certFile.getBytes())) {
+                SSLContext sslContext = createSSLContext(certificates);
+                HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
+            } catch (Exception e) {
+                throw new BizException(e);
+            }
+        }
+        DirContext ctx = new InitialDirContext(env);
+        return ctx;
+    }
+    protected SSLContext createSSLContext(InputStream certFile) throws Exception {
+        // load custom cert
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+        X509Certificate caCert = (X509Certificate) cf.generateCertificate(certFile);
+        certFile.close();
+
+        // create KeyStore and import cert
+        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+        // init empty keyStore
+        keyStore.load(null, null);
+        keyStore.setCertificateEntry("caCert", caCert);
+
+        // create TrustManagerFactory and init KeyStore
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        tmf.init(keyStore);
+
+        TrustManager[] trustManagers = tmf.getTrustManagers();
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, trustManagers, null);
+
+        return sslContext;
+    }
 }
diff --git a/manager/tm/src/main/resources/init/idaas/3.14-1.json b/manager/tm/src/main/resources/init/idaas/3.14-1.json
new file mode 100644
index 0000000000..dbebed8702
--- /dev/null
+++ b/manager/tm/src/main/resources/init/idaas/3.14-1.json
@@ -0,0 +1,112 @@
+[
+  {
+    "insert": "Settings",
+    "documents": [
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap_Server_Host_doc",
+        "hot_reloading": true,
+        "key": "ldap.server.host",
+        "key_label": "Ldap Server Host",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "1",
+        "user_visible": true,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap_Server_Port_doc",
+        "hot_reloading": true,
+        "key": "ldap.server.port",
+        "key_label": "Ldap Server Port",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "2",
+        "user_visible": true,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap_Base_DN_doc",
+        "hot_reloading": true,
+        "key": "ldap.base.dn",
+        "key_label": "Ldap Base DN",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "3",
+        "user_visible": true,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap_Bind_DN_doc",
+        "hot_reloading": true,
+        "key": "ldap.bind.dn",
+        "key_label": "Ldap Bind DN",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "4",
+        "user_visible": true,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap_Bind_Password_doc",
+        "hot_reloading": true,
+        "key": "ldap.bind.password",
+        "key_label": "Ldap Bind Password",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "5",
+        "user_visible": true,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "documentation": "Ldap SSL Enable",
+        "hot_reloading": true,
+        "key": "ldap.ssl.enable",
+        "key_label": "Ldap SSL Enable",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "7",
+        "user_visible": true,
+        "open": false,
+        "value": ""
+      },
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "documentation": "Ldap Login Enable",
+        "hot_reloading": true,
+        "key": "ldap.login.enable",
+        "key_label": "Ldap Login Enable",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "0",
+        "user_visible": true,
+        "open": false,
+        "value": ""
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/init/idaas/3.14-2.json b/manager/tm/src/main/resources/init/idaas/3.14-2.json
new file mode 100644
index 0000000000..23ab0025ef
--- /dev/null
+++ b/manager/tm/src/main/resources/init/idaas/3.14-2.json
@@ -0,0 +1,22 @@
+[
+  {
+    "insert": "Settings",
+    "documents": [
+      {
+        "category": "LDAP",
+        "category_sort": "91",
+        "default_value": " ",
+        "documentation": "Ldap SSL Cert",
+        "hot_reloading": true,
+        "key": "ldap.ssl.cert",
+        "key_label": "Ldap SSL Cert",
+        "last_update": 0,
+        "last_update_by": "",
+        "scope": "global",
+        "sort": "8",
+        "user_visible": true,
+        "value": ""
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/init/idaas/version b/manager/tm/src/main/resources/init/idaas/version
index 8dddc5581f..fcf88bcc8c 100644
--- a/manager/tm/src/main/resources/init/idaas/version
+++ b/manager/tm/src/main/resources/init/idaas/version
@@ -1,1 +1,1 @@
-3.13-2
\ No newline at end of file
+3.14-2
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/messages.properties b/manager/tm/src/main/resources/messages.properties
index 7f4d343703..64ed4559cb 100644
--- a/manager/tm/src/main/resources/messages.properties
+++ b/manager/tm/src/main/resources/messages.properties
@@ -125,6 +125,10 @@ ValidateCode.Not.Incorrect=validateCode is incorrect
 ValidateCode.Is.expired=validateCode Expires
 Role.Unable.Delete=The role cannot be deleted because a user is bound to the role
 
+AD.Login.Fail=login by LDAP failed, please contact the administrator
+AD.Search.Fail=search LDAP user failed, please contact the administrator
+AD.Account.Not.Exists=LDAP account does not exist in current group, please contact the administrator
+
 PDK.DOWNLOAD.SOURCE.FAILED=pdk source not belong to current user
 Task.Deleted=Task has been deleted
 Email.TestSendFailed=test send email failed
diff --git a/manager/tm/src/main/resources/messages_en_US.properties b/manager/tm/src/main/resources/messages_en_US.properties
index f0f1ad8a9a..9df327d808 100644
--- a/manager/tm/src/main/resources/messages_en_US.properties
+++ b/manager/tm/src/main/resources/messages_en_US.properties
@@ -76,6 +76,9 @@ User.Not.Exist= user not exist
 User.email.Found=can not find this email
 ValidateCode.Not.Incorrect=validateCode is incorrect
 ValidateCode.Is.expired=validateCode Expires
+AD.Login.Fail=login by LDAP failed, please contact the administrator
+AD.Search.Fail=search LDAP user failed, please contact the administrator
+AD.Account.Not.Exists=LDAP account does not exist in current group, please contact the administrator
 
 Task.CanNotSupportInspect=data source not support inspect
 InvalidPaidPlan=Invalid paid plan, please check and subscribe paid plan
diff --git a/manager/tm/src/main/resources/messages_zh_CN.properties b/manager/tm/src/main/resources/messages_zh_CN.properties
index 14ada1cf08..fa000c42f4 100644
--- a/manager/tm/src/main/resources/messages_zh_CN.properties
+++ b/manager/tm/src/main/resources/messages_zh_CN.properties
@@ -72,8 +72,8 @@ Task.nodeRefresh=\u4EFB\u52A1\u8282\u70B9\u5DF2\u7ECF\u88AB\u5237\u65B0
 Task.DateProcessConfigInvalid=\u65F6\u95F4\u5904\u7406\u8282\u70B9\u9009\u62E9\u65F6\u95F4\u7C7B\u578B\u5F02\u5E38
 Task.ScheduleLimit=\u5F15\u64CE\u53EF\u4EE5\u88AB\u8C03\u7528\u7684\u4EFB\u52A1\u8D85\u8FC7\u4E86\u9650\u5236\u6570\uFF0C\u8BF7\u5347\u7EA7\u8BA2\u9605\u4EE5\u83B7\u53D6\u66F4\u591A\u4EFB\u52A1\u6570\u91CF
 Task.ManuallyScheduleLimit=\u7528\u6237\u6307\u5B9A\u5F15\u64CE\u53EF\u4EE5\u88AB\u8C03\u7528\u7684\u4EFB\u52A1\u8D85\u8FC7\u4E86\u9650\u5236\u6570\uFF0C\u8BF7\u66FF\u6362\u5176\u4ED6\u5F15\u64CE {0}
-Task.LicenseScheduleLimit=\u53ef\u4ee5\u8c03\u5ea6\u7684\u901a\u9053\u6570\u8d85\u8fc7\u4e86\u914d\u989d\uff0c\u8bf7\u66f4\u65b0\u006c\u0069\u0063\u0065\u006e\u0073\u0065\u914d\u989d\u6216\u505c\u6b62\u76f8\u5173\u4efb\u52a1
-License.NodeInstanceIdInvalid=\u8bf7\u786e\u4fdd\u8282\u70b9 [{0}] \u4e3a\u6570\u636e\u6e90\u8282\u70b9
+Task.LicenseScheduleLimit=\u53EF\u4EE5\u8C03\u5EA6\u7684\u901A\u9053\u6570\u8D85\u8FC7\u4E86\u914D\u989D\uFF0C\u8BF7\u66F4\u65B0license\u914D\u989D\u6216\u505C\u6B62\u76F8\u5173\u4EFB\u52A1
+License.NodeInstanceIdInvalid=\u8BF7\u786E\u4FDD\u8282\u70B9 [{0}] \u4E3A\u6570\u636E\u6E90\u8282\u70B9
 Task.ResetAgentNotFound=\u91CD\u7F6E\u4EFB\u52A1\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684\u5F15\u64CE\uFF0C\u8BF7\u542F\u52A8/\u91CD\u542F\u5F15\u64CE\u540E\u518D\u8BD5
 Task.ResetStatusInvalid=\u5F53\u524D\u9700\u8981\u91CD\u7F6E\u7684\u4EFB\u52A1\u72B6\u6001\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u518D\u8BD5
 Task.NotfoundDatasource=\u627E\u4E0D\u5230\u6570\u636E\u6E90 '{0}'
@@ -149,6 +149,9 @@ User.Not.Exist= \u7528\u6237\u4E0D\u5B58\u5728
 User.email.Found=\u627E\u4E0D\u5230\u5BF9\u5E94\u7684\u90AE\u7BB1
 ValidateCode.Not.Incorrect=\u9A8C\u8BC1\u7801\u9519\u8BEF
 ValidateCode.Is.expired=\u9A8C\u8BC1\u7801\u5DF2\u8FC7\u671F
+AD.Login.Fail=\u901A\u8FC7LDAP\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
+AD.Search.Fail=\u67E5\u8BE2LDAP\u7528\u6237\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
+AD.Account.Not.Exists=AD\u7528\u6237\u5728\u5F53\u524D\u7EC4\u4E0D\u5B58\u5728\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
 Account.disabled=\u7528\u6237\u5DF2\u51BB\u7ED3
 Role.Unable.Delete=\u65E0\u6CD5\u5220\u9664,\u5F53\u524D\u89D2\u8272\u5B58\u5728\u7ED1\u5B9A\u7528\u6237
 
diff --git a/manager/tm/src/main/resources/messages_zh_TW.properties b/manager/tm/src/main/resources/messages_zh_TW.properties
index 1e12ca342b..d20f058e70 100644
--- a/manager/tm/src/main/resources/messages_zh_TW.properties
+++ b/manager/tm/src/main/resources/messages_zh_TW.properties
@@ -6,14 +6,14 @@ InvalidPaidPlan=\u7121\u6548\u7684\u4ED8\u8CBB\u65B9\u6848\uFF0C\u8ACB\u6AA2\u67
 Task.CronError = \u8ABF\u5EA6\u904B\u7B97\u5F0F\u932F\u8AA4
 Role.Unable.Delete=\u7121\u6CD5\u522A\u9664,\u7576\u524D\u89D2\u8272\u5B58\u5728\u7D81\u5B9A\u7528\u6236
 
-Inspect.Recovery.NotFieldMethod=\u975e\u5b57\u6bb5\u6821\u9a57\u4efb\u52d9
-Inspect.Recovery.StatusNotDone=\u6821\u9a57\u4efb\u52d9\u4e0d\u662f\u5df2\u5b8c\u6210\u72c0\u614b
-Inspect.Recovery.ResultNotFound=\u6821\u9a57\u4efb\u52d9\u6c92\u6709\u5dee\u7570\u7d50\u679c
-Inspect.Recovery.IsNotWithTask=\u6821\u9a57\u4e0d\u662f\u57fa\u65bc\u540c\u6b65\u4efb\u52d9\u5275\u5efa
-Inspect.Recovery.TaskNotFound=\u540c\u6b65\u4efb\u52d9\u4e0d\u5b58\u5728
-Inspect.Recovery.TaskTypeIsNotCDC=\u540c\u6b65\u4efb\u52d9\u4e0d\u5305\u542b\u589e\u91cf
-Inspect.Recovery.TaskIsNotRunning=\u540c\u6b65\u4efb\u52d9\u975e\u904b\u884c\u72c0\u614b
-Inspect.Recovery.TaskSyncStatusIsNotCDC=\u540c\u6b65\u4efb\u52d9\u4e0d\u5728\u589e\u91cf\u968e\u6bb5
+Inspect.Recovery.NotFieldMethod=\u975E\u5B57\u6BB5\u6821\u9A57\u4EFB\u52D9
+Inspect.Recovery.StatusNotDone=\u6821\u9A57\u4EFB\u52D9\u4E0D\u662F\u5DF2\u5B8C\u6210\u72C0\u614B
+Inspect.Recovery.ResultNotFound=\u6821\u9A57\u4EFB\u52D9\u6C92\u6709\u5DEE\u7570\u7D50\u679C
+Inspect.Recovery.IsNotWithTask=\u6821\u9A57\u4E0D\u662F\u57FA\u65BC\u540C\u6B65\u4EFB\u52D9\u5275\u5EFA
+Inspect.Recovery.TaskNotFound=\u540C\u6B65\u4EFB\u52D9\u4E0D\u5B58\u5728
+Inspect.Recovery.TaskTypeIsNotCDC=\u540C\u6B65\u4EFB\u52D9\u4E0D\u5305\u542B\u589E\u91CF
+Inspect.Recovery.TaskIsNotRunning=\u540C\u6B65\u4EFB\u52D9\u975E\u904B\u884C\u72C0\u614B
+Inspect.Recovery.TaskSyncStatusIsNotCDC=\u540C\u6B65\u4EFB\u52D9\u4E0D\u5728\u589E\u91CF\u968E\u6BB5
 
 # external storage
 External.Storage.Name.Blank=\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
@@ -135,9 +135,13 @@ webhook.history.page.size.error=\u5206\u9801\u53C3\u6578[pageSize]\u7121\u6548\u
 insufficient.permissions=\u8A31\u53EF\u6B0A\u4E0D\u8DB3: {0}\uFF0C\u5F53\u524D\u64CD\u4F5C\u9700\u8981\u6743\u9650\uFF1A{1}
 inspect.result.not.exists=\u7576\u524D\u6821\u9A57\u4EFB\u52D9\u7684\u6821\u9A57\u8A18\u9304\u5DF2\u7D93\u4E0D\u5B58\u5728\uFF1A{0}
 
-source.setting.check.cdc= \u3010\u589e\u91cf\u6aa2\u67e5\u3011\u0020\u6578\u64da\u6e90\u003a\u007b\u0030\u007d\u7684\u914d\u7f6e\u589e\u91cf\u529f\u80fd\u4e0d\u652f\u6301\uff0c\u4e0d\u80fd\u914d\u7f6e\u589e\u91cf\u4efb\u52d9\u000d\u000a
+source.setting.check.cdc= \u3010\u589E\u91CF\u6AA2\u67E5\u3011\u0020\u6578\u64DA\u6E90:{0}\u7684\u914D\u7F6E\u589E\u91CF\u529F\u80FD\u4E0D\u652F\u6301\uFF0C\u4E0D\u80FD\u914D\u7F6E\u589E\u91CF\u4EFB\u52D9\u000d\u000a
 
-Modules.Permission.Scope.Null=\u6b0a\u9650\u7bc4\u570d\u4e0d\u80fd\u7232\u7a7a
-Modules.Tags.Null=\u6240\u5c6c\u61c9\u7528\u4e0d\u80fd\u70ba\u7a7a
+Modules.Permission.Scope.Null=\u6B0A\u9650\u7BC4\u570D\u4E0D\u80FD\u7232\u7A7A
+Modules.Tags.Null=\u6240\u5C6C\u61C9\u7528\u4E0D\u80FD\u70BA\u7A7A
 
-export.inspectDetails.failed = \u4e0b\u8f09\u6821\u9a57\u8a73\u60c5\u5931\u6557 :{0}
\ No newline at end of file
+export.inspectDetails.failed = \u4E0B\u8F09\u6821\u9A57\u8A73\u60C5\u5931\u6557 :{0}
+
+AD.Login.Fail=\u901A\u904ELDAP\u767B\u9304\u5931\u6557\uFF0C\u8ACB\u806F\u7CFB\u7BA1\u7406\u54E1
+AD.Search.Fail=\u67E5\u8A62LDAP\u7528\u6236\u5931\u6557\uFF0C\u8ACB\u806F\u7CFB\u7BA1\u7406\u54E1
+AD.Account.Not.Exists=AD\u7528\u6236\u5728\u7576\u524D\u7D44\u4E0D\u5B58\u5728\uFF0C\u8ACB\u806F\u7CFB\u7BA1\u7406\u54E1
\ No newline at end of file
diff --git a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/commands/RegisterCli.java b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/commands/RegisterCli.java
index 1aa7a1b5f2..ad5fcb4a13 100644
--- a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/commands/RegisterCli.java
+++ b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/commands/RegisterCli.java
@@ -3,6 +3,7 @@
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import io.tapdata.entity.codec.TapCodecsRegistry;
+import io.tapdata.entity.logger.TapLogger;
 import io.tapdata.entity.utils.DataMap;
 import io.tapdata.entity.utils.InstanceFactory;
 import io.tapdata.entity.utils.JsonParser;
@@ -22,21 +23,10 @@
 import io.tapdata.pdk.core.utils.IOUtils;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.output.WriterOutputStream;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.poi.ss.formula.functions.T;
-import org.voovan.tools.collection.ArraySet;
 import picocli.CommandLine;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.Writer;
-import java.nio.charset.Charset;
+import java.io.*;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
 
@@ -80,6 +70,8 @@ public class RegisterCli extends CommonCli {
 
     public Integer execute() throws Exception {
         printUtil = new PrintUtil(showAllMessage);
+        TapLogger.setLogListener(printUtil.getLogListener());
+
         List<String> filterTypes = generateSkipTypes();
         if (!filterTypes.isEmpty()) {
             printUtil.print(PrintUtil.TYPE.TIP, String.format("* Starting to register data sources, plan to skip data sources that are not within the registration scope.\n* The types of data sources that need to be registered are: %s", filterTypes));
@@ -344,15 +336,17 @@ private void addFile(String filePath, Map<String, InputStream> inputStreamMap, T
     }
 
     protected static final String path = "tapdata-cli/src/main/resources/replace/";
-    private Map<String, Object> needReplaceKeyWords(TapNodeInfo nodeInfo, String replacePath){
-        if (null != this.replaceName && !"".equals(replaceName.trim())){
+
+    private Map<String, Object> needReplaceKeyWords(TapNodeInfo nodeInfo, String replacePath) {
+        if (null != this.replaceName && !"".equals(replaceName.trim())) {
             replacePath = replaceName;
         }
         if (null == replacePath || "".equals(replacePath.trim())) return null;
         try {
             InputStream as = FileUtils.openInputStream(new File(path + replacePath + ".json"));//nodeInfo.readResource((String) replacePath);
             return JSON.parseObject(as, StandardCharsets.UTF_8, LinkedHashMap.class);
-        }catch (IOException e){}
+        } catch (IOException e) {
+        }
         return null;
     }
 
diff --git a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
index 9d51a12b69..3b58882e7c 100644
--- a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
+++ b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
@@ -218,7 +218,7 @@ public static void upload(Map<String, InputStream> inputStreamMap, File file, Li
     } else {
       printUtil.print(PrintUtil.TYPE.INFO, String.format("* Register Connector: %s Completed", file.getName()));
     }
-    printUtil.print(PrintUtil.TYPE.DEBUG, "result:" + result + ", name:" + file.getName() + ", msg:" + msg + ", response:" + response);
+    printUtil.print(PrintUtil.TYPE.WARN, "result:" + result + ", name:" + file.getName() + ", msg:" + msg + ", response:" + response);
   }
 
   public static RequestBody create(final MediaType mediaType, final InputStream inputStream) {
diff --git a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/utils/PrintUtil.java b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/utils/PrintUtil.java
index 20a646b8bd..08363a64bf 100644
--- a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/utils/PrintUtil.java
+++ b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/utils/PrintUtil.java
@@ -1,13 +1,16 @@
 package io.tapdata.pdk.cli.utils;
 
 
+import io.tapdata.entity.logger.TapLogger;
 import picocli.CommandLine;
 
 public class PrintUtil {
     boolean showAllMessage;
+
     public PrintUtil(boolean showAllMessage) {
         this.showAllMessage = showAllMessage;
     }
+
     public void print(TYPE type, String message) {
         if (type == TYPE.DEBUG && !showAllMessage) {
             return;
@@ -40,9 +43,11 @@ public void print(TYPE type, String message) {
                 print(message);
         }
     }
+
     public void printAppend(String message) {
         System.out.print(CommandLine.Help.Ansi.AUTO.string("@|bold,fg(250) " + message + "|@"));
     }
+
     public void print(String message) {
         System.out.println(message);
     }
@@ -56,4 +61,38 @@ public enum TYPE {
         TIP,
         ERROR
     }
+
+    public TapLogger.LogListener getLogListener() {
+        return new TapLogger.LogListener() {
+            @Override
+            public void debug(String log) {
+                print(TYPE.DEBUG, log);
+            }
+
+            @Override
+            public void info(String log) {
+                print(TYPE.INFO, log);
+            }
+
+            @Override
+            public void warn(String log) {
+                print(TYPE.WARN, log);
+            }
+
+            @Override
+            public void error(String log) {
+                print(TYPE.ERROR, log);
+            }
+
+            @Override
+            public void fatal(String log) {
+                print(TYPE.ERROR, log);
+            }
+
+            @Override
+            public void memory(String memoryLog) {
+                print(TYPE.APPEND, memoryLog);
+            }
+        };
+    }
 }


Test patch:
diff --git a/manager/tm-api/src/test/java/com/tapdata/tm/inspect/service/InspectDetailsServiceTest.java b/manager/tm-api/src/test/java/com/tapdata/tm/inspect/service/InspectDetailsServiceTest.java
index 2bc3590a0d..d2e77dfbd5 100644
--- a/manager/tm-api/src/test/java/com/tapdata/tm/inspect/service/InspectDetailsServiceTest.java
+++ b/manager/tm-api/src/test/java/com/tapdata/tm/inspect/service/InspectDetailsServiceTest.java
@@ -13,6 +13,7 @@
 import org.bson.types.ObjectId;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.mockito.internal.verification.Times;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.mongodb.core.query.Criteria;
 
@@ -300,9 +301,16 @@ void testExport_fullFiled_false_index() throws IOException {
 
         }
 
-
-
-
+        @Test
+        void testExport_fullFiled_false_message_null() {
+            InspectDetailsService inspectDetailsService = mock(InspectDetailsService.class);
+            InspectDetailsDto inspectDetailsDto = mock(InspectDetailsDto.class);
+            InspectResultDto inspectResultDto = mock(InspectResultDto.class);
+            doCallRealMethod().when(inspectDetailsService).compareDifferenceFields(inspectDetailsDto, inspectResultDto);
+            inspectDetailsService.compareDifferenceFields(inspectDetailsDto, inspectResultDto);
+            verify(inspectDetailsDto, new Times(0)).setSource(anyMap());
+            verify(inspectDetailsDto, new Times(0)).setTarget(anyMap());
+        }
 
     }
 
diff --git a/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
index 3998ddfa4c..50fe3eebd5 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
@@ -1,9 +1,8 @@
 package com.tapdata.tm.Settings.service;
 
 import com.tapdata.tm.Settings.constant.SettingUtil;
-import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
-import com.tapdata.tm.Settings.dto.SendMailResponseDto;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -183,6 +182,24 @@ void testFindALl_isDASS() {
             final List<SettingsDto> result = settingsService.findALl("decode", filter);
             assertThat(result.get(0).getValue()).isEqualTo(settings.getValue());
         }
+        @Test
+        void testFindALlWithPwd() {
+            final Filter filter = new Filter();
+            Settings settings1 = new Settings();
+            settings1.setKey("smtp.server.password");
+            settings1.setValue("123456");
+            Settings settings2 = new Settings();
+            settings2.setKey("ad.bind.password");
+            settings2.setValue("12345");
+            List<Settings> list = new ArrayList<>();
+            list.add(settings1);
+            list.add(settings2);
+            when(mongoTemplate.find(any(Query.class), eq(Settings.class))).thenReturn(list);
+            when(mockSettingsRepository.findAll()).thenReturn(list);
+            final List<SettingsDto> result = settingsService.findALl("decode", filter);
+            assertEquals("*****", result.get(0).getValue());
+            assertEquals("*****", result.get(0).getValue());
+        }
     }
     @Nested
     class getMailAccountWithTestMailDtoTest{
@@ -229,7 +246,7 @@ void testSendMailNormal(){
                 when(mailAccountDto.getPass()).thenReturn("*****");
                 when(settingsService.getMailAccount(testMailDto)).thenReturn(mailAccountDto);
                 doCallRealMethod().when(settingsService).testSendMail(testMailDto);
-                SendMailResponseDto actual = settingsService.testSendMail(testMailDto);
+                TestResponseDto actual = settingsService.testSendMail(testMailDto);
                 assertEquals(false,actual.isResult());
             }
         }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionServiceTest.java
index 1b3d608ec9..01e65c56cf 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/metadatadefinition/service/MetadataDefinitionServiceTest.java
@@ -1,9 +1,12 @@
 package com.tapdata.tm.metadatadefinition.service;
 
 import com.mongodb.client.result.UpdateResult;
+import com.tapdata.tm.Settings.service.SettingsService;
 import com.tapdata.tm.agent.dto.GroupDto;
+import com.tapdata.tm.base.dto.Filter;
 import com.tapdata.tm.config.security.UserDetail;
 import com.tapdata.tm.metadatadefinition.param.BatchUpdateParam;
+import com.tapdata.tm.metadatadefinition.repository.MetadataDefinitionRepository;
 import com.tapdata.tm.utils.Lists;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
@@ -71,4 +74,32 @@ void testOther() {
             doVerify("", 0);
         }
     }
+    @Nested
+    class FindTest{
+        SettingsService settingsService;
+        MetadataDefinitionRepository metadataDefinitionRepository;
+        MetadataDefinitionService metadataDefinitionService;
+        @BeforeEach
+        void init() {
+            settingsService = mock(SettingsService.class);
+            metadataDefinitionRepository = mock(MetadataDefinitionRepository.class);
+            metadataDefinitionService = new MetadataDefinitionService(metadataDefinitionRepository);
+            ReflectionTestUtils.setField(metadataDefinitionService, "settingsService", settingsService);
+        }
+        @Test
+        void testCloud() {
+            when(settingsService.isCloud()).thenReturn(true);
+            Filter filter = new Filter();
+            metadataDefinitionService.find(filter,mock(UserDetail.class));
+            verify(metadataDefinitionRepository,times(1)).findAll(any(Filter.class),any(UserDetail.class));
+        }
+
+        @Test
+        void testDass() {
+            when(settingsService.isCloud()).thenReturn(false);
+            Filter filter = new Filter();
+            metadataDefinitionService.find(filter,mock(UserDetail.class));
+            verify(metadataDefinitionRepository,times(1)).findAll(any(Filter.class));
+        }
+    }
 }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java
index ab155d18dd..a76830aeec 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java
@@ -1,44 +1,67 @@
 package com.tapdata.tm.user.service;
 
+import com.tapdata.tm.Settings.constant.CategoryEnum;
+import com.tapdata.tm.Settings.constant.KeyEnum;
+import com.tapdata.tm.Settings.constant.SettingUtil;
+import com.tapdata.tm.Settings.dto.TestResponseDto;
+import com.tapdata.tm.Settings.entity.Settings;
+import com.tapdata.tm.Settings.service.SettingsService;
+import com.tapdata.tm.base.exception.BizException;
 import com.tapdata.tm.config.security.UserDetail;
 import com.tapdata.tm.roleMapping.dto.RoleMappingDto;
 import com.tapdata.tm.roleMapping.service.RoleMappingService;
-import com.tapdata.tm.user.service.UserServiceImpl;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
+import com.tapdata.tm.user.dto.LdapLoginDto;
+import com.tapdata.tm.user.dto.TestLdapDto;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.*;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 import org.mockito.internal.verification.Times;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.test.util.ReflectionTestUtils;
 
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.*;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
 public class UserServiceImplTest {
     private UserServiceImpl userService;
     private RoleMappingService roleMappingService;
+    private SettingsService settingsService;
+
     @BeforeEach
     void beforeEach(){
         userService = mock(UserServiceImpl.class);
         roleMappingService = mock(RoleMappingService.class);
-        ReflectionTestUtils.setField(userService,"roleMappingService",roleMappingService);
+        settingsService = mock(SettingsService.class);
+        ReflectionTestUtils.setField(userService, "settingsService", settingsService);
+        ReflectionTestUtils.setField(userService, "roleMappingService", roleMappingService);
     }
+
     @Nested
     class updateRoleMapping{
         private String userId;
         private List<Object> roleusers;
         private UserDetail userDetail;
+
         @BeforeEach
         void beforeEach(){
             userId = "66c84372a5921a16459c2cef";
             roleusers = new ArrayList<>();
             userDetail = mock(UserDetail.class);
         }
+
         @Test
         void testForAdmin(){
             doCallRealMethod().when(userService).updateRoleMapping(userId,roleusers,userDetail);
@@ -46,6 +69,7 @@ void testForAdmin(){
             verify(roleMappingService,new Times(0)).deleteAll(any(Query.class));
             assertNull(actual);
         }
+
         @Test
         void testForUser(){
             when(userDetail.getEmail()).thenReturn("test@tapdata.com");
@@ -56,4 +80,451 @@ void testForUser(){
             verify(roleMappingService,new Times(1)).updateUserRoleMapping(anyList(), any(UserDetail.class));
         }
     }
+
+    @Nested
+    class checkADLoginEnable {
+        @BeforeEach
+        void beforeEach() {
+            doCallRealMethod().when(userService).checkLdapLoginEnable();
+        }
+
+        @Test
+        @DisplayName("test checkADLoginEnable method when open is true")
+        void test1() {
+            Settings settings = new Settings();
+            settings.setOpen(true);
+            when(settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_LOGIN_ENABLE)).thenReturn(settings);
+            boolean result = userService.checkLdapLoginEnable();
+            assertTrue(result);
+        }
+
+        @Test
+        @DisplayName("test checkADLoginEnable method when settings is null")
+        void test2() {
+            when(settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_LOGIN_ENABLE)).thenReturn(null);
+            boolean result = userService.checkLdapLoginEnable();
+            assertFalse(result);
+        }
+    }
+
+    @Nested
+    class testLoginByADTest {
+        private DirContext dirContext;
+        private TestLdapDto testAdDto;
+
+        @BeforeEach
+        @SneakyThrows
+        void beforeEach() {
+            dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(dirContext);
+            doCallRealMethod().when(userService).testLoginByLdap(any(TestLdapDto.class));
+            testAdDto = new TestLdapDto();
+            testAdDto.setLdap_Server_Host("ldap://ad.example.com");
+            testAdDto.setLdap_Server_Port("389");
+            testAdDto.setLdap_Bind_DN("CN=Admin,CN=Users,DC=example,DC=com");
+            testAdDto.setLdap_SSL_Enable(false);
+        }
+
+        @Test
+        @SneakyThrows
+        void testLoginByAD_SuccessfulConnection() {
+            testAdDto.setLdap_Bind_Password("password");
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(dirContext);
+            TestResponseDto response = userService.testLoginByLdap(testAdDto);
+            assertTrue(response.isResult());
+            assertNull(response.getStack());
+        }
+
+        @Test
+        @SneakyThrows
+        void testLoginByAD_FailedConnection() {
+            testAdDto.setLdap_Bind_Password("password");
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(null);
+            TestResponseDto response = userService.testLoginByLdap(testAdDto);
+            assertFalse(response.isResult());
+            assertEquals("connect to active directory server failed", response.getStack());
+        }
+
+        @Test
+        @Disabled
+        @SneakyThrows
+        void testLoginByAD_ThrowsNamingException() {
+            testAdDto.setLdap_Bind_Password("password");
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenThrow(new NamingException("LDAP connection failed"));
+            TestResponseDto response = userService.testLoginByLdap(testAdDto);
+            assertFalse(response.isResult());
+            assertTrue(response.getStack().contains("LDAP connection failed"));
+        }
+
+        @Test
+        @SneakyThrows
+        void testLoginByAD_EncryptedPassword() {
+            try (MockedStatic<SettingUtil> mb = Mockito
+                    .mockStatic(SettingUtil.class)) {
+                mb.when(() -> SettingUtil.getValue("Active_Directory", "ad.bind.password")).thenReturn("123456");
+                testAdDto.setLdap_Bind_Password("*****");
+                when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(dirContext);
+                TestResponseDto response = userService.testLoginByLdap(testAdDto);
+                assertTrue(response.isResult());
+                assertNull(response.getStack());
+            }
+        }
+    }
+
+    @Nested
+    class loginByADTest {
+        private List<Settings> settingsList;
+        private String username;
+        private String password;
+
+        @BeforeEach
+        void beforeEach() {
+            username = "user1";
+            password = "password1";
+            Settings settings1 = new Settings();
+            settings1.setKey("ad.server.host");
+            settings1.setValue("ldap://ad.example.com");
+            Settings settings2 = new Settings();
+            settings2.setKey("ad.server.port");
+            settings2.setValue("389");
+            Settings settings3 = new Settings();
+            settings3.setKey("ad.bind.dn");
+            settings3.setValue("CN=Admin,CN=Users,DC=example,DC=com");
+            Settings settings4 = new Settings();
+            settings4.setKey("ad.bind.password");
+            settings4.setValue("adminPassword");
+            Settings settings5 = new Settings();
+            settings5.setKey("ad.base.dn");
+            settings5.setValue("DC=example,DC=com");
+            settingsList = Arrays.asList(
+                    settings1, settings2, settings3, settings4, settings5
+            );
+            doCallRealMethod().when(userService).loginByLdap(username, password);
+        }
+
+        @Test
+        void testLoginByAD_Success() throws NamingException {
+            when(settingsService.findAll()).thenReturn(settingsList);
+            Settings settings = new Settings();
+            settings.setKey("ad.ssl.enable");
+            settings.setOpen(true);
+            when(settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_SSL_ENABLE))
+                    .thenReturn(settings);
+            when(userService.searchUser(any(LdapLoginDto.class), eq(username))).thenReturn(true);
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(dirContext);
+            boolean result = userService.loginByLdap(username, password);
+            assertTrue(result);
+        }
+
+        @Test
+        void testLoginByAD_UserNotExists() throws NamingException {
+            when(settingsService.findAll()).thenReturn(settingsList);
+            when(userService.searchUser(any(LdapLoginDto.class), eq(username))).thenReturn(false);
+            BizException thrown = assertThrows(BizException.class, () -> userService.loginByLdap(username, password));
+            assertEquals("AD.Account.Not.Exists", thrown.getErrorCode());
+        }
+
+        @Test
+        void testLoginByAD_NamingException() throws NamingException {
+            when(settingsService.findAll()).thenReturn(settingsList);
+            when(userService.searchUser(any(LdapLoginDto.class), eq(username))).thenReturn(true);
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenThrow(new NamingException("LDAP connection failed"));
+            BizException thrown = assertThrows(BizException.class, () -> userService.loginByLdap(username, password));
+            assertTrue(thrown.getErrorCode().contains("AD.Login.Fail"));
+        }
+
+        @Test
+        void testLoginByAD_SSLDisabled() throws NamingException {
+            when(settingsService.findAll()).thenReturn(settingsList);
+            Settings settings = new Settings();
+            settings.setKey("ad.ssl.enable");
+            settings.setOpen(true);
+            when(settingsService.getByCategoryAndKey(CategoryEnum.LDAP, KeyEnum.LDAP_SSL_ENABLE))
+                    .thenReturn(settings);
+            when(userService.searchUser(any(LdapLoginDto.class), eq(username))).thenReturn(true);
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(any(LdapLoginDto.class))).thenReturn(dirContext);
+            boolean result = userService.loginByLdap(username, password);
+            assertTrue(result);
+        }
+    }
+
+    @Nested
+    class searchUserTest {
+        @BeforeEach
+        @SneakyThrows
+        void beforeEach() {
+            doCallRealMethod().when(userService).searchUser(any(LdapLoginDto.class), anyString());
+        }
+
+        @Test
+        void testSearchUser_SuccessWithSAMAccountName() throws NamingException {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .baseDN("DC=example,DC=com")
+                    .ldapUrl("ldap://ad.example.com:389")
+                    .build();
+            String username = "user1";
+            String sAMAccountNameFilter = "(sAMAccountName=user1)";
+            String userPrincipalNameFilter = "(userPrincipalName=user1)";
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(adLoginDto)).thenReturn(dirContext);
+            when(userService.searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(sAMAccountNameFilter), any(SearchControls.class)))
+                    .thenReturn(true);
+            boolean result = userService.searchUser(adLoginDto, username);
+            assertTrue(result);
+            verify(userService, times(1)).searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(sAMAccountNameFilter), any(SearchControls.class));
+        }
+
+        @Test
+        void testSearchUser_SuccessWithUserPrincipalName() throws NamingException {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .baseDN("DC=example,DC=com")
+                    .ldapUrl("ldap://ad.example.com:389")
+                    .build();
+            String username = "user1";
+            String sAMAccountNameFilter = "(sAMAccountName=user1)";
+            String userPrincipalNameFilter = "(userPrincipalName=user1)";
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(adLoginDto)).thenReturn(dirContext);
+            when(userService.searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(sAMAccountNameFilter), any(SearchControls.class)))
+                    .thenReturn(false);
+            when(userService.searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(userPrincipalNameFilter), any(SearchControls.class)))
+                    .thenReturn(true);
+
+            boolean result = userService.searchUser(adLoginDto, username);
+
+            assertTrue(result);
+            verify(userService, times(1)).searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(userPrincipalNameFilter), any(SearchControls.class));
+        }
+
+        @Test
+        void testSearchUser_UserNotFound() throws NamingException {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .baseDN("DC=example,DC=com")
+                    .ldapUrl("ldap://ad.example.com:389")
+                    .build();
+            String username = "user1";
+            String sAMAccountNameFilter = "(sAMAccountName=user1)";
+            String userPrincipalNameFilter = "(userPrincipalName=user1)";
+
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(adLoginDto)).thenReturn(dirContext);
+            when(userService.searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(sAMAccountNameFilter), any(SearchControls.class)))
+                    .thenReturn(false);
+            when(userService.searchWithFilter(eq(dirContext), eq(adLoginDto.getBaseDN()), eq(userPrincipalNameFilter), any(SearchControls.class)))
+                    .thenReturn(false);
+            boolean result = userService.searchUser(adLoginDto, username);
+            assertFalse(result);
+        }
+
+        @Test
+        void testSearchUser_NamingExceptionThrown() throws NamingException {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .baseDN("DC=example,DC=com")
+                    .ldapUrl("ldap://ad.example.com:389")
+                    .build();
+            String username = "user1";
+            DirContext dirContext = mock(DirContext.class);
+            when(userService.buildDirContext(adLoginDto)).thenReturn(dirContext);
+            when(userService.searchWithFilter(any(), any(), any(), any())).thenThrow(new NamingException("Search failed"));
+            BizException thrown = assertThrows(BizException.class, () -> userService.searchUser(adLoginDto, username));
+            assertTrue(thrown.getErrorCode().contains("AD.Search.Fail"));
+        }
+    }
+
+    @Nested
+    class searchWithFilterTest {
+        private DirContext dirContext;
+        private NamingEnumeration<SearchResult> namingEnum;
+        private SearchResult searchResult;
+        private Attributes attributes;
+        private String searchBase;
+        private String filter;
+
+        @BeforeEach
+        @SneakyThrows
+        void beforeEach() {
+            searchBase = "DC=example,DC=com";
+            filter = "(sAMAccountName=user1)";
+            searchResult = mock(SearchResult.class);
+            dirContext = mock(DirContext.class);
+            namingEnum = mock(NamingEnumeration.class);
+            attributes = mock(Attributes.class);
+            doCallRealMethod().when(userService).searchWithFilter(any(DirContext.class), anyString(), anyString(), any(SearchControls.class));
+        }
+
+        @Test
+        public void testSearchWithFilter_FoundUser() throws NamingException {
+            SearchControls searchControls = new SearchControls();
+            when(dirContext.search(searchBase, filter, searchControls)).thenReturn(namingEnum);
+            when(namingEnum.hasMore()).thenReturn(true).thenReturn(false); // First call returns true, then false
+            when(namingEnum.next()).thenReturn(searchResult);
+            when(searchResult.getAttributes()).thenReturn(attributes);
+            Attribute attribute = mock(Attribute.class);
+            when(attribute.get()).thenReturn("user1@example.com");
+            when(attributes.get("userPrincipalName")).thenReturn(attribute);
+            Attribute attribute1 = mock(Attribute.class);
+            when(attribute1.get()).thenReturn("User One");
+            when(attributes.get("displayName")).thenReturn(attribute1);
+            boolean result = userService.searchWithFilter(dirContext, searchBase, filter, searchControls);
+            assertTrue(result);
+        }
+
+        @Test
+        public void testSearchWithFilter_UserNotFound() throws NamingException {
+            SearchControls searchControls = new SearchControls();
+            when(dirContext.search(searchBase, filter, searchControls)).thenReturn(namingEnum);
+            when(namingEnum.hasMore()).thenReturn(false);
+            boolean result = userService.searchWithFilter(dirContext, searchBase, filter, searchControls);
+            assertFalse(result);
+        }
+
+        @Test
+        public void testSearchWithFilter_UserPrincipalNameMissing() throws NamingException {
+            SearchControls searchControls = new SearchControls();
+            when(dirContext.search(searchBase, filter, searchControls)).thenReturn(namingEnum);
+            when(namingEnum.hasMore()).thenReturn(true).thenReturn(false);
+            when(namingEnum.next()).thenReturn(searchResult);
+            when(searchResult.getAttributes()).thenReturn(attributes);
+            when(attributes.get("userPrincipalName")).thenReturn(null);
+            Attribute attribute1 = mock(Attribute.class);
+            when(attribute1.get()).thenReturn("User One");
+            when(attributes.get("displayName")).thenReturn(attribute1);
+            boolean result = userService.searchWithFilter(dirContext, searchBase, filter, searchControls);
+            assertTrue(result);
+        }
+
+        @Test
+        public void testSearchWithFilter_DisplayNameMissing() throws NamingException {
+            String searchBase = "DC=example,DC=com";
+            String filter = "(sAMAccountName=user1)";
+            SearchControls searchControls = new SearchControls();
+            when(dirContext.search(searchBase, filter, searchControls)).thenReturn(namingEnum);
+            when(namingEnum.hasMore()).thenReturn(true).thenReturn(false);
+            when(namingEnum.next()).thenReturn(searchResult);
+            when(searchResult.getAttributes()).thenReturn(attributes);
+            Attribute attribute = mock(Attribute.class);
+            when(attribute.get()).thenReturn("user1@example.com");
+            when(attributes.get("userPrincipalName")).thenReturn(attribute);
+            when(attributes.get("displayName")).thenReturn(null);
+            boolean result = userService.searchWithFilter(dirContext, searchBase, filter, searchControls);
+            assertTrue(result);
+        }
+
+        @Test
+        void testSearchWithFilter_NamingExceptionThrown() throws NamingException {
+            String searchBase = "DC=example,DC=com";
+            String filter = "(sAMAccountName=user1)";
+            SearchControls searchControls = new SearchControls();
+            when(dirContext.search(searchBase, filter, searchControls)).thenThrow(new NamingException("Search failed"));
+            NamingException thrown = assertThrows(NamingException.class, () -> {
+                userService.searchWithFilter(dirContext, searchBase, filter, searchControls);
+            });
+            assertEquals("Search failed", thrown.getMessage());
+        }
+    }
+
+    @Nested
+    class buildDirContextTest {
+        private InitialDirContext dirContext;
+
+        @BeforeEach
+        @SneakyThrows
+        void beforeEach() {
+            dirContext = mock(InitialDirContext.class);
+            doCallRealMethod().when(userService).buildDirContext(any(LdapLoginDto.class));
+        }
+
+        @Test
+        public void testBuildDirContext_NoSSL() throws NamingException {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .ldapUrl("ldap://example.com:389")
+                    .bindDN("cn=admin,dc=example,dc=com")
+                    .password("password")
+                    .sslEnable(false)
+                    .build();
+            assertThrows(NamingException.class, () -> userService.buildDirContext(adLoginDto));
+        }
+
+        @Test
+        public void testBuildDirContext_NamingExceptionThrown() {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .ldapUrl("ldap://example.com:389")
+                    .bindDN("cn=admin,dc=example,dc=com")
+                    .password("password")
+                    .sslEnable(true)
+                    .build();
+            assertThrows(BizException.class, () -> userService.buildDirContext(adLoginDto));
+        }
+
+        @Test
+        @SneakyThrows
+        public void testBuildDirContext_SSL() {
+            LdapLoginDto adLoginDto = LdapLoginDto.builder()
+                    .ldapUrl("ldap://example.com:389")
+                    .bindDN("cn=admin,dc=example,dc=com")
+                    .password("password")
+                    .sslEnable(true)
+                    .cert(certString)
+                    .build();
+            SSLContext sslContext = mock(SSLContext.class);
+            when(userService.createSSLContext(any(InputStream.class))).thenReturn(sslContext);
+            when(sslContext.getSocketFactory()).thenReturn(mock(SSLSocketFactory.class));
+            assertThrows(NamingException.class, () -> userService.buildDirContext(adLoginDto));
+        }
+    }
+
+    @Nested
+    class createSSLContextTest {
+        @Test
+        public void testCreateSSLContext() throws Exception {
+            InputStream certFile = new ByteArrayInputStream(certString.getBytes());
+
+            doCallRealMethod().when(userService).createSSLContext(certFile);
+            SSLContext sslContext = userService.createSSLContext(certFile);
+
+            assertNotNull(sslContext);
+            assertNotNull(sslContext.getSocketFactory());
+
+        }
+    }
+    private String certString = "-----BEGIN CERTIFICATE-----\n" +
+            "MIIGpzCCBY+gAwIBAgITQwAAAAJuc4zIBYqYIwAAAAAAAjANBgkqhkiG9w0BAQUF\n" +
+            "ADBwMRIwEAYKCZImiZPyLGQBGRYCaW8xFzAVBgoJkiaJk/IsZAEZFgd0YXBkYXRh\n" +
+            "MRgwFgYKCZImiZPyLGQBGRYIaW50ZXJuYWwxEjAQBgoJkiaJk/IsZAEZFgJhZDET\n" +
+            "MBEGA1UEAxMKdGFwZGF0YS1DQTAeFw0yNDA5MTMxNTQ1NTJaFw0yNTA5MTMxNTQ1\n" +
+            "NTJaMDExLzAtBgNVBAMTJmlaOTk4eHJoNnkyYjNiWi5hZC5pbnRlcm5hbC50YXBk\n" +
+            "YXRhLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo8mqTkTvFfu+\n" +
+            "LXH69clyyxGw9Na8FKdmySUnI7zF2csyfryfU2+Lxi/Xq1q8WkjCpMQ+lRJ5K5WJ\n" +
+            "7TOZ8FaWELFXGJDSdixRuK5aORjWz1KzMWZPW6w62FNLQIfpOtgVQebLe8it7eQR\n" +
+            "WA6PtILbwEtkQtgagwtWmuf9linElM48WSEmpz78eFkW83vgJGfr2nYXyP7VqY83\n" +
+            "XvcagE75nvg1CzgADaCm1plxoSyyx887C3AagSnamenZPH4Dtk3vi2RSJUsVGPcA\n" +
+            "RQg6IViA1cradLu+Q2Htxwh2hL2+lJ/979qILEewFFTIwt9cOT5GoElrW21TSSpI\n" +
+            "p5tQywmPGwIDAQABo4IDdzCCA3MwLwYJKwYBBAGCNxQCBCIeIABEAG8AbQBhAGkA\n" +
+            "bgBDAG8AbgB0AHIAbwBsAGwAZQByMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF\n" +
+            "BQcDATAOBgNVHQ8BAf8EBAMCBaAweAYJKoZIhvcNAQkPBGswaTAOBggqhkiG9w0D\n" +
+            "AgICAIAwDgYIKoZIhvcNAwQCAgCAMAsGCWCGSAFlAwQBKjALBglghkgBZQMEAS0w\n" +
+            "CwYJYIZIAWUDBAECMAsGCWCGSAFlAwQBBTAHBgUrDgMCBzAKBggqhkiG9w0DBzAd\n" +
+            "BgNVHQ4EFgQU9hwjXlc86SCkrWFyFlkg+KWh/IMwHwYDVR0jBBgwFoAU0PZvyVVN\n" +
+            "5kp+RcZAZ76MbVHAIw8wgeEGA1UdHwSB2TCB1jCB06CB0KCBzYaBymxkYXA6Ly8v\n" +
+            "Q049dGFwZGF0YS1DQSxDTj1pWjk5OHhyaDZ5MmIzYlosQ049Q0RQLENOPVB1Ymxp\n" +
+            "YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24s\n" +
+            "REM9YWQsREM9aW50ZXJuYWwsREM9dGFwZGF0YSxEQz1pbz9jZXJ0aWZpY2F0ZVJl\n" +
+            "dm9jYXRpb25MaXN0P2Jhc2U/b2JqZWN0Q2xhc3M9Y1JMRGlzdHJpYnV0aW9uUG9p\n" +
+            "bnQwgc0GCCsGAQUFBwEBBIHAMIG9MIG6BggrBgEFBQcwAoaBrWxkYXA6Ly8vQ049\n" +
+            "dGFwZGF0YS1DQSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049\n" +
+            "U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1pbnRlcm5hbCxEQz10\n" +
+            "YXBkYXRhLERDPWlvP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0\n" +
+            "aWZpY2F0aW9uQXV0aG9yaXR5MFIGA1UdEQRLMEmgHwYJKwYBBAGCNxkBoBIEEHRb\n" +
+            "dGzUREpNjZzesBnzoguCJmlaOTk4eHJoNnkyYjNiWi5hZC5pbnRlcm5hbC50YXBk\n" +
+            "YXRhLmlvME8GCSsGAQQBgjcZAgRCMECgPgYKKwYBBAGCNxkCAaAwBC5TLTEtNS0y\n" +
+            "MS0yNTA2MzQzMDk4LTM0MzExNDI3NzUtMzEwMjg3OTkwMi0xMDAwMA0GCSqGSIb3\n" +
+            "DQEBBQUAA4IBAQCFJ40kMxvgjpFF/cTglqKBO4MJSMH3wJpN+VG33/WccM62osm4\n" +
+            "TB/QydzHuSWtn52fdtp8Q82dmDvI2QldYyJLvifR+1TGbOt8dc5866jRNqUeRr7Q\n" +
+            "KmTTPxGInM64b/aYYF0cQxC9HX1ct8aTlTArfSztoWgLWG3aXtBa0+TBMuvIDn5o\n" +
+            "dUxc9/NvLsksOecz/AHfIPWmEKu9w+OtnbBnoo5ymB7tmSPUUR54pd5Ul06LvlWs\n" +
+            "hinNPHvFAMDUY7HfrdDfCoEIZSK4u/xFQscsNsE3hywVYYN7uz8wy502fEHzwIgp\n" +
+            "/3vC2igEWcqhxJn9iiIHAzA0mZoS1DOKhgFG\n" +
+            "-----END CERTIFICATE-----";
 }