add edit and all in one menu for hosts and services

This commit is contained in:
redlineo committed 2026-07-22 15:54:44 +03:00
1 parent 8c1b93c14e
commit e39ad275b5
15 files changed
+1136 -675

No files matched your search

+50 -25
View File
@@ -19,15 +19,26 @@ TUI работает поверх уже существующего SSH+`sudo`-
реализовать вручную (см. «Конкурентность» ниже). Для небольшой команды
администраторов это разумный компромисс.
## Модель доступа: Host → Service → Forward
## Модель доступа: каталог хостов → группы → Forward
Хост (`Host`) — это адрес (например, `172.16.1.20`). На одном хосте может
быть несколько сервисов (`Service`) — независимых TCP-портов: `ssh` на 22,
какое-то приложение на 7888, и так далее. Группа (`Group`) — это набор
хостов (со всеми их сервисами), пользователь может состоять в нескольких
группах; его реальный эффективный доступ — объединение сервисов всех хостов
всех его групп, это и называется `Forward` (`internal/model/types.go`,
`State.AllowedForwards`).
Хосты и их сервисы — **общий каталог** (`State.Hosts`), определённый один
раз, а не заново в каждой группе. Хост (`Host`) — это адрес (например,
`172.16.1.20`) с описанием; на одном хосте может быть несколько сервисов
(`Service`) — независимых TCP-портов: `ssh` на 22, какое-то приложение на
7888, и так далее. Группа (`Group`) не хранит хосты сама — она хранит
только список **имён** хостов из каталога (`Group.Hosts []string`),
которые в неё входят (все сервисы такого хоста включаются целиком). Если
одни и те же хосты/сервисы нужны нескольким группам — они описываются один
раз в каталоге, а группы просто отмечают их галочками, вместо повторного
набора адресов и портов.
Пользователь может состоять в нескольких группах; его реальный эффективный
доступ — объединение сервисов всех хостов всех его групп, это и называется
`Forward` (`internal/model/types.go`, `State.AllowedForwards`). Переименование
хоста автоматически обновляет ссылки на него во всех группах
(`State.RenameHost`); удаление хоста снимает его со всех групп, которые его
использовали (`State.DeleteHost`) — ссылочная целостность каталога держится
в модели, а не проверяется вручную в UI.
Ключевой момент: пользователь **никогда не подключается по SSH к целевому
хосту напрямую** (ни через `ProxyJump`, ни как-то ещё) — он открывает одно
@@ -85,17 +96,19 @@ Reverse/remote forwarding (`-R`) в этой модели не выдаётся
Пользователю нужно пробросить сразу много сервисов за одно соединение
(например, 10 хостов, на каждом порт 22 и порт 7888 — 20 форвардов сразу).
Локально на его машине два разных удалённых назначения не могут занимать
один и тот же локальный порт одновременно, а несколько хостов с одинаковым
портом (22 или 7888 у всех десяти) — обычная ситуация. Поэтому локальный
порт **не всегда равен удалённому**: `internal/keymgr/portalloc.go`
(`AllocateLocalPorts`) для каждого форварда сначала пробует занять локальный
порт, совпадающий с удалённым (удобно и предсказуемо, когда порт встречается
один раз), а при конфликте — берёт следующий свободный из диапазона `20000+`.
Присвоение детерминировано (зависит только от порядка и состава форвардов
пользователя), поэтому повторный экспорт архива с тем же набором хостов даёт
те же локальные порты. Итоговое соответствие "локальный порт → сервис"
попадает в `INSTRUCTIONS.txt` внутри архива пользователя.
Локальный порт **никогда не равен удалённому порту** — не только чтобы
избежать коллизий между несколькими хостами с одинаковым портом (обычная
ситуация: 22 или 7888 у всех десяти), но и потому, что многие удалённые
порты привилегированные (`< 1024`, в первую очередь сам ssh на 22) —
слушателя на таком порту локально можно открыть только с правами
администратора, которых у обычного пользователя на его Windows-машине нет.
`internal/keymgr/portalloc.go` (`AllocateLocalPorts`) поэтому просто выдаёт
локальные порты последовательно из диапазона `20000+`, без попытки
переиспользовать номер удалённого порта. Присвоение детерминировано
(зависит только от порядка и состава форвардов пользователя), поэтому
повторный экспорт архива с тем же набором хостов даёт те же локальные
порты. Итоговое соответствие "локальный порт → сервис" попадает в
`INSTRUCTIONS.txt` внутри архива пользователя.
## Жизненный цикл приватного ключа
@@ -115,6 +128,19 @@ fingerprint. Это осознанное решение: кража бэкапа
ключей, выпущенных в конкретной партии, — передавайте его получателям
отдельным от архива каналом (голосом, в личку, не в общий чат).
## Подтверждение деструктивных действий
Блокировка/удаление пользователя, удаление хоста/сервиса/группы и
перевыпуск уже выданного ключа (экспорт поверх существующего) — все эти
действия либо необратимы, либо мгновенно и широко режут доступ, поэтому
подтверждаются не клавишей (`enter`/`y`, которую легко нажать рефлекторно),
а вводом заново сгенерированной случайной строки (`internal/ui/confirm.go`,
`generateConfirmToken` — 5 латинских букв, дефис, 3 цифры). Неверный ввод
просто сбрасывает поле с пояснением и оставляет на этом же экране; выйти без
последствий можно только через `esc`. Выдача **нового** ключа пользователю,
у которого его ещё не было, ничего не ломает и подтверждения не требует —
трение нужно только там, где есть что сломать.
## Бэкап и аварийное восстановление
Каталог состояния (`/var/lib/ssh-access-tui/state`) — git-репозиторий.
@@ -172,12 +198,11 @@ session" вместо тихой порчи данных. Если реальн
## Известные ограничения / что дальше
- Обновление клиентского конфига пользователя при появлении **нового**
разрешённого хоста в его группе требует повторного экспорта архива
(что заодно ротирует ключ пользователя — так и задумано: ротация ключа
время от времени — отдельная хорошая практика, просто в MVP она не
отделена от "обновить конфиг"). Удаление хоста из группы вступает в силу
немедленно на сервере (permitopen), без участия клиента.
- Обновление клиентского конфига пользователя при появлении нового
сервиса/хоста в его группе — отдельный ручной шаг («обновить конфиг»,
не требует переиздания ключа); права доступа на сервере (`permitopen=`)
обновляются немедленно и автоматически в любом случае, независимо от
того, обновил ли пользователь свой локальный `config`.
- Архивы в `export_dir` не удаляются автоматически после доставки —
администратор должен удалить их вручную, когда получатель подтвердил
получение.