afc5776347
- журнал ИШ: номер процесса выравнивается пробелами — опросы НРД не распознавались (серый индикатор) - «Проверить СКЗИ и ключ»: Валидата, датчик случайных чисел, подпись профилем ИШ (а не старый bj-crypto); индикатор «Криптографии» — по готовности ключа; карточка автозагрузки УЦ — только для разработки - шаг «Шлюз НРД»: папки обмена по сети (Samba): логин/пароль, ручная настройка не трогается - «Учётные записи»: необязательно — подключить домен, проверить связь, найти сотрудника и добавить с ролью; роль из bj действует, пока группы домена с ролями не сопоставлены - настройка QUIK МНП: «Только чтение / Чтение и запись», тонкая настройка цикла свёрнута - индикатор «Обновления» — только когда есть что обновить; «Мастер настройки» в меню и в настройках Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
130 lines
5.4 KiB
Go
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
|
|
}
|