Пароль домена не попадает в страницу; домен в мастере — открыт, крупно, группы; справочник DEPO без переводов; 1С — бэк-офис
- Безопасность: в мастере подпись к паролю выводила сам сохранённый пароль служебной записи домена — теперь только «задан/введён» (тест: пароль не попадает в HTML). Список сессий в настройках больше не содержит токены (по ним можно было войти под чужим именем) — короткий ID. - Домен: после подключения и «Доверять» блок остаётся открытым, страница возвращается к нему; карточка «Домен подключён» и «Найден домен» — крупно; поиск сотрудников и групп; группе — доступ с ролью (ADGroups), список групп с «убрать». - «Обмен с DEPO»: справочник FTV_M2M_HANDBOOK считается и когда переводов ещё нет (на свежей установке писало «не создана»). - Модуль 1С: onec-back «1С: база бэк-офиса (тест)» вместо мидл-офиса (в лицензиях не выдавался). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -28,6 +28,7 @@ func (h *setupHandlers) registerADWizard(mux *http.ServeMux) {
|
||||
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 находит
|
||||
@@ -412,15 +413,18 @@ func adFetchCert(a AuthSettings) (*adCertInfo, error) {
|
||||
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 — сотрудник домена в результатах поиска.
|
||||
// adUserView — сотрудник или группа домена в результатах поиска.
|
||||
type adUserView struct {
|
||||
Kind string `json:"kind"` // user | group
|
||||
Login string `json:"login"` // sAMAccountName
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Have bool `json:"have,omitempty"` // уже есть в bj
|
||||
Desc string `json:"desc,omitempty"` // описание группы
|
||||
Have bool `json:"have,omitempty"` // сотрудник уже есть в bj / группе уже дан доступ
|
||||
}
|
||||
|
||||
// adSearch — GET ?q=: сотрудники домена по фамилии, логину или почте (до 20, только включённые учётные записи).
|
||||
// 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"))
|
||||
@@ -432,7 +436,7 @@ func (h *setupHandlers) adSearch(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOut(w, map[string]any{"ok": true, "users": []adUserView{}})
|
||||
return
|
||||
}
|
||||
users, err := adSearchUsers(a, q, 20)
|
||||
users, err := adSearchUsers(a, q, 25)
|
||||
if err != nil {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
@@ -455,20 +459,89 @@ func adSearchUsers(a AuthSettings, q string, limit int) ([]adUserView, error) {
|
||||
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*)))", e, e, e, e)
|
||||
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"}, nil))
|
||||
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 out []adUserView
|
||||
var users, groups []adUserView
|
||||
for _, en := range sr.Entries {
|
||||
u := adUserView{Login: en.GetAttributeValue("sAMAccountName"), Name: en.GetAttributeValue("displayName"), Email: en.GetAttributeValue("mail")}
|
||||
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
|
||||
out = append(out, u)
|
||||
users = append(users, u)
|
||||
}
|
||||
return out, nil
|
||||
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
|
||||
}
|
||||
h.usersFlash(w, r, msg)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user