mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-12 06:10:58 +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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||||
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
|
ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
|
||||||
@@ -114,52 +112,41 @@ func (j *LdapSyncJob) Run() {
|
|||||||
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
||||||
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
||||||
|
|
||||||
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
|
resolvedInboundIds := make([]int, 0, len(inboundTags))
|
||||||
clientsToEnable := map[string][]string{} // tag -> []email
|
resolvedTags := make([]string, 0, len(inboundTags))
|
||||||
clientsToDisable := map[string][]string{} // tag -> []email
|
for _, tag := range inboundTags {
|
||||||
|
ib := inboundMap[tag]
|
||||||
for email, allowed := range flags {
|
if ib == nil {
|
||||||
exists := allClients[email] != nil
|
logger.Warningf("LDAP inbound tag %s does not match any inbound", tag)
|
||||||
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 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ib := inboundMap[tag]
|
resolvedInboundIds = append(resolvedInboundIds, ib.Id)
|
||||||
created := 0
|
resolvedTags = append(resolvedTags, tag)
|
||||||
restartNeeded := false
|
}
|
||||||
for _, c := range newClients {
|
|
||||||
nr, err := j.clientService.CreateOne(&j.inboundService, ib.Id, c)
|
clientsToCreate := []model.Client{}
|
||||||
if err != nil {
|
clientsToEnable := map[string][]string{} // tag -> []email
|
||||||
logger.Warningf("Failed to add client %s for tag %s: %v", c.Email, tag, err)
|
clientsToDisable := map[string][]string{} // tag -> []email
|
||||||
continue
|
|
||||||
}
|
for email, allowed := range flags {
|
||||||
created++
|
existing := allClients[email]
|
||||||
if nr {
|
if existing == nil {
|
||||||
restartNeeded = true
|
if allowed && autoCreate {
|
||||||
|
clientsToCreate = append(clientsToCreate, j.buildClient(email, defGB, defExpiryDays, defLimitIP))
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if created > 0 {
|
for _, tag := range resolvedTags {
|
||||||
logger.Infof("LDAP auto-create: %d clients for %s", created, tag)
|
if allowed && !existing.Enable {
|
||||||
if restartNeeded {
|
clientsToEnable[tag] = append(clientsToEnable[tag], email)
|
||||||
j.xrayService.SetToNeedRestart()
|
} else if !allowed && existing.Enable {
|
||||||
|
clientsToDisable[tag] = append(clientsToDisable[tag], email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.createClients(clientsToCreate, resolvedInboundIds, resolvedTags)
|
||||||
|
|
||||||
// --- Execute enable/disable batch ---
|
// --- Execute enable/disable batch ---
|
||||||
for tag, emails := range clientsToEnable {
|
for tag, emails := range clientsToEnable {
|
||||||
j.batchSetEnable(inboundMap[tag], emails, true)
|
j.batchSetEnable(inboundMap[tag], emails, true)
|
||||||
@@ -196,8 +183,8 @@ func splitCsv(s string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildClient creates a new client for auto-create
|
// buildClient creates a new client for auto-create; ClientService.Create fills per-protocol credentials
|
||||||
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
func (j *LdapSyncJob) buildClient(email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
||||||
c := model.Client{
|
c := model.Client{
|
||||||
Email: email,
|
Email: email,
|
||||||
Enable: true,
|
Enable: true,
|
||||||
@@ -207,15 +194,37 @@ func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExp
|
|||||||
if defExpiryDays > 0 {
|
if defExpiryDays > 0 {
|
||||||
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
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
|
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) {
|
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
return
|
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