Files
Bridge-and-Join-s/internal/lkgateway/igateshare.go
T
zuevav 9553bd6e22 Папки шлюза по сети — два режима: простой (один логин) или через домен (группы и сотрудники bj)
- «Через домен»: сервер bj подключается к домену AD (winbind, один раз — учётной записью администратора домена, bj её не сохраняет); папки igate_exchange(_prod) открыты группам с ролью в bj и доменным сотрудникам bj — своим входом в Windows; краткое имя домена bj берёт из AD (crossRef nETBIOSName).
- Список доступа bj сверяет сам: после изменения групп, сотрудников и настроек входа (через несколько секунд) и раз в 15 минут.
- «Простой»: как было — один логин и пароль Samba; переход с домена выключает вход через домен (учётная запись компьютера в домене остаётся).
- bj-igate-admin share: режимы simple/domain, join (realm, workgroup, admin), проверка списка доступа на недопустимые символы, состояние — mode/joined/realm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 09:01:46 +03:00

271 lines
11 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 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
}
extra, _ := json.Marshal(map[string]string{"enabled": "true", "mode": "domain", "valid": valid})
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", valid)
return "доступ обновлён: " + valid, nil
}
// 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()
for {
select {
case <-ctx.Done():
return
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()
}
}