Ver código fonte

fix(ldap): attach auto-created clients to every configured inbound tag

The sync job built an independent client per configured tag and called
CreateOne once per tag. Each call generated a fresh random subId, and
the email-uniqueness check in ClientService.Create only re-admits a
taken email when the incoming subId matches the stored one - so the
first tag succeeded and every other tag failed with "email already in
use", leaving new LDAP users on a single inbound.

Build the client once per email and hand ClientService.Create the full
list of resolved inbound ids, the same path the panel's own client
create endpoint uses: one identity (email, subId) attached to all
configured tags, with per-protocol credentials filled per inbound.
Unknown tags are now skipped with a warning instead of building
clients against a nil inbound.

Closes #5846
MHSanaei 11 horas atrás
pai
commit
52d4af71bc
2 arquivos alterados com 141 adições e 47 exclusões
  1. 56 47
      internal/web/job/ldap_sync_job.go
  2. 85 0
      internal/web/job/ldap_sync_job_test.go

+ 56 - 47
internal/web/job/ldap_sync_job.go

@@ -4,8 +4,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/google/uuid"
-
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 	ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
@@ -114,52 +112,41 @@ func (j *LdapSyncJob) Run() {
 	defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
 	defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
 
-	clientsToCreate := map[string][]model.Client{} // tag -> []new clients
-	clientsToEnable := map[string][]string{}       // tag -> []email
-	clientsToDisable := map[string][]string{}      // tag -> []email
-
-	for email, allowed := range flags {
-		exists := allClients[email] != nil
-		for _, tag := range inboundTags {
-			if !exists && allowed && autoCreate {
-				newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
-				clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
-			} else if exists {
-				if allowed && !allClients[email].Enable {
-					clientsToEnable[tag] = append(clientsToEnable[tag], email)
-				} else if !allowed && allClients[email].Enable {
-					clientsToDisable[tag] = append(clientsToDisable[tag], email)
-				}
-			}
+	resolvedInboundIds := make([]int, 0, len(inboundTags))
+	resolvedTags := make([]string, 0, len(inboundTags))
+	for _, tag := range inboundTags {
+		ib := inboundMap[tag]
+		if ib == nil {
+			logger.Warningf("LDAP inbound tag %s does not match any inbound", tag)
+			continue
 		}
+		resolvedInboundIds = append(resolvedInboundIds, ib.Id)
+		resolvedTags = append(resolvedTags, tag)
 	}
 
-	for tag, newClients := range clientsToCreate {
-		if len(newClients) == 0 {
-			continue
-		}
-		ib := inboundMap[tag]
-		created := 0
-		restartNeeded := false
-		for _, c := range newClients {
-			nr, err := j.clientService.CreateOne(&j.inboundService, ib.Id, c)
-			if err != nil {
-				logger.Warningf("Failed to add client %s for tag %s: %v", c.Email, tag, err)
-				continue
-			}
-			created++
-			if nr {
-				restartNeeded = true
+	clientsToCreate := []model.Client{}
+	clientsToEnable := map[string][]string{}  // tag -> []email
+	clientsToDisable := map[string][]string{} // tag -> []email
+
+	for email, allowed := range flags {
+		existing := allClients[email]
+		if existing == nil {
+			if allowed && autoCreate {
+				clientsToCreate = append(clientsToCreate, j.buildClient(email, defGB, defExpiryDays, defLimitIP))
 			}
+			continue
 		}
-		if created > 0 {
-			logger.Infof("LDAP auto-create: %d clients for %s", created, tag)
-			if restartNeeded {
-				j.xrayService.SetToNeedRestart()
+		for _, tag := range resolvedTags {
+			if allowed && !existing.Enable {
+				clientsToEnable[tag] = append(clientsToEnable[tag], email)
+			} else if !allowed && existing.Enable {
+				clientsToDisable[tag] = append(clientsToDisable[tag], email)
 			}
 		}
 	}
 
+	j.createClients(clientsToCreate, resolvedInboundIds, resolvedTags)
+
 	// --- Execute enable/disable batch ---
 	for tag, emails := range clientsToEnable {
 		j.batchSetEnable(inboundMap[tag], emails, true)
@@ -196,8 +183,8 @@ func splitCsv(s string) []string {
 	return out
 }
 
-// buildClient creates a new client for auto-create
-func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
+// buildClient creates a new client for auto-create; ClientService.Create fills per-protocol credentials
+func (j *LdapSyncJob) buildClient(email string, defGB, defExpiryDays, defLimitIP int) model.Client {
 	c := model.Client{
 		Email:   email,
 		Enable:  true,
@@ -207,15 +194,37 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp
 	if defExpiryDays > 0 {
 		c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
 	}
-	switch ib.Protocol {
-	case model.Trojan, model.Shadowsocks:
-		c.Password = uuid.NewString()
-	default:
-		c.ID = uuid.NewString()
-	}
 	return c
 }
 
+// createClients adds each new LDAP client once, attached to every configured inbound
+func (j *LdapSyncJob) createClients(newClients []model.Client, inboundIds []int, tags []string) {
+	if len(newClients) == 0 || len(inboundIds) == 0 {
+		return
+	}
+	tagList := strings.Join(tags, ",")
+	created := 0
+	restartNeeded := false
+	for _, c := range newClients {
+		nr, err := j.clientService.Create(&j.inboundService, &service.ClientCreatePayload{Client: c, InboundIds: inboundIds})
+		if err != nil {
+			logger.Warningf("Failed to add client %s for tags %s: %v", c.Email, tagList, err)
+			continue
+		}
+		created++
+		if nr {
+			restartNeeded = true
+		}
+	}
+	if created == 0 {
+		return
+	}
+	logger.Infof("LDAP auto-create: %d clients for %s", created, tagList)
+	if restartNeeded {
+		j.xrayService.SetToNeedRestart()
+	}
+}
+
 func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
 	if len(emails) == 0 {
 		return

+ 85 - 0
internal/web/job/ldap_sync_job_test.go

@@ -0,0 +1,85 @@
+package job
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database"
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
+)
+
+func initLdapJobDB(t *testing.T) {
+	t.Helper()
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB: %v", err)
+	}
+	t.Cleanup(func() { _ = database.CloseDB() })
+}
+
+func TestLdapCreateClients_AttachesToAllConfiguredInbounds(t *testing.T) {
+	initLdapJobDB(t)
+	db := database.GetDB()
+
+	tags := []string{"in-1080-tcp", "in-1081-tcp", "in-1082-tcp"}
+	protocols := []model.Protocol{model.VLESS, model.Trojan, model.VLESS}
+	inboundIds := make([]int, 0, len(tags))
+	for i, tag := range tags {
+		ib := &model.Inbound{
+			UserId:         1,
+			Tag:            tag,
+			Enable:         true,
+			Port:           42080 + i,
+			Protocol:       protocols[i],
+			Settings:       `{"clients": []}`,
+			StreamSettings: `{"network":"tcp","security":"none"}`,
+		}
+		if err := db.Create(ib).Error; err != nil {
+			t.Fatalf("create inbound %s: %v", tag, err)
+		}
+		inboundIds = append(inboundIds, ib.Id)
+	}
+
+	j := NewLdapSyncJob()
+	const email = "[email protected]"
+	j.createClients([]model.Client{j.buildClient(email, 0, 0, 0)}, inboundIds, tags)
+
+	rec := &model.ClientRecord{}
+	if err := db.Where("email = ?", email).First(rec).Error; err != nil {
+		t.Fatalf("client record for %s not created: %v", email, err)
+	}
+	if rec.SubID == "" {
+		t.Error("created LDAP client must carry a subId")
+	}
+
+	clientSvc := &service.ClientService{}
+	for i, id := range inboundIds {
+		clients, err := clientSvc.ListForInbound(nil, id)
+		if err != nil {
+			t.Fatalf("ListForInbound(%s): %v", tags[i], err)
+		}
+		if len(clients) != 1 || clients[0].Email != email {
+			t.Fatalf("inbound %s must carry exactly the LDAP client, got %d clients", tags[i], len(clients))
+		}
+		if clients[0].SubID != rec.SubID {
+			t.Errorf("inbound %s client subId = %q, want the shared %q", tags[i], clients[0].SubID, rec.SubID)
+		}
+	}
+
+	trojanClients, err := clientSvc.ListForInbound(nil, inboundIds[1])
+	if err != nil {
+		t.Fatalf("ListForInbound(trojan): %v", err)
+	}
+	if trojanClients[0].Password == "" {
+		t.Error("trojan inbound client must get a generated password")
+	}
+	vlessClients, err := clientSvc.ListForInbound(nil, inboundIds[0])
+	if err != nil {
+		t.Fatalf("ListForInbound(vless): %v", err)
+	}
+	if vlessClients[0].ID == "" {
+		t.Error("vless inbound client must get a generated uuid")
+	}
+}