Files
Bridge-and-Join-s/internal/lkgateway/igateshare.go
zuevav 6e6ab3a4fd Папки шлюза: тестовый — igate_exchange_test, боевой — igate_exchange; все папки в сообщении; Samba — в установщике
- Имена папок: тестовый контур — igate_exchange_test, боевой — igate_exchange (было igate_exchange и igate_exchange_prod); bj переименовывает открытые по прежней схеме сам — при сверке, в первый раз через 30 с после запуска.
- После сохранения — все открытые папки с контуром: \\IP\igate_exchange_test (тестовый TEST3), \\IP\igate_exchange (боевой).
- Закрыть папки — Samba выключается (сервер не отвечает по SMB, пока папок нет).
- Установщик (bj-installer, preseed Debian, слой пакетов образа Ubuntu): samba, winbind, libnss-winbind — заранее, чтобы в закрытом контуре папки открывались без интернета; необязательной частью (нет на диске и нет сети — установка bj продолжается); службы выключены до открытия папок.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:10:18 +03:00

304 lines
12 KiB
Go

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()
}
}