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
}
@@ -54,9 +54,24 @@
<button type="submit" class="btn btn-ok">Дать доступ группе</button>
</form>
{{if $A.ADGroups}}<div class="ad-groups"><div class="muted" style="font-size:13px;margin-bottom:6px">Группы домена с доступом в bj — их сотрудники входят без отдельной учётной записи:</div>
{{range $A.ADGroups}}<div class="ad-grp"><span><b>{{.Name}}</b> → {{roleTitle .Role}}</span>
{{range $A.ADGroups}}<div class="ad-grp"><span><b>{{.Name}}</b> → {{roleTitle .Role}} <details class="ad-mem" data-dn="{{.Group}}" style="display:inline-block;margin-left:8px;vertical-align:top"><summary style="cursor:pointer;font-size:12.5px;color:var(--accent)">участники: …</summary><div class="ad-mem-list" style="font-size:12.5px;margin:6px 0 2px;max-height:260px;overflow:auto"></div></details></span>
<form method="post" action="/admin/setup/users/ad-group"><input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="group" value="{{.Name}}"><input type="hidden" name="remove" value="1"><button type="submit" class="dq-link">убрать</button></form></div>{{end}}
</div>{{end}}
</div>
<script>(function(){ // участники групп — кто получает доступ в bj через группу (с вложенными группами)
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
document.querySelectorAll('details.ad-mem').forEach(function(d){
var sum=d.querySelector('summary'), box=d.querySelector('.ad-mem-list');
fetch('/admin/setup/users/ad-group-members?dn='+encodeURIComponent(d.getAttribute('data-dn')),{cache:'no-store'}).then(function(r){return r.json()}).then(function(x){
if(!x.ok){ sum.textContent='участники: не получены'; box.innerHTML='<span style="color:var(--err)">'+esc(x.error)+'</span>'; return; }
var m=x.members||[], off=m.filter(function(u){return u.disabled}).length;
sum.textContent='участники: '+(m.length-off)+(off?' (+'+off+' отключены)':'');
box.innerHTML=m.length?m.map(function(u){
return '<div'+(u.disabled?' style="opacity:.55"':'')+'>'+esc(u.name||u.login)+' <span class="muted">'+esc(u.login)+(u.email?' · '+esc(u.email):'')+'</span>'+
(u.disabled?' <span class="muted">— отключён в AD</span>':'')+(u.in_bj?' <span class="muted">· входил в bj</span>':'')+'</div>';
}).join(''):'<span class="muted">в группе никого нет</span>';
}).catch(function(){ sum.textContent='участники: нет связи'; });
});
})();</script>{{end}}
<script>(function(){
var q=document.getElementById('acc-ad-q'), st=document.getElementById('acc-ad-st'), ul=document.getElementById('acc-ad-res'), f=document.getElementById('acc-ad-form'), gf=document.getElementById('acc-ad-gform'), who=document.getElementById('acc-ad-who'), gwho=document.getElementById('acc-ad-gwho'), t=0;
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}