Documentation
¶
Index ¶
- Variables
- func Actor(ctx context.Context) (userID, email string, ok bool)
- func ActorTenant(ctx context.Context) (tenantID string, ok bool)
- func NewRouter(db *storage.DB, asynqClient *asynq.Client, jwtSecret []byte, ...) http.Handler
- func ParseTrustedProxies(raw string) ([]netip.Prefix, error)
- func RequireHTTPSURL(raw string) error
- func ValidatePassword(p string) string
- type DirectoryConfig
- type DirectoryService
- type DirectorySyncResult
- type Handler
- type LockModePolicy
- type OIDCProviderInput
- type OIDCProviderView
- type OIDCService
- type RouterOption
- func WithAdminRoutes(mount func(*Handler, chi.Router)) RouterOption
- func WithDirectoryService(svc DirectoryService) RouterOption
- func WithHumanAdminRoutes(mount func(*Handler, chi.Router)) RouterOption
- func WithLockModePolicy(p LockModePolicy) RouterOption
- func WithOIDCService(svc OIDCService) RouterOption
- func WithPublicRoutes(mount func(*Handler, chi.Router)) RouterOption
- func WithReleasePubKey(key string) RouterOption
- func WithRoutes(mount func(*Handler, chi.Router)) RouterOption
- func WithScreenPurger(p ScreenPurger) RouterOption
- func WithTelegramBotUsername(fn func(context.Context) string) RouterOption
- func WithTrustedProxies(prefixes []netip.Prefix) RouterOption
- type ScreenPurger
Constants ¶
This section is empty.
Variables ¶
var ( ErrLockModeInvalid = errors.New("invalid lock mode") )
Шов режима блокировки. Open-core допускает только overlay; enterprise-оверлей (internal/server/escrow) регистрирует Policy, разрешающую filevault при готовом escrow. lockDevice маппит эти ошибки: Unavailable→409 (фича не готова), Invalid→400.
var ErrOIDCNotRegistered = oidcNotRegisteredError{}
ErrOIDCNotRegistered — пользователь аутентифицирован в IdP, но строки в users нет. Сигнализирует хендлеру выдать 401 (не 500).
ErrOIDCProviderUnavailable — провайдера нет либо он выключен. Хендлеру это 404, а не 500: выключенный IdP — штатное состояние настройки, а не сбой сервера.
Functions ¶
func Actor ¶
Actor извлекает аутентифицированного пользователя из контекста запроса (за jwtMiddleware). Экспорт для enterprise-хендлеров (напр. аудит применения лицензии), которым нужен актор, но недоступен внутренний claimsKey. ok=false вне authed-группы.
func ActorTenant ¶
ActorTenant — скоуп для enterprise-хендлеров (рядом с Actor).
func ParseTrustedProxies ¶
ParseTrustedProxies разбирает список TRUSTED_PROXIES: CIDR и голые адреса через запятую или пробел. Пустая строка = дефолтный набор.
Ошибка возвращается, а не проглатывается с откатом на дефолт: опечатка в CIDR означает, что оператор ЗНАЛ про нестандартную топологию и настраивал её. Молчаливый откат оставил бы его с лимитом, который он считает настроенным, — ровно тот класс, где гейт зелёный и не проверяет ничего.
func RequireHTTPSURL ¶
RequireHTTPSURL требует https:// у адреса IdP. По этому адресу едут discovery и, через него, JWKS — то есть ключи, которыми проверяется подпись id_token. Посредник на открытом канале подменяет их и выдаёт себя за IdP целиком, так что http здесь обесценивает всю проверку токена.
Loopback оставлен открытым осознанно: локальный стенд IdP поднимают без сертификата, а подслушивать петлю снаружи некому.
func ValidatePassword ¶
ValidatePassword — экспортируемая обёртка над политикой сложности, чтобы её мог применять код вне пакета api (seed-admin в cmd/server). Возвращает "" если ок.
Types ¶
type DirectoryConfig ¶
type DirectoryConfig struct {
Enabled bool `json:"enabled"`
URL string `json:"url"` // ldaps://host:636
BindDN string `json:"bind_dn"`
BaseDN string `json:"base_dn"`
UserFilter string `json:"user_filter"` // напр. (&(objectClass=user)(objectCategory=person))
SyncIntervalMin int `json:"sync_interval_min"` // 0 = только вручную
HasPassword bool `json:"has_password"` // только в ответе: задан ли bind-пароль
// StartTLS — поднять TLS на уже открытом ldap://-соединении. Третий режим поверх
// схемы URL: ldaps:// = неявный TLS, ldap:// = открытый канал, ldap:// + start_tls =
// апгрейд. Взаимоисключающ с ldaps:// (двойной TLS бессмыслен) — validate отбивает.
StartTLS bool `json:"start_tls"`
// HasCACert — только в ответе: задан ли корневой сертификат каталога. Сам PEM
// наружу не отдаётся никогда (симметрично bind-паролю): он не секрет, но его
// наличие достаточно, а лишний вынос содержимого — лишняя поверхность.
HasCACert bool `json:"has_ca_cert"`
// LoginEnabled — разрешить вход в панель по паролю каталога. ОТДЕЛЬНЫЙ флаг, а не
// следствие Enabled: синк персон и приём пароля на вход — разные по риску вещи, и
// включение каталога ради инвентаря не должно молча открывать второй путь
// аутентификации. Нулевое значение = сегодняшнее поведение, поэтому уже
// сохранённые config.json миграции не требуют (та же логика, что у StartTLS).
LoginEnabled bool `json:"login_enabled"`
}
DirectoryConfig — конфиг подключения к каталогу. Bind-пароль сюда НЕ входит (секрет, хранится отдельно в rw-томе); в ответе GET отдаётся HasPassword-флаг, сам пароль — нет.
type DirectoryService ¶
type DirectoryService interface {
GetConfig(ctx context.Context) (DirectoryConfig, error)
// SetConfig: bindPassword=="" и caCertPEM=="" НЕ трогают уже сохранённые значения —
// UI не показывает ни пароль, ни PEM и шлёт пустые строки при правке прочих полей.
SetConfig(ctx context.Context, cfg DirectoryConfig, bindPassword, caCertPEM string) error
TestConnection(ctx context.Context) error
SyncNow(ctx context.Context) (DirectorySyncResult, error)
// Authenticate проверяет пару логин/пароль по каталогу: ищет ровно одну запись под
// сервисным bind, затем делает simple bind её DN с присланным паролем.
//
// ok=false, err==nil — каталог ответил «не тот пароль/нет такого». err!=nil —
// каталог недоступен или настроен не так; звонящий обязан различать эти два случая
// и НЕ выдавать токен во втором. Ответ каталога никогда не заменяет проверку того,
// что аккаунт в панели существует: заводить пользователей вход по LDAP не должен.
Authenticate(ctx context.Context, login, password string) (ok bool, err error)
}
DirectoryService — enterprise-каталог. Реализация — internal/server/directory (//go:build enterprise).
type DirectorySyncResult ¶
type DirectorySyncResult struct {
Synced int `json:"synced"` // персон записано/обновлено
Disabled int `json:"disabled"` // помечено disabled (исчезли из выдачи)
Matched int `json:"matched"` // устройств привязано к владельцу
}
DirectorySyncResult — итог синка каталога.
type LockModePolicy ¶
LockModePolicy валидирует запрошенный режим лока. nil-возврат = режим разрешён.
type OIDCProviderInput ¶
type OIDCProviderInput struct {
Name string `json:"name"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"` // "" при обновлении = не менять
IssuerURL string `json:"issuer_url"`
RedirectURI string `json:"redirect_uri"`
Enabled bool `json:"enabled"`
}
OIDCProviderInput — тело запроса создания/обновления.
type OIDCProviderView ¶
type OIDCProviderView struct {
ID string `json:"id"`
Name string `json:"name"`
ClientID string `json:"client_id"`
IssuerURL string `json:"issuer_url"`
RedirectURI string `json:"redirect_uri"`
Enabled bool `json:"enabled"`
HasSecret bool `json:"has_secret"` // секрет задан; сам PEM/secret не отдаётся
}
OIDCProviderView — данные провайдера для UI. client_secret никогда не отдаётся наружу; HasSecret показывает, задан ли он.
type OIDCService ¶
type OIDCService interface {
// ListProviders возвращает провайдеров тенанта (без секретов).
ListProviders(ctx context.Context, tenantID string) ([]OIDCProviderView, error)
// CreateProvider создаёт нового IdP в тенанте. clientSecret шифруется внутри.
CreateProvider(ctx context.Context, tenantID string, in OIDCProviderInput) (OIDCProviderView, error)
// UpdateProvider обновляет провайдера тенанта. Если in.ClientSecret == "", секрет не меняется.
UpdateProvider(ctx context.Context, tenantID, id string, in OIDCProviderInput) error
// DeleteProvider удаляет провайдера тенанта.
DeleteProvider(ctx context.Context, tenantID, id string) error
// BeginFlow генерирует PKCE + state, сохраняет в Redis (TTL 10мин), возвращает URL
// авторизации IdP, на который нужно перенаправить пользователя.
BeginFlow(ctx context.Context, providerID string) (authURL string, err error)
// HandleCallback принимает code+state из IdP, верифицирует state, обменивает code
// на ID-token, извлекает email и ищет пользователя В ТЕНАНТЕ ПРОВАЙДЕРА.
// Возвращает (userID, email, role) для выдачи JWT, или error.
// ErrNotRegistered — email не найден в users (аккаунты OIDC не создаёт).
HandleCallback(ctx context.Context, providerID, code, state string) (userID, email, role string, err error)
}
OIDCService — enterprise-SSO. Реализация — internal/server/oidc (//go:build enterprise).
Управление провайдерами принимает tenantID явно (контракт §4): IdP принадлежит тенанту, чужой по id не найдётся. begin/callback тенант НЕ принимают — они анонимные, и тенант там резолвится из самой строки провайдера (051).
type RouterOption ¶
type RouterOption func(*Handler)
RouterOption — расширение роутера enterprise-оверлеем (внутри authed-группы). RouterOption настраивает хендлер ДО объявления роутов.
🔴 Роутера в сигнатуре НЕТ намеренно. Пока он был, опция могла смонтировать роут прямо на переданный роутер — и ровно это уронило прод: опции стали применяться раньше, чем существуют группы, и WithOIDCService получил nil. Монтирование теперь возможно ТОЛЬКО через списки (WithPublicRoutes, WithRoutes, WithAdminRoutes, WithHumanAdminRoutes), которые NewRouter разворачивает каждый в своём месте. Класс ошибки закрыт типом, а не памятью.
func WithAdminRoutes ¶
func WithAdminRoutes(mount func(*Handler, chi.Router)) RouterOption
WithAdminRoutes монтирует enterprise-роуты в подгруппу с гейтом it_admin (для мутирующих/чувствительных операций вроде применения лицензии). Инфраструктура (не enterprise-логика): open-core просто не передаёт таких опций.
func WithDirectoryService ¶
func WithDirectoryService(svc DirectoryService) RouterOption
WithDirectoryService подключает enterprise-каталог. Зовётся ТОЛЬКО в enterprise composition-root (cmd/server, //go:build enterprise) после лиц-гейта.
func WithHumanAdminRoutes ¶
func WithHumanAdminRoutes(mount func(*Handler, chi.Router)) RouterOption
WithHumanAdminRoutes — как WithAdminRoutes, но дополнительно режет сервисные токены (requireHuman). Для операций, которые ВЫДАЮТ доступ: выгрузка заэскроенного recovery-ключа — это ключ от зашифрованного диска, автоматике такое не отдаём. Сам requireHuman не экспортируем: снаружи он бесполезен без claimsKey.
func WithLockModePolicy ¶
func WithLockModePolicy(p LockModePolicy) RouterOption
WithLockModePolicy заменяет дефолтную overlay-only политику (enterprise).
func WithOIDCService ¶
func WithOIDCService(svc OIDCService) RouterOption
WithOIDCService подключает enterprise-SSO. Зовётся ТОЛЬКО в enterprise composition-root.
func WithPublicRoutes ¶
func WithPublicRoutes(mount func(*Handler, chi.Router)) RouterOption
WithPublicRoutes монтирует роуты ДО аутентификации (SSO-редиректы, приём assertion от IdP). Открывать здесь что-либо кроме входа нельзя.
func WithReleasePubKey ¶
func WithReleasePubKey(key string) RouterOption
WithReleasePubKey задаёт base64 ed25519 публичного ключа релиза; сервер отдаёт его агенту в enroll-ответе (release_pubkey). Универсальный (не привязанный к деплою) агент проверяет самообновление этим ключом вместо вшитого на сборке.
func WithRoutes ¶
func WithRoutes(mount func(*Handler, chi.Router)) RouterOption
WithRoutes монтирует дополнительные роуты enterprise (напр. /escrow/status) в authed-группу (все роли).
func WithScreenPurger ¶
func WithScreenPurger(p ScreenPurger) RouterOption
WithScreenPurger подключает enterprise-реализацию. Отдельной опции в composition-root нет намеренно: её ставит screen.Routes вместе с самими ручками — подключить удалённый стол и забыть про удаление его следов должно быть невозможно.
func WithTelegramBotUsername ¶
func WithTelegramBotUsername(fn func(context.Context) string) RouterOption
WithTelegramBotUsername отдаёт функцию, возвращающую @username бота этого деплоя (getMe). Функция, а не строка: getMe ходит в сеть и на старте может ещё не ответить.
func WithTrustedProxies ¶
func WithTrustedProxies(prefixes []netip.Prefix) RouterOption
WithTrustedProxies задаёт список доверенных прокси. Без него действует defaultTrustedProxies.
type ScreenPurger ¶
type ScreenPurger interface {
// PurgeDevice — все записи одного устройства.
PurgeDevice(ctx context.Context, tenantID, deviceID string) error
// PurgeTenant — все записи тенанта (его устройства перестают существовать).
PurgeTenant(ctx context.Context, tenantID string) error
}
ScreenPurger — физическое удаление записей экрана. Реализуется enterprise-оверлеем (internal/server/screen); в open-core записей не существует, и хэндл остаётся nil.
🔴 Зачем отдельный шов, когда есть каскады в схеме. Каскад удаляет СТРОКИ: у screen_sessions FK на tenants и devices стоят ON DELETE CASCADE (миграция 067). Файлы записей лежат на диске и каскадом не удаляются ничем. То есть удаление устройства или тенанта уносило именно то, из чего можно узнать, какие файлы остались, — и запись экрана сотрудника оставалась на томе навсегда, уже без единой ссылки. §6 контракта требует обратного: персональные данные не остаются у прежнего контролёра и не переезжают к другому.
Отсюда и порядок: purge ВСЕГДА до удаления/переноса строки, и его отказ отменяет всю операцию. Удалить устройство, не сумев удалить его записи, — худший из исходов: оператору сказали «удалено», а данные остались и стали неотслеживаемыми.