diff --git a/deploy/linux/sbin/bj-igate-admin b/deploy/linux/sbin/bj-igate-admin index e2c9fba..db1d977 100755 --- a/deploy/linux/sbin/bj-igate-admin +++ b/deploy/linux/sbin/bj-igate-admin @@ -324,9 +324,14 @@ SQL # папки обмена шлюза по сети (SMB): для программ организации, которые кладут/забирают файлы через ИШ. bj это не # нужно. Блок общих папок в smb.conf — между метками bj; заведённые вручную [igate_exchange] не трогаем. share) + # Папки обмена шлюза по сети (Samba). Два режима: + # simple — один логин и пароль (пользователь Samba, файлы пишутся от имени шлюза); + # domain — сервер в домене AD (winbind): входят сотрудники и группы домена с доступом к bj (список даёт bj). CONF=/etc/samba/smb.conf; B='# >>> bj: папки шлюза НРД'; E='# <<< bj: папки шлюза НРД' - on=$(j enabled); su=$(j user); sp=$(j password) + GB='# >>> bj: вход через домен'; GE='# <<< bj: вход через домен' + on=$(j enabled); su=$(j user); sp=$(j password); mode=$(j mode); vu=$(j valid) manual=false; [ -f "$CONF" ] && sed "/^$B/,/^$E/d" "$CONF" | grep -q '^\[igate_exchange\]' && manual=true + dmode=simple; [ -f "$CONF" ] && grep -q "^$GB" "$CONF" && dmode=domain if [ "${on:-}" = status ] || [ -z "${on:-}" ]; then mine=false; [ -f "$CONF" ] && grep -q "^$B" "$CONF" && mine=true u=$( [ "$mine" = true ] && sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^ *valid users *= *//p' | head -1 ) @@ -335,7 +340,9 @@ SQL ip=$(hostname -I 2>/dev/null | awk '{print $1}'); hn=$(hostname -s 2>/dev/null) sh=""; [ "$mine" = true ] && for n in $(sed -n "/^$B/,/^$E/p" "$CONF" | sed -n 's/^\[\(.*\)\]$/\1/p'); do sh="$sh${sh:+,}$(q "$n")"; done [ "$manual" = true ] && [ -z "$sh" ] && sh=$(q igate_exchange) - echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)"),\"ip\":$(q "$ip"),\"host\":$(q "$hn"),\"shares\":[$sh]}"; exit 0 + realm=$( [ "$dmode" = domain ] && sed -n "/^$GB/,/^$GE/p" "$CONF" | sed -n 's/^ *realm *= *//p' | head -1 ) + joined=false; [ "$dmode" = domain ] && command -v net >/dev/null 2>&1 && net ads testjoin >/dev/null 2>&1 && joined=true + echo "{\"ok\":true,\"on\":$( [ "$mine" = true ] || [ "$manual" = true ] && echo true || echo false),\"manual\":$manual,\"mode\":$(q "$dmode"),\"joined\":$joined,\"realm\":$(q "$realm"),\"user\":$(q "$u"),\"smbd\":$(q "$(systemctl is-active smbd 2>/dev/null)"),\"ip\":$(q "$ip"),\"host\":$(q "$hn"),\"shares\":[$sh]}"; exit 0 fi [ "$manual" = true ] && err "папка igate_exchange уже открыта вручную в $CONF — bj её не меняет; чтобы управлять из bj, уберите её из smb.conf" if [ "$on" != true ]; then @@ -343,24 +350,56 @@ SQL systemctl reload smbd >/dev/null 2>&1 || true ok "доступ к папкам шлюза по сети закрыт"; exit 0 fi - [[ "$su" =~ ^[a-z][a-z0-9_-]{2,31}$ ]] || err "логин — латинские строчные буквы, цифры, _ и -, от 3 символов, начинается с буквы" - case "$su" in root|igate|bj|postgres) err "логин $su занят системой — выберите другой" ;; esac + [ -n "$mode" ] || mode=$dmode if ! command -v smbd >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y -q samba >/dev/null 2>&1 || err "не удалось установить Samba (apt-get install samba) — нужен доступ к репозиторию" fi - id "$su" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$su" || err "пользователь $su не создан" - if [ -n "$sp" ]; then - [ ${#sp} -ge 8 ] || err "пароль — не короче 8 символов" - printf '%s\n%s\n' "$sp" "$sp" | smbpasswd -s -a "$su" >/dev/null 2>&1 || err "пароль для $su не задан (smbpasswd)" - smbpasswd -e "$su" >/dev/null 2>&1 - elif ! pdbedit -L 2>/dev/null | grep -q "^$su:"; then - err "задайте пароль для $su" + note="" + if [ "$mode" = domain ]; then + # подключение сервера к домену — один раз: учётная запись администратора домена не сохраняется + if [ "$(j join)" = 1 ]; then + realm=$(j realm | tr '[:lower:]' '[:upper:]'); wg=$(j workgroup | tr '[:lower:]' '[:upper:]'); au=$(j admin); ap=$(j adminpw) + [[ "$realm" =~ ^[A-Z0-9][A-Z0-9.-]+$ ]] || err "домен не задан — подключите домен в bj: Настройка → Пользователи" + [[ "$wg" =~ ^[A-Z0-9][A-Z0-9_-]{0,14}$ ]] || err "не определено краткое имя домена (NetBIOS)" + [ -n "$au" ] && [ -n "$ap" ] || err "нужны логин и пароль администратора домена" + command -v winbindd >/dev/null 2>&1 || DEBIAN_FRONTEND=noninteractive apt-get install -y -q winbind libnss-winbind >/dev/null 2>&1 || err "не удалось установить winbind (apt-get install winbind) — нужен доступ к репозиторию" + cp -p "$CONF" "$CONF.bj-before-join" 2>/dev/null + sed -i "/^$GB/,/^$GE/d" "$CONF" + { echo "$GB" + printf '[global]\n workgroup = %s\n realm = %s\n security = ads\n kerberos method = secrets and keytab\n winbind refresh tickets = yes\n idmap config * : backend = tdb\n idmap config * : range = 3000-7999\n idmap config %s : backend = rid\n idmap config %s : range = 10000-999999\n map to guest = never\n' "$wg" "$realm" "$wg" "$wg" + echo "$GE"; } >> "$CONF" + # блок домена — раньше папок: папки дописываются после него + if grep -q "^$B" "$CONF"; then blk=$(sed -n "/^$B/,/^$E/p" "$CONF"); sed -i "/^$B/,/^$E/d" "$CONF"; printf '%s\n' "$blk" >> "$CONF"; fi + testparm -s "$CONF" >/dev/null 2>&1 || { cp -p "$CONF.bj-before-join" "$CONF"; err "smb.conf с параметрами домена не проходит проверку (testparm) — правка отменена"; } + out=$(PASSWD="$ap" net ads join -U "$au" 2>&1) || { cp -p "$CONF.bj-before-join" "$CONF"; systemctl restart smbd >/dev/null 2>&1; err "сервер не подключён к домену: $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"; } + systemctl enable winbind >/dev/null 2>&1; systemctl restart winbind >/dev/null 2>&1 + net ads testjoin >/dev/null 2>&1 || err "сервер подключён, но проверка подключения к домену не прошла (net ads testjoin)" + note="сервер подключён к домену $realm; "; dmode=domain + fi + [ "$dmode" = domain ] || err "сервер ещё не подключён к домену — нажмите «Подключить сервер к домену»" + [ -n "$vu" ] || err "в bj нет групп и сотрудников домена с доступом — добавьте их в Настройка → Пользователи" + case "$vu" in *$'\n'*|*'['*|*']'*|*';'*|*'#'*|*'%'*) err "недопустимые символы в списке доступа" ;; esac + valid="$vu" + else + [[ "$su" =~ ^[a-z][a-z0-9_-]{2,31}$ ]] || err "логин — латинские строчные буквы, цифры, _ и -, от 3 символов, начинается с буквы" + case "$su" in root|igate|bj|postgres) err "логин $su занят системой — выберите другой" ;; esac + id "$su" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$su" || err "пользователь $su не создан" + if [ -n "$sp" ]; then + [ ${#sp} -ge 8 ] || err "пароль — не короче 8 символов" + printf '%s\n%s\n' "$sp" "$sp" | smbpasswd -s -a "$su" >/dev/null 2>&1 || err "пароль для $su не задан (smbpasswd)" + smbpasswd -e "$su" >/dev/null 2>&1 + elif ! pdbedit -L 2>/dev/null | grep -q "^$su:"; then + err "задайте пароль для $su" + fi + # простой режим — без домена: вход учётной записью Samba + if [ "$dmode" = domain ]; then sed -i "/^$GB/,/^$GE/d" "$CONF"; systemctl disable --now winbind >/dev/null 2>&1; note="вход через домен выключен (учётная запись компьютера в домене осталась); "; fi + valid="$su" fi [ -f "$CONF" ] && sed -i "/^$B/,/^$E/d" "$CONF" { echo "$B" for c in test3 prod; do d=$(exchange_dir "$c"); [ -d "$d" ] || continue n=igate_exchange; [ "$c" = prod ] && n=igate_exchange_prod - printf '[%s]\n comment = Шлюз НРД: папки обмена %s (INBOX/OUTBOX/SENT/ERRORS)\n path = %s\n browseable = yes\n read only = no\n valid users = %s\n force user = %s\n force group = %s\n' "$n" "$c" "$d" "$su" "$SVC_USER" "$SVC_USER" + printf '[%s]\n comment = Шлюз НРД: папки обмена %s (INBOX/OUTBOX/SENT/ERRORS)\n path = %s\n browseable = yes\n read only = no\n valid users = %s\n force user = %s\n force group = %s\n' "$n" "$c" "$d" "$valid" "$SVC_USER" "$SVC_USER" done echo "$E"; } >> "$CONF" # Windows сначала пробует войти текущей учётной записью; с «map to guest = bad user» Samba пускает её гостем, @@ -368,7 +407,8 @@ SQL sed -i -E 's/^([[:space:]]*)map to guest[[:space:]]*=[[:space:]]*bad user.*$/\1map to guest = never/I' "$CONF" testparm -s "$CONF" >/dev/null 2>&1 || { sed -i "/^$B/,/^$E/d" "$CONF"; err "smb.conf после правки не проходит проверку (testparm) — правка отменена"; } systemctl enable smbd >/dev/null 2>&1; systemctl restart smbd >/dev/null 2>&1 || err "служба smbd не перезапустилась" - ok "папки шлюза открыты по сети для $su: \\\\$(hostname -I | awk '{print $1}')\\igate_exchange" ;; + if [ "$dmode" = domain ]; then ok "${note}папки шлюза открыты по сети для сотрудников домена с доступом к bj: \\\\$(hostname -I | awk '{print $1}')\\igate_exchange" + else ok "${note}папки шлюза открыты по сети для $su: \\\\$(hostname -I | awk '{print $1}')\\igate_exchange"; fi ;; *) err "неизвестная команда: $cmd" ;; esac diff --git a/internal/lkgateway/adwizard.go b/internal/lkgateway/adwizard.go index b71897a..2519e17 100644 --- a/internal/lkgateway/adwizard.go +++ b/internal/lkgateway/adwizard.go @@ -543,5 +543,6 @@ func (h *setupHandlers) adGroupSave(w http.ResponseWriter, r *http.Request) { h.usersFlash(w, r, "Группа домена: "+err.Error()) return } + igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам h.usersFlash(w, r, msg) } diff --git a/internal/lkgateway/auth.go b/internal/lkgateway/auth.go index fb95191..4d64b23 100644 --- a/internal/lkgateway/auth.go +++ b/internal/lkgateway/auth.go @@ -1366,6 +1366,7 @@ func (h *setupHandlers) saveUser(w http.ResponseWriter, r *http.Request) { h.usersFlash(w, r, "Создан первый администратор "+u.Login+". Теперь вход обязателен — войдите под ним.") return } + igateShareSyncSoon(h.rc) // доменный сотрудник — и к папкам шлюза «через домен» h.usersFlash(w, r, "Пользователь "+u.Login+" сохранён.") } @@ -1390,6 +1391,7 @@ func (h *setupHandlers) deleteUser(w http.ResponseWriter, r *http.Request) { return } _ = globalUsers.Delete(login) + igateShareSyncSoon(h.rc) globalSessions.DropUser(login) h.usersFlash(w, r, "Пользователь "+login+" удалён.") } @@ -1445,6 +1447,7 @@ func (h *setupHandlers) saveAuth(w http.ResponseWriter, r *http.Request) { h.usersFlash(w, r, "Вход: "+err.Error()) return } + igateShareSyncSoon(h.rc) h.usersFlash(w, r, "Настройки входа сохранены.") } diff --git a/internal/lkgateway/igate.go b/internal/lkgateway/igate.go index 5445a71..20bcba1 100644 --- a/internal/lkgateway/igate.go +++ b/internal/lkgateway/igate.go @@ -14,7 +14,6 @@ import ( "os/exec" "path/filepath" "regexp" - "strconv" "strings" "time" @@ -259,17 +258,7 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) { go igateContourRun(rc, contour, crypto, dep) jsonOut(w, map[string]any{"ok": true, "started": true}) }) - // папки обмена шлюза по сети (SMB) — для программ организации; 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() - p := map[string]string{"enabled": "status"} - if r.Method == http.MethodPost { - p = map[string]string{"enabled": strconv.FormatBool(r.FormValue("enabled") != ""), "user": strings.TrimSpace(r.FormValue("user")), "password": r.FormValue("password")} - } - extra, _ := json.Marshal(p) - jsonOut(w, opResult(runKeymediaOp(ctx, "igate-share", map[string]string{"extra": string(extra)}))) - }) + registerIgateShare(mux, rc) // папки обмена шлюза по сети (igateshare.go) // ход записи канала: страница показывает его и после перезагрузки mux.HandleFunc("/admin/setup/igate/contour-status", func(w http.ResponseWriter, r *http.Request) { jsonOut(w, igateContourJob.view()) diff --git a/internal/lkgateway/igateshare.go b/internal/lkgateway/igateshare.go new file mode 100644 index 0000000..d9811ab --- /dev/null +++ b/internal/lkgateway/igateshare.go @@ -0,0 +1,270 @@ +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() + } +} diff --git a/internal/lkgateway/igateshare_test.go b/internal/lkgateway/igateshare_test.go new file mode 100644 index 0000000..315f160 --- /dev/null +++ b/internal/lkgateway/igateshare_test.go @@ -0,0 +1,32 @@ +package lkgateway + +import ( + "strings" + "testing" +) + +// Папки шлюза «через домен»: доступ — группам с ролью в bj и доменным сотрудникам bj (включённым). +func TestIgateShareValid(t *testing.T) { + prev := globalUsers + globalUsers = &userStore{users: map[string]*User{}} + defer func() { globalUsers = prev }() + globalUsers.users["azuev"] = &User{Login: "azuev", Source: "ad", Active: true, Role: "operator"} + globalUsers.users["old"] = &User{Login: "old", Source: "ad", Active: false, Role: "viewer"} + globalUsers.users["admin"] = &User{Login: "admin", Source: "local", Active: true, Role: "admin"} + a := AuthSettings{ADGroups: []ADGroupRole{{Group: "CN=BJ-Operators,OU=Groups,DC=corp,DC=local", Role: "operator"}, {Group: "Депозитарий \"Главный\"", Role: "viewer"}}} + got, err := igateShareValidWG(a, "CORP") + if err != nil { + t.Fatal(err) + } + want := `@"CORP\BJ-Operators", @"CORP\Депозитарий Главный", "CORP\azuev"` + if got != want { + t.Fatalf("\n got %s\nwant %s", got, want) + } + if strings.Contains(got, "old") || strings.Contains(got, "admin") { + t.Fatal("в доступ попали отключённый или локальный пользователь") + } + globalUsers = &userStore{users: map[string]*User{}} + if _, err := igateShareValidWG(AuthSettings{}, "CORP"); err == nil { + t.Fatal("пустой список доступа должен быть ошибкой") + } +} diff --git a/internal/lkgateway/server.go b/internal/lkgateway/server.go index cab0ece..7341e43 100644 --- a/internal/lkgateway/server.go +++ b/internal/lkgateway/server.go @@ -303,6 +303,7 @@ func (s *Server) Run(ctx context.Context) error { go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions) go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск go superviseLoop(ctx, "units-watch", s.rc, s.unitsWatchLoop) // службы модулей и ключей Валидаты: письмо администратору + go superviseLoop(ctx, "igate-share", s.rc, s.igateShareSyncLoop) // папки шлюза «через домен»: список доступа = группы и сотрудники bj go handbookLoop(ctx, s.rc) // справочник участников M2M с сервера лицензий (для установок без шлюза НРД) // Поллер входящих от НРД (только в реальном режиме ИШ): забирает diff --git a/internal/lkgateway/web/templates/part_igate.html b/internal/lkgateway/web/templates/part_igate.html index ff8e33f..48bfa44 100644 --- a/internal/lkgateway/web/templates/part_igate.html +++ b/internal/lkgateway/web/templates/part_igate.html @@ -24,6 +24,9 @@ .ct-act .btn[disabled]{opacity:.5;cursor:not-allowed} @media (max-width:760px){.ct-grid{grid-template-columns:minmax(0,1fr)}} .ig-share{margin-top:18px;padding-top:16px;border-top:1px solid var(--border)} +.ig-sh-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px} +.ig-sh-grid label.f input{width:100%;box-sizing:border-box} +@media (max-width:600px){.ig-sh-grid{grid-template-columns:minmax(0,1fr)}} .ig-addr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:6px} .ig-addr code{font-size:14px;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid var(--border);user-select:all} .ig-addr .muted{font-size:12.5px} @@ -81,13 +84,29 @@

Папки шлюза по сети — необязательно

-

Папки обмена шлюза (INBOX, OUTBOX, SENT, ERRORS) можно открыть по сети Windows — если другие программы организации кладут или забирают файлы через этот шлюз. bj для работы это не нужно. Доступ — по своему логину и паролю, файлы пишутся от имени шлюза.

+

Папки обмена шлюза (INBOX, OUTBOX, SENT, ERRORS) можно открыть по сети Windows — если другие программы организации кладут или забирают файлы через этот шлюз. bj для работы это не нужно; файлы пишутся от имени шлюза.

Проверяю…
-
- - - -
+ +
+ + +
+ +
+ + +
+ +