api

package
v0.0.0-...-d555a40 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 40 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrLockModeUnavailable = errors.New("lock mode unavailable")
	ErrLockModeInvalid     = errors.New("invalid lock mode")
)

Шов режима блокировки. Open-core допускает только overlay; enterprise-оверлей (internal/server/escrow) регистрирует Policy, разрешающую filevault при готовом escrow. lockDevice маппит эти ошибки: Unavailable→409 (фича не готова), Invalid→400.

View Source
var ErrOIDCNotRegistered = oidcNotRegisteredError{}

ErrOIDCNotRegistered — пользователь аутентифицирован в IdP, но строки в users нет. Сигнализирует хендлеру выдать 401 (не 500).

View Source
var ErrOIDCProviderUnavailable = errors.New("oidc provider not found or disabled")

ErrOIDCProviderUnavailable — провайдера нет либо он выключен. Хендлеру это 404, а не 500: выключенный IdP — штатное состояние настройки, а не сбой сервера.

Functions

func Actor

func Actor(ctx context.Context) (userID, email string, ok bool)

Actor извлекает аутентифицированного пользователя из контекста запроса (за jwtMiddleware). Экспорт для enterprise-хендлеров (напр. аудит применения лицензии), которым нужен актор, но недоступен внутренний claimsKey. ok=false вне authed-группы.

func ActorTenant

func ActorTenant(ctx context.Context) (tenantID string, ok bool)

ActorTenant — скоуп для enterprise-хендлеров (рядом с Actor).

func NewRouter

func NewRouter(db *storage.DB, asynqClient *asynq.Client, jwtSecret []byte, ca *enroll.CASigner, publicWebURL, releasesDir string, m *mailer.Mailer, cookieSecure bool, opts ...RouterOption) http.Handler

func ParseTrustedProxies

func ParseTrustedProxies(raw string) ([]netip.Prefix, error)

ParseTrustedProxies разбирает список TRUSTED_PROXIES: CIDR и голые адреса через запятую или пробел. Пустая строка = дефолтный набор.

Ошибка возвращается, а не проглатывается с откатом на дефолт: опечатка в CIDR означает, что оператор ЗНАЛ про нестандартную топологию и настраивал её. Молчаливый откат оставил бы его с лимитом, который он считает настроенным, — ровно тот класс, где гейт зелёный и не проверяет ничего.

func RequireHTTPSURL

func RequireHTTPSURL(raw string) error

RequireHTTPSURL требует https:// у адреса IdP. По этому адресу едут discovery и, через него, JWKS — то есть ключи, которыми проверяется подпись id_token. Посредник на открытом канале подменяет их и выдаёт себя за IdP целиком, так что http здесь обесценивает всю проверку токена.

Loopback оставлен открытым осознанно: локальный стенд IdP поднимают без сертификата, а подслушивать петлю снаружи некому.

func ValidatePassword

func ValidatePassword(p string) string

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 Handler

type Handler struct {
	// contains filtered or unexported fields
}

type LockModePolicy

type LockModePolicy interface {
	ValidateMode(mode string) error
}

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 ВСЕГДА до удаления/переноса строки, и его отказ отменяет всю операцию. Удалить устройство, не сумев удалить его записи, — худший из исходов: оператору сказали «удалено», а данные остались и стали неотслеживаемыми.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL