package lkgateway // Папки обмена шлюза НРД по сети (07.10.2026): два режима на выбор. // simple — один логин и пароль Samba (как было); // domain — сервер bj подключён к домену AD (winbind), папки открыты тем же группам и сотрудникам домена, // у которых есть доступ к bj: группы с ролью (Auth.ADGroups) и доменные учётные записи bj. Сотрудник // открывает папку своим входом в Windows. Список доступа bj сверяет сам — при изменении групп и // сотрудников и раз в 15 минут. // Подключение к домену — один раз, учётной записью администратора домена; bj её не сохраняет. import ( "context" "encoding/json" "fmt" "log" "net/http" "sort" "strings" "sync" "time" "github.com/go-ldap/ldap/v3" ) func registerIgateShare(mux *http.ServeMux, rc *RuntimeConfig) { // GET — состояние (+ что даст режим «через домен»), POST — открыть/закрыть, режим mux.HandleFunc("/admin/setup/igate/share", func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) // первый раз — установка Samba defer cancel() if r.Method != http.MethodPost { st := opResult(igateShareStatus(ctx)) a := rc.Snapshot().Auth st["ad_ready"] = a.ADEnabled && a.ADBindUser != "" && a.ADDomain != "" st["ad_domain"] = a.ADDomain st["access"] = igateShareAccessView(a) jsonOut(w, st) return } p := map[string]string{"enabled": fmt.Sprint(r.FormValue("enabled") != ""), "mode": "simple", "user": strings.TrimSpace(r.FormValue("user")), "password": r.FormValue("password")} if r.FormValue("mode") == "domain" { valid, err := igateShareValid(rc.Snapshot().Auth) if err != nil { jsonOut(w, map[string]any{"ok": false, "error": err.Error()}) return } p = map[string]string{"enabled": fmt.Sprint(r.FormValue("enabled") != ""), "mode": "domain", "valid": valid} } extra, _ := json.Marshal(p) jsonOut(w, opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)}))) }) // подключить сервер к домену (один раз) и открыть папки сотрудникам домена с доступом к bj mux.HandleFunc("/admin/setup/igate/share-join", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } a := rc.Snapshot().Auth admin, pw := strings.TrimSpace(r.FormValue("admin")), r.FormValue("adminpw") if !a.ADEnabled || a.ADDomain == "" || a.ADBindUser == "" { jsonOut(w, map[string]any{"ok": false, "error": "домен в bj не подключён — Настройка → Пользователи → «Сотрудники из домена»"}) return } if admin == "" || pw == "" { jsonOut(w, map[string]any{"ok": false, "error": "не заполнено: логин и пароль администратора домена"}) return } wg, err := adNetbiosName(a) if err != nil { jsonOut(w, map[string]any{"ok": false, "error": "краткое имя домена не определено: " + err.Error()}) return } valid, err := igateShareValidWG(a, wg) if err != nil { jsonOut(w, map[string]any{"ok": false, "error": err.Error()}) return } ctx, cancel := context.WithTimeout(r.Context(), 6*time.Minute) // установка winbind и подключение к домену defer cancel() extra, _ := json.Marshal(map[string]string{"enabled": "true", "mode": "domain", "join": "1", "realm": a.ADDomain, "workgroup": wg, "admin": admin, "adminpw": pw, "valid": valid}) res := opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)})) if ok, _ := res["ok"].(bool); ok { log.Printf("igate-share: сервер подключён к домену %s (%s), папки открыты: %s", a.ADDomain, wg, valid) } jsonOut(w, res) }) // сейчас сверить список доступа (после изменения групп и сотрудников) mux.HandleFunc("/admin/setup/igate/share-sync", func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute) defer cancel() msg, err := igateShareSync(ctx, rc) if err != nil { jsonOut(w, map[string]any{"ok": false, "error": err.Error()}) return } jsonOut(w, map[string]any{"ok": true, "output": msg}) }) } func igateShareStatus(ctx context.Context) (map[string]any, error) { extra, _ := json.Marshal(map[string]string{"enabled": "status"}) return runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)}) } // adNetbiosName — краткое имя домена (CORP) из раздела конфигурации AD; запоминается на время работы bj. var netbiosCache sync.Map // ADDomain → имя func adNetbiosName(a AuthSettings) (string, error) { if v, ok := netbiosCache.Load(strings.ToLower(a.ADDomain)); ok { return v.(string), nil } conn, err := adDial(a) if err != nil { return "", err } defer conn.Close() if err := adBindService(conn, a); err != nil { return "", err } base := adDefaultBase(conn) if base == "" { return "", fmt.Errorf("контроллер не назвал базу домена") } sr, err := conn.Search(ldap.NewSearchRequest("CN=Partitions,CN=Configuration,"+base, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 5, 10, false, "(&(objectClass=crossRef)(nETBIOSName=*))", []string{"nETBIOSName", "dnsRoot"}, nil)) if err != nil { return "", err } for _, e := range sr.Entries { if strings.EqualFold(e.GetAttributeValue("dnsRoot"), a.ADDomain) || len(sr.Entries) == 1 { n := strings.ToUpper(e.GetAttributeValue("nETBIOSName")) netbiosCache.Store(strings.ToLower(a.ADDomain), n) return n, nil } } return "", fmt.Errorf("в домене %s не найдено краткое имя", a.ADDomain) } // igateShareMembers — кому открыть папки: группы домена с ролью в bj и доменные учётные записи bj (включённые). func igateShareMembers(a AuthSettings) (groups, users []string) { seen := map[string]bool{} for _, g := range a.ADGroups { n := strings.TrimSpace(dnCN(g.Group)) if n == "" || !roleAvailable(g.Role) || seen["g:"+strings.ToLower(n)] { continue } seen["g:"+strings.ToLower(n)] = true groups = append(groups, n) } if globalUsers != nil { for _, u := range globalUsers.List() { if u.Source != "ad" || !u.Active || seen["u:"+strings.ToLower(u.Login)] { continue } seen["u:"+strings.ToLower(u.Login)] = true users = append(users, u.Login) } } sort.Strings(groups) sort.Strings(users) return groups, users } // igateShareAccessView — список доступа словами (для страницы). func igateShareAccessView(a AuthSettings) map[string]any { g, u := igateShareMembers(a) return map[string]any{"groups": g, "users": u} } func igateShareValid(a AuthSettings) (string, error) { wg, err := adNetbiosName(a) if err != nil { return "", fmt.Errorf("краткое имя домена не определено: %v", err) } return igateShareValidWG(a, wg) } // igateShareValidWG — строка «valid users» для Samba: @"CORP\Группа", "CORP\логин". func igateShareValidWG(a AuthSettings, wg string) (string, error) { groups, users := igateShareMembers(a) var parts []string clean := func(s string) string { return strings.NewReplacer(`"`, "", "\n", "", "\r", "", ",", "").Replace(s) } for _, g := range groups { parts = append(parts, `@"`+wg+`\`+clean(g)+`"`) } for _, u := range users { parts = append(parts, `"`+wg+`\`+clean(u)+`"`) } if len(parts) == 0 { return "", fmt.Errorf("в bj нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи («Сотрудники из домена»)") } return strings.Join(parts, ", "), nil } // igateShareSync — режим «через домен»: если состав групп и сотрудников с доступом к bj изменился, переписать // доступ к папкам. В простом режиме и при закрытых папках ничего не делает. func igateShareSync(ctx context.Context, rc *RuntimeConfig) (string, error) { st, err := igateShareStatus(ctx) if err != nil { return "", err } if on, _ := st["on"].(bool); !on { return "папки закрыты — сверять нечего", nil } if man, _ := st["manual"].(bool); man { return "папки открыты вручную — bj их не меняет", nil } // прежние имена (тестовый — igate_exchange, боевой — igate_exchange_prod) → новые (_test и без суффикса) if sh, _ := st["shares"].([]any); igateShareOldNames(sh) { p := map[string]string{"enabled": "true", "mode": "simple"} if m, _ := st["mode"].(string); m == "domain" { valid, err := igateShareValid(rc.Snapshot().Auth) if err != nil { return "", err } p = map[string]string{"enabled": "true", "mode": "domain", "valid": valid} } else { p["user"], _ = st["user"].(string) } return igateShareApply(ctx, p, "папки переименованы: тестовый — igate_exchange_test, боевой — igate_exchange") } if m, _ := st["mode"].(string); m != "domain" { return "простой режим — доступ по одному логину", nil } valid, err := igateShareValid(rc.Snapshot().Auth) if err != nil { return "", err } if cur, _ := st["user"].(string); cur == valid { return "список доступа не изменился", nil } return igateShareApply(ctx, map[string]string{"enabled": "true", "mode": "domain", "valid": valid}, "доступ к папкам шлюза обновлён: "+valid) } func igateShareApply(ctx context.Context, p map[string]string, done string) (string, error) { extra, _ := json.Marshal(p) res, err := runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)}) if err != nil { return "", err } if ok, _ := res["ok"].(bool); !ok { e, _ := res["error"].(string) return "", fmt.Errorf("%s", e) } log.Printf("igate-share: %s", done) return done, nil } // igateShareOldNames — папки открыты по прежней схеме имён (до 07.10.2026: боевой был igate_exchange_prod). func igateShareOldNames(shares []any) bool { for _, x := range shares { if n, _ := x.(string); n == "igate_exchange_prod" { return true } } return false } // igateShareSyncSoon — после изменения групп или сотрудников: сверить доступ к папкам в фоне (раз в несколько // секунд, сколько бы изменений ни пришло подряд). var igateShareSyncTimer struct { sync.Mutex t *time.Timer } func igateShareSyncSoon(rc *RuntimeConfig) { if rc == nil { return } igateShareSyncTimer.Lock() defer igateShareSyncTimer.Unlock() if igateShareSyncTimer.t != nil { igateShareSyncTimer.t.Stop() } igateShareSyncTimer.t = time.AfterFunc(5*time.Second, func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() if _, err := igateShareSync(ctx, rc); err != nil { log.Printf("igate-share: сверка доступа к папкам: %v", err) } }) } // igateShareSyncLoop — подстраховка: раз в 15 минут (группы могли поменять в настройках, пока bj перезапускался). func (s *Server) igateShareSyncLoop(ctx context.Context) { t := time.NewTicker(15 * time.Minute) defer t.Stop() first := time.After(30 * time.Second) // после запуска (обновления) — сразу, а не через 15 минут for { select { case <-ctx.Done(): return case <-first: case <-t.C: } c, cancel := context.WithTimeout(ctx, 2*time.Minute) if _, err := igateShareSync(c, s.rc); err != nil { log.Printf("igate-share: сверка доступа к папкам: %v", err) } cancel() } }