9553bd6e22
- «Через домен»: сервер 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
271 lines
11 KiB
Go
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()
|
|
}
|
|
}
|