clients

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package clients — peer-gRPC clients for kaname.

kaname is the leaf-owner of Account/Project/User and does not initiate peer-domain calls itself. The clients here are dependencies on adjacent systems (OIDC providers, HSM/PKCS#11, S3 report-store, etc.) — see individual files for each client's scope. A client that pushed cache invalidation to the edge used to be listed here; that edge was retired (the edge now reads the subject-change journal itself), so nothing dials it any more. A client to an external relations engine used to stand here too; the verdict is now computed in this service's own database, so there is nothing to dial.

Peer-client template — `internal/clients/builder.go`-style: TTL+LRU cache + retries/dialTimeout/keepalive + TLS + optional dns:///+round_robin.

hydra_admin_client.go — client for the Ory Hydra Admin API.

Carries the shared connection config (base URL, bearer, HTTP client) for the Hydra admin surfaces iam actually drives, each in its own file:

  • hydra_oauth_clients.go — OAuth2 client lifecycle (/admin/clients).

It no longer publishes or deletes JWKs, и причина — НЕ в том, что своих ключей у платформы нет.

Здесь стояло «iam owns no signing keyset: it mints nothing, Hydra is the issuer and signer». Утверждение пережило свой предмет: ключница у платформы есть (`kaname.token_signing_keys`, package internal/signingkeygen), свои токены она подписывает сама (internal/tokensigner), а публикатор :9097 отдаёт ДВЕ записи — байт-в-байт зеркало набора провайдера и НАШУ, каждая по своему объявленному пути.

Настоящая причина снятия: пара PublishKey/DeleteKey писала ключи В ЧУЖОЕ хранилище — она существовала исключительно ради ночной JWKSRotationService, снятой (713f7e1) вместе с хранилищем, которое ротировала (миграция 0065). Наша ротация живёт ВНУТРИ службы и админ-API провайдера не касается вовсе, поэтому интерфейс service.JWKSPublisher не имеет здесь предмета.

Authentication: if HYDRA_ADMIN_TOKEN env is set — Bearer; otherwise anonymous (default Hydra config in the kind dev-stand exposes an anonymous admin port).

hydra_login_sessions.go — the provider's login-session surface.

DELETE /admin/oauth2/auth/sessions/login?subject={sub}

One call, used by admin force-logout. The self-service logout path at the edge already pulls this same lever for the caller's own subject; force-logout pulled nothing, so an administrator could record that a person was logged out while that person's browser still held a live session at the provider.

Why the cutoff alone is not enough. With the session alive, the browser can obtain a fresh authorization code without re-authenticating, and the session it presents keeps its ORIGINAL authentication instant — so the cutoff refuses it, correctly, forever. The subject is then wedged: refused at every turn, with nothing prompting the re-authentication that would clear it. Ending the session is what turns a refusal into a logout.

hydra_oauth_clients.go — Hydra Admin API for OAuth2 client CRUD, supporting Class A static service-account keys.

Endpoints used:

POST   /admin/clients              — create OAuth2 client (returns
                                     {client_id, client_secret, ...}).
GET    /admin/clients/{client_id}  — get OAuth2 client (no secret).
DELETE /admin/clients/{client_id}  — delete OAuth2 client.

The plaintext `client_secret` is returned EXACTLY ONCE by Create — we propagate it back through Operation.response.IssueSAKeyResponse and never persist it (security rule: secrets are never stored; only `hydra_client_id` is kept in `service_account_oauth_clients`).

hydra_token_exchange.go — client for the Ory Hydra PUBLIC OAuth2 token endpoint (`POST /oauth2/token`). Used by the Docker Registry v2 `/iam/token` shim: the shim signs an ES256 client_assertion from the presented SA-key and brokers a `client_credentials` + `private_key_jwt` exchange, returning the provider's access_token to the docker client.

ЭТОТ КЛИЕНТ — ПОЛОСА НЕПЕРЕВЕДЁННОГО КОНТУРА, и только она. Здесь стояло «kaname no longer mints registry tokens itself», сказанное о платформе; верно это ровно про путь, идущий через ЭТОТ файл. Там, где своя чеканка объявлена, докерный токен выпускает наш подписант (internal/registrytokenwire, LocalMintAdapter), и до этого клиента запрос не доходит вовсе.

(Плоскость данных сверяет полученный токен по записи набора ключей ЕГО издателя; обе записи отдаёт публикатор iam — internal/handler/jwksproxyhttp, зеркало ничего не переподписывает.)

Failure classification (fail-closed, no-leak):

  • network failure / timeout / 5xx / malformed 2xx → ErrHydraUnavailable (the issuer is a hard dependency of the mint path; the shim returns 503).
  • 4xx OAuth2 error (invalid_client / invalid_grant) → ErrHydraRejected (bad/expired/revoked credential; the shim returns a 401 challenge).

The raw Hydra body is never embedded in the returned sentinel (no auth oracle).

invite_mail.go — НАШ отправитель письма приглашения.

Почему отправитель здесь, а не у поставщика личности

Писем в продукте три вида, и производители у них РАЗНЫЕ (приёмка ID-MAIL-1, Р23): подтверждение адреса и восстановление доступа отправляет почтовый процесс поставщика — их предъявители принадлежат ему; приглашение отправляем МЫ, потому что предмет приглашения — наша строка в нашей базе, и о ней поставщик не знает ничего.

Что здесь лежит — три части одной цепочки

  • InviteMailSender — транспорт: один разговор с почтовым узлом, СВОИМ пределом времени ограниченный;
  • DecodeInviteMail — Decoder[T] для общего дренажа;
  • NewInviteMailApplier — Applier[T]: зовёт транспорт и раскладывает исход по ЗАКРЫТОМУ набору клеток счётчика.

Две величины, и они РАЗНЫЕ

Предел времени на ПОПЫТКУ (`MailRelay.AttemptTimeout`) и число ПОВТОРОВ (`drainer.Config.MaxAttempts`, композиционный корень) — разные величины с разными предметами. «Ограниченный повтор» без первой ограничивает ЧИСЛО попыток, каждая из которых вправе висеть вечно, — а это `architecture.md` §«Per-call deadline на КАЖДОМ внешнем вызове» в чистом виде. Наблюдаема первая только на узле, который ПРИНИМАЕТ соединение и молчит: на отказе в соединении обрыв даёт ядро, а не наша величина.

Настройка отделена от сбоя — собственной клеткой

Недоступность узла лечится временем; ответ не по протоколу почты по объявленному адресу не лечится никогда. Схлопнуть их в один ряд значит сделать постоянную неверную настройку штатным режимом — `security.md` §Hardening п. 8. Поэтому клеток три, набор ЗАКРЫТ и приходит из констант, а не из ответа узла: иначе кардинальность росла бы с трафиком.

provider_compensation_outbox.go — компенсация частично исполненной саги «зарегистрировать OAuth-клиента у провайдера → закоммитить свою строку».

Здесь три части одной цепочки:

  • ProviderCompensationOutbox — writer намерения; порт объявлен у каждого потребителя (dependency rule), здесь живёт единственная реализация;
  • DecodeProviderCompensation — Decoder[T] для corelib-дренажа;
  • NewProviderCompensationApplier — Applier[T], исполняющий снятие у провайдера идемпотентно.

Почему намерение коммитится СВОЕЙ транзакцией, а не writer-TX: компенсируется именно та транзакция, которая откатилась, поэтому нести намерение ей нечем. Это осознанно слабее co-commit'а внутри одной БД и сильнее вызова «в надежде»: пережившее рестарт намерение доставит дренаж.

provider_hop_tls.go — one implementation of "verify this hop against the anchor the operator pinned, and against nothing else", shared by every hop iam makes to the identity provider.

It exists because there were three hops and one of them had the property. The admin hop was moved to TLS with a pinned anchor and a boot guard; the two hops to the provider's PUBLIC listener — the token exchange and the JWKS upstream — were still built as `&http.Client{Timeout: …}`, i.e. with no way to be given an anchor at all. That is not merely a missing knob: it made the obvious fix impossible, because flipping such an address to https lands on the SYSTEM roots, which an internal-CA certificate never chains to. So "cannot be done" was true of the code and read as true of the platform.

Keeping one implementation is the point. The next hop added to the facade gets the property by construction instead of by whoever remembers to copy it.

Index

Constants

View Source
const (
	// InviteMailTable — полное имя очереди писем приглашения. ШЕСТАЯ очередь
	// сервиса; форма не изобретается — она в дереве пятикратна.
	InviteMailTable = "kaname.invite_mail_outbox"
	// InviteMailChannel — LISTEN-канал (триггер миграции).
	InviteMailChannel = "kaname_invite_mail_outbox"
	// EventInviteMailSend — единственный вид события очереди. Словарь закрыт
	// CHECK'ом миграции: расширение требует и кода, и миграции.
	EventInviteMailSend = "mail.invite.send"
)
View Source
const (
	// InviteMailOutcomeSent — письмо СДАНО почтовому узлу.
	//
	// КЛЕТКА НАЗЫВАЕТСЯ «sent», А НЕ «delivered», И ЭТО НЕ ПРИДИРКА К СЛОВУ.
	// Дальше ретранслятора наш вердикт не идёт (Р15): продукт видит сдачу, а не
	// получение адресатом. Ряд с именем «delivered» читался бы дежурным в три
	// часа ночи как «письма доходят» — то есть утверждал бы ровно то, чего
	// продукт не знает, и делал бы это на поверхности, где комментария нет.
	// Имя ряда и есть утверждение; комментарий, поясняющий, что имя означает не
	// то, что говорит, — второе место об одном предмете, и верным было бы одно.
	InviteMailOutcomeSent = "sent"
	// InviteMailOutcomeTransient — узел не принял письмо по причине, которая
	// лечится временем: не поднят, не ответил, ответил временным отказом.
	InviteMailOutcomeTransient = "transient"
	// InviteMailOutcomeMisconfigured — по объявленному адресу не почтовый узел,
	// величина не задана либо задана вырожденно, удостоверение отвергнуто.
	// Временем НЕ лечится.
	InviteMailOutcomeMisconfigured = "misconfigured"
)

Клетки счётчика исходов отправки. Набор ЗАКРЫТ (Р25): форма взята у зеркала набора ключей вместе с обоснованием — успехи считаются НАРАВНЕ с отказами, иначе ноль отказов неотличим от «сюда никто не приходил», а настройка стоит СВОЕЙ клеткой, потому что временем не лечится.

View Source
const (
	// ProviderCompensationTable — полное имя очереди компенсаций.
	ProviderCompensationTable = "kaname.provider_compensation_outbox"
	// ProviderCompensationChannel — LISTEN-канал (триггер миграции 0079).
	ProviderCompensationChannel = "kaname_provider_compensation_outbox"
	// EventProviderOAuthClientDelete — снять OAuth-клиента у провайдера.
	// Словарь закрыт CHECK'ом в миграции: расширение требует и кода, и миграции.
	EventProviderOAuthClientDelete = "provider.oauth_client.delete"
)
View Source
const (

	// JWKSHopCASetting is exported because the JWKS upstream client is assembled
	// at the composition root (the mirror handler takes an injected client), so
	// the refusal has to be able to name the setting from there.
	JWKSHopCASetting = "authn.hydra-jwks-ca-file (env KANAME_HYDRA_JWKS_CA_FILE)"
)

Settings naming each hop's trust anchor. Held as constants so a refusal names the thing an operator edits, in both the YAML and the ENV spelling.

Variables

View Source
var (
	// ErrMailMisconfigured — отказ, который повтор не вылечит.
	ErrMailMisconfigured = errors.New("invite mail: relay is misconfigured")
	// ErrMailTransient — отказ, который лечится временем.
	ErrMailTransient = errors.New("invite mail: relay is temporarily unavailable")
)

Сигнальные ошибки классификации. КАЖДАЯ ветка возврата транспорта заворачивает свой отказ ровно в одну из них — корзины «прочее» у отправителя нет by construction.

View Source
var ErrHydraRejected = errors.New("hydra rejected the token exchange")

ErrHydraRejected — Hydra rejected the exchange (4xx OAuth2 error). The credential is invalid / expired / revoked; the client re-authenticates.

View Source
var ErrHydraUnavailable = errors.New("hydra token endpoint unavailable")

ErrHydraUnavailable — the Hydra token endpoint is unreachable or misbehaving (network / timeout / 5xx / malformed response). Fail-closed: no token.

InviteMailOutcomes — закрытый набор клеток семейства.

Functions

func ClassifyInviteMailOutcome

func ClassifyInviteMailOutcome(err error) string

ClassifyInviteMailOutcome раскладывает исход одной попытки по ЗАКРЫТОМУ набору клеток.

nil — сдано. Прочее приходит завёрнутым в одну из двух сигнальных ошибок: каждая ветка возврата транспорта заворачивает свой отказ явно, поэтому неклассифицированного отказа НАШ отправитель не производит. Неназванный отказ (он мог бы прийти от чужого транспорта в пробе) считается ВРЕМЕННЫМ осознанно: повтор безопаснее отравления.

func IsConflict

func IsConflict(err error) bool

IsConflict reports whether err is a Hydra Admin 409 Conflict (e.g. a CreateOAuthClient for an already-registered client_id). Callers with a deterministic client_id treat this as idempotent success — the client already exists. Keeps HTTP-status semantics inside the adapter package.

func NewInviteMailApplier

func NewInviteMailApplier(
	transport InviteMailTransport, obs InviteMailObserver, logger *slog.Logger,
) drainer.Applier[InviteMailEvent]

NewInviteMailApplier — drainer.Applier: сдаёт письмо узлу и раскладывает исход по закрытому набору клеток.

ОТКАЗ ПО НАСТРОЙКЕ ВОЗВРАЩАЕТСЯ ПОСТОЯННЫМ (MAIL-33: «без бесконечных повторов»): строка отравляется, остаётся в очереди видимой и повторно исполнимой, когда настройку починят, — а не крутится вечно, изображая работу. Временный отказ ретраится: он лечится временем.

Ни один исход не остаётся тихим: клетка ставится на КАЖДОЙ ветке возврата, и отказ по настройке пишется в журнал уровнем ошибки, а не предупреждения (`security.md` §Hardening п. 8: настройка — громко, никогда тихим Warn).

func NewProviderCompensationApplier

func NewProviderCompensationApplier(
	releaser ProviderClientReleaser, obs CompensationObserver,
) drainer.Applier[ProviderCompensationEvent]

NewProviderCompensationApplier — drainer.Applier: снимает клиента у провайдера.

Идемпотентность лежит на releaser'е: провайдер отвечает 404 на повторное снятие, и клиент трактует 404 как исполнено. Поэтому повтор доставки безопасен, и никакой отдельной защиты от двойного применения не нужно.

Классификация отказа — обычная (drainer.Classify): недоступность провайдера транзиентна и ретраится, отказ по существу запроса приезжает завёрнутым в ErrPermanent из декодера. Отдельной корзины «прочее» здесь нет by construction: применение делает ровно один вызов.

func ProviderHopHTTPClient

func ProviderHopHTTPClient(timeout time.Duration, caFile, setting string) (*http.Client, error)

ProviderHopHTTPClient builds the HTTP client for one hop to the identity provider: a per-call timeout always, and the pinned anchor when one is configured.

caFile empty ⇒ the default transport, unchanged. That is not an oversight: an in-cluster listener served over plaintext http needs no anchor, and inventing one would refuse a stand deliberately configured that way. The production boot guard (config.Validate) is what forbids claiming https without one.

caFile set ⇒ the returned client trusts that bundle ALONE. Not "in addition to the system roots": an internal-CA hop has no business accepting a publicly issued certificate for the same name, and narrowing the anchor is the whole point of pinning it.

An anchor that cannot be read, or that holds no certificate, is an ERROR rather than a fallback. Continuing on the system roots would produce the one state nobody can see — the operator has configured verification against the internal CA, the process is not doing it, and everything works until a certificate rotates.

func RenderInviteMail

func RenderInviteMail(relay MailRelay, ev InviteMailEvent) []byte

RenderInviteMail собирает тело письма.

Письмо говорит об ОТПРАВКЕ и НИГДЕ не говорит «доставлено»: продукт видит сдачу ретранслятору, а не получение адресатом, и утверждать второе значило бы обещать то, чего он не знает (Р15).

Предъявителя письмо НЕ несёт (Р24): в нём призыв и адрес страницы входа, а доступ даёт владение почтовым ящиком, доказанное подтверждением адреса.

Types

type ClientCredentialsRequest

type ClientCredentialsRequest struct {
	// ClientAssertion — the signed ES256 JWS (RFC 7523) proving possession of the
	// SA-key private half; identifies the Hydra client (iss=sub=client_id).
	ClientAssertion string
	// Audience — requested `aud` for the minted token (the registry service).
	// Empty → not sent (Hydra falls back to the client's configured audience).
	Audience string
	// Scope — requested scope. Empty → not sent.
	Scope string
}

ClientCredentialsRequest — inputs for the private_key_jwt exchange.

type CompensationEmitObserver

type CompensationEmitObserver interface {
	// IncCompensationEmitted — outcome "ok" (намерение записано) либо "error"
	// (записать не удалось, вызывающий уходит на запасной прямой путь).
	IncCompensationEmitted(origin, outcome string)
}

CompensationEmitObserver — счётчик ЗАПИСАННЫХ намерений. Считается отдельно от исполненных: без обеих величин «ноль компенсаций» у здорового облака и «ноль компенсаций» у непровязанного механизма выглядят одинаково.

type CompensationObserver

type CompensationObserver interface {
	// IncCompensationApplied — компенсация исполнена (клиент снят ИЛИ его уже
	// не было). origin — атрибуция саги.
	IncCompensationApplied(origin string)
}

CompensationObserver — счётчики исполненных компенсаций. Нужны, чтобы «ноль компенсаций за всю жизнь» было ОТЛИЧИМО от «механизм мёртв»: без счётчика оба состояния выглядят одинаково — тихо.

type ConditionalTuple

type ConditionalTuple = authztypes.ConditionalTuple

ConditionalTuple — псевдоним authztypes.ConditionalTuple.

type CreateOAuthClientRequest

type CreateOAuthClientRequest struct {
	// ClientID is optional — if empty, Hydra auto-generates.
	ClientID string
	// ClientName is a human-readable identifier (e.g. "kaname-sak-XYZ").
	ClientName string
	// Owner is the kaname ServiceAccount id (used by Hydra's `owner`
	// filter for List by SA).
	Owner string
	// Scope — space-separated set granted to this client.
	Scope string
	// Audience — `aud` claim placed in minted tokens.
	Audience []string
	// AuthMethod — "client_secret_basic" / "client_secret_post" /
	// "private_key_jwt" (the default).
	AuthMethod string
	// GrantTypes — OAuth2 grants the client may exercise. Defaults to
	// `["client_credentials"]` when nil/empty.
	GrantTypes []string
	// TokenEndpointAuthMethod — explicit override of AuthMethod for clients
	// migrated to private_key_jwt. When non-empty, takes precedence over
	// AuthMethod. Set to "private_key_jwt" for SA keys.
	TokenEndpointAuthMethod string
	// TokenEndpointAuthSigningAlg — JOSE-alg client_assertion ("ES256" для SA-ключей).
	TokenEndpointAuthSigningAlg string
	// JWKS — embedded public-key set published with the client (private_key_jwt:
	// kaname mints the keypair and registers the
	// public JWK here). Hydra stores it, validates `client_assertion`
	// signatures against it, and never sees the private half.
	JWKS *JWKS
	// AccessTokenLifespan — per-client access-token lifetime (Go duration string).
	// Empty → Hydra's global default.
	AccessTokenLifespan string
	// DPoPBoundAccessTokens — request sender-constrained (RFC 9449) tokens for
	// this client. See the field of the same name on HydraOAuthClient.
	DPoPBoundAccessTokens bool
	// TLSClientCertificateBoundAccessTokens — RFC 8705 mTLS-bound tokens.
	TLSClientCertificateBoundAccessTokens bool
	// RedirectURIs / PostLogoutRedirectURIs — interactive-login client only.
	RedirectURIs           []string
	PostLogoutRedirectURIs []string
	// ResponseTypes — overrides the default ["token"]. The authorization-code
	// ceremony needs ["code"]; leaving the default would register a client the
	// provider refuses to run a code flow for.
	ResponseTypes []string
}

CreateOAuthClientRequest — input for HydraAdminClient.CreateOAuthClient.

type HydraAPIError

type HydraAPIError struct {
	StatusCode int
	Body       string
}

HydraAPIError — Hydra Admin returned non-2xx.

func (*HydraAPIError) Error

func (e *HydraAPIError) Error() string

type HydraAdminClient

type HydraAdminClient struct {
	BaseURL     string
	BearerToken string
	HTTPClient  *http.Client
}

HydraAdminClient — HTTP-клиент к Hydra admin API.

func NewHydraAdminClient

func NewHydraAdminClient(baseURL, bearerToken string) *HydraAdminClient

NewHydraAdminClient — constructor without a pinned trust anchor. Default timeout 10s. Kept for call sites that address a plaintext in-cluster admin API (a developer stand); production addresses it over TLS and must therefore use NewHydraAdminClientWithCA.

func NewHydraAdminClientWithCA

func NewHydraAdminClientWithCA(baseURL, bearerToken, caFile string) (*HydraAdminClient, error)

NewHydraAdminClientWithCA builds the client and, when an anchor is configured, verifies the provider against THAT bundle and nothing else.

caFile empty ⇒ the default transport, unchanged. That is not an oversight: an in-cluster admin API served over plaintext http needs no anchor, and inventing one would refuse a stand deliberately configured that way. The production boot guard is what forbids that combination in production (config.Validate).

caFile set ⇒ the returned client trusts that bundle ALONE. Not "in addition to the system roots": an internal-CA hop has no business accepting a publicly issued certificate for the same name, and narrowing the anchor is the whole point of pinning it.

An anchor that cannot be read, or that holds no certificate, is an ERROR rather than a fallback. Continuing on the system roots would produce the one state nobody can see — the operator has configured verification against the internal CA, the process is not doing it, and everything works until a certificate rotates.

func (*HydraAdminClient) CreateOAuthClient

CreateOAuthClient registers a new client_credentials OAuth2 client with Hydra.

When `req.TokenEndpointAuthMethod == "private_key_jwt"` the caller supplies `req.JWKS` with the public half of the keypair; Hydra validates `client_assertion` (RFC 7521/7523) signatures against it and returns NO `client_secret`. Otherwise (legacy `client_secret_basic`) Hydra mints + returns the plaintext `client_secret` exactly once.

func (*HydraAdminClient) DeleteLoginSessions

func (c *HydraAdminClient) DeleteLoginSessions(ctx context.Context, subject string) error

DeleteLoginSessions ends every login session the provider holds for a subject, so the next request must authenticate again.

The subject is the EXTERNAL identity the provider knows (what it puts in `sub`), not a kacho `users.id` — the caller resolves that.

Idempotent: "no such session" is success. Deleting sessions is a converging operation and the caller retries the whole force-logout, so a second call finding nothing left is the desired state reached, not a failure.

func (*HydraAdminClient) DeleteOAuthClient

func (c *HydraAdminClient) DeleteOAuthClient(ctx context.Context, clientID string) error

DeleteOAuthClient revokes an OAuth2 client. Returns nil on success or if Hydra returns 404 (idempotent).

type HydraOAuthClient

type HydraOAuthClient struct {
	ClientID                string   `json:"client_id"`
	ClientSecret            string   `json:"client_secret,omitempty"` // present only on Create (legacy client_secret_basic)
	ClientName              string   `json:"client_name,omitempty"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	ResponseTypes           []string `json:"response_types,omitempty"`
	Scope                   string   `json:"scope,omitempty"`
	Audience                []string `json:"audience,omitempty"`
	Owner                   string   `json:"owner,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
	// TokenEndpointAuthSigningAlg — JOSE-alg, которым Hydra обязан проверять
	// client_assertion (private_key_jwt). Для SA-ключей — "ES256"; без него Hydra
	// дефолтит на RS256 и отвергает ES256-assertion (invalid_client).
	TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"`
	// JWKS — embedded JSON Web Key Set; populated when
	// `token_endpoint_auth_method == "private_key_jwt"`.
	JWKS *JWKS `json:"jwks,omitempty"`
	// AccessTokenLifespan — per-client access-token lifetime (Go duration string,
	// e.g. "15m0s"). Empty → Hydra's global default. Set for the bootstrap client
	// so its minted tokens are deliberately short-lived (#58, IBT-09).
	AccessTokenLifespan string `json:"access_token_lifespan,omitempty"`
	// DPoPBoundAccessTokens — RFC 9449 §5.2 client-registration metadata. True →
	// tokens minted for this client are sender-constrained: they carry a `cnf.jkt`
	// and are usable only by the holder of the matching key.
	//
	// Sender-constraining is per-client metadata, not a global switch (the global
	// `oauth2.dpop` config block does not exist in the pinned provider version),
	// so it MUST be requested here at registration. Omitted when false — an
	// existing client registration is untouched.
	DPoPBoundAccessTokens bool `json:"dpop_bound_access_tokens,omitempty"`
	// TLSClientCertificateBoundAccessTokens — RFC 8705 §3.4 counterpart: binds
	// minted tokens to the client's TLS certificate (`cnf.x5t#S256`). Reserved
	// for mTLS-authenticating clients; kaname SA keys use DPoP.
	TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"`
	// RedirectURIs — where the provider may deliver an authorization code.
	// Meaningful ONLY for the interactive-login client (IAM-INT-1): the
	// machine grants this file otherwise registers (client_credentials,
	// jwt-bearer) never redirect anywhere. Omitted when empty, so no existing
	// registration changes shape.
	RedirectURIs []string `json:"redirect_uris,omitempty"`
	// PostLogoutRedirectURIs — RP-initiated-logout counterpart of the above.
	PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"`
}

HydraOAuthClient — minimal Hydra Admin OAuth2-client representation.

type HydraTokenClient

type HydraTokenClient struct {
	// TokenURL — the FULL token endpoint URL the shim POSTs to (cluster-internal
	// in production, e.g. http://kacho-umbrella-hydra-public.<ns>.svc:4444/oauth2/token).
	TokenURL   string
	HTTPClient *http.Client
}

HydraTokenClient — HTTP client for the Hydra public `/oauth2/token` endpoint.

func NewHydraTokenClientWithCA

func NewHydraTokenClientWithCA(tokenURL, caFile string) (*HydraTokenClient, error)

NewHydraTokenClientWithCA builds the client and, when an anchor is configured, verifies the provider against THAT bundle and nothing else.

This hop carries a signed client assertion out and the minted bearer back in the response body, so an unverified peer on it is not a degraded hop — it is a credential handed to whoever answered. The anchor semantics (empty ⇒ default transport; set ⇒ the only pool; unusable ⇒ refuse) live in one place, ProviderHopHTTPClient, shared with every other hop to the provider.

func (*HydraTokenClient) ClientCredentials

func (c *HydraTokenClient) ClientCredentials(ctx context.Context, req ClientCredentialsRequest) (TokenResponse, error)

ClientCredentials brokers a `grant_type=client_credentials` exchange with a `private_key_jwt` client_assertion and returns Hydra's access_token.

type InteractiveClientProvider

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

InteractiveClientProvider adapts HydraAdminClient to the use-case port.

func NewInteractiveClientProvider

func NewInteractiveClientProvider(admin *HydraAdminClient) *InteractiveClientProvider

NewInteractiveClientProvider — constructor. A nil admin client is refused at call time rather than dereferenced: an unwired provider must fail closed, not panic the listener.

func (*InteractiveClientProvider) Deregister

func (p *InteractiveClientProvider) Deregister(ctx context.Context, providerClientID string) error

Deregister — removes the client at the provider. Used both on Delete and as the compensation when the row insert fails after a successful registration.

func (*InteractiveClientProvider) Register

Register — creates the authorization-code client at the provider.

`response_types` is set to `code` explicitly. Leaving the admin client's default (`token`) would register a client the provider refuses to run a code ceremony for — the registration would succeed and the ceremony would fail later, at the point furthest from the cause.

type InviteMailEvent

type InviteMailEvent struct {
	// To — адрес приглашённого. Единственная координата, без которой письмо
	// отправить некому.
	To string `json:"to"`
	// AccountID — аккаунт, в который приглашают. Атрибуция и тело письма.
	AccountID string `json:"account_id"`
	// UserID — строка приглашения. Атрибуция; отправка от неё не зависит.
	UserID string `json:"user_id"`
	// LoginURL — адрес страницы входа. Пусто → берётся из настройки установки.
	LoginURL string `json:"login_url,omitempty"`
}

InviteMailEvent — расшифрованная нагрузка одной строки очереди.

Предъявителя здесь НЕТ и быть не должно (Р24): письмо приглашения несёт призыв и адрес страницы входа, а доступ даёт владение почтовым ящиком, доказанное подтверждением адреса у поставщика.

func DecodeInviteMail

func DecodeInviteMail(payload []byte) (InviteMailEvent, error)

DecodeInviteMail — drainer.Decoder.

Строка, не назвавшая адресата, нерастолковываема и повтором таковой не станет — это ПОСТОЯННЫЙ отказ, а не вечный ретрай. Условие закрыто и ограничением миграции, поэтому записать такую строку НЕЛЬЗЯ; проверка остаётся вторым рубежом, а не единственным.

type InviteMailObserver

type InviteMailObserver interface {
	// IncInviteMailOutcome — исход одной попытки отправки; outcome — клетка из
	// InviteMailOutcomes.
	IncInviteMailOutcome(outcome string)
}

InviteMailObserver — писатель счётчика исходов отправки.

Нужен, чтобы «ноль писем за всю жизнь очереди» было ЗАМЕТНО: без него мёртвый отправитель и здоровое облако, куда никто не приходил, выглядят одинаково — тихо (`data-integrity.md`, класс мёртвой очереди регистраций).

type InviteMailSender

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

InviteMailSender — транспорт поверх SMTP.

func NewInviteMailSender

func NewInviteMailSender(relay MailRelay) *InviteMailSender

NewInviteMailSender конструирует транспорт. Величины НЕ проверяются здесь: вырожденная настройка обязана дать наблюдаемый исход `misconfigured` на попытке, а не тихий отказ конструирования, который никто не считает.

func (*InviteMailSender) Send

Send — один разговор с почтовым узлом, ограниченный СВОИМ пределом времени.

Предел ставится дважды и намеренно: контекстом (он обрывает установление соединения) и абсолютным сроком на самом соединении (он обрывает узел, который СОЕДИНЕНИЕ ПРИНЯЛ и молчит). Одного контекста мало: после того как соединение установлено, чтения и записи по нему контекст уже не сторожит.

type InviteMailTransport

type InviteMailTransport interface {
	Send(ctx context.Context, ev InviteMailEvent) error
}

InviteMailTransport — порт: то, что умеет сдать письмо почтовому узлу. Реализуется InviteMailSender; в пробах — подставным транспортом, который снисходительнее настоящего быть не вправе.

type JWK

type JWK struct {
	Kty string `json:"kty"`           // "EC"
	Crv string `json:"crv,omitempty"` // "P-256"
	X   string `json:"x,omitempty"`   // base64url ECDSA X
	Y   string `json:"y,omitempty"`   // base64url ECDSA Y
	Kid string `json:"kid,omitempty"`
	Alg string `json:"alg,omitempty"` // "ES256"
	Use string `json:"use,omitempty"` // "sig"
}

JWK — JSON Web Key (RFC 7517), the subset relevant to Hydra client registration. Only EC keys are required (ES256); RS256 / OKP fields are reserved but not populated by kaname.

type JWKS

type JWKS struct {
	Keys []JWK `json:"keys"`
}

JWKS — JWK Set wrapper.

type MailRelay

type MailRelay struct {
	// Addr — `host:port` почтового узла.
	Addr string
	// From — адрес отправителя, ОДИН на установку (Р16).
	From string
	// FromName — отображаемое имя отправителя; необязательно.
	FromName string
	// Username/Password — удостоверение. Приезжает из СЕКРЕТА, не из карты
	// настроек (Р6). ПАРА: половина настройки хуже отсутствия обеих, потому что
	// выглядит настроенной (Р4).
	Username string
	Password string
	// AttemptTimeout — СОБСТВЕННЫЙ предел времени на ОДНУ попытку: весь разговор
	// с узлом, от соединения до принятого письма. Отдельная величина от числа
	// повторов, и наблюдаема она на узле, который принимает соединение и молчит.
	AttemptTimeout time.Duration
	// TLSMode — посадка полосы. Р5: шифрование обязательно и на стенде тоже.
	TLSMode MailTLSMode
	// RootCAs — якорь доверия для проверки сертификата узла. nil означает
	// системный набор, а НЕ отключённую проверку: проверка не отключается ничем.
	RootCAs *x509.CertPool
	// ServerName — имя, по которому сверяется сертификат узла. Пусто → берётся
	// из Addr.
	ServerName string
	// LoginURL — адрес страницы входа, который несёт письмо. Ссылки-предъявителя
	// приглашение НЕ несёт (Р24): обладание письмом доступа не даёт.
	LoginURL string
}

MailRelay — величины НАШЕГО исходящего соединения к почтовому узлу.

Встроенных умолчаний у узла, отправителя и удостоверения НЕТ (Р3): пустое значение означает «не задано» и даёт отказ по настройке, а не «разумное значение». Величина, которую построение подставляет молча, предметом стража быть не может — он зелен при любом входе.

type MailTLSMode

type MailTLSMode int

MailTLSMode — посадка полосы до почтового узла.

const (
	// MailTLSStartTLS — открытая полоса, поднимаемая до шифрованной командой
	// STARTTLS, с проверкой сертификата по объявленному якорю. Посадка по
	// умолчанию: Р5 требует шифрования И на стенде тоже.
	MailTLSStartTLS MailTLSMode = iota
	// MailTLSImplicit — шифрование с первого байта (submissions, 465).
	MailTLSImplicit
	// MailTLSDisabledForTest — НЕЗАЩИЩЁННАЯ полоса, допустимая ТОЛЬКО в
	// in-process фикстурах (ban #16: dev-insecure posture запрещена на любом
	// поднятом стенде).
	//
	// РАЗБОР КОНФИГУРАЦИИ ЭТО ЗНАЧЕНИЕ НЕ ПРОИЗВОДИТ НИ ПРИ КАКОМ ВХОДЕ, и это
	// не соглашение, а построение: `ParseMailTLSMode` принимает два имени и
	// отвергает всё прочее, поэтому оператор не может выбрать незащищённую
	// полосу, как бы он ни написал значение. Свойство закреплено пробой
	// `Test_ParseMailTLSMode_NeverYieldsThePlaintextMode`.
	MailTLSDisabledForTest
)

func ParseMailTLSMode

func ParseMailTLSMode(s string) (MailTLSMode, error)

ParseMailTLSMode переводит объявление профиля в посадку полосы.

Принимаются РОВНО два имени. Незащищённой полосы среди них нет: значение, которого разбор не производит, оператор выбрать не может (ban #16).

type ProviderClientReleaser

type ProviderClientReleaser interface {
	DeleteOAuthClient(ctx context.Context, clientID string) error
}

ProviderClientReleaser — то, что умеет снять у провайдера занятое сагой. Реализуется clients.HydraAdminClient; обе операции идемпотентны (404 → nil).

type ProviderCompensationEvent

type ProviderCompensationEvent struct {
	// ClientID — идентификатор клиента У ПРОВАЙДЕРА (не наш id). Именно он и
	// есть единственная координата, по которой осиротевший объект можно снять.
	ClientID string `json:"client_id"`
	// Origin — какая сага его заняла (sa_key / user_token / interactive_client).
	// Только атрибуция: применение от него не зависит.
	Origin string `json:"origin"`
	// Reason — почему компенсируем. Короткая своя строка, НЕ текст ошибки
	// провайдера (тот мог бы утащить в очередь его внутренние подробности).
	Reason string `json:"reason"`
}

ProviderCompensationEvent — расшифрованный payload одной строки очереди.

func DecodeProviderCompensation

func DecodeProviderCompensation(payload []byte) (ProviderCompensationEvent, error)

DecodeProviderCompensation — drainer.Decoder. Строка, у которой предмет не назван РОВНО ОДИН раз, нерастолковываема и не станет таковой при повторе → это ПОСТОЯННАЯ ошибка (дренаж отравляет строку и показывает её оператору), а не вечный ретрай.

Предмет у намерения теперь ровно один — клиент у провайдера: вид «снять доверительный грант» снят вместе со своим предметом (#1124). Строка, не назвавшая клиента, нерастолковываема; строка ПРЕЖНЕГО вида её тоже не называет и получает тот же постоянный отказ — она не применится и партию не заклинит.

type ProviderCompensationOutbox

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

ProviderCompensationOutbox — durable writer компенсирующих намерений поверх пула iam.

ПОЧЕМУ НАМЕРЕНИЕ, А НЕ ПРЯМОЙ ВЫЗОВ. Клиента у провайдера приходится регистрировать ДО коммита своей строки: строка обязана нести client_id, который назначает провайдер. Значит между «создано у провайдера» и «закоммичено у нас» есть окно, и если коммит не прошёл, снять созданное обязаны мы. Прямой вызов снятия гарантией не является: он сам может отказать (тот же провайдер и лежит), а процесс может умереть между провалом коммита и уборкой. В обоих случаях у провайдера остаётся объект, о котором в нашей БД нет ни одной записи, — назвать его нечем и убрать некому.

Транзакция, чей провал компенсируется, ОТКАЧЕНА, поэтому намерение не может ехать в ней: оно коммитится собственной транзакцией до того, как Operation помечается ошибкой. Это осознанно слабее co-commit'а внутри одной БД и сильнее вызова «в надежде».

func NewProviderCompensationOutbox

func NewProviderCompensationOutbox(pool *pgxpool.Pool) *ProviderCompensationOutbox

NewProviderCompensationOutbox конструирует writer.

func (*ProviderCompensationOutbox) EmitHydraClientDelete

func (o *ProviderCompensationOutbox) EmitHydraClientDelete(ctx context.Context, clientID, origin, reason string) error

EmitHydraClientDelete записывает намерение снять OAuth-клиента.

Собственная транзакция и собственный commit: вызывающий уже откатил свою. Ошибка ВОЗВРАЩАЕТСЯ, а не глотается — вызывающий обязан по ней перейти на запасной прямой вызов, иначе «намерение записано» стало бы утверждением, которое никто не проверял.

func (*ProviderCompensationOutbox) WithEmitObserver

WithEmitObserver подключает счётчик записанных намерений. Composition-root only.

type RelationQueries

type RelationQueries interface {
	// CheckWithContext — вердикт с условным контекстом запроса.
	CheckWithContext(ctx context.Context, subject, relation, object string, condCtx map[string]any) (allowed bool, err error)

	// BatchCheckWithContext — ОДИН вопрос о МНОГИХ объектах одного типа; по
	// вердикту на объект, позиционно.
	//
	// Объявлен здесь, а не отдельной способностью, НАМЕРЕННО: этот порт — тип
	// поля в каждом читающем use-case, а фильтр страницы (`internal/authzfilter`)
	// выбирает батчевый путь. Провязка, потерявшая способность, продолжала бы
	// возвращать верные строки, платя по обращению за строку, и заметить это было
	// бы нечем. Здесь потеря — ошибка компиляции.
	//
	// Стоимость страницы принадлежит ЗАПРОСУ: ответ собирается одной читающей
	// транзакцией, поэтому все объекты страницы видят один снимок базы.
	BatchCheckWithContext(ctx context.Context, subject, relation string, objects []string,
		condCtx map[string]any) (allowed []bool, err error)

	// ListSubjects — кто держит это отношение на объекте, СТРАНИЦЕЙ С КУРСОРОМ.
	//
	// Курсор обязателен и это не оформление: перечисление без продолжения
	// оставляет остаток недостижимым при живых правах — ровно то, ради чего снято
	// перечисление объектов (решение Р1 приёмки R7-3).
	ListSubjects(ctx context.Context, objectType, objectID, relation string,
		pageSize int, pageToken string) (subjects []string, nextPageToken string, err error)
}

RelationQueries — вопрос С условным контекстом и страничные формы.

type RelationStore

type RelationStore interface {
	// Check — держит ли субъект это отношение на этом объекте.
	//
	// Ошибка означает «ответа нет», а не «доступа нет»: вызывающий обязан
	// различать их, иначе недоступность базы читалась бы как законный отказ.
	Check(ctx context.Context, subject, relation, object string) (allowed bool, err error)
}

RelationStore — вопрос о доступе БЕЗ условного контекста.

type RelationTuple

type RelationTuple struct {
	User     string
	Relation string
	Object   string
}

RelationTuple — тройка «субъект, отношение, объект».

ОСТАЁТСЯ ПОСЛЕ СНЯТИЯ ДВИЖКА: это форма полезной нагрузки СТРОКИ ЖУРНАЛА намерений (`kaname.fga_outbox`), из которой триггер складывает прямой факт. Журнал и триггер — решение Р7 приёмки R7-3: снимается потребитель журнала, а не журнал.

type TokenResponse

type TokenResponse struct {
	AccessToken string
	ExpiresIn   int
}

TokenResponse — the subset of the Hydra token response the shim relays.

type TupleConditionRef

type TupleConditionRef = authztypes.TupleConditionRef

TupleConditionRef — псевдоним authztypes.TupleConditionRef.

Jump to

Keyboard shortcuts

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