mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-08 05:36:09 +00:00
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
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for tag, newClients := range clientsToCreate {
|
||||
if len(newClients) == 0 {
|
||||
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
|
||||
}
|
||||
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
|
||||
resolvedInboundIds = append(resolvedInboundIds, ib.Id)
|
||||
resolvedTags = append(resolvedTags, tag)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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 = "user@example.com"
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user