Files
Bridge-and-Join-s/internal/lkgateway/web/templates/part_accounts.html
T
fontvielle 8f32f79372 AD: скрипт svc-bj сам генерирует пароль и сообщает итог
Пароль — 24 случайных символа (RandomNumberGenerator), показывается один
раз в конце. Итог: создан пользователь (где) или уже был (пароль не
менялся), по каждой группе bj-* — создана (где) или уже есть (DN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-22 12:55:35 +03:00

186 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{/* Учётные записи: добавить сотрудника (роль плиткой, логин, пароль) и сотрудники из домена (подключение,
поиск сотрудников и групп, доступ группам). Общий блок шага мастера «Учётные записи» и раздела
Настройка → Пользователи. Вход: 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="[^ /\@&quot;'<>]+"></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}}</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}}
<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]})}
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&amp;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&amp;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&amp;Join: $($groups[$g])"
$report += "Создана группа $g ($($groups[$g])) в $ou"
}
}
Write-Host ""
Write-Host "===== Bridge&amp;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&amp;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{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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}}