Files
Bridge-and-Join-s/internal/lkgateway/depo_procs.go
T
fontvielle 188691557a DEPO: FTV_M2M_JOB ставится WITH EXECUTE AS SYSDBA — обработка от SYSDBA и при вызове из bj, и джобой SQL Agent
22.10.2026, первый боевой входящий перевод на 9: в режиме «процедуру вызывает bj» процедуры работали правами
bj_user, Fancy отказал «Создание документа запрещено для данного пользователя» (FU_HAVE_RIGHT пропускает только
SYSDBA%), поручение в DEPO не создалось. На тесте была джоба SQL Agent (dbo → execute as SYSDBA).
Новый отпечаток процедур — плашка «Применить скрипты» на установках.

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

440 lines
22 KiB
Go
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.
package lkgateway
// depo_procs.go — установка процедур обработки bj2depo (FTV_M2M_PROCESS_* и FTV_M2M_JOB)
// в базу DEPO прямо из bj: «Проверить возможности» (предпросмотр без изменений) и
// «Установить» (выполняется логином администратора SQL — разово, не сохраняется).
// Процедуры встроены в bj (internal/lkgateway/depoprocs/*.sql), порядок установки —
// WIRING → OPERATION → UPDATE_HANDBOOK → EVENTS → CREATE → JOB; затем GRANT EXECUTE логину bj.
// Тексты — с тестовой DEPO Фонтвьель на 22.10.2026 (правки программиста 09–21.10; EVENTS вызывает
// UPDATE_HANDBOOK — без неё обработка событий падает).
import (
"context"
"crypto/sha256"
"database/sql"
"embed"
"encoding/hex"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
)
//go:embed depoprocs/*.sql
var depoProcFS embed.FS
// depoProcInstallOrder — имя процедуры и файл; порядок важен для зависимостей.
var depoProcInstallOrder = []struct{ Name, File string }{
{"FTV_M2M_PROCESS_WIRING", "depoprocs/FTV_M2M_PROCESS_WIRING.sql"},
{"FTV_M2M_PROCESS_OPERATION", "depoprocs/FTV_M2M_PROCESS_OPERATION.sql"},
{"FTV_M2M_UPDATE_HANDBOOK", "depoprocs/FTV_M2M_UPDATE_HANDBOOK.sql"}, // вызывается из EVENTS (HANDBOOK)
{"FTV_M2M_PROCESS_EVENTS", "depoprocs/FTV_M2M_PROCESS_EVENTS.sql"},
{"FTV_M2M_PROCESS_CREATE", "depoprocs/FTV_M2M_PROCESS_CREATE.sql"},
{"FTV_M2M_JOB", "depoprocs/FTV_M2M_JOB.sql"},
}
var goBatchRe = regexp.MustCompile(`(?im)^\s*GO\s*$`)
// splitSQLBatches — разбить скрипт на батчи по строкам «GO» (директива клиента, не T-SQL).
func splitSQLBatches(s string) []string {
var out []string
for _, part := range goBatchRe.Split(s, -1) {
if strings.TrimSpace(part) != "" {
out = append(out, part)
}
}
return out
}
// depoProcFile — тело встроенной (исходной) процедуры из файла.
func depoProcFile(file string) (string, error) {
b, err := depoProcFS.ReadFile(file)
if err != nil {
return "", err
}
return string(b), nil
}
// embeddedProcBody — встроенный текст процедуры по имени.
func embeddedProcBody(name string) string {
for _, p := range depoProcInstallOrder {
if p.Name == name {
b, _ := depoProcFile(p.File)
return b
}
}
return ""
}
// depoProcEffective — текст процедуры с учётом правки под клиента: сохранённый
// в настройках вариант, иначе встроенный. Используется в предпросмотре, установке
// и скачиваемом скрипте.
func depoProcEffective(f FancyMSSQLSettings, name string) string {
if v, ok := f.DepoProcOverrides[name]; ok && strings.TrimSpace(v) != "" {
return v
}
return embeddedProcBody(name)
}
// depoProcInstallBody — текст процедуры, который bj устанавливает: depoProcEffective, а у FTV_M2M_JOB —
// с WITH EXECUTE AS '<пользователь процедур>' (обычно SYSDBA). Процедуры Фонтвьель переходят на SYSDBA только
// при вызове от dbo (джоба SQL Agent); вызванные bj, они работали правами bj_user, и Fancy отказывал:
// «Создание документа запрещено для данного пользователя» (FU_HAVE_RIGHT пропускает только SYSDBA%;
// 22.10.2026, первый боевой входящий перевод на 9). С EXECUTE AS на JOB обработка идёт от SYSDBA при любом
// способе запуска — джобой SQL Agent или из bj.
func depoProcInstallBody(f FancyMSSQLSettings, name string) string {
body := depoProcEffective(f, name)
if name != "FTV_M2M_JOB" || withExecAsRe.MatchString(body) {
return body
}
user := depoProcExecAsUser(f)
if !depoLoginRe.MatchString(user) {
return body
}
loc := procAsLineRe.FindStringIndex(body)
if loc == nil {
return body
}
return body[:loc[0]] + "with execute as '" + user + "'\n" + body[loc[0]:]
}
var (
withExecAsRe = regexp.MustCompile(`(?i)\bwith\s+execute\s+as\b`)
procAsLineRe = regexp.MustCompile(`(?im)^[ \t]*as[ \t]*\r?$`) // строка «as» перед телом процедуры
)
// depoProcOrderText — порядок установки коротко: WIRING→OPERATION→UPDATE_HANDBOOK→…
func depoProcOrderText(sep string) string {
var parts []string
for _, p := range depoProcInstallOrder {
parts = append(parts, strings.TrimPrefix(strings.TrimPrefix(p.Name, "FTV_M2M_PROCESS_"), "FTV_M2M_"))
}
return strings.Join(parts, sep)
}
// depoSchemaFixes — правки структуры трансферных таблиц, которые логин bj сделать не может (нужны права
// администратора БД); выполняются вместе с процедурами. Повторный запуск безопасен.
var depoSchemaFixes = []struct{ Name, SQL string }{
// 22.10.2026: служебные события (HANDBOOK) — без GUID; в таблицах, созданных bj до этого, GUID NOT NULL
{"FTV_M2M_EVENTS.GUID NULL", `IF EXISTS (SELECT 1 FROM sys.columns WHERE object_id=OBJECT_ID(N'dbo.FTV_M2M_EVENTS') AND name=N'GUID' AND is_nullable=0)
ALTER TABLE dbo.FTV_M2M_EVENTS ALTER COLUMN GUID uniqueidentifier NULL`},
}
// depoProcsHash — отпечаток набора процедур, который bj установит (с учётом правки под клиента).
func depoProcsHash(f FancyMSSQLSettings) string {
h := sha256.New()
for _, fx := range depoSchemaFixes {
h.Write([]byte(fx.Name + "\n" + fx.SQL + "\n"))
}
h.Write([]byte("grant db_datawriter\n")) // 22.10.2026: роль записи логину bj — новый отпечаток, плашка на установках
for _, p := range depoProcInstallOrder {
h.Write([]byte(p.Name + "\n" + strings.TrimSpace(depoProcInstallBody(f, p.Name)) + "\n"))
}
return hex.EncodeToString(h.Sum(nil))[:16]
}
// depoProcsPending — обновление bj принесло процедуры DEPO, которые ещё не применены в базе: bj работает с
// DEPO (подключение есть, обработка включена), а отпечаток установленного набора не совпадает со встроенным.
func depoProcsPending() bool {
if globalRC == nil {
return false
}
f := globalRC.Snapshot().Fancy
if strings.TrimSpace(f.Server) == "" || (!f.DepoJobRun && f.DepoJobMode == "") {
return false
}
return f.DepoProcsApplied != depoProcsHash(f)
}
// depoProcList — порядок процедур для шаблона (редактор текстов).
func depoProcList() []struct{ Name, File string } { return depoProcInstallOrder }
// depoProcsCombined — единый скрипт установки (для скачивания администратору): USE, процедуры, GRANT.
func depoProcsCombined(f FancyMSSQLSettings) string {
return depoProcsScriptFor(f, f.DepoDB(), depoSetupLogin(f))
}
// depoProcsScriptFor — скрипт установки процедур в базу db с выдачей EXECUTE логину login.
// Тексты процедур — с учётом правки под клиента (depoProcEffective).
func depoProcsScriptFor(f FancyMSSQLSettings, db, login string) string {
var b strings.Builder
b.WriteString("/* Bridge&Join: процедуры обработки bj2depo для базы DEPO.\n")
b.WriteString(" Порядок " + depoProcOrderText(" -> ") + ". Запуск под sysadmin в SSMS.\n")
b.WriteString(" CREATE OR ALTER — повторный запуск безопасен (данные не меняются). */\n")
fmt.Fprintf(&b, "USE %s;\nGO\n\n", quoteIdent(db))
for _, fx := range depoSchemaFixes {
fmt.Fprintf(&b, "/* ===== структура: %s ===== */\n%s;\nGO\n\n", fx.Name, fx.SQL)
}
for _, p := range depoProcInstallOrder {
body := depoProcInstallBody(f, p.Name)
if strings.TrimSpace(body) == "" {
continue
}
fmt.Fprintf(&b, "/* ===== %s ===== */\n%s\nGO\n\n", p.Name, strings.TrimRight(body, "\n"))
}
if depoLoginRe.MatchString(login) {
b.WriteString("/* Запись в таблицы Fancy: процедуры обработки M2M работают правами логина bj (без неё — «Запрещено разрешение INSERT на объект OD_OBJECTS») */\n")
fmt.Fprintf(&b, "IF IS_ROLEMEMBER(N'db_datawriter', N'%[1]s') = 0 ALTER ROLE db_datawriter ADD MEMBER %[2]s;\nGO\n", login, quoteIdent(login))
b.WriteString("/* Права логину bj на все процедуры: без прав OBJECT_ID не видит их и проверка пишет «нет процедур» */\n")
for _, p := range depoProcInstallOrder {
fmt.Fprintf(&b, "GRANT EXECUTE ON dbo.%s TO %s;\n", p.Name, quoteIdent(login))
}
b.WriteString("GO\n")
}
return b.String()
}
// depoProcsInstallInto — установить процедуры (CREATE OR ALTER, с учётом правки под клиента) в базу
// dbName соединением adm (логин администратора) и выдать EXECUTE логину grantLogin.
func depoProcsInstallInto(ctx context.Context, adm, f FancyMSSQLSettings, dbName, grantLogin string) error {
db, err := fancyOpen(adm, dbName)
if err != nil {
return err
}
defer db.Close()
for _, fx := range depoSchemaFixes {
if _, err := db.ExecContext(ctx, fx.SQL); err != nil {
return fmt.Errorf("структура таблиц (%s): %s", fx.Name, quikErrorDetail(err))
}
}
for _, p := range depoProcInstallOrder {
body := depoProcInstallBody(f, p.Name)
if strings.TrimSpace(body) == "" {
return fmt.Errorf("пустой текст процедуры %s", p.Name)
}
for _, batch := range splitSQLBatches(body) {
if _, err := db.ExecContext(ctx, batch); err != nil {
return fmt.Errorf("установка %s: %s", p.Name, quikErrorDetail(err))
}
}
}
if depoLoginRe.MatchString(grantLogin) {
// Запись в таблицы Fancy: процедуры Фонтвьель, вызванные bj, работают правами логина bj.
if _, err := db.ExecContext(ctx, "IF DATABASE_PRINCIPAL_ID(N'"+grantLogin+"') IS NOT NULL AND IS_ROLEMEMBER(N'db_datawriter', N'"+grantLogin+"') = 0 ALTER ROLE db_datawriter ADD MEMBER "+quoteIdent(grantLogin)); err != nil {
return fmt.Errorf("роль db_datawriter логину %s: %s", grantLogin, quikErrorDetail(err))
}
// Право на ВСЕ процедуры: без него OBJECT_ID под bj_user = NULL, и проверка пишет «нет процедур».
for _, p := range depoProcInstallOrder {
if _, err := db.ExecContext(ctx, "GRANT EXECUTE ON dbo."+p.Name+" TO "+quoteIdent(grantLogin)); err != nil {
return fmt.Errorf("право EXECUTE логину %s на %s: %s", grantLogin, p.Name, quikErrorDetail(err))
}
}
}
return nil
}
// quoteIdent — квадратные скобки для идентификатора T-SQL.
func quoteIdent(s string) string { return "[" + strings.ReplaceAll(s, "]", "]]") + "]" }
// depoProcsPreflight — проверка возможности установки (без изменений в базе). Возвращает отчёт
// для экрана, признак «можно применять» и ошибку подключения/проверки.
func depoProcsPreflight(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (report string, canApply bool, err error) {
if f.IsPostgres() {
return "", false, fmt.Errorf("установка процедур bj2depo поддержана только для MSSQL")
}
conn := f
admin := strings.TrimSpace(adminUser)
if admin != "" {
conn.User, conn.Password = admin, adminPass
}
db, err := fancyOpen(conn, f.DepoDB())
if err != nil {
return "", false, err
}
defer db.Close()
if err := db.PingContext(ctx); err != nil {
return "", false, fmt.Errorf("подключение к базе %s логином %s: %s", f.DepoDB(), conn.User, quikErrorDetail(err))
}
if err := depoCheckIsDepo(ctx, conn, f.DepoDB()); err != nil {
return "", false, err
}
missing := depoMissing(ctx, db, depoBj2depoProcs, "P")
login := depoSetupLogin(f)
execAs := depoProcExecAsUser(f) // пользователь для EXECUTE AS в теле процедур (обычно SYSDBA)
var canCreate, sysadmin, bjUser, execAsOK int
_ = db.QueryRowContext(ctx,
`SELECT CAST(ISNULL(HAS_PERMS_BY_NAME(NULL,'DATABASE','CREATE PROCEDURE'),0) AS int),
CAST(ISNULL(IS_SRVROLEMEMBER('sysadmin'),0) AS int),
CASE WHEN DATABASE_PRINCIPAL_ID(@u) IS NULL THEN 0 ELSE 1 END,
CASE WHEN DATABASE_PRINCIPAL_ID(@e) IS NULL THEN 0 ELSE 1 END`,
sql.Named("u", login), sql.Named("e", execAs)).Scan(&canCreate, &sysadmin, &bjUser, &execAsOK)
canApply = canCreate == 1 || sysadmin == 1
var b strings.Builder
fmt.Fprintf(&b, "Сервер %s, база %s, логин %s.\n", f.Server, f.DepoDB(), conn.User)
if len(missing) == 0 {
fmt.Fprintf(&b, "Процедуры: все %d уже установлены (будут обновлены CREATE OR ALTER).\n", len(depoProcInstallOrder))
} else {
fmt.Fprintf(&b, "Процедуры: нет %d из %d (%s).\n", len(missing), len(depoProcInstallOrder), strings.Join(missing, ", "))
}
if canApply {
b.WriteString("Право создавать процедуры: есть.\n")
} else {
b.WriteString("Право создавать процедуры: НЕТ — введите логин администратора SQL (sysadmin, обычно sa).\n")
}
if bjUser == 1 {
fmt.Fprintf(&b, "Логин %s в базе: есть — выдам ему EXECUTE на FTV_M2M_JOB.\n", login)
} else {
fmt.Fprintf(&b, "Логин %s в базе: нет — сначала подготовьте базу (логин/таблицы), GRANT будет пропущен.\n", login)
}
if execAsOK == 1 {
fmt.Fprintf(&b, "Пользователь %s (для EXECUTE AS в процедурах): есть.\n", execAs)
} else {
fmt.Fprintf(&b, "Пользователь %s (для EXECUTE AS в процедурах): НЕТ — процедуры FTV_M2M_PROCESS_* используют EXECUTE AS USER='%s' и не создадутся без него. Создайте этого пользователя в базе (как в рабочей базе Fancy) или замените имя в «Тексты процедур».\n", execAs, execAs)
}
fmt.Fprintf(&b, "Будет выполнено: CREATE OR ALTER %d процедур (%s) + GRANT EXECUTE логину bj.", len(depoProcInstallOrder), depoProcOrderText("→"))
return b.String(), canApply, nil
}
// depoProcExecAsUser — пользователь, под которым процедуры выполняют EXECUTE AS (из их текста,
// строка «execute as user = '...'»). По умолчанию SYSDBA.
func depoProcExecAsUser(f FancyMSSQLSettings) string {
body := depoProcEffective(f, "FTV_M2M_PROCESS_WIRING")
if m := execAsUserRe.FindStringSubmatch(body); len(m) == 2 {
return m[1]
}
return "SYSDBA"
}
var execAsUserRe = regexp.MustCompile(`(?i)execute\s+as\s+user\s*=\s*'([^']+)'`)
// depoProcErrHint — подсказка по типичной причине отказа CREATE процедур.
func depoProcErrHint(msg string) string {
l := strings.ToLower(msg)
if strings.Contains(l, "sysdba") || strings.Contains(l, "principal") || strings.Contains(l, "15517") ||
strings.Contains(l, "execute as") || strings.Contains(l, "не удается олицетворить") || strings.Contains(l, "олицетвор") {
return " Процедуры FTV_M2M_PROCESS_* используют EXECUTE AS USER='SYSDBA' — в базе DEPO должен быть пользователь SYSDBA (как в рабочей базе Fancy). Создайте его или замените имя пользователя в «Тексты процедур» и повторите."
}
return ""
}
// depoProcsInstall — установить процедуры логином администратора. Сначала preflight; при отсутствии
// прав — отказ без изменений. Возвращает итоговое сообщение.
func depoProcsInstall(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, error) {
report, canApply, err := depoProcsPreflight(ctx, f, adminUser, adminPass)
if err != nil {
return "", err
}
if !canApply {
return "", fmt.Errorf("нет прав на установку. %s", report)
}
adm := f
if u := strings.TrimSpace(adminUser); u != "" {
adm.User, adm.Password = u, adminPass
}
login := depoSetupLogin(f)
if err := depoProcsInstallInto(ctx, adm, f, f.DepoDB(), login); err != nil {
return "", err
}
msg := fmt.Sprintf("структура таблиц проверена; установлено процедур: %d (%s); логину %s — EXECUTE и роль db_datawriter", len(depoProcInstallOrder), depoProcOrderText(", "), login)
log.Printf("depo-procs: %s", msg)
return msg, nil
}
// ---- хендлеры ----
// checkFancyDepoProcs — POST /admin/setup/fancy/depo-procs/check: проверка возможностей (без изменений).
func (h *setupHandlers) checkFancyDepoProcs(w http.ResponseWriter, r *http.Request) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
if s.Server == "" {
setupFlash(w, r, "Процедуры DEPO: сначала сохраните подключение к серверу БД")
return
}
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), time.Minute)
defer cancel()
report, canApply, err := depoProcsPreflight(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password"))
if err != nil {
setupFlash(w, r, "Процедуры DEPO: проверка не удалась — "+err.Error())
return
}
verdict := "можно устанавливать"
if !canApply {
verdict = "установка невозможна"
}
setupFlash(w, r, "Процедуры DEPO ("+verdict+"): "+report)
}
// installFancyDepoProcs — POST /admin/setup/fancy/depo-procs/install: установить процедуры.
func (h *setupHandlers) installFancyDepoProcs(w http.ResponseWriter, r *http.Request) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
if s.Server == "" {
setupFlash(w, r, "Процедуры DEPO: сначала сохраните подключение к серверу БД")
return
}
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 2*time.Minute)
defer cancel()
msg, err := depoProcsInstall(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password"))
if err != nil {
log.Printf("depo-procs: установка не удалась: %v", err)
txt := "установка не удалась: " + err.Error() + depoProcErrHint(err.Error())
s2 := h.rc.Snapshot().Fancy // сохранить ошибку, чтобы не мелькала во флеше
s2.DepoProcCheck, s2.DepoProcCheckOK, s2.DepoProcCheckAt = txt, false, time.Now()
_ = h.rc.UpdateFancy(s2)
setupFlash(w, r, "Процедуры DEPO: "+txt)
return
}
pmsg, pok := depoProcCheck(ctx, h.rc)
s = h.rc.Snapshot().Fancy
s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, time.Now()
s.DepoProcsApplied, s.DepoProcsAppliedAt = depoProcsHash(s), time.Now() // применено — плашка обновления уходит
_ = h.rc.UpdateFancy(s)
setupFlash(w, r, "Процедуры DEPO: "+msg+". Проверка обработки: "+pmsg)
}
// fancyDepoProcsScript — GET /admin/setup/fancy/depo-procs/script.sql: единый скрипт для администратора.
func (h *setupHandlers) fancyDepoProcsScript(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Fancy
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="bj-depo-procs-mssql.sql"`)
// UTF-8 BOM (0xEF 0xBB 0xBF): SSMS без него читает кириллицу в комментариях как ANSI.
_, _ = w.Write(append([]byte{0xEF, 0xBB, 0xBF}, depoProcsCombined(s)...))
}
// saveFancyDepoProcsText — POST /admin/setup/fancy/depo-procs/save-text: сохранить правку текстов процедур
// под клиента. Пустое поле или совпадение со встроенным текстом — вернуть встроенную версию (убрать правку).
func (h *setupHandlers) saveFancyDepoProcsText(w http.ResponseWriter, r *http.Request) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
ov := map[string]string{}
changed := 0
for _, p := range depoProcInstallOrder {
v := strings.ReplaceAll(r.FormValue("proc_"+p.Name), "\r\n", "\n")
if strings.TrimSpace(v) != "" && strings.TrimSpace(v) != strings.TrimSpace(embeddedProcBody(p.Name)) {
ov[p.Name] = v
changed++
}
}
if changed == 0 {
s.DepoProcOverrides = nil
} else {
s.DepoProcOverrides = ov
}
if err := h.rc.UpdateFancy(s); err != nil {
setupFlash(w, r, "Тексты процедур: ошибка сохранения — "+err.Error())
return
}
setupFlash(w, r, fmt.Sprintf("Тексты процедур сохранены. Изменённых под клиента: %d (пустые или совпадающие со встроенными — используется встроенная версия).", changed))
}