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
549 lines
24 KiB
Go
549 lines
24 KiB
Go
package lkgateway
|
|
|
|
// Сотрудники из домена в шаге «Учётные записи» (07.10.2026). Основной путь — создать учётную запись bj сразу
|
|
// (логин и пароль); второй, необязательный, — подключить домен Active Directory прямо здесь, проверить связь и
|
|
// выбрать сотрудника поиском: он входит своим доменным логином и паролем, bj проверяет пароль в домене.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
)
|
|
|
|
func (h *setupHandlers) registerADWizard(mux *http.ServeMux) {
|
|
mux.HandleFunc("/admin/setup/users/ad-connect", h.adConnect)
|
|
mux.HandleFunc("/admin/setup/users/ad-search", h.adSearch)
|
|
mux.HandleFunc("/admin/setup/users/ad-trust", h.adTrust)
|
|
mux.HandleFunc("/admin/setup/users/ad-discover", h.adDiscoverHTTP)
|
|
mux.HandleFunc("/admin/setup/users/ad-group", h.adGroupSave)
|
|
}
|
|
|
|
// adConnect — POST: учётная запись домена и пароль; домен и контроллер — из расширенных настроек или bj находит
|
|
// их сам (addiscover.go). Контроллеры пробуются по очереди: сначала LDAPS (636), если порт закрыт — LDAP с
|
|
// StartTLS (389). Остальные настройки входа не трогает.
|
|
func (h *setupHandlers) adConnect(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
cur := h.rc.Snapshot().Auth
|
|
draft := adDraftAuth()
|
|
login := strings.TrimSpace(r.FormValue("ad_bind_user"))
|
|
pw := r.FormValue("ad_bind_password")
|
|
if pw == "" && draft != nil && draft.ADBindUser == login {
|
|
pw = draft.ADBindPassword // пароль, введённый в прошлой неудачной попытке
|
|
}
|
|
if pw == "" && cur.ADBindUser == login {
|
|
pw = cur.ADBindPassword
|
|
}
|
|
form := adForm{Server: strings.TrimSpace(r.FormValue("ad_server")), Domain: strings.ToLower(strings.Trim(strings.TrimSpace(strings.TrimPrefix(r.FormValue("ad_domain"), "@")), "."))}
|
|
a := cur
|
|
a.ADBindUser, a.ADBindPassword = login, pw
|
|
keep := func(b AuthSettings, msg string, cert *adCertInfo, adv bool) {
|
|
form.Adv = adv || form.Server != "" || form.Domain != ""
|
|
adDraftSet(b, msg, cert, form)
|
|
h.usersFlash(w, r, "Домен: "+msg)
|
|
}
|
|
var miss []string
|
|
if login == "" {
|
|
miss = append(miss, "учётная запись домена")
|
|
}
|
|
if pw == "" {
|
|
miss = append(miss, "пароль")
|
|
}
|
|
if len(miss) > 0 {
|
|
keep(a, "не заполнено: "+strings.Join(miss, ", "), nil, false)
|
|
return
|
|
}
|
|
domain := form.Domain
|
|
if _, d, ok := strings.Cut(login, "@"); ok && domain == "" {
|
|
domain = strings.ToLower(d)
|
|
}
|
|
var servers []string
|
|
if form.Server != "" {
|
|
servers = []string{form.Server}
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
|
if domain == "" || len(servers) == 0 {
|
|
if f := adDiscover(ctx, domain); f != nil {
|
|
if domain == "" {
|
|
domain = f.Domain
|
|
}
|
|
if len(servers) == 0 {
|
|
servers = f.DCs
|
|
}
|
|
}
|
|
}
|
|
cancel()
|
|
if domain == "" {
|
|
keep(a, "bj не нашёл домен сам: в сетевых настройках сервера bj его нет, а DNS-серверы не отвечают как контроллеры домена. Укажите домен в «Расширенных настройках» (например corp.example.ru) или учётную запись с доменом: svc-bj@corp.example.ru", nil, true)
|
|
return
|
|
}
|
|
a.ADDomain = domain
|
|
if len(servers) == 0 {
|
|
keep(a, "контроллеры домена "+domain+" не нашлись в DNS (_ldap._tcp.dc._msdcs."+domain+") — укажите контроллер в «Расширенных настройках»", nil, true)
|
|
return
|
|
}
|
|
if draft != nil && draft.ADCAPEM != "" && draft.ADDomain == domain {
|
|
a.ADCAPEM = draft.ADCAPEM // сертификат, которому уже доверились
|
|
}
|
|
var errs []string
|
|
for _, srv := range servers {
|
|
for _, v := range adVariants(srv) {
|
|
b := a
|
|
b.ADServer, b.ADStartTLS = v.url, v.startTLS
|
|
probe, cert, msg, next := adAttempt(b)
|
|
if probe != "" {
|
|
h.adSave(w, r, b, probe)
|
|
return
|
|
}
|
|
if !next {
|
|
keep(b, msg, cert, false)
|
|
return
|
|
}
|
|
errs = append(errs, msg)
|
|
}
|
|
}
|
|
keep(a, "ни один контроллер домена "+domain+" не подключился: "+strings.Join(errs, "; "), nil, true)
|
|
}
|
|
|
|
type adVariant struct {
|
|
url string
|
|
startTLS bool
|
|
}
|
|
|
|
// adVariants — как подключаться к контроллеру: адрес со схемой — как указан; имя — LDAPS, затем LDAP + StartTLS.
|
|
func adVariants(srv string) []adVariant {
|
|
if strings.Contains(srv, "://") {
|
|
return []adVariant{{srv, strings.HasPrefix(srv, "ldap://")}}
|
|
}
|
|
return []adVariant{{"ldaps://" + srv, false}, {"ldap://" + srv, true}}
|
|
}
|
|
|
|
// adAttempt — подключиться к одному контроллеру и войти сервисной записью. Успех — описание контроллера; иначе
|
|
// причина словами, сертификат (если дело в нём) и стоит ли пробовать другой контроллер или способ.
|
|
func adAttempt(a AuthSettings) (probe string, cert *adCertInfo, msg string, next bool) {
|
|
probe, err := adProbe(a)
|
|
if err != nil {
|
|
if adCertUntrusted(err) {
|
|
cert, _ = adFetchCert(a)
|
|
}
|
|
return "", cert, adExplain(err, a, cert), cert == nil
|
|
}
|
|
conn, err := adDial(a)
|
|
if err == nil {
|
|
err = adBindService(conn, a)
|
|
conn.Close()
|
|
}
|
|
if err != nil {
|
|
return "", nil, err.Error() + " — поправьте учётную запись или пароль", false
|
|
}
|
|
return probe, nil, "", false
|
|
}
|
|
|
|
// adSave — подключение прошло: сохранить. Если доверились сертификату самого контроллера, bj берёт из домена
|
|
// сертификат центра, который его выдал: тогда подходят все контроллеры и их новые сертификаты.
|
|
func (h *setupHandlers) adSave(w http.ResponseWriter, r *http.Request, a AuthSettings, probe string) {
|
|
note := ""
|
|
if a.ADCAPEM != "" {
|
|
if caPEM, ca, err := adUpgradeCA(a); err == nil && caPEM != "" {
|
|
a.ADCAPEM = caPEM
|
|
note = " Сертификаты контроллеров проверяются по центру сертификации «" + ca + "» — подойдут все контроллеры домена."
|
|
} else if err != nil {
|
|
log.Printf("ad: сертификат ЦС из домена не получен (%v) — проверка по сертификату контроллера", err)
|
|
note = " Сертификат контроллера сохранён; если его заменят, bj попросит подтвердить новый."
|
|
}
|
|
}
|
|
if safeReturnPath(r.FormValue("return_to")) != "" {
|
|
a.ADEnabled = true // в мастере подключение домена и есть включение входа через домен; в настройках — своя галочка
|
|
}
|
|
if err := h.rc.UpdateAuth(a); err != nil {
|
|
adDraftSet(a, "настройки не сохранены — "+err.Error(), nil, adForm{})
|
|
h.usersFlash(w, r, "Домен: настройки не сохранены — "+err.Error())
|
|
return
|
|
}
|
|
adDraftClear()
|
|
h.usersFlash(w, r, "Домен подключён ("+probe+"): можно добавлять сотрудников из домена — они входят своим доменным логином и паролем."+note)
|
|
}
|
|
|
|
// adUpgradeCA — сохранён сертификат самого контроллера (контроллер не прислал цепочку): войти по нему и прочитать
|
|
// в домене сертификат выдавшего центра (адрес — в сертификате, AIA; иначе — контейнер центров сертификации).
|
|
// Берётся только центр, чьей подписью подписан сертификат контроллера. Уже центр — ничего не делает.
|
|
func adUpgradeCA(a AuthSettings) (string, string, error) {
|
|
blk, _ := pem.Decode([]byte(a.ADCAPEM))
|
|
if blk == nil {
|
|
return "", "", fmt.Errorf("сертификат не разобран")
|
|
}
|
|
leaf, err := x509.ParseCertificate(blk.Bytes)
|
|
if err != nil || leaf.IsCA {
|
|
return "", "", err
|
|
}
|
|
conn, err := adDial(a)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer conn.Close()
|
|
if err := adBindService(conn, a); err != nil {
|
|
return "", "", err
|
|
}
|
|
var bases []string
|
|
for _, u := range leaf.IssuingCertificateURL {
|
|
if dn, ok := strings.CutPrefix(u, "ldap:///"); ok {
|
|
dn, _, _ = strings.Cut(dn, "?")
|
|
if d, err := url.PathUnescape(dn); err == nil {
|
|
bases = append(bases, d)
|
|
}
|
|
}
|
|
}
|
|
if def := adDefaultBase(conn); def != "" {
|
|
bases = append(bases, "CN=AIA,CN=Public Key Services,CN=Services,CN=Configuration,"+def,
|
|
"CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration,"+def)
|
|
}
|
|
for _, base := range bases {
|
|
sr, err := conn.Search(ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 50, 10, false,
|
|
"(objectClass=certificationAuthority)", []string{"cACertificate"}, nil))
|
|
if err != nil || sr == nil {
|
|
continue
|
|
}
|
|
for _, e := range sr.Entries {
|
|
for _, raw := range e.GetRawAttributeValues("cACertificate") {
|
|
ca, err := x509.ParseCertificate(raw)
|
|
if err != nil || !ca.IsCA || leaf.CheckSignatureFrom(ca) != nil {
|
|
continue
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw})), ca.Subject.CommonName, nil
|
|
}
|
|
}
|
|
}
|
|
return "", "", fmt.Errorf("центр сертификации %q в домене не найден", leaf.Issuer.CommonName)
|
|
}
|
|
|
|
// adTrust — POST: «Доверять» сертификату, который показал контроллер (черновик неудачной попытки). Отпечаток из
|
|
// формы должен совпасть с полученным: человек подтверждает именно тот сертификат, который видел на экране.
|
|
func (h *setupHandlers) adTrust(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
adDraft.Lock()
|
|
a, cert, form := adDraft.a, adDraft.cert, adDraft.form
|
|
adDraft.Unlock()
|
|
if a == nil || cert == nil || cert.SHA256 != r.FormValue("fp") {
|
|
h.usersFlash(w, r, "Домен: сертификат на странице устарел — нажмите «Подключить» ещё раз")
|
|
return
|
|
}
|
|
b := *a
|
|
b.ADCAPEM, b.ADSkipVerify = cert.PEM, false
|
|
probe, c2, msg, _ := adAttempt(b)
|
|
if probe == "" {
|
|
adDraftSet(b, msg, c2, form)
|
|
h.usersFlash(w, r, "Домен: "+msg)
|
|
return
|
|
}
|
|
h.adSave(w, r, b, probe)
|
|
}
|
|
|
|
// ---------- черновик подключения и сертификат контроллера ----------
|
|
|
|
// adCertInfo — сертификат, который предъявил контроллер домена (для решения «доверять»).
|
|
type adCertInfo struct {
|
|
Subject, Issuer, Until, SHA256 string
|
|
SHA1 string // «Отпечаток» в окне сертификата Windows — чтобы сверить на контроллере
|
|
IsCA bool // центр сертификации из цепочки (иначе — сертификат самого контроллера)
|
|
PEM string // что сохранится в настройках, если доверять
|
|
}
|
|
|
|
// adForm — расширенные настройки, как их ввёл человек (пусто — bj ищет сам).
|
|
type adForm struct {
|
|
Server, Domain string
|
|
Adv bool // открыть расширенные настройки: домен или контроллер не нашлись сами
|
|
}
|
|
|
|
// adDraftView — неудачная попытка подключить домен: что было введено и что не так (для формы).
|
|
type adDraftView struct {
|
|
Server string // контроллер, с которым не получилось (для карточки сертификата)
|
|
BindUser string
|
|
HasPassword bool
|
|
Form adForm
|
|
Err string
|
|
Cert *adCertInfo
|
|
}
|
|
|
|
const adDraftTTL = 2 * time.Hour
|
|
|
|
var adDraft struct {
|
|
sync.Mutex
|
|
a *AuthSettings
|
|
err string
|
|
cert *adCertInfo
|
|
form adForm
|
|
at time.Time
|
|
}
|
|
|
|
func adDraftSet(a AuthSettings, msg string, cert *adCertInfo, form adForm) {
|
|
adDraft.Lock()
|
|
defer adDraft.Unlock()
|
|
adDraft.a, adDraft.err, adDraft.cert, adDraft.form, adDraft.at = &a, msg, cert, form, time.Now()
|
|
}
|
|
|
|
func adDraftClear() {
|
|
adDraft.Lock()
|
|
defer adDraft.Unlock()
|
|
adDraft.a, adDraft.err, adDraft.cert = nil, "", nil
|
|
}
|
|
|
|
// adDraftAuth — введённое в последней неудачной попытке (nil — попытки не было или она давно).
|
|
func adDraftAuth() *AuthSettings {
|
|
adDraft.Lock()
|
|
defer adDraft.Unlock()
|
|
if adDraft.a == nil || time.Since(adDraft.at) > adDraftTTL {
|
|
return nil
|
|
}
|
|
a := *adDraft.a
|
|
return &a
|
|
}
|
|
|
|
// adDraftForView — для шаблона: черновик без пароля.
|
|
func adDraftForView() *adDraftView {
|
|
adDraft.Lock()
|
|
defer adDraft.Unlock()
|
|
if adDraft.a == nil || time.Since(adDraft.at) > adDraftTTL {
|
|
return nil
|
|
}
|
|
a := adDraft.a
|
|
host := a.ADServer
|
|
if u, err := url.Parse(a.ADServer); err == nil && u.Hostname() != "" {
|
|
host = u.Hostname()
|
|
}
|
|
return &adDraftView{Server: host, BindUser: a.ADBindUser, HasPassword: a.ADBindPassword != "", Form: adDraft.form, Err: adDraft.err, Cert: adDraft.cert}
|
|
}
|
|
|
|
// adCertUntrusted — TLS не прошёл из-за того, что bj не знает, кто выдал сертификат контроллера.
|
|
func adCertUntrusted(err error) bool {
|
|
s := err.Error()
|
|
return strings.Contains(s, "x509: certificate signed by unknown authority") || strings.Contains(s, "certificate is not trusted")
|
|
}
|
|
|
|
// adExplain — причина неудачи словами и что сделать.
|
|
func adExplain(err error, a AuthSettings, cert *adCertInfo) string {
|
|
s := err.Error()
|
|
host := a.ADServer
|
|
if u, e := url.Parse(a.ADServer); e == nil && u.Hostname() != "" {
|
|
host = u.Hostname()
|
|
}
|
|
switch {
|
|
case adCertUntrusted(err) && cert != nil:
|
|
return "контроллер " + host + " отвечает, но его сертификат выдан центром сертификации «" + cert.Issuer + "», которого bj пока не знает (обычно это центр сертификации самого домена). Проверьте сертификат ниже и нажмите «Доверять»."
|
|
case adCertUntrusted(err):
|
|
return "контроллер " + host + " отвечает, но его сертификат выдан неизвестным центром сертификации; вставьте сертификат корпоративного ЦС в Настройка → Пользователи → Active Directory"
|
|
case strings.Contains(s, "doesn't contain any IP SANs"):
|
|
return "контроллер указан IP-адресом, а сертификат выдан на имя — укажите имя контроллера, например dc1." + pickStr(a.ADDomain, "corp.local")
|
|
case strings.Contains(s, "x509: certificate is valid for"):
|
|
names := s[strings.Index(s, "x509: certificate is valid for")+len("x509: certificate is valid for "):]
|
|
if i := strings.Index(names, ", not "); i >= 0 {
|
|
names = names[:i]
|
|
}
|
|
return "сертификат контроллера выдан на другое имя (" + names + ") — укажите контроллер одним из этих имён"
|
|
case strings.Contains(s, "certificate has expired") || strings.Contains(s, "is not yet valid"):
|
|
return "срок сертификата контроллера " + host + " истёк или ещё не начался — обновите сертификат на контроллере или проверьте время на сервере bj"
|
|
case strings.Contains(s, "connection refused"):
|
|
port := "636"
|
|
if strings.HasPrefix(a.ADServer, "ldap://") {
|
|
port = "389"
|
|
}
|
|
return "контроллер " + host + " не принимает подключения на порту " + port + ": на нём не включён LDAPS или порт закрыт сетевым экраном"
|
|
case strings.Contains(s, "no such host"):
|
|
return "имя " + host + " не находится в DNS сервера bj — проверьте имя контроллера или укажите его полностью (dc1." + pickStr(a.ADDomain, "corp.local") + ")"
|
|
case strings.Contains(s, "i/o timeout") || strings.Contains(s, "deadline exceeded"):
|
|
return "контроллер " + host + " не ответил вовремя — проверьте, что сервер bj видит его по сети (порт 636)"
|
|
}
|
|
return "не удалось подключиться к контроллеру: " + s
|
|
}
|
|
|
|
// adFetchCert — сертификат, который предъявляет контроллер: центр сертификации из цепочки (верхний, что прислал
|
|
// контроллер), а если цепочки нет — сертификат самого контроллера. Соединение — без проверки и только для чтения
|
|
// сертификата; пароль не отправляется.
|
|
func adFetchCert(a AuthSettings) (*adCertInfo, error) {
|
|
b := a
|
|
b.ADSkipVerify, b.ADCAPEM = true, ""
|
|
conn, err := adDial(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
st, ok := conn.TLSConnectionState()
|
|
if !ok || len(st.PeerCertificates) == 0 {
|
|
return nil, fmt.Errorf("контроллер не предъявил сертификат")
|
|
}
|
|
pcs := st.PeerCertificates
|
|
c := pcs[0]
|
|
for i := len(pcs) - 1; i >= 1; i-- {
|
|
if pcs[i].IsCA {
|
|
c = pcs[i]
|
|
break
|
|
}
|
|
}
|
|
sum := sha256.Sum256(c.Raw)
|
|
hx := strings.ToUpper(hex.EncodeToString(sum[:]))
|
|
var fp []string
|
|
for i := 0; i < len(hx); i += 2 {
|
|
fp = append(fp, hx[i:i+2])
|
|
}
|
|
s1 := sha1.Sum(c.Raw)
|
|
name := func(n pkix.Name) string {
|
|
if n.CommonName != "" {
|
|
return n.CommonName
|
|
}
|
|
return n.String()
|
|
}
|
|
return &adCertInfo{Subject: name(c.Subject), Issuer: name(c.Issuer), Until: c.NotAfter.In(time.Local).Format("02.01.2006"),
|
|
SHA256: strings.Join(fp, ":"), SHA1: strings.ToLower(hex.EncodeToString(s1[:])), IsCA: c.IsCA, PEM: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}))}, nil
|
|
}
|
|
|
|
// adUserView — сотрудник или группа домена в результатах поиска.
|
|
type adUserView struct {
|
|
Kind string `json:"kind"` // user | group
|
|
Login string `json:"login"` // sAMAccountName
|
|
Name string `json:"name"`
|
|
Email string `json:"email,omitempty"`
|
|
Desc string `json:"desc,omitempty"` // описание группы
|
|
Have bool `json:"have,omitempty"` // сотрудник уже есть в bj / группе уже дан доступ
|
|
}
|
|
|
|
// adSearch — GET ?q=: сотрудники (только включённые учётные записи) и группы домена по фамилии, логину, почте
|
|
// или названию группы (до 25).
|
|
func (h *setupHandlers) adSearch(w http.ResponseWriter, r *http.Request) {
|
|
a := h.rc.Snapshot().Auth
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
if !a.ADEnabled || a.ADBindUser == "" {
|
|
jsonOut(w, map[string]any{"ok": false, "error": "домен не подключён"})
|
|
return
|
|
}
|
|
if len([]rune(q)) < 2 {
|
|
jsonOut(w, map[string]any{"ok": true, "users": []adUserView{}})
|
|
return
|
|
}
|
|
users, err := adSearchUsers(a, q, 25)
|
|
if err != nil {
|
|
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
jsonOut(w, map[string]any{"ok": true, "users": users})
|
|
}
|
|
|
|
func adSearchUsers(a AuthSettings, q string, limit int) ([]adUserView, error) {
|
|
conn, err := adDial(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
conn.SetTimeout(10 * time.Second)
|
|
if err := adBindService(conn, a); err != nil {
|
|
return nil, err
|
|
}
|
|
base := a.ADBaseDN
|
|
if base == "" {
|
|
base = adDefaultBase(conn)
|
|
}
|
|
e := ldap.EscapeFilter(q)
|
|
filter := fmt.Sprintf("(|(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(sAMAccountName=*%s*)(displayName=*%s*)(sn=*%s*)(mail=*%s*)))"+
|
|
"(&(objectCategory=group)(|(sAMAccountName=*%s*)(cn=*%s*)(description=*%s*))))", e, e, e, e, e, e, e)
|
|
sr, err := conn.Search(ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, limit, 10, false,
|
|
filter, []string{"sAMAccountName", "displayName", "mail", "cn", "description", "objectClass"}, nil))
|
|
if err != nil && (sr == nil || len(sr.Entries) == 0) { // превышение лимита — не ошибка, берём найденное
|
|
return nil, fmt.Errorf("поиск в домене: %v", err)
|
|
}
|
|
var users, groups []adUserView
|
|
for _, en := range sr.Entries {
|
|
u := adUserView{Kind: "user", Login: en.GetAttributeValue("sAMAccountName"), Name: en.GetAttributeValue("displayName"), Email: en.GetAttributeValue("mail")}
|
|
if u.Login == "" {
|
|
continue
|
|
}
|
|
for _, c := range en.GetAttributeValues("objectClass") {
|
|
if strings.EqualFold(c, "group") {
|
|
u.Kind, u.Name, u.Email, u.Desc = "group", pickStr(en.GetAttributeValue("cn"), u.Login), "", en.GetAttributeValue("description")
|
|
}
|
|
}
|
|
if u.Kind == "group" {
|
|
u.Have = adGroupRoleOf(a, u.Login) != ""
|
|
groups = append(groups, u)
|
|
continue
|
|
}
|
|
u.Have = globalUsers.Get(u.Login) != nil
|
|
users = append(users, u)
|
|
}
|
|
return append(users, groups...), nil
|
|
}
|
|
|
|
// Name — имя группы для экрана (из DN — CN).
|
|
func (g ADGroupRole) Name() string { return dnCN(g.Group) }
|
|
|
|
// ADServerHost — имя контроллера без схемы и порта (для экрана).
|
|
func (a AuthSettings) ADServerHost() string {
|
|
if u, err := url.Parse(a.ADServer); err == nil && u.Hostname() != "" {
|
|
return u.Hostname()
|
|
}
|
|
return a.ADServer
|
|
}
|
|
|
|
// adGroupRoleOf — роль, которую даёт группа домена (пусто — группе доступ не дан).
|
|
func adGroupRoleOf(a AuthSettings, group string) string {
|
|
for _, g := range a.ADGroups {
|
|
if strings.EqualFold(dnCN(g.Group), group) || strings.EqualFold(g.Group, group) {
|
|
return g.Role
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// adGroupSave — POST: всем сотрудникам группы домена — вход в bj с ролью (remove=1 — убрать группу). Сотрудник
|
|
// входит своим доменным логином; учётная запись в bj заводится при первом входе.
|
|
func (h *setupHandlers) adGroupSave(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
a := h.rc.Snapshot().Auth
|
|
g := strings.TrimSpace(r.FormValue("group"))
|
|
if g == "" {
|
|
h.usersFlash(w, r, "Группа домена: не выбрана")
|
|
return
|
|
}
|
|
var keep []ADGroupRole
|
|
for _, x := range a.ADGroups {
|
|
if !strings.EqualFold(dnCN(x.Group), g) && !strings.EqualFold(x.Group, g) {
|
|
keep = append(keep, x)
|
|
}
|
|
}
|
|
msg := "Группа «" + g + "» убрана: её сотрудники больше не входят в bj (кроме заведённых отдельно)."
|
|
if r.FormValue("remove") == "" {
|
|
rl, ok := roleByKey(r.FormValue("role"))
|
|
if !ok || !roleAvailable(rl.Key) {
|
|
h.usersFlash(w, r, "Группа домена: выберите роль")
|
|
return
|
|
}
|
|
keep = append(keep, ADGroupRole{Group: g, Role: rl.Key})
|
|
msg = "Группа «" + g + "»: все её сотрудники входят в bj своим доменным логином и паролем с ролью «" + rl.Title + "»."
|
|
}
|
|
a.ADGroups = keep
|
|
if err := h.rc.UpdateAuth(a); err != nil {
|
|
h.usersFlash(w, r, "Группа домена: "+err.Error())
|
|
return
|
|
}
|
|
igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам
|
|
h.usersFlash(w, r, msg)
|
|
}
|