Files
Bridge-and-Join-s/internal/lkgateway/adrecheck.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

240 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Перепроверка групп AD. Роль доменного пользователя назначается при входе
// (adAuthenticate), а этот цикл раз в N минут (по умолчанию 15) перечитывает
// его группы от имени сервисной учётной записи: сменилась группа — меняется роль,
// удалён из групп, отключён или удалён в AD — доступ снимается и сессии
// завершаются. Пароли пользователей bj не хранит, поэтому без сервисной
// учётной записи перепроверка не выполняется.
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
)
const adRecheckDefault = 15 * time.Minute
var (
errADNotFound = errors.New("пользователь не найден в AD")
errADDisabled = errors.New("учётная запись отключена в AD")
)
func (a AuthSettings) recheckEvery() time.Duration {
switch {
case a.ADRecheckMinutes <= 0:
return adRecheckDefault
case a.ADRecheckMinutes < 5:
return 5 * time.Minute
}
return time.Duration(a.ADRecheckMinutes) * time.Minute
}
func (a AuthSettings) recheckOn() bool {
return a.ADEnabled && strings.TrimSpace(a.ADBindUser) != "" && a.ADBindPassword != ""
}
// ADRecheckStatus — итог последней перепроверки (для страницы настроек).
type ADRecheckStatus struct {
At time.Time
Checked int
Changed int
Err string
}
var (
adRecheckMu sync.Mutex
adRecheckState ADRecheckStatus
)
func adRecheckSnapshot() ADRecheckStatus {
adRecheckMu.Lock()
defer adRecheckMu.Unlock()
return adRecheckState
}
// adSplitLogin — sAMAccountName и UPN из ivanov, ivanov@corp.ru или CORP\ivanov.
func adSplitLogin(a AuthSettings, login string) (sam, upn string) {
login = strings.TrimSpace(login)
sam, upn = login, login
if i := strings.Index(login, "@"); i > 0 {
sam = login[:i]
} else if i := strings.Index(login, `\`); i > 0 {
sam = login[i+1:]
upn = sam + "@" + a.ADDomain
} else if a.ADDomain != "" {
upn = login + "@" + a.ADDomain
}
return sam, upn
}
func adBindService(conn *ldap.Conn, a AuthSettings) error {
user := strings.TrimSpace(a.ADBindUser)
if !strings.ContainsAny(user, `@\=`) && a.ADDomain != "" {
user += "@" + a.ADDomain
}
if err := conn.Bind(user, a.ADBindPassword); err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
return fmt.Errorf("сервисная учётная запись %s: неверный логин или пароль", user)
}
return fmt.Errorf("сервисная учётная запись %s: %v", user, err)
}
return nil
}
// adApplyRecheck применяет итог проверки к пользователю. lookupErr — nil,
// errADNotFound или errADDisabled; roleErr — nil или *adNoGroupError.
// dropSessions — доступ снят, сессии нужно завершить.
func adApplyRecheck(u *User, res *adResult, lookupErr, roleErr error) (changed, dropSessions bool, msg string) {
var noGroup *adNoGroupError
if lookupErr == nil && errors.As(roleErr, &noGroup) && u.BJRole != "" && roleAvailable(u.BJRole) {
// групп с ролью у сотрудника нет, но роль назначена в bj — она и действует
roleErr, res = nil, &adResult{Role: u.BJRole, MatchedBy: "роль, назначенная в bj", Groups: groupsOf(res)}
}
if lookupErr != nil || roleErr != nil {
reason := "нет в сопоставленных группах"
if lookupErr != nil {
reason = lookupErr.Error()
}
if u.Role == "" {
return false, false, "" // доступ уже снят прошлой проверкой
}
msg = fmt.Sprintf("доступ снят (%s), была роль «%s»", reason, roleTitle(u.Role))
u.Role = ""
u.Note = "доступ снят по AD: " + reason
return true, true, msg
}
if res.Name != "" {
u.Name = res.Name
}
if res.Email != "" {
u.Email = res.Email
}
u.ADGroups = nil
for _, g := range res.Groups {
u.ADGroups = append(u.ADGroups, dnCN(g))
}
u.Note = "роль по AD: " + res.MatchedBy
if res.Role == u.Role {
return false, false, ""
}
was := "без роли"
if u.Role != "" {
was = roleTitle(u.Role)
}
msg = fmt.Sprintf("роль «%s» → «%s» (по «%s»)", was, roleTitle(res.Role), res.MatchedBy)
u.Role = res.Role
return true, false, msg
}
// adRecheckRun — одна перепроверка всех активных доменных пользователей.
func adRecheckRun(a AuthSettings) ADRecheckStatus {
st := ADRecheckStatus{At: time.Now()}
if err := adRecheckUsers(a, &st); err != nil {
st.Err = err.Error()
log.Printf("auth: перепроверка групп AD не выполнена: %v", err)
}
adRecheckMu.Lock()
adRecheckState = st
adRecheckMu.Unlock()
return st
}
func adRecheckUsers(a AuthSettings, st *ADRecheckStatus) error {
var logins []string
for _, u := range globalUsers.List() {
if u.Source == "ad" && u.Active {
logins = append(logins, u.Login)
}
}
if len(logins) == 0 {
return nil
}
conn, err := adDial(a)
if err != nil {
return err
}
defer conn.Close()
if err := adBindService(conn, a); err != nil {
return err
}
for _, login := range logins {
sam, upn := adSplitLogin(a, login)
res, base, lerr := adLookup(conn, a, sam, upn)
if lerr != nil && lerr != errADNotFound && lerr != errADDisabled {
// сбой поиска — роли не трогаем, иначе обрыв связи снял бы доступ у всех
return fmt.Errorf("%s: %v", login, lerr)
}
var rerr error
if lerr == nil {
if rerr = adResolveRole(conn, a, base, sam, res); rerr != nil {
var ng *adNoGroupError
if !errors.As(rerr, &ng) {
return fmt.Errorf("%s: %v", login, rerr)
}
}
}
st.Checked++
var changed, drop bool
var msg string
_ = globalUsers.update(login, func(u *User) { changed, drop, msg = adApplyRecheck(u, res, lerr, rerr) })
if changed {
st.Changed++
log.Printf("auth: перепроверка AD — %s: %s", login, msg)
}
if drop {
globalSessions.DropUser(login)
}
}
return nil
}
func (s *Server) adRecheckLoop(ctx context.Context) {
timer := time.NewTimer(time.Minute)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
}
a := s.rc.Snapshot().Auth
if a.recheckOn() {
adRecheckRun(a)
}
timer.Reset(a.recheckEvery())
}
}
func (h *setupHandlers) recheckAD(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
a := h.rc.Snapshot().Auth
if !a.recheckOn() {
h.usersFlash(w, r, "AD: для перепроверки групп включите вход через AD и укажите сервисную учётную запись с паролем.")
return
}
st := adRecheckRun(a)
if st.Err != "" {
h.usersFlash(w, r, "AD: перепроверка не выполнена — "+st.Err)
return
}
h.usersFlash(w, r, fmt.Sprintf("AD: перепроверено пользователей %d, изменений %d.", st.Checked, st.Changed))
}
func groupsOf(res *adResult) []string {
if res == nil {
return nil
}
return res.Groups
}