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:
@@ -16,6 +16,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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-trust", h.adTrust)
|
||||||
mux.HandleFunc("/admin/setup/users/ad-discover", h.adDiscoverHTTP)
|
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", h.adGroupSave)
|
||||||
|
mux.HandleFunc("/admin/setup/users/ad-group-members", h.adGroupMembersHTTP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adConnect — POST: учётная запись домена и пароль; домен и контроллер — из расширенных настроек или bj находит
|
// adConnect — POST: учётная запись домена и пароль; домен и контроллер — из расширенных настроек или bj находит
|
||||||
@@ -546,3 +549,78 @@ func (h *setupHandlers) adGroupSave(w http.ResponseWriter, r *http.Request) {
|
|||||||
igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам
|
igateShareSyncSoon(h.rc) // папки шлюза «через домен» — тем же группам
|
||||||
h.usersFlash(w, r, msg)
|
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>
|
<button type="submit" class="btn btn-ok">Дать доступ группе</button>
|
||||||
</form>
|
</form>
|
||||||
{{if $A.ADGroups}}<div class="ad-groups"><div class="muted" style="font-size:13px;margin-bottom:6px">Группы домена с доступом в bj — их сотрудники входят без отдельной учётной записи:</div>
|
{{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}}
|
<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{'&':'&','<':'<','>':'>','"':'"'}[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(){
|
<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;
|
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{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||||
|
|||||||
Reference in New Issue
Block a user