fbcc859289
У каждой группы домена в «Пользователи и вход» — «участники: 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
201 lines
23 KiB
HTML
201 lines
23 KiB
HTML
{{/* Учётные записи: добавить сотрудника (роль плиткой, логин, пароль) и сотрудники из домена (подключение,
|
||
поиск сотрудников и групп, доступ группам). Общий блок шага мастера «Учётные записи» и раздела
|
||
Настройка → Пользователи. Вход: withRet <SetupData> <куда вернуться после сохранения>. */}}
|
||
{{define "accountsAdd"}}{{$U := .D.Users}}
|
||
<form method="post" action="/admin/setup/users" id="acc-form">
|
||
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="create" value="1">
|
||
<input type="hidden" name="active" value="1"><input type="hidden" name="source" value="local">
|
||
<h3 style="margin:16px 0 0">Кого добавить</h3>
|
||
<div class="acc-roles">
|
||
{{range $r := $U.Roles}}
|
||
<label class="acc-role"><input type="radio" name="role" value="{{$r.Key}}" {{if eq $r.Key (or (and $U.Open "admin") "operator")}}checked{{end}}>
|
||
<span><b>{{$r.Title}}</b><small>{{$r.Desc}}</small>
|
||
<span class="acc-who">Уже есть: {{$n := 0}}{{range $U.Users}}{{if eq .Role $r.Key}}{{if $n}}, {{end}}{{$n = 1}}{{.Login}}{{if .Name}} ({{.Name}}){{end}}{{if not .Active}} — заблокирован{{end}}{{end}}{{end}}{{if not $n}}<span class="muted">никого</span>{{end}}</span></span></label>
|
||
{{end}}
|
||
</div>
|
||
<div class="acc-form">
|
||
<label>Логин<input type="text" name="login" placeholder="ivanov" required autocomplete="off" pattern="[^ /\@"'<>]+"></label>
|
||
<label>Имя<input type="text" name="name" placeholder="Иванов Иван Иванович" autocomplete="off"></label>
|
||
<label>E-mail<input type="email" name="email" placeholder="ivanov@example.ru" autocomplete="off"></label>
|
||
<div class="acc-pw"><label style="flex:1">Пароль (от 8 символов)<input type="text" name="password" id="acc-pw" minlength="8" required autocomplete="off" spellcheck="false"></label>
|
||
<button type="button" class="btn btn-secondary" id="acc-gen" style="white-space:nowrap">Придумать</button></div>
|
||
</div>
|
||
{{if $U.Modules}}<div class="acc-mods"><span class="muted">Дополнительно к роли:</span>{{range $U.Modules}}<label><input type="checkbox" name="modules" value="{{.Key}}">{{.Title}}</label>{{end}}</div>{{end}}
|
||
<p class="muted" style="font-size:12.5px;margin:8px 0 0">Пароль передайте сотруднику лично; сменить его можно позже: Настройка → Пользователи → «Изменить».</p>
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-ok">Создать учётную запись</button></div>
|
||
</form>
|
||
<script>(function(){
|
||
var g=document.getElementById('acc-gen'), f=document.getElementById('acc-pw'); if(!g||!f||!window.crypto) return;
|
||
g.addEventListener('click',function(){ var a='abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789', b=new Uint32Array(12), p='';
|
||
crypto.getRandomValues(b); for(var i=0;i<b.length;i++){ p+=a[b[i]%a.length]; if(i===3||i===7) p+='-'; } f.value=p; f.select(); });
|
||
})();</script>
|
||
{{end}}
|
||
|
||
{{define "accountsAD"}}
|
||
{{$A := $.D.Settings.Auth}}{{$D := adDraft}}{{$on := and $A.ADEnabled $A.ADBindUser}}
|
||
<details class="acc-ad" id="acc-ad" {{if or $D $on}}open{{end}} style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border)">
|
||
<summary style="cursor:pointer;color:var(--accent);font-size:14px">Сотрудники из домена (Active Directory) — необязательно</summary>
|
||
{{if $on}}
|
||
<div class="ad-st ad-st-ok"><span class="ad-st-ic"></span><div><b>Домен {{$A.ADDomain}} подключён</b><small>контроллер {{$A.ADServerHost}} · служебная запись {{$A.ADBindUser}}{{if $A.ADCAPEM}} · сертификат контроллера проверяется{{end}}</small></div></div>
|
||
<label class="ad-q-l">Найдите сотрудника или группу<input type="search" id="acc-ad-q" placeholder="Фамилия, логин, почта или название группы" autocomplete="off"></label>
|
||
<div id="acc-ad-st" class="muted" style="font-size:13px;margin-top:6px" aria-live="polite"></div>
|
||
<ul id="acc-ad-res" class="ad-res"></ul>
|
||
<form method="post" action="/admin/setup/users" id="acc-ad-form" class="ad-pick" hidden>
|
||
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="create" value="1"><input type="hidden" name="active" value="1"><input type="hidden" name="source" value="ad">
|
||
<input type="hidden" name="login"><input type="hidden" name="name"><input type="hidden" name="email">
|
||
<span>Сотрудник <b id="acc-ad-who"></b> — входит своим доменным логином и паролем, с ролью</span>
|
||
<select name="role">{{range $.D.Users.Roles}}<option value="{{.Key}}" {{if eq .Key "operator"}}selected{{end}}>{{.Title}}</option>{{end}}</select>
|
||
<button type="submit" class="btn btn-ok">Добавить сотрудника</button>
|
||
</form>
|
||
<form method="post" action="/admin/setup/users/ad-group" id="acc-ad-gform" class="ad-pick" hidden>
|
||
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad"><input type="hidden" name="group">
|
||
<span>Все сотрудники группы <b id="acc-ad-gwho"></b> — входят своим доменным логином и паролем, с ролью</span>
|
||
<select name="role">{{range $.D.Users.Roles}}<option value="{{.Key}}" {{if eq .Key "operator"}}selected{{end}}>{{.Title}}</option>{{end}}</select>
|
||
<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}} <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>
|
||
<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(){
|
||
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]})}
|
||
q.addEventListener('input',function(){ clearTimeout(t); t=setTimeout(function(){
|
||
var v=q.value.trim(); f.hidden=true; gf.hidden=true; if(v.length<2){ ul.innerHTML=''; st.textContent=''; return; } st.textContent='Ищу в домене…';
|
||
fetch('/admin/setup/users/ad-search?q='+encodeURIComponent(v),{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
|
||
if(!d.ok){ st.textContent='Не получилось: '+d.error; ul.innerHTML=''; return; }
|
||
st.textContent=d.users.length?'Найдено: '+d.users.length+' — выберите, кого добавить':'Никого не нашлось';
|
||
ul.innerHTML=d.users.map(function(u,i){ var g=u.kind==='group';
|
||
return '<li><button type="button" class="ad-hit" data-i="'+i+'"'+(u.have?' disabled':'')+'><span class="ad-kind'+(g?' ad-kind-g':'')+'">'+(g?'группа':'сотрудник')+'</span><b>'+esc(u.name||u.login)+'</b> <span class="muted">'+(g?esc(u.desc||u.login):esc(u.login)+(u.email?' · '+esc(u.email):''))+(u.have?(g?' · доступ уже дан':' · уже есть в bj'):'')+'</span></button></li>'; }).join('');
|
||
ul.querySelectorAll('button').forEach(function(b){ b.onclick=function(){ var u=d.users[+b.dataset.i];
|
||
ul.querySelectorAll('.ad-hit').forEach(function(x){ x.classList.toggle('is-sel',x===b); });
|
||
if(u.kind==='group'){ gf.group.value=u.login; gwho.textContent='«'+(u.name||u.login)+'»'; gf.hidden=false; f.hidden=true; gf.scrollIntoView({block:'nearest',behavior:'smooth'}); return; }
|
||
f.login.value=u.login; f.name.value=u.name||''; f.email.value=u.email||''; who.textContent=(u.name||u.login)+' ('+u.login+')'; f.hidden=false; gf.hidden=true; f.scrollIntoView({block:'nearest',behavior:'smooth'}); }; });
|
||
}).catch(function(){ st.textContent='Домен не ответил'; });
|
||
},350); });
|
||
if(document.getElementById('acc-ad').open && !document.querySelector('#ad-connect[open]')) q.focus({preventScroll:true});
|
||
})();</script>
|
||
{{end}}
|
||
<details id="ad-connect" {{if or $D (not (and $A.ADEnabled $A.ADBindUser))}}open{{end}} style="margin-top:12px"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">{{if and $A.ADEnabled $A.ADBindUser}}Другой домен или учётная запись{{else}}Подключить домен{{end}}</summary>
|
||
{{/* пароль в страницу не попадает никогда — только пометка, что он есть */}}
|
||
{{$bu := $A.ADBindUser}}{{$pw := ""}}{{if $A.ADBindPassword}}{{$pw = "задан"}}{{end}}{{$fs := ""}}{{$fd := ""}}{{$adv := false}}
|
||
{{if $D}}{{$bu = $D.BindUser}}{{$pw = ""}}{{if $D.HasPassword}}{{$pw = "введён"}}{{end}}{{$fs = $D.Form.Server}}{{$fd = $D.Form.Domain}}{{$adv = $D.Form.Adv}}{{end}}
|
||
<p class="muted" style="font-size:13px;margin:8px 0 0">Введите учётную запись домена и пароль — домен и его контроллеры bj найдёт сам. Прав администратора домена не нужно; лучше отдельная служебная запись (например svc-bj): bj хранит её пароль, чтобы искать сотрудников.</p>
|
||
<div id="ad-found" class="ad-st" hidden aria-live="polite"></div>
|
||
<details id="ad-svc" style="margin:10px 0 0"><summary style="cursor:pointer;font-size:13px;color:var(--accent)">Как создать служебную учётную запись для bj</summary>
|
||
<div style="font-size:13px;line-height:1.5;margin-top:8px">
|
||
<p style="margin:0 0 6px">bj читает AD обычной доменной учётной записью: проверяет пароль пользователя и его группы. Прав администратора домена ей не нужно — читать каталог могут все пользователи домена. Создаёт запись администратор домена:</p>
|
||
<ol style="margin:0 0 8px;padding-left:20px">
|
||
<li>На контроллере домена (или компьютере с RSAT) откройте PowerShell <b>от имени администратора домена</b>.</li>
|
||
<li>Скопируйте и выполните скрипт ниже. Он создаст <code>svc-bj</code> со случайным паролем и группы (или сообщит, что они уже есть) и в конце покажет итог и пароль.</li>
|
||
<li>Добавьте сотрудников в группы <code>bj-admins</code>, <code>bj-operators</code>, <code>bj-risk</code>, <code>bj-viewers</code> — по ним bj назначит роли (группы можно переназначить после подключения).</li>
|
||
<li>Здесь введите <code>svc-bj@<span class="ad-svc-dom">{{with $A.ADDomain}}{{.}}{{else}}ваш-домен{{end}}</span></code> и пароль — «Подключить».</li>
|
||
</ol>
|
||
<pre class="copyable" id="ad-svc-ps" style="white-space:pre;overflow:auto;font-size:12px"># Bridge&Join: служебная учётная запись для чтения AD и группы ролей
|
||
# Запуск: PowerShell от имени администратора домена (контроллер домена или компьютер с RSAT).
|
||
Import-Module ActiveDirectory
|
||
$dom = Get-ADDomain # домен <span class="ad-svc-dom">{{with $A.ADDomain}}{{.}}{{else}}ваш-домен{{end}}</span>
|
||
$name = "svc-bj"
|
||
$ou = $dom.UsersContainer # или своё OU, например "OU=Service Accounts,$($dom.DistinguishedName)"
|
||
$report = @()
|
||
|
||
# пароль — 24 случайных символа (буквы, цифры, знаки)
|
||
$chars = [char[]]'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%*+-=?@'
|
||
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
||
$bytes = New-Object byte[] 24; $rng.GetBytes($bytes)
|
||
$plain = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
|
||
|
||
$user = Get-ADUser -Filter "SamAccountName -eq '$name'" -ErrorAction SilentlyContinue
|
||
if ($user) {
|
||
$report += "Пользователь $name уже есть: $($user.DistinguishedName) — пароль НЕ менялся"
|
||
$plain = $null
|
||
} else {
|
||
New-ADUser -Name $name -SamAccountName $name -UserPrincipalName "$name@$($dom.DNSRoot)" `
|
||
-Path $ou -AccountPassword (ConvertTo-SecureString $plain -AsPlainText -Force) -Enabled $true `
|
||
-PasswordNeverExpires $true -CannotChangePassword $true `
|
||
-Description "Bridge&Join: чтение AD для входа пользователей (без прав администратора)"
|
||
$report += "Создан пользователь $name@$($dom.DNSRoot) в $ou"
|
||
}
|
||
|
||
# группы, по которым bj назначает роли
|
||
$groups = [ordered]@{ "bj-admins" = "администраторы bj"; "bj-operators" = "операторы депозитария";
|
||
"bj-risk" = "риск-менеджеры (НПР)"; "bj-viewers" = "наблюдатели" }
|
||
foreach ($g in $groups.Keys) {
|
||
$ex = Get-ADGroup -Filter "Name -eq '$g'" -ErrorAction SilentlyContinue
|
||
if ($ex) {
|
||
$report += "Группа $g уже есть: $($ex.DistinguishedName)"
|
||
} else {
|
||
New-ADGroup -Name $g -GroupScope Global -GroupCategory Security -Path $ou -Description "Bridge&Join: $($groups[$g])"
|
||
$report += "Создана группа $g ($($groups[$g])) в $ou"
|
||
}
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "===== Bridge&Join: итог =====" -ForegroundColor Cyan
|
||
$report | ForEach-Object { Write-Host " - $_" }
|
||
Write-Host ""
|
||
Write-Host "В bj (Пользователи и вход → Подключить домен):" -ForegroundColor Cyan
|
||
Write-Host " учётная запись: $name@$($dom.DNSRoot)"
|
||
if ($plain) {
|
||
Write-Host " пароль: $plain" -ForegroundColor Yellow
|
||
Write-Host " Скопируйте пароль в bj и закройте это окно — больше он нигде не показывается." -ForegroundColor Yellow
|
||
} else {
|
||
Write-Host " пароль: прежний (запись уже была). Новый: Set-ADAccountPassword $name -Reset"
|
||
}
|
||
Write-Host "Добавьте сотрудников в группы bj-* — по ним bj назначит роли."</pre>
|
||
<p class="muted" style="margin:6px 0 4px">Без модуля ActiveDirectory (cmd от администратора домена, <code>*</code> — запросит пароль):</p>
|
||
<pre class="copyable" id="ad-svc-cmd" style="white-space:pre;overflow:auto;font-size:12px">dsadd user "CN=svc-bj,CN=Users,<span class="ad-svc-dn">{{with $A.ADBaseDN}}{{.}}{{else}}DC=corp,DC=example,DC=ru{{end}}</span>" -samid svc-bj -upn svc-bj@<span class="ad-svc-dom">{{with $A.ADDomain}}{{.}}{{else}}ваш-домен{{end}}</span> -pwd * -pwdneverexpires yes -canchpwd no -desc "Bridge&Join: чтение AD"</pre>
|
||
<p class="muted" style="margin:6px 0 0">Рекомендуем: запретить этой записи интерактивный вход и вход по RDP (групповая политика «Запретить локальный вход», «Запретить вход через службу удалённых рабочих столов») — она нужна только для чтения каталога. Пароль bj хранит у себя; при смене пароля в AD обновите его здесь.</p>
|
||
</div>
|
||
</details>
|
||
<form method="post" action="/admin/setup/users/ad-connect" class="acc-form" id="ad-form">
|
||
<input type="hidden" name="return_to" value="{{$.Ret}}"><input type="hidden" name="section" value="acc-ad">
|
||
<label>Учётная запись домена<input type="text" name="ad_bind_user" data-need="учётная запись домена" value="{{$bu}}" placeholder="svc-bj или svc-bj@corp.example.ru" autocomplete="off"></label>
|
||
<label>Пароль{{if $pw}} <span class="muted">({{$pw}}; пусто — оставить)</span>{{end}}<input type="password" name="ad_bind_password" {{if not $pw}}data-need="пароль"{{end}} autocomplete="new-password"></label>
|
||
<details id="ad-adv" {{if $adv}}open{{end}} style="grid-column:1/-1;margin-top:2px"><summary style="cursor:pointer;font-size:13px;color:var(--muted)">Расширенные настройки — если домен не найдётся сам</summary>
|
||
<div class="acc-form" style="margin-top:8px">
|
||
<label>Домен<input type="text" name="ad_domain" value="{{$fd}}" placeholder="найдётся сам, например corp.example.ru" autocomplete="off"></label>
|
||
<label>Контроллер домена<input type="text" name="ad_server" value="{{$fs}}" placeholder="найдётся сам, например dc1.corp.example.ru" autocomplete="off"></label>
|
||
</div>
|
||
<p class="muted" style="font-size:12.5px;margin:6px 0 0">Пусто — bj ищет сам. Контроллер можно указать именем (bj попробует LDAPS 636, затем LDAP 389 со StartTLS) или адресом со схемой: ldaps://dc1.corp.example.ru:636.</p>
|
||
</details>
|
||
<div><button type="submit" class="btn">Подключить</button></div>
|
||
</form>
|
||
{{if $D}}<p style="margin:12px 0 0;padding:10px 12px;border-radius:10px;background:var(--card-2);font-size:13.5px;line-height:1.45"><b style="color:var(--err)">Не подключено:</b> {{$D.Err}}{{if not $D.Cert}} Поправьте и нажмите «Подключить» ещё раз — введённое сохранено.{{end}}</p>
|
||
{{template "adTrustCard" (withRet $D $.Ret)}}{{end}}
|
||
<script>(function(){
|
||
var box=document.getElementById('ad-connect'), outer=box.closest('details.acc-ad'), out=document.getElementById('ad-found'), adv=document.getElementById('ad-adv'), f=document.getElementById('ad-form'), done=false;
|
||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||
function look(){ if(done||!box.open||(outer&&!outer.open)) return; done=true; out.hidden=false; out.className='ad-st'; out.innerHTML='<span class="ad-st-ic ad-st-spin"></span><div><b>Ищу домен…</b><small>по сетевым настройкам сервера bj</small></div>';
|
||
fetch('/admin/setup/users/ad-discover',{cache:'no-store',headers:{'Accept':'application/json'}}).then(function(r){return r.json()}).then(function(d){
|
||
var x=d.found;
|
||
out.hidden=false;
|
||
if(x&&x.domain){ document.querySelectorAll('.ad-svc-dom').forEach(function(e){ e.textContent=x.domain; });
|
||
document.querySelectorAll('.ad-svc-dn').forEach(function(e){ e.textContent=x.domain.split('.').map(function(p){return 'DC='+p}).join(','); }); }
|
||
if(d.ok){ out.className='ad-st ad-st-ok'; out.innerHTML='<span class="ad-st-ic"></span><div><b>Найден домен '+esc(x.domain)+'</b><small>контроллеры: '+x.dcs.map(esc).join(', ')+' · '+esc(x.from)+'</small></div>';
|
||
f.ad_domain.placeholder=x.domain+' — найден сам'; f.ad_server.placeholder=x.dcs[0]+' — найден сам'; return; }
|
||
out.className='ad-st ad-st-warn'; out.innerHTML='<span class="ad-st-ic"></span><div><b>'+(x&&x.domain?'Домен '+esc(x.domain)+' найден, но его контроллеры — нет':'Домен сам не нашёлся')+'</b><small>'+(x&&x.domain?'укажите контроллер в расширенных настройках ниже':'укажите домен в расширенных настройках ниже или впишите учётную запись с доменом: svc-bj@corp.example.ru')+'</small></div>';
|
||
adv.open=true;
|
||
}).catch(function(){ out.hidden=true; done=false; });
|
||
}
|
||
box.addEventListener('toggle',look); if(outer) outer.addEventListener('toggle',look); look();
|
||
})();</script>
|
||
</details>
|
||
</details>
|
||
{{end}}
|