Мастер: шапка на месте; домен — только логин и пароль; модуль «1С: база мидл-офиса» (тест)
- Шапка мастера: правый блок (шлюз НРД, пользователь, тема) снова справа — после правила [hidden] скрытое меню перестало его распирать. - Домен в шаге «Учётные записи»: bj сам находит домен и контроллеры (логин с @домен, домены поиска DNS, rootDSE DNS-серверов, SRV _ldap._tcp.dc._msdcs); адрес и домен — в расширенных настройках, открываются сами, если не нашлось. Контроллеры по очереди, LDAPS → LDAP+StartTLS. - Неудачная попытка ничего не теряет: введённое (и пароль) — черновиком, форма заполнена; причины словами (неизвестный ЦС, имя в сертификате, IP вместо имени, порт закрыт, нет в DNS). - Сертификат неизвестного центра: карточка «кем выдан, отпечаток (SHA-1 как в Windows и SHA-256)» и «Доверять и подключить»; после входа bj берёт из домена сертификат центра (AIA / Certification Authorities), подписавший сертификат контроллера, — подходят все контроллеры. То же в Настройка → Пользователи при «Проверить». - Лицензия: модуль onec-middle «1С: база мидл-офиса (тест)»; раздел настроек — адрес публикации, пользователь, пароль, проверка через OData $metadata; в мастере — в «Модулях». - ЛК (callback, API, готовность) — только при модуле «ЕСИА Финанс»; карточка режима QUIK и стили настроек как в мастере. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
+366
-21
@@ -5,9 +5,19 @@ package lkgateway
|
||||
// выбрать сотрудника поиском: он входит своим доменным логином и паролем, 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"
|
||||
@@ -16,39 +26,121 @@ import (
|
||||
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)
|
||||
}
|
||||
|
||||
// adConnect — POST: адрес контроллера, домен, сервисная учётная запись (для поиска сотрудников); остальные
|
||||
// настройки входа не трогает. Сразу проверяет связь: контроллер отвечает и сервисная запись входит.
|
||||
// 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
|
||||
}
|
||||
a := h.rc.Snapshot().Auth
|
||||
a.ADServer = strings.TrimSpace(r.FormValue("ad_server"))
|
||||
a.ADDomain = strings.TrimSpace(strings.TrimPrefix(r.FormValue("ad_domain"), "@"))
|
||||
a.ADBindUser = strings.TrimSpace(r.FormValue("ad_bind_user"))
|
||||
if pw := r.FormValue("ad_bind_password"); pw != "" {
|
||||
a.ADBindPassword = pw
|
||||
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 a.ADServer != "" && !strings.Contains(a.ADServer, "://") {
|
||||
a.ADServer = "ldaps://" + a.ADServer // как в проводнике вводят имя — по умолчанию защищённое подключение
|
||||
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
|
||||
for _, f := range []struct{ v, n string }{{a.ADServer, "контроллер домена"}, {a.ADDomain, "домен"}, {a.ADBindUser, "сервисная учётная запись"}, {a.ADBindPassword, "её пароль"}} {
|
||||
if f.v == "" {
|
||||
miss = append(miss, f.n)
|
||||
}
|
||||
if login == "" {
|
||||
miss = append(miss, "учётная запись домена")
|
||||
}
|
||||
if pw == "" {
|
||||
miss = append(miss, "пароль")
|
||||
}
|
||||
if len(miss) > 0 {
|
||||
h.usersFlash(w, r, "Домен: не заполнено: "+strings.Join(miss, ", "))
|
||||
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 {
|
||||
h.usersFlash(w, r, "Домен: контроллер не отвечает — "+err.Error()+". Настройки не сохранены.")
|
||||
return
|
||||
if adCertUntrusted(err) {
|
||||
cert, _ = adFetchCert(a)
|
||||
}
|
||||
return "", cert, adExplain(err, a, cert), cert == nil
|
||||
}
|
||||
conn, err := adDial(a)
|
||||
if err == nil {
|
||||
@@ -56,15 +148,268 @@ func (h *setupHandlers) adConnect(w http.ResponseWriter, r *http.Request) {
|
||||
conn.Close()
|
||||
}
|
||||
if err != nil {
|
||||
h.usersFlash(w, r, "Домен: "+err.Error()+". Настройки не сохранены.")
|
||||
return
|
||||
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 // в мастере подключение домена и есть включение входа через домен; в настройках — своя галочка
|
||||
}
|
||||
a.ADEnabled = true
|
||||
if err := h.rc.UpdateAuth(a); err != nil {
|
||||
adDraftSet(a, "настройки не сохранены — "+err.Error(), nil, adForm{})
|
||||
h.usersFlash(w, r, "Домен: настройки не сохранены — "+err.Error())
|
||||
return
|
||||
}
|
||||
h.usersFlash(w, r, "Домен подключён ("+probe+"): можно добавлять сотрудников из домена — они входят своим доменным логином и паролем.")
|
||||
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 — сотрудник домена в результатах поиска.
|
||||
|
||||
Reference in New Issue
Block a user