Documentation
¶
Overview ¶
Package user — UserService (public CRUD + Invite) + InternalUserService (UpsertFromIdentity / Get).
Index ¶
- type ActivationObserver
- type AuthzChecker
- type BlockUserUseCase
- type DeleteUserUseCase
- type GetUserUseCase
- type Handler
- func (h *Handler) Block(ctx context.Context, req *iamv1.BlockUserRequest) (*operationpb.Operation, error)
- func (h *Handler) Delete(ctx context.Context, req *iamv1.DeleteUserRequest) (*operationpb.Operation, error)
- func (h *Handler) Get(ctx context.Context, req *iamv1.GetUserRequest) (*iamv1.User, error)
- func (h *Handler) Invite(ctx context.Context, req *iamv1.InviteUserRequest) (*operationpb.Operation, error)
- func (h *Handler) List(ctx context.Context, req *iamv1.ListUsersRequest) (*iamv1.ListUsersResponse, error)
- func (h *Handler) ListOperations(ctx context.Context, req *iamv1.ListUserOperationsRequest) (*iamv1.ListUserOperationsResponse, error)
- func (h *Handler) RemoveFromAccount(ctx context.Context, req *iamv1.RemoveUserFromAccountRequest) (*operationpb.Operation, error)
- func (h *Handler) Unblock(ctx context.Context, req *iamv1.UnblockUserRequest) (*operationpb.Operation, error)
- func (h *Handler) Update(ctx context.Context, req *iamv1.UpdateUserRequest) (*operationpb.Operation, error)
- func (h *Handler) WithListOperations(uc *shared.ListOperationsUseCase) *Handler
- type InternalHandler
- func (h *InternalHandler) Get(ctx context.Context, req *iamv1.GetUserRequest) (*iamv1.User, error)
- func (h *InternalHandler) OnRecoveryCompleted(ctx context.Context, req *iamv1.OnRecoveryCompletedRequest) (*operationpb.Operation, error)
- func (h *InternalHandler) UpsertFromIdentity(ctx context.Context, req *iamv1.UpsertFromIdentityRequest) (*operationpb.Operation, error)
- type InviteUserInput
- type InviteUserUseCase
- func (uc *InviteUserUseCase) Execute(ctx context.Context, in InviteUserInput) (*operations.Operation, error)
- func (uc *InviteUserUseCase) WithObjectReconciler(r ObjectReconciler) *InviteUserUseCase
- func (uc *InviteUserUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *InviteUserUseCase
- type ListUsersUseCase
- func (uc *ListUsersUseCase) Execute(ctx context.Context, f user.ListFilter) ([]domain.User, string, error)
- func (uc *ListUsersUseCase) WithListScanRecorder(rec shared.ListScanRecorder) *ListUsersUseCase
- func (uc *ListUsersUseCase) WithRelationStore(relations clients.RelationQueries) *ListUsersUseCase
- type ObjectForwardReconciler
- type ObjectReconciler
- type OnRecoveryCompletedInput
- type OnRecoveryCompletedUseCase
- type OwnerBindingReconciler
- type Reader
- type RemoveFromAccountUseCase
- type Repo
- type UnblockUserUseCase
- type UpdateUserInput
- type UpdateUserUseCase
- type UpsertFromIdentityInput
- type UpsertFromIdentityUseCase
- func (uc *UpsertFromIdentityUseCase) Execute(ctx context.Context, in UpsertFromIdentityInput) (*operations.Operation, error)
- func (uc *UpsertFromIdentityUseCase) WithActivationObserver(obs ActivationObserver) *UpsertFromIdentityUseCase
- func (uc *UpsertFromIdentityUseCase) WithLogger(logger *slog.Logger) *UpsertFromIdentityUseCase
- func (uc *UpsertFromIdentityUseCase) WithReconciler(r OwnerBindingReconciler) *UpsertFromIdentityUseCase
- type Writer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ActivationObserver ¶
type ActivationObserver interface {
IncInviteActivation(outcome string)
}
ActivationObserver — наблюдатель исходов активации приглашения.
Порт объявлен здесь, а реализация живёт в слое наблюдаемости: use-case не знает про prometheus (иначе адаптер протёк бы в бизнес-логику).
type AuthzChecker ¶
type AuthzChecker interface {
Check(ctx context.Context, subject, relation, object string) (allowed bool, err error)
}
AuthzChecker — narrow port for cascade-traversal Check (same signature as clients.RelationStore.Check). InviteUserUseCase depends on this narrow iface, not the full RelationStore (Interface Segregation).
type BlockUserUseCase ¶
type BlockUserUseCase struct {
// contains filtered or unexported fields
}
BlockUserUseCase — the membership may no longer authenticate into its Account.
func NewBlockUserUseCase ¶
func NewBlockUserUseCase(r Repo, opsRepo operations.Repo) *BlockUserUseCase
NewBlockUserUseCase — the membership may no longer authenticate.
type DeleteUserUseCase ¶
type DeleteUserUseCase struct {
// contains filtered or unexported fields
}
func NewDeleteUserUseCase ¶
func NewDeleteUserUseCase(r Repo, opsRepo operations.Repo) *DeleteUserUseCase
func (*DeleteUserUseCase) Execute ¶
func (uc *DeleteUserUseCase) Execute(ctx context.Context, id domain.UserID) (*operations.Operation, error)
type GetUserUseCase ¶
type GetUserUseCase struct {
// contains filtered or unexported fields
}
func NewGetUserUseCase ¶
func NewGetUserUseCase(r Repo) *GetUserUseCase
func (*GetUserUseCase) Execute ¶
Execute — sync read.
Authz (Design B, D-6/D-9):
- anonymous → NotFound (hide existence).
- self (principal == target user) → ALLOW (a user may always read itself; additive fast-path independent of FGA materialization).
- otherwise: the caller must hold `v_get` on iam_user:<id> OR be a cluster-admin. Else → NotFound (hide existence; never PermissionDenied).
Replaces the legacy owner-only `IsSelf(account.OwnerUserID)` cross-user gate that denied a delegate explicitly granted `iam.user.get`.
func (*GetUserUseCase) WithRelationStore ¶
func (u *GetUserUseCase) WithRelationStore(relations clients.RelationStore) *GetUserUseCase
WithRelationStore wires the FGA client authorizing a cross-user read via the verb-bearing `v_get` relation on iam_user:<id> (+ cluster-admin short-circuit). Without it only self-read passes (fail-closed for everyone else).
type Handler ¶
type Handler struct {
iamv1.UnimplementedUserServiceServer
// contains filtered or unexported fields
}
Handler — публичный UserService (Get/List/Invite/Update/Delete).
func NewHandler ¶
func NewHandler(g *GetUserUseCase, l *ListUsersUseCase, u *UpdateUserUseCase, d *DeleteUserUseCase, i *InviteUserUseCase, block *BlockUserUseCase, unblock *UnblockUserUseCase, remove *RemoveFromAccountUseCase) *Handler
func (*Handler) Block ¶
func (h *Handler) Block(ctx context.Context, req *iamv1.BlockUserRequest) (*operationpb.Operation, error)
Block — участие в Account'е запрещается. Действие, а не поле маски: почему разница не косметическая — см. set_blocked.go.
func (*Handler) Delete ¶
func (h *Handler) Delete(ctx context.Context, req *iamv1.DeleteUserRequest) (*operationpb.Operation, error)
func (*Handler) Invite ¶
func (h *Handler) Invite(ctx context.Context, req *iamv1.InviteUserRequest) (*operationpb.Operation, error)
Invite — invite-or-bind use-case. Возвращает Operation (LRO).
func (*Handler) List ¶
func (h *Handler) List(ctx context.Context, req *iamv1.ListUsersRequest) (*iamv1.ListUsersResponse, error)
List — sync read with pagination.
Формат страницы судится по СЫРОМУ запросу первым стейтментом: сужение int64→int32 ниже насыщающее, и отрицательный page_size превратился бы в 0 («умолчание») до того, как его кто-либо увидит.
func (*Handler) ListOperations ¶
func (h *Handler) ListOperations(ctx context.Context, req *iamv1.ListUserOperationsRequest) (*iamv1.ListUserOperationsResponse, error)
ListOperations — sync read of the operations recorded for the user (resource_id=usr-…, e.g. Delete + Invite ops). Malformed id → InvalidArgument (first statement); well-formed-but-no-ops → empty list, not NotFound (parity with the existing five, D-6). Viewer-tier authz is enforced by the api-gateway permission-catalog (acceptance 1.2-05).
func (*Handler) RemoveFromAccount ¶
func (h *Handler) RemoveFromAccount(ctx context.Context, req *iamv1.RemoveUserFromAccountRequest) (*operationpb.Operation, error)
RemoveFromAccount — человек перестаёт состоять в названном аккаунте. Пара к Invite: тот вводит человека в аккаунт, этот выводит. Строку личности не трогает — её снятие спрашивает `identity_remover` (#1131), а это отношение аккаунта `member_remover` (#1127).
func (*Handler) Unblock ¶
func (h *Handler) Unblock(ctx context.Context, req *iamv1.UnblockUserRequest) (*operationpb.Operation, error)
Unblock — участие разрешается снова.
func (*Handler) Update ¶
func (h *Handler) Update(ctx context.Context, req *iamv1.UpdateUserRequest) (*operationpb.Operation, error)
Update — публичный UpdateUser RPC. Тонкий transport: parse → use-case → format. Request — flat-форма: единственное mutable-поле `labels` лежит на верхнем уровне (паритет с UpdateRole/ServiceAccount/AccessBinding). labels- валидация и update_mask discipline — в use-case. Async → Operation.
func (*Handler) WithListOperations ¶
func (h *Handler) WithListOperations(uc *shared.ListOperationsUseCase) *Handler
WithListOperations wires the per-resource operation-listing use-case. Mirrors Account/Project/Role/Group/ServiceAccount.
type InternalHandler ¶
type InternalHandler struct {
iamv1.UnimplementedInternalUserServiceServer
// contains filtered or unexported fields
}
InternalHandler — InternalUserService (UpsertFromIdentity / Get / OnRecoveryCompleted).
func NewInternalHandler ¶
func NewInternalHandler(u *UpsertFromIdentityUseCase, g *GetUserUseCase, r *OnRecoveryCompletedUseCase) *InternalHandler
func (*InternalHandler) Get ¶
func (h *InternalHandler) Get(ctx context.Context, req *iamv1.GetUserRequest) (*iamv1.User, error)
func (*InternalHandler) OnRecoveryCompleted ¶
func (h *InternalHandler) OnRecoveryCompleted(ctx context.Context, req *iamv1.OnRecoveryCompletedRequest) (*operationpb.Operation, error)
OnRecoveryCompleted — Kratos password-recovery webhook. Mutation → async Operation.
func (*InternalHandler) UpsertFromIdentity ¶
func (h *InternalHandler) UpsertFromIdentity(ctx context.Context, req *iamv1.UpsertFromIdentityRequest) (*operationpb.Operation, error)
type InviteUserInput ¶
type InviteUserInput struct {
AccountID domain.AccountID
Email domain.Email
DisplayName domain.DisplayName // optional; если "" — defaults к email
ProjectID domain.ProjectID // optional; если set → role_id обязателен
RoleID domain.RoleID // required IFF ProjectID set
}
InviteUserInput — параметры use-case'а (resolved из gRPC request).
type InviteUserUseCase ¶
type InviteUserUseCase struct {
// contains filtered or unexported fields
}
InviteUserUseCase — invite-or-bind use-case.
func NewInviteUserUseCase ¶
func NewInviteUserUseCase( r Repo, opsRepo operations.Repo, authz AuthzChecker, ) *InviteUserUseCase
func (*InviteUserUseCase) Execute ¶
func (uc *InviteUserUseCase) Execute(ctx context.Context, in InviteUserInput) (*operations.Operation, error)
Execute — основной entry-point.
**Sync validation** (все до Operation):
- AccountID required.
- Email format (RFC 5321 lite via domain.Email.Validate).
- ProjectID+RoleID consistency.
- Permission check (CanInviteUsers cascade). 401/PERMISSION_DENIED — НЕ создаем Operation.
**Async work** в LRO worker'е:
- GetByAccountEmail → idempotent path или INSERT PENDING.
- Optionally AB-Insert (idempotent через ON CONFLICT).
- Magic-link generation.
func (*InviteUserUseCase) WithObjectReconciler ¶
func (uc *InviteUserUseCase) WithObjectReconciler(r ObjectReconciler) *InviteUserUseCase
WithObjectReconciler wires the post-commit synchronous per-object materializer. nil-safe.
func (*InviteUserUseCase) WithRelationStore ¶
func (uc *InviteUserUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *InviteUserUseCase
WithRelationStore wires the invite-flow AccessBinding FGA tuple writer.
It also re-points the `CanInviteUsers` permission checker at the real FGA client: NewInviteUserUseCase is constructed with the no-op authzStub, so without this the invite permission gate always denies even for an account admin ("Permission denied to invite users" for the account owner). RelationStore satisfies the narrow AuthzChecker interface (both expose Check), so the same client backs the permission gate and the tuple writer.
type ListUsersUseCase ¶
type ListUsersUseCase struct {
// contains filtered or unexported fields
}
func NewListUsersUseCase ¶
func NewListUsersUseCase(r Repo) *ListUsersUseCase
func (*ListUsersUseCase) Execute ¶
func (uc *ListUsersUseCase) Execute(ctx context.Context, f user.ListFilter) ([]domain.User, string, error)
func (*ListUsersUseCase) WithListScanRecorder ¶
func (uc *ListUsersUseCase) WithListScanRecorder(rec shared.ListScanRecorder) *ListUsersUseCase
WithRelationStore wires the FGA ListObjects client (паритет с account/SA/role List). WithListScanRecorder провязывает съём стоимости страницы (#653).
func (*ListUsersUseCase) WithRelationStore ¶
func (uc *ListUsersUseCase) WithRelationStore(relations clients.RelationQueries) *ListUsersUseCase
type ObjectForwardReconciler ¶
type ObjectForwardReconciler interface {
// ReconcileObjectForward is the ADDITIVE forward fast-path for one object: it
// materializes ONLY that object's per-object tuples across the matching bindings
// while holding NO advisory lock at all (neither EXCLUSIVE nor SHARE, no O(scope)
// recompute). It transparently
// delegates to the FULL ReconcileObject when the object already has members
// (delete-stale guard) — which is the branch a REVOCATION takes.
ReconcileObjectForward(ctx context.Context, objectType, objectID string) error
// ReconcileObject is the FULL EXCLUSIVE object-fan-out (async at-least-once backstop
// — delete-stale / audit / sweep), driven by the reconcile worker off the
// co-committed reconcile-outbox event.
ReconcileObject(ctx context.Context, objectType, objectID string) error
}
ObjectForwardReconciler — narrow post-commit port: re-materialize the per-object access of ONE iam-native object across the bindings whose selectors match it. Deliberately narrower than the invite-flow ObjectReconciler (which also carries ReconcileBinding): this path never materializes a binding, only an object. Implemented by reconcile.Reconciler (the SAME single materialization path the reconcile worker and the cross-service RegisterResource drive). nil-safe: when unwired, the co-committed reconcile event + the periodic sweep remain the at-least-once backstop.
type ObjectReconciler ¶
type ObjectReconciler interface {
// ReconcileObjectForward is the ADDITIVE forward fast-path for the invite-flow's
// freshly-created iam-native objects (iam.user + the project-scoped iam.accessBinding):
// it materializes ONLY that new object's per-object owner/admin tuples across the
// matching bindings while holding NO advisory lock at all (neither EXCLUSIVE nor SHARE,
// no O(scope) recompute),
// the throughput fix for the owner-tuple materialization lag under a parallel
// invite burst. It transparently delegates to the FULL ReconcileObject if the object
// already has members (delete-stale guard).
ReconcileObjectForwardNoStale(ctx context.Context, objectType, objectID string) error
// ReconcileObjectForward — СТОРОЖЕВОЙ вход того же прохода: он сперва читает,
// есть ли у объекта члены, и при непустом наборе уходит на полный проход ради
// снятия устаревших. Пути СОЗДАНИЯ он не нужен (доказательство — выше), но
// остаётся в порту: его зовёт правка того же пакета, где прежние факты есть
// и снятие устаревших — как раз предмет.
ReconcileObjectForward(ctx context.Context, objectType, objectID string) error
// ReconcileObject is the FULL EXCLUSIVE object-fan-out (async at-least-once backstop —
// delete-stale / audit / sweep), driven by the reconcile worker off the co-committed
// reconcile-outbox event, not the invite hot-path.
ReconcileObject(ctx context.Context, objectType, objectID string) error
// ReconcileBinding materializes the invite-flow AccessBinding's OWN grant
// membership through the unified reconciler — the per-object verb-bearing v_*
// (+ back-compat tier) tuples derived from the granted role's verbs. Under
// Design-B (flat-authz verb-bearing) enforcement resolves get→v_get,
// update→v_update, … so the grant MUST carry v_* — a tier-only emit (the old
// writeInviteBindingTuples path) leaves the invitee with `editor` but no
// `v_get`/`v_update`, denied on GET/PATCH of the granted project. This is the
// SAME materialization path
// AccessBindingService.Create drives, so the invite-flow grant is identical to a
// direct binding.
ReconcileBinding(ctx context.Context, bindingID domain.AccessBindingID) error
}
ObjectReconciler — narrow port: SYNCHRONOUSLY materialize the per-object access of every binding whose selector matches the invite-flow's freshly-created iam-native objects (the project-scoped AccessBinding + a brand-new invitee user), right after the invite tx commits. Under the flat rights model the `from <scope>` ACCESS cascade on these leaf types is gone, so the owner/account-admin per-object tuple is materialized per-object; the sync call closes the GET-after-create race the async drain would otherwise lose. Implemented by reconcile.Reconciler. nil-safe (the co-committed reconcile event + periodic sweep are the at-least-once backstop).
type OnRecoveryCompletedInput ¶
type OnRecoveryCompletedInput struct {
ExternalID domain.ExternalSubject
RecoveryJTI string
Email domain.Email
}
OnRecoveryCompletedInput — transport-agnostic input.
type OnRecoveryCompletedUseCase ¶
type OnRecoveryCompletedUseCase struct {
// contains filtered or unexported fields
}
OnRecoveryCompletedUseCase — orchestrates the recovery webhook.
func NewOnRecoveryCompletedUseCase ¶
func NewOnRecoveryCompletedUseCase(r Repo, opsRepo operations.Repo) *OnRecoveryCompletedUseCase
NewOnRecoveryCompletedUseCase — constructor.
func (*OnRecoveryCompletedUseCase) Execute ¶
func (uc *OnRecoveryCompletedUseCase) Execute(ctx context.Context, in OnRecoveryCompletedInput) (*operations.Operation, error)
func (*OnRecoveryCompletedUseCase) WithLogger ¶
func (uc *OnRecoveryCompletedUseCase) WithLogger(logger *slog.Logger) *OnRecoveryCompletedUseCase
WithLogger wires a logger for non-fatal warnings (composition root).
type OwnerBindingReconciler ¶
type OwnerBindingReconciler interface {
ReconcileBinding(ctx context.Context, bindingID domain.AccessBindingID) error
}
OwnerBindingReconciler — narrow port (rbac-contract-a-flat-fallout): materialize the bootstrap user's owner-binding per-object membership (scope-self verb-bearing tuples on account:<A> + the owner `*.*` ARM_ANCHOR forward over the account's content — project, iam-native, cross-service) after the bootstrap tx commits. Implemented by reconcile.Reconciler — the SAME single materialization path as Account.Create's owner auto-binding (account/create.go OwnerBindingReconciler). Under the FLAT rights model the hierarchy parent-pointers grant no access, so without this the bootstrap user is 403 on the content of their own account. nil-safe: when unwired the periodic sweep materializes it, just not synchronously.
type Reader ¶
type Reader = kanamerepo.Reader
type RemoveFromAccountUseCase ¶
type RemoveFromAccountUseCase struct {
// contains filtered or unexported fields
}
RemoveFromAccountUseCase — исключение человека из аккаунта.
func NewRemoveFromAccountUseCase ¶
func NewRemoveFromAccountUseCase(r Repo, opsRepo operations.Repo) *RemoveFromAccountUseCase
NewRemoveFromAccountUseCase — конструктор.
type Repo ¶
type Repo = kanamerepo.Repository
type UnblockUserUseCase ¶
type UnblockUserUseCase struct {
// contains filtered or unexported fields
}
UnblockUserUseCase — the membership may authenticate again.
func NewUnblockUserUseCase ¶
func NewUnblockUserUseCase(r Repo, opsRepo operations.Repo) *UnblockUserUseCase
NewUnblockUserUseCase — the membership may authenticate again.
type UpdateUserInput ¶
UpdateUserInput — вход UpdateUser. `Labels` — единственное mutable-поле (flat-форма request'а несет только его). identity-поля User (external_id и пр.) в request не переносятся: их единственный путь — `update_mask`, где они reject'атся как hard-immutable.
type UpdateUserUseCase ¶
type UpdateUserUseCase struct {
// contains filtered or unexported fields
}
func NewUpdateUserUseCase ¶
func NewUpdateUserUseCase(r Repo, opsRepo operations.Repo) *UpdateUserUseCase
func (*UpdateUserUseCase) Execute ¶
func (u *UpdateUserUseCase) Execute(ctx context.Context, in UpdateUserInput) (*operations.Operation, error)
func (*UpdateUserUseCase) WithObjectReconciler ¶
func (u *UpdateUserUseCase) WithObjectReconciler(r ObjectForwardReconciler, logger *slog.Logger) *UpdateUserUseCase
WithObjectReconciler wires the post-commit per-object materializer used on a LABEL change (parity with the cross-service RegisterResource re-register path — see doUpdate). Optional; nil keeps the queue-only behaviour. The logger is used only to report a failed pass (the durable event + sweep still re-converge).
type UpsertFromIdentityInput ¶
type UpsertFromIdentityInput struct {
ExternalID domain.ExternalSubject
Email domain.Email
DisplayName domain.DisplayName
}
UpsertFromIdentityInput — параметры (ExternalID required для non-bootstrap path; Email обязателен для PENDING-matching).
type UpsertFromIdentityUseCase ¶
type UpsertFromIdentityUseCase struct {
// contains filtered or unexported fields
}
func NewUpsertFromIdentityUseCase ¶
func NewUpsertFromIdentityUseCase(r Repo, opsRepo operations.Repo) *UpsertFromIdentityUseCase
func (*UpsertFromIdentityUseCase) Execute ¶
func (uc *UpsertFromIdentityUseCase) Execute(ctx context.Context, in UpsertFromIdentityInput) (*operations.Operation, error)
func (*UpsertFromIdentityUseCase) WithActivationObserver ¶
func (uc *UpsertFromIdentityUseCase) WithActivationObserver(obs ActivationObserver) *UpsertFromIdentityUseCase
WithActivationObserver wires the invite-activation outcome counter. nil-safe.
func (*UpsertFromIdentityUseCase) WithLogger ¶
func (uc *UpsertFromIdentityUseCase) WithLogger(logger *slog.Logger) *UpsertFromIdentityUseCase
WithLogger провязывает диагностику необязательных пост-коммитных шагов.
Прежде метод назывался `WithRelationStore` и принимал дверь решения вторым параметром — но исхода она не меняла, а после снятия внешнего движка перестала читаться вовсе. Имя, обещающее провязку источника вердикта, на такой функции вводит в заблуждение сильнее, чем отсутствие функции.
func (*UpsertFromIdentityUseCase) WithReconciler ¶
func (uc *UpsertFromIdentityUseCase) WithReconciler(r OwnerBindingReconciler) *UpsertFromIdentityUseCase
WithReconciler wires the post-commit owner-binding materializer for the bootstrap path (rbac-contract-a-flat-fallout). Without it the bootstrap user's owner-binding is only materialized by the periodic sweep (not synchronously) — under the flat model the user is then 403 on their own account's content until the sweep runs. nil-safe.
type Writer ¶
type Writer = kanamerepo.Writer