Скрипт создания логина DEPO: пароль из поля мастера подставляется в скрипт (передаётся только в теле POST, не сохраняется, кавычки экранируются)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 13:21:51 +03:00
parent cb047b93f6
commit 2999eaf189
3 changed files with 60 additions and 22 deletions
+22 -10
View File
@@ -10,26 +10,32 @@ import (
"strings"
)
// depoLoginScript — скрипт создания логина bj. pg — PostgreSQL; db — база DEPO; login — имя логина.
func depoLoginScript(pg bool, db, login string) string {
// depoLoginScript — скрипт создания логина bj. pg — PostgreSQL; db — база DEPO; login — имя логина;
// pass — пароль из поля мастера (пусто — заглушка, администратор впишет сам). Пароль только вставляется
// в текст скрипта, нигде не сохраняется.
func depoLoginScript(pg bool, db, login, pass string) string {
if !validDBName(db) {
db = depoDefaultDatabase
}
if !depoLoginRe.MatchString(login) {
login = "bj_user"
}
pw := "ЗАМЕНИТЕ_НА_ПАРОЛЬ"
if pass != "" {
pw = strings.ReplaceAll(pass, "'", "''") // строковый литерал SQL: одинарная кавычка удваивается
}
var b strings.Builder
if pg {
fmt.Fprintf(&b, `-- Bridge&Join: роль (логин) для bj в базе DEPO. PostgreSQL 10 и новее.
-- Выполнить суперпользователем или владельцем базы, например:
-- psql -h <сервер> -U postgres -f bj-depo-login-postgres.sql
-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»).
-- Пароль — из поля мастера; если там пусто, замените ЗАМЕНИТЕ_НА_ПАРОЛЬ на свой. Скрипт с паролем не храните.
-- Права: только подключение и чтение учётных таблиц DEPO. Запись — только в таблицы обмена FTV_M2M*,
-- её выдаёт скрипт таблиц на следующем шаге мастера.
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[2]s') THEN
CREATE ROLE "%[2]s" LOGIN PASSWORD 'ЗАМЕНИТЕ_НА_ПАРОЛЬ';
CREATE ROLE "%[2]s" LOGIN PASSWORD '%[3]s';
END IF;
END $$;
@@ -39,12 +45,12 @@ GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s";
GRANT USAGE ON SCHEMA public TO "%[2]s";
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "%[2]s";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "%[2]s";
`, db, login)
`, db, login, pw)
return b.String()
}
fmt.Fprintf(&b, `-- Bridge&Join: логин для bj в базе DEPO. Microsoft SQL Server.
-- Выполнить в SSMS или sqlcmd под логином с правами sysadmin (или securityadmin + db_owner в базе %[1]s).
-- Пароль замените на свой и передайте его тому, кто настраивает bj (в мастере, шаг «Подключение»).
-- Пароль — из поля мастера; если там пусто, замените ЗАМЕНИТЕ_НА_ПАРОЛЬ на свой. Скрипт с паролем не храните.
-- Права: только чтение учётных таблиц DEPO (db_datareader). Запись — только в таблицы обмена FTV_M2M*,
-- её выдаёт скрипт таблиц на следующем шаге мастера. Скрипт можно запускать повторно.
@@ -52,7 +58,7 @@ USE [master];
GO
IF SUSER_ID(N'%[2]s') IS NULL
CREATE LOGIN [%[2]s] WITH PASSWORD = N'ЗАМЕНИТЕ_НА_ПАРОЛЬ', CHECK_POLICY = ON, DEFAULT_DATABASE = [%[1]s];
CREATE LOGIN [%[2]s] WITH PASSWORD = N'%[3]s', CHECK_POLICY = ON, DEFAULT_DATABASE = [%[1]s];
GO
USE [%[1]s];
@@ -69,7 +75,7 @@ GO
IF IS_ROLEMEMBER(N'db_datareader', N'%[2]s') = 0 ALTER ROLE db_datareader ADD MEMBER [%[2]s];
GO
`, db, login)
`, db, login, pw)
return b.String()
}
@@ -77,7 +83,8 @@ GO
// Без параметров — по сохранённым настройкам; download=0 — показать текстом (предпросмотр в мастере).
func (h *setupHandlers) fancyDepoLoginScript(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Fancy
q := r.URL.Query()
_ = r.ParseForm() // пароль приходит только в теле POST, не в адресе
q := r.Form
pg := s.IsPostgres()
if t := q.Get("type"); t != "" {
pg = t == "postgres"
@@ -90,7 +97,12 @@ func (h *setupHandlers) fancyDepoLoginScript(w http.ResponseWriter, r *http.Requ
if login == "" {
login = pickStr(strings.TrimSpace(s.User), "bj_user")
}
body := depoLoginScript(pg, db, login)
pass := ""
if r.Method == http.MethodPost {
pass = r.PostForm.Get("password")
}
w.Header().Set("Cache-Control", "no-store")
body := depoLoginScript(pg, db, login, pass)
name := "bj-depo-login-mssql.sql"
if pg {
name = "bj-depo-login-postgres.sql"
+15 -3
View File
@@ -6,19 +6,31 @@ import (
)
func TestDepoLoginScript(t *testing.T) {
ms := depoLoginScript(false, "CLIENT_DEPO", "bj_svc")
ms := depoLoginScript(false, "CLIENT_DEPO", "bj_svc", "")
for _, w := range []string{"CREATE LOGIN [bj_svc]", "USE [CLIENT_DEPO]", "CREATE USER [bj_svc] FOR LOGIN [bj_svc]", "db_datareader", "ALTER USER [bj_svc] WITH LOGIN = [bj_svc]"} {
if !strings.Contains(ms, w) {
t.Fatalf("MS SQL: нет %q", w)
}
}
pg := depoLoginScript(true, "depo", "bj_svc")
pg := depoLoginScript(true, "depo", "bj_svc", "")
for _, w := range []string{`CREATE ROLE "bj_svc" LOGIN`, `GRANT CONNECT ON DATABASE "depo"`, `GRANT SELECT ON ALL TABLES`} {
if !strings.Contains(pg, w) {
t.Fatalf("PostgreSQL: нет %q", w)
}
}
if s := depoLoginScript(false, "bad;db", "x'; DROP"); !strings.Contains(s, "[BJ_DEPO_DATA]") || !strings.Contains(s, "[bj_user]") {
if s := depoLoginScript(false, "bad;db", "x'; DROP", ""); !strings.Contains(s, "[BJ_DEPO_DATA]") || !strings.Contains(s, "[bj_user]") {
t.Fatal("недопустимые имена — подставляются значения по умолчанию")
}
}
func TestDepoLoginScriptPassword(t *testing.T) {
if s := depoLoginScript(false, "D", "u", "Pa'ss"); !strings.Contains(s, "PASSWORD = N'Pa''ss'") || strings.Contains(s, "N'ЗАМЕНИТЕ_НА_ПАРОЛЬ'") {
t.Fatal("MS SQL: пароль из поля с экранированием кавычки")
}
if s := depoLoginScript(true, "d", "u", "p'w%x"); !strings.Contains(s, "PASSWORD 'p''w%x'") {
t.Fatal("PostgreSQL: пароль из поля")
}
if s := depoLoginScript(false, "D", "u", ""); !strings.Contains(s, "N'ЗАМЕНИТЕ_НА_ПАРОЛЬ'") {
t.Fatal("пустой пароль — заглушка")
}
}
@@ -119,26 +119,40 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
<li><b>чтение</b> учётных таблиц DEPO (лица, счета, остатки, бумаги) — для сверки переводов;</li>
<li><b>запись только в таблицы обмена</b> <code>FTV_M2M*</code> — их права выдаются на следующем экране, вместе с созданием таблиц.</li>
</ul>
<p style="margin:0 0 6px">Вариант 1 — администратор создаёт логин сам с этими правами. Вариант 2 — передайте ему скрипт: в нём уже подставлены тип СУБД, база и имя логина из полей выше, нужно только заменить пароль.</p>
<a id="wiz-login-dl" href="/admin/setup/fancy/depo-login/script.sql">Скачать скрипт создания логина</a>
<p style="margin:0 0 6px">Вариант 1 — администратор создаёт логин сам с этими правами. Вариант 2 — передайте ему скрипт: в нём подставлены тип СУБД, база, имя логина и пароль из полей выше (если пароль не введён — впишите его в скрипт сами). Скрипт с паролем не храните.</p>
<a id="wiz-login-dl" href="#">Скачать скрипт создания логина</a> <span class="muted" id="wiz-login-pwnote" style="font-size:12px"></span>
<pre class="copyable" id="wiz-login-pre" style="max-height:260px;margin-top:6px;font-size:12px">…</pre>
<p class="muted" style="margin:4px 0 0">После выполнения скрипта впишите логин и пароль в поля выше и нажмите «Проверить и далее».</p>
</div>
</details>
<script>
(function(){
var f=document.currentScript.closest('form'), d=document.getElementById('wiz-login-help'), pre=document.getElementById('wiz-login-pre'), a=document.getElementById('wiz-login-dl');
function q(){
var t=f.querySelector('[name=db_type]'), db=f.querySelector('[name=depo_database]'), u=f.querySelector('[name=user]');
return 'type='+encodeURIComponent(t?t.value:'mssql')+'&db='+encodeURIComponent(db&&db.value.trim()?db.value.trim():(db?db.placeholder:''))+'&login='+encodeURIComponent(u&&u.value.trim()?u.value.trim():'bj_user');
var f=document.currentScript.closest('form'), d=document.getElementById('wiz-login-help'), pre=document.getElementById('wiz-login-pre'), a=document.getElementById('wiz-login-dl'), note=document.getElementById('wiz-login-pwnote');
function val(n){ var e=f.querySelector('[name='+n+']'); return e?e.value:''; }
function params(){
var db=f.querySelector('[name=depo_database]');
var p=new URLSearchParams();
p.set('type', val('db_type')||'mssql');
p.set('db', db&&db.value.trim()?db.value.trim():(db?db.placeholder:''));
p.set('login', val('user').trim()||'bj_user');
p.set('password', val('password')); // только в теле POST — не в адресе и не в журналах
return p;
}
function upd(){
a.href='/admin/setup/fancy/depo-login/script.sql?'+q();
note.textContent = val('password') ? '— пароль из поля подставлен в скрипт' : '— пароль не введён: в скрипте заглушка, замените её';
if(!d.open) return;
fetch('/admin/setup/fancy/depo-login/script.sql?download=0&'+q(),{cache:'no-store'}).then(function(r){return r.text()}).then(function(t){pre.textContent=t}).catch(function(){});
var p=params(); p.set('download','0');
fetch('/admin/setup/fancy/depo-login/script.sql',{method:'POST',body:p,cache:'no-store'}).then(function(r){return r.text()}).then(function(t){pre.textContent=t}).catch(function(){});
}
a.addEventListener('click', function(e){
e.preventDefault();
var p=params(), fm=document.createElement('form');
fm.method='POST'; fm.action='/admin/setup/fancy/depo-login/script.sql'; fm.style.display='none';
p.forEach(function(v,k){ var i=document.createElement('input'); i.type='hidden'; i.name=k; i.value=v; fm.appendChild(i); });
document.body.appendChild(fm); fm.submit(); fm.remove();
});
d.addEventListener('toggle',upd);
['db_type','depo_database','user'].forEach(function(n){var e=f.querySelector('[name='+n+']'); if(e) e.addEventListener('change',upd);});
['db_type','depo_database','user','password'].forEach(function(n){var e=f.querySelector('[name='+n+']'); if(e){ e.addEventListener('change',upd); e.addEventListener('input',function(){ clearTimeout(e._t); e._t=setTimeout(upd,400); }); }});
upd();
})();
</script>