6e6ab3a4fd
- Имена папок: тестовый контур — 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>
304 lines
12 KiB
Go
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()
|
|
}
|
|
}
|