Files
Bridge-and-Join-s/internal/lkgateway/adwizard.go
T
zuevav afc5776347 Индикатор «Шлюз НРД», проверка ключа ИШ, папки шлюза по сети, сотрудники из домена, переключатель QUIK в настройках
- журнал ИШ: номер процесса выравнивается пробелами — опросы НРД не распознавались (серый индикатор)
- «Проверить СКЗИ и ключ»: Валидата, датчик случайных чисел, подпись профилем ИШ (а не старый bj-crypto);
  индикатор «Криптографии» — по готовности ключа; карточка автозагрузки УЦ — только для разработки
- шаг «Шлюз НРД»: папки обмена по сети (Samba): логин/пароль, ручная настройка не трогается
- «Учётные записи»: необязательно — подключить домен, проверить связь, найти сотрудника и добавить с ролью;
  роль из bj действует, пока группы домена с ролями не сопоставлены
- настройка QUIK МНП: «Только чтение / Чтение и запись», тонкая настройка цикла свёрнута
- индикатор «Обновления» — только когда есть что обновить; «Мастер настройки» в меню и в настройках

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:24:19 +03:00

130 lines
5.4 KiB
Go

package lkgateway
// Сотрудники из домена в шаге «Учётные записи» (07.10.2026). Основной путь — создать учётную запись bj сразу
// (логин и пароль); второй, необязательный, — подключить домен Active Directory прямо здесь, проверить связь и
// выбрать сотрудника поиском: он входит своим доменным логином и паролем, bj проверяет пароль в домене.
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/go-ldap/ldap/v3"
)
func (h *setupHandlers) registerADWizard(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/users/ad-connect", h.adConnect)
mux.HandleFunc("/admin/setup/users/ad-search", h.adSearch)
}
// adConnect — POST: адрес контроллера, домен, сервисная учётная запись (для поиска сотрудников); остальные
// настройки входа не трогает. Сразу проверяет связь: контроллер отвечает и сервисная запись входит.
func (h *setupHandlers) adConnect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
a := h.rc.Snapshot().Auth
a.ADServer = strings.TrimSpace(r.FormValue("ad_server"))
a.ADDomain = strings.TrimSpace(strings.TrimPrefix(r.FormValue("ad_domain"), "@"))
a.ADBindUser = strings.TrimSpace(r.FormValue("ad_bind_user"))
if pw := r.FormValue("ad_bind_password"); pw != "" {
a.ADBindPassword = pw
}
if a.ADServer != "" && !strings.Contains(a.ADServer, "://") {
a.ADServer = "ldaps://" + a.ADServer // как в проводнике вводят имя — по умолчанию защищённое подключение
}
var miss []string
for _, f := range []struct{ v, n string }{{a.ADServer, "контроллер домена"}, {a.ADDomain, "домен"}, {a.ADBindUser, "сервисная учётная запись"}, {a.ADBindPassword, "её пароль"}} {
if f.v == "" {
miss = append(miss, f.n)
}
}
if len(miss) > 0 {
h.usersFlash(w, r, "Домен: не заполнено: "+strings.Join(miss, ", "))
return
}
probe, err := adProbe(a)
if err != nil {
h.usersFlash(w, r, "Домен: контроллер не отвечает — "+err.Error()+". Настройки не сохранены.")
return
}
conn, err := adDial(a)
if err == nil {
err = adBindService(conn, a)
conn.Close()
}
if err != nil {
h.usersFlash(w, r, "Домен: "+err.Error()+". Настройки не сохранены.")
return
}
a.ADEnabled = true
if err := h.rc.UpdateAuth(a); err != nil {
h.usersFlash(w, r, "Домен: настройки не сохранены — "+err.Error())
return
}
h.usersFlash(w, r, "Домен подключён ("+probe+"): можно добавлять сотрудников из домена — они входят своим доменным логином и паролем.")
}
// adUserView — сотрудник домена в результатах поиска.
type adUserView struct {
Login string `json:"login"` // sAMAccountName
Name string `json:"name"`
Email string `json:"email,omitempty"`
Have bool `json:"have,omitempty"` // уже есть в bj
}
// adSearch — GET ?q=: сотрудники домена по фамилии, логину или почте (до 20, только включённые учётные записи).
func (h *setupHandlers) adSearch(w http.ResponseWriter, r *http.Request) {
a := h.rc.Snapshot().Auth
q := strings.TrimSpace(r.URL.Query().Get("q"))
if !a.ADEnabled || a.ADBindUser == "" {
jsonOut(w, map[string]any{"ok": false, "error": "домен не подключён"})
return
}
if len([]rune(q)) < 2 {
jsonOut(w, map[string]any{"ok": true, "users": []adUserView{}})
return
}
users, err := adSearchUsers(a, q, 20)
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
jsonOut(w, map[string]any{"ok": true, "users": users})
}
func adSearchUsers(a AuthSettings, q string, limit int) ([]adUserView, error) {
conn, err := adDial(a)
if err != nil {
return nil, err
}
defer conn.Close()
conn.SetTimeout(10 * time.Second)
if err := adBindService(conn, a); err != nil {
return nil, err
}
base := a.ADBaseDN
if base == "" {
base = adDefaultBase(conn)
}
e := ldap.EscapeFilter(q)
filter := fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(sAMAccountName=*%s*)(displayName=*%s*)(sn=*%s*)(mail=*%s*)))", e, e, e, e)
sr, err := conn.Search(ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, limit, 10, false,
filter, []string{"sAMAccountName", "displayName", "mail"}, nil))
if err != nil && (sr == nil || len(sr.Entries) == 0) { // превышение лимита — не ошибка, берём найденное
return nil, fmt.Errorf("поиск в домене: %v", err)
}
var out []adUserView
for _, en := range sr.Entries {
u := adUserView{Login: en.GetAttributeValue("sAMAccountName"), Name: en.GetAttributeValue("displayName"), Email: en.GetAttributeValue("mail")}
if u.Login == "" {
continue
}
u.Have = globalUsers.Get(u.Login) != nil
out = append(out, u)
}
return out, nil
}