AD: участники групп с доступом в bj

У каждой группы домена в «Пользователи и вход» — «участники: N»: ФИО,
логин, почта, отметки «отключён в AD» и «входил в bj»; с вложенными
группами (LDAP_MATCHING_RULE_IN_CHAIN). GET
/admin/setup/users/ad-group-members — только для групп из настроек bj.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-22 13:37:51 +03:00
parent 76e6f82369
commit fbcc859289
2 changed files with 95 additions and 2 deletions
+78
View File
@@ -16,6 +16,8 @@ import (
"log"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -29,6 +31,7 @@ func (h *setupHandlers) registerADWizard(mux *http.ServeMux) {
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)
mux.HandleFunc("/admin/setup/users/ad-group-members", h.adGroupMembersHTTP)
}
// adConnect — POST: учётная запись домена и пароль; домен и контроллер — из расширенных настроек или bj находит
@@ -546,3 +549,78 @@ func (h *setupHandlers) adGroupSave(w http.ResponseWriter, r *http.Request) {
igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам
h.usersFlash(w, r, msg)
}
// adMember — участник группы домена (для контроля, кто получает доступ в bj через группу).
type adMember struct {
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Disabled bool `json:"disabled,omitempty"` // учётная запись отключена в AD — войти не сможет
InBJ bool `json:"in_bj,omitempty"` // уже входил в bj (есть пользователь bj)
}
// adGroupMembersHTTP — GET /admin/setup/users/ad-group-members?dn=…: участники группы с доступом в bj,
// включая вложенные группы (LDAP_MATCHING_RULE_IN_CHAIN). Только группы из настроек bj.
func (h *setupHandlers) adGroupMembersHTTP(w http.ResponseWriter, r *http.Request) {
a := h.rc.Snapshot().Auth
dn := strings.TrimSpace(r.URL.Query().Get("dn"))
if !a.ADEnabled || a.ADBindUser == "" {
jsonOut(w, map[string]any{"ok": false, "error": "домен не подключён"})
return
}
known := false
for _, g := range a.ADGroups {
known = known || strings.EqualFold(g.Group, dn)
}
if !known {
jsonOut(w, map[string]any{"ok": false, "error": "группы нет в настройках bj"})
return
}
list, err := adGroupMembers(a, dn, 1000)
if err != nil {
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
return
}
jsonOut(w, map[string]any{"ok": true, "members": list})
}
func adGroupMembers(a AuthSettings, groupDN string, limit int) ([]adMember, error) {
conn, err := adDial(a)
if err != nil {
return nil, err
}
defer conn.Close()
conn.SetTimeout(15 * time.Second)
if err := adBindService(conn, a); err != nil {
return nil, err
}
base := a.ADBaseDN
if base == "" {
base = adDefaultBase(conn)
}
filter := "(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=" + ldap.EscapeFilter(groupDN) + "))"
sr, err := conn.Search(ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, limit, 15, false,
filter, []string{"sAMAccountName", "displayName", "mail", "userAccountControl"}, nil))
if err != nil && (sr == nil || len(sr.Entries) == 0) {
return nil, fmt.Errorf("участники группы: %v", err)
}
out := []adMember{}
for _, en := range sr.Entries {
m := adMember{Login: en.GetAttributeValue("sAMAccountName"), Name: en.GetAttributeValue("displayName"), Email: en.GetAttributeValue("mail")}
if m.Login == "" {
continue
}
if uac, err := strconv.Atoi(en.GetAttributeValue("userAccountControl")); err == nil && uac&2 != 0 {
m.Disabled = true
}
m.InBJ = globalUsers.Get(m.Login) != nil
out = append(out, m)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Disabled != out[j].Disabled {
return !out[i].Disabled
}
return strings.ToLower(pickStr(out[i].Name, out[i].Login)) < strings.ToLower(pickStr(out[j].Name, out[j].Login))
})
return out, nil
}