rc
This commit is contained in:
1 parent
e39ad275b5
commit
3d7e3a88f2
14 files changed
+710
-234
No files matched your search
+53
-12
@@ -25,20 +25,26 @@ TUI работает поверх уже существующего SSH+`sudo`-
|
||||
раз, а не заново в каждой группе. Хост (`Host`) — это адрес (например,
|
||||
`172.16.1.20`) с описанием; на одном хосте может быть несколько сервисов
|
||||
(`Service`) — независимых TCP-портов: `ssh` на 22, какое-то приложение на
|
||||
7888, и так далее. Группа (`Group`) не хранит хосты сама — она хранит
|
||||
только список **имён** хостов из каталога (`Group.Hosts []string`),
|
||||
которые в неё входят (все сервисы такого хоста включаются целиком). Если
|
||||
одни и те же хосты/сервисы нужны нескольким группам — они описываются один
|
||||
раз в каталоге, а группы просто отмечают их галочками, вместо повторного
|
||||
набора адресов и портов.
|
||||
7888, и так далее. Группа (`Group`) не хранит хосты — она хранит список
|
||||
**ссылок на конкретные сервисы** каталога (`Group.Services []ServiceRef`,
|
||||
каждая — пара `{Host, Service}`). Доступ раздаётся по сервису, а не по
|
||||
хосту целиком: группа может включать `db1/ssh`, не давая заодно
|
||||
`db1/web-admin` того же хоста. UI показывает выбор как дерево "хост →
|
||||
сервисы" (`internal/ui/groups.go`), где отметка на строке хоста — это
|
||||
просто ярлык "выбрать все его сервисы", а не отдельный уровень доступа в
|
||||
модели. Если одни и те же хосты/сервисы нужны нескольким группам — они
|
||||
описываются один раз в каталоге, а группы просто отмечают нужные сервисы,
|
||||
вместо повторного набора адресов и портов.
|
||||
|
||||
Пользователь может состоять в нескольких группах; его реальный эффективный
|
||||
доступ — объединение сервисов всех хостов всех его групп, это и называется
|
||||
`Forward` (`internal/model/types.go`, `State.AllowedForwards`). Переименование
|
||||
хоста автоматически обновляет ссылки на него во всех группах
|
||||
(`State.RenameHost`); удаление хоста снимает его со всех групп, которые его
|
||||
использовали (`State.DeleteHost`) — ссылочная целостность каталога держится
|
||||
в модели, а не проверяется вручную в UI.
|
||||
доступ — объединение сервисов, на которые ссылаются все его группы, это и
|
||||
называется `Forward` (`internal/model/types.go`, `State.AllowedForwards`).
|
||||
Ссылочная целостность каталога держится в модели, а не проверяется вручную
|
||||
в UI: переименование хоста или сервиса обновляет `ServiceRef` во всех
|
||||
группах (`State.RenameHost`, `State.RenameServiceRefs`); удаление хоста или
|
||||
одного его сервиса снимает соответствующие `ServiceRef` со всех групп
|
||||
(`State.DeleteHost`, `State.RemoveServiceRefs`) — переименование/удаление
|
||||
одного сервиса никогда не задевает остальные сервисы того же хоста.
|
||||
|
||||
Ключевой момент: пользователь **никогда не подключается по SSH к целевому
|
||||
хосту напрямую** (ни через `ProxyJump`, ни как-то ещё) — он открывает одно
|
||||
@@ -110,6 +116,31 @@ Reverse/remote forwarding (`-R`) в этой модели не выдаётся
|
||||
порты. Итоговое соответствие "локальный порт → сервис" попадает в
|
||||
`INSTRUCTIONS.txt` внутри архива пользователя.
|
||||
|
||||
## Отслеживание устаревших конфигов
|
||||
|
||||
Права на сервере (`permitopen=` в `authorized_keys`) обновляются немедленно
|
||||
при любом изменении групп/каталога — но клиентский `config` пользователя
|
||||
лежит у него на компьютере и сам не меняется. Чтобы админ не забывал, кому
|
||||
нужно прислать новый `config`, каждому пользователю в состоянии хранится
|
||||
`ForwardsHash` (`internal/model/hash.go`) — детерминированный (порядок
|
||||
форвардов не важен, список сортируется перед хешированием) хэш набора
|
||||
`Forward`, который был у него на момент последнего экспорта или "обновить
|
||||
конфиг". `State.IsConfigStale` просто сравнивает этот сохранённый хэш с
|
||||
хэшем текущего `AllowedForwards(u)`: если они разошлись — где-то в
|
||||
группах/каталоге, от которых зависит доступ этого пользователя, что-то
|
||||
поменялось, а он ещё не получил обновлённый `config`.
|
||||
|
||||
Это используется в двух местах: в главном меню (живой счётчик под списком
|
||||
пунктов, пересчитывается на каждый кадр — `internal/ui/menu.go`,
|
||||
`viewMenu`) и на экране подтверждения выхода (`attemptQuit`/
|
||||
`viewQuitConfirm`), отдельно от пользователей без ключа вообще
|
||||
(`State.NeverExportedUsernames`). "Обновить конфиги"
|
||||
(`internal/ui/configupdate.go`) — пакетный аналог экспорта, но без
|
||||
переиздания ключа: строит только `config`+`INSTRUCTIONS.txt` для каждого
|
||||
выбранного пользователя (`keymgr.BuildConfigOnlyArchive`) и обновляет его
|
||||
`ForwardsHash`, ничего не трогая в `authorized_keys` и не требуя
|
||||
подтверждения строкой — это не деструктивное действие.
|
||||
|
||||
## Жизненный цикл приватного ключа
|
||||
|
||||
Приватный ключ **никогда не хранится на диске дольше одной операции
|
||||
@@ -141,6 +172,16 @@ fingerprint. Это осознанное решение: кража бэкапа
|
||||
у которого его ещё не было, ничего не ломает и подтверждения не требует —
|
||||
трение нужно только там, где есть что сломать.
|
||||
|
||||
**Готча при добавлении новых экранов-списков:** `bubbles/list` по умолчанию
|
||||
сам перехватывает `q` и мгновенно возвращает `tea.Quit`, в обход всей этой
|
||||
логики — на любом экране, где необработанная клавиша проваливается в
|
||||
`list.Model.Update`. `internal/ui/app.go` (`newList`) вызывает
|
||||
`l.DisableQuitKeybindings()` на каждом создаваемом списке ровно из-за
|
||||
этого — при добавлении нового списка мимо `newList` эта защита пропадает
|
||||
незаметно (сам список продолжит работать, просто `q` будет закрывать
|
||||
программу без единого предупреждения). Обнаружено и подтверждено тестом
|
||||
именно в такой форме: экран без нужного `case "q"` в своём `Update`.
|
||||
|
||||
## Бэкап и аварийное восстановление
|
||||
|
||||
Каталог состояния (`/var/lib/ssh-access-tui/state`) — git-репозиторий.
|
||||
|
||||
Reference in new issue
Block a user