773fa8e0bf
- Шапка мастера: правый блок (шлюз НРД, пользователь, тема) снова справа — после правила [hidden] скрытое меню перестало его распирать. - Домен в шаге «Учётные записи»: bj сам находит домен и контроллеры (логин с @домен, домены поиска DNS, rootDSE DNS-серверов, SRV _ldap._tcp.dc._msdcs); адрес и домен — в расширенных настройках, открываются сами, если не нашлось. Контроллеры по очереди, LDAPS → LDAP+StartTLS. - Неудачная попытка ничего не теряет: введённое (и пароль) — черновиком, форма заполнена; причины словами (неизвестный ЦС, имя в сертификате, IP вместо имени, порт закрыт, нет в DNS). - Сертификат неизвестного центра: карточка «кем выдан, отпечаток (SHA-1 как в Windows и SHA-256)» и «Доверять и подключить»; после входа bj берёт из домена сертификат центра (AIA / Certification Authorities), подписавший сертификат контроллера, — подходят все контроллеры. То же в Настройка → Пользователи при «Проверить». - Лицензия: модуль onec-middle «1С: база мидл-офиса (тест)»; раздел настроек — адрес публикации, пользователь, пароль, проверка через OData $metadata; в мастере — в «Модулях». - ЛК (callback, API, готовность) — только при модуле «ЕСИА Финанс»; карточка режима QUIK и стили настроек как в мастере. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
176 lines
6.4 KiB
Go
176 lines
6.4 KiB
Go
package lkgateway
|
|
|
|
// Поиск домена без вопросов (07.10.2026): человеку в мастере достаточно ввести учётную запись и пароль — домен и
|
|
// его контроллеры bj находит сам. Откуда: суффикс в логине (ivanov@corp.example.ru), домены поиска DNS и имя
|
|
// сервера bj; если их нет — DNS-серверы сервера bj (в сети Active Directory это обычно сами контроллеры): у
|
|
// контроллера без пароля читается rootDSE — имя домена и самого контроллера. Список контроллеров — по DNS
|
|
// (_ldap._tcp.dc._msdcs.<домен>). Не нашлось — мастер открывает расширенные настройки.
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
)
|
|
|
|
// adFound — найденный домен.
|
|
type adFound struct {
|
|
Domain string `json:"domain"`
|
|
DCs []string `json:"dcs"` // контроллеры (имена из DNS)
|
|
From string `json:"from"` // откуда узнали домен — для подписи в мастере
|
|
}
|
|
|
|
// adDiscover — домен и контроллеры. domain — если уже известен (из логина или расширенных настроек).
|
|
func adDiscover(ctx context.Context, domain string) *adFound {
|
|
if domain = strings.ToLower(strings.Trim(strings.TrimSpace(domain), ".")); domain != "" {
|
|
dcs := adLookupDCs(ctx, domain)
|
|
return &adFound{Domain: domain, DCs: dcs, From: "указан"}
|
|
}
|
|
for _, c := range adDomainCandidates() {
|
|
if dcs := adLookupDCs(ctx, c.name); len(dcs) > 0 {
|
|
return &adFound{Domain: c.name, DCs: dcs, From: c.from}
|
|
}
|
|
}
|
|
for _, ns := range adNameservers() {
|
|
dom, host := adRootDSEDomain(ctx, ns)
|
|
if dom == "" {
|
|
continue
|
|
}
|
|
dcs := adLookupDCs(ctx, dom)
|
|
if len(dcs) == 0 && host != "" {
|
|
dcs = []string{host}
|
|
}
|
|
return &adFound{Domain: dom, DCs: dcs, From: "DNS-сервер " + ns + " — контроллер этого домена"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type adCand struct{ name, from string }
|
|
|
|
// adDomainCandidates — домены поиска DNS и домен в имени сервера (с родительскими, но не короче двух уровней).
|
|
func adDomainCandidates() []adCand {
|
|
var out []adCand
|
|
seen := map[string]bool{}
|
|
add := func(name, from string) {
|
|
name = strings.ToLower(strings.Trim(name, "."))
|
|
for name != "" && strings.Count(name, ".") >= 1 {
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
out = append(out, adCand{name, from})
|
|
}
|
|
_, name, _ = strings.Cut(name, ".")
|
|
}
|
|
}
|
|
for _, f := range []string{"/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"} {
|
|
b, _ := os.ReadFile(f)
|
|
for _, ln := range strings.Split(string(b), "\n") {
|
|
fs := strings.Fields(ln)
|
|
if len(fs) > 1 && (fs[0] == "search" || fs[0] == "domain") {
|
|
for _, d := range fs[1:] {
|
|
add(d, "домен поиска DNS сервера bj")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if h, err := os.Hostname(); err == nil {
|
|
if _, d, ok := strings.Cut(h, "."); ok {
|
|
add(d, "имя сервера bj")
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// adNameservers — DNS-серверы сервера bj (настоящие, не локальная заглушка systemd-resolved).
|
|
func adNameservers() []string {
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, f := range []string{"/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"} {
|
|
b, _ := os.ReadFile(f)
|
|
for _, ln := range strings.Split(string(b), "\n") {
|
|
fs := strings.Fields(ln)
|
|
if len(fs) < 2 || fs[0] != "nameserver" {
|
|
continue
|
|
}
|
|
ip := net.ParseIP(fs[1])
|
|
if ip == nil || ip.IsLoopback() || seen[fs[1]] {
|
|
continue
|
|
}
|
|
seen[fs[1]] = true
|
|
out = append(out, fs[1])
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// adLookupDCs — контроллеры домена по записям DNS SRV (по приоритету).
|
|
func adLookupDCs(ctx context.Context, domain string) []string {
|
|
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
|
|
defer cancel()
|
|
var srvs []*net.SRV
|
|
for _, name := range []string{"dc._msdcs." + domain, domain} {
|
|
if _, s, err := net.DefaultResolver.LookupSRV(ctx, "ldap", "tcp", name); err == nil && len(s) > 0 {
|
|
srvs = s
|
|
break
|
|
}
|
|
}
|
|
sort.SliceStable(srvs, func(i, j int) bool { return srvs[i].Priority < srvs[j].Priority })
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, s := range srvs {
|
|
h := strings.ToLower(strings.TrimSuffix(s.Target, "."))
|
|
if h != "" && !seen[h] {
|
|
seen[h] = true
|
|
out = append(out, h)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// adRootDSEDomain — спросить у сервера (порт 389, без пароля) rootDSE: контроллер AD отвечает именем домена
|
|
// (defaultNamingContext) и своим именем (dnsHostName). Не контроллер — пусто.
|
|
func adRootDSEDomain(ctx context.Context, host string) (domain, dnsHost string) {
|
|
d := &net.Dialer{Timeout: 3 * time.Second}
|
|
conn, err := ldap.DialURL("ldap://"+net.JoinHostPort(host, "389"), ldap.DialWithDialer(d))
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
defer conn.Close()
|
|
conn.SetTimeout(4 * time.Second)
|
|
sr, err := conn.Search(ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 4, false,
|
|
"(objectClass=*)", []string{"defaultNamingContext", "dnsHostName"}, nil))
|
|
if err != nil || len(sr.Entries) == 0 {
|
|
return "", ""
|
|
}
|
|
e := sr.Entries[0]
|
|
return adDNToDomain(e.GetAttributeValue("defaultNamingContext")), strings.ToLower(e.GetAttributeValue("dnsHostName"))
|
|
}
|
|
|
|
// adDNToDomain — DC=corp,DC=example,DC=ru → corp.example.ru.
|
|
func adDNToDomain(dn string) string {
|
|
var parts []string
|
|
for _, p := range strings.Split(dn, ",") {
|
|
k, v, ok := strings.Cut(strings.TrimSpace(p), "=")
|
|
if ok && strings.EqualFold(k, "DC") && v != "" {
|
|
parts = append(parts, strings.ToLower(v))
|
|
}
|
|
}
|
|
return strings.Join(parts, ".")
|
|
}
|
|
|
|
// adDiscoverHTTP — GET /admin/setup/users/ad-discover: что bj нашёл сам (для подписи в мастере).
|
|
func (h *setupHandlers) adDiscoverHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
|
defer cancel()
|
|
f := adDiscover(ctx, r.URL.Query().Get("domain"))
|
|
if f == nil || len(f.DCs) == 0 {
|
|
jsonOut(w, map[string]any{"ok": false, "found": f})
|
|
return
|
|
}
|
|
jsonOut(w, map[string]any{"ok": true, "found": f})
|
|
}
|