Files
zitadel/backend/v3/domain/user_repo.go
T
f5ce288ea5 feat(domain): define user repositories (#11059)
This pull request introduces a new set of domain models and repositories
for managing users within the new v3 structure. The goal is to establish
a clean, robust, and extensible foundation for user management.

### Summary

This PR accomplishes the following:

- **Polymorphic User Model**: Introduces a `User` model in user.go that
can represent either a `Human` or a `Machine` user. This provides a
unified way to handle different user types while allowing for specific
attributes and logic for each.
- **Repository Pattern**: Implements the repository pattern for
accessing and manipulating user data. This decouples the business logic
from the data storage implementation. The main repository interfaces
(`UserRepository`, `HumanUserRepository`, `MachineUserRepository`) are
now defined in `backend/v3/domain/user_repository.go`.
- **Comprehensive User Models**: The user.go file now also defines the
domain models for associated user data, including `UserMetadata`,
`IdentityProviderLink`, `Passkey`, `PersonalAccessToken`, and
`MachineKey`.
- **Generic Verification and Check System**: Introduces verification.go
and check.go to provide a standardized way of handling various
verification flows (e.g., email verification, OTP checks).
- **Generated Code**: Adds new generated files for enums
(`humangender_enumer.go`, `machinekeytype_enumer.go`,
`passkeytype_enumer.go`, `personalaccesstokentype_enumer.go`,
`userstate_enumer.go`, `usertype_enumer.go`) and mocks for the new
repositories.
- **Session Domain Refactoring**: Updates the session domain
(`backend/v3/domain/session*.go`) to align with the new user model
structure.

This foundational work paves the way for migrating user management logic
to the new `v3` backend, with a more maintainable and testable
architecture.

part of #10224

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
2025-11-25 11:54:32 +01:00

315 lines
14 KiB
Go

package domain
import (
"context"
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
//go:generate mockgen -typed -package domainmock -destination ./mock/user.mock.go . UserRepository
type UserRepository interface {
Repository
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*User, error)
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*User, error)
Create(ctx context.Context, client database.QueryExecutor, user *User) error
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
Human() HumanUserRepository
Machine() MachineUserRepository
LoadMetadata() UserRepository
LoadPasskeys() UserRepository
LoadIdentityProviderLinks() UserRepository
LoadVerifications() UserRepository
LoadPATs() UserRepository
LoadKeys() UserRepository
userConditions
userChanges
userColumns
}
type userConditions interface {
PrimaryKeyCondition(instanceID, userID string) database.Condition
InstanceIDCondition(instanceID string) database.Condition
OrgIDCondition(orgID string) database.Condition
IDCondition(userID string) database.Condition
UsernameCondition(op database.TextOperation, username string) database.Condition
StateCondition(state UserState) database.Condition
TypeCondition(userType UserType) database.Condition
LoginNameCondition(op database.TextOperation, loginName string) database.Condition
userMetadataConditions
}
type userMetadataConditions interface {
ExistsMetadata(condition database.Condition) database.Condition
MetadataKeyCondition(op database.TextOperation, key string) database.Condition
MetadataValueCondition(op database.BytesOperation, value []byte) database.Condition
}
type userChanges interface {
// SetUsername sets the username field
SetUsername(username string) database.Change
// SetState sets the state field
// [UserStateUnspecified] is not allowed and will result in no change
SetState(state UserState) database.Change
// SetUpdatedAt sets the updated at field
// This is used to replay events
SetUpdatedAt(updatedAt time.Time) database.Change
// AddMetadata adds metadata for the user
AddMetadata(metadata ...*Metadata) database.Change
// RemoveMetadata removes the metadata for the user
// Use one of the following conditions to specify which metadata to remove:
// - [userMetadataConditions.MetadataKeyCondition]
// - [userMetadataConditions.MetadataValueCondition]
// - nil to remove all metadata for the user
RemoveMetadata(condition database.Condition) database.Change
}
type userColumns interface {
PrimaryKeyColumns() []database.Column
UsernameColumn() database.Column
StateColumn() database.Column
TypeColumn() database.Column
CreatedAtColumn() database.Column
}
//go:generate mockgen -typed -package domainmock -destination ./mock/human_user.mock.go . HumanUserRepository
type HumanUserRepository interface {
// Update updates the human user
// It ensures that updates are only applied to human users
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
humanConditions
humanChanges
humanColumns
}
type humanConditions interface {
userConditions
FirstNameCondition(op database.TextOperation, firstName string) database.Condition
LastNameCondition(op database.TextOperation, lastName string) database.Condition
NicknameCondition(op database.TextOperation, nickname string) database.Condition
DisplayNameCondition(op database.TextOperation, displayName string) database.Condition
PreferredLanguageCondition(lang language.Tag) database.Condition
EmailCondition(op database.TextOperation, email string) database.Condition
PhoneCondition(op database.TextOperation, phone string) database.Condition
PasskeyIDCondition(passkeyID string) database.Condition
IdentityProviderIDCondition(idpID string) database.Condition
ProvidedUserIDCondition(providedUserID string) database.Condition
ProvidedUsernameCondition(username string) database.Condition
humanPasskeyConditions
}
type humanChanges interface {
userChanges
// SetFirstName sets the first name field
SetFirstName(firstName string) database.Change
// SetLastName sets the last name field
SetLastName(lastName string) database.Change
// SetNickname sets the nickname field
SetNickname(nickname string) database.Change
// SetDisplayName sets the display name field
SetDisplayName(displayName string) database.Change
// SetPreferredLanguage sets the preferred language field
// If preferredLanguage is [language.Und], it will be set to NULL in the database
SetPreferredLanguage(preferredLanguage language.Tag) database.Change
// SetGender sets the gender field
// If gender is [HumanGenderUnspecified], it will be set to NULL in the database
SetGender(gender HumanGender) database.Change
// SetAvatarKey sets the avatar key field
// If avatarKey is nil, it will be set to NULL in the database
SetAvatarKey(avatarKey *string) database.Change
// SetMultifactorInitializationSkippedAt sets the multifactor initialization skipped at field
SetMultifactorInitializationSkippedAt(skippedAt time.Time) database.Change
// SetPassword sets the password based on the verification
SetPassword(verification VerificationType) database.Change
// SetPasswordChangeRequired sets whether a password change is required
SetPasswordChangeRequired(required bool) database.Change
// CheckPassword sets the password check based on the check type
// * [CheckTypeFailed] increments failed attempts
// * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
CheckPassword(check PasswordCheckType) database.Change
// SetVerification sets the verification based on the verification type
SetVerification(id string, verification VerificationType) database.Change
humanEmailChanges
humanPhoneChanges
humanTOTPChanges
identityProviderLinkChanges
humanPasskeyChanges
}
type humanEmailChanges interface {
// SetEmail sets the email based on the verification
// * [VerificationTypeInit] to initialize email verification, previously verified email remains verified
// * [VerificationTypeVerified] to mark email as verified, a verification must exist
// * [VerificationTypeUpdate] to update email verification, a verification must exist (e.g. resend code)
// * [VerificationTypeSkipped] to skip email verification, existing verification is removed (e.g. admin set email)
SetEmail(verification VerificationType) database.Change
// CheckEmailOTP sets the email OTP based on the check
// * [CheckTypeInit] to initialize a new check, previous check is overwritten
// * [CheckTypeFailed] increments failed attempts
// * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
CheckEmailOTP(check CheckType) database.Change
// EnableEmailOTPAt enables the email OTP
// If enabledAt is zero, it will be set to NOW()
EnableEmailOTPAt(enabledAt time.Time) database.Change
// EnableEmailOTP sets the enabled at time to NOW()
EnableEmailOTP() database.Change
// DisableEmailOTP clears the enabled at time
DisableEmailOTP() database.Change
}
type humanPhoneChanges interface {
// SetPhone sets the phone based on the verification
// * [VerificationTypeInit] to initialize phone verification, previously verified phone remains verified
// * [VerificationTypeVerified] to mark phone as verified, a verification must exist
// * [VerificationTypeUpdate] to update phone verification, a verification must exist (e.g. resend code)
// * [VerificationTypeSkipped] to skip phone verification, existing verification is removed (e.g. admin set phone)
SetPhone(verification VerificationType) database.Change
// RemovePhone removes the phone number
RemovePhone() database.Change
// CheckSMSOTP sets the SMS OTP based on the check
// * [CheckTypeInit] to initialize a new check, previous check is overwritten
// * [CheckTypeFailed] increments failed attempts
// * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
CheckSMSOTP(check CheckType) database.Change
// EnableSMSOTPAt enables the SMS OTP
// If enabledAt is zero, it will be set to NOW()
EnableSMSOTPAt(enabledAt time.Time) database.Change
// EnableSMSOTP sets the enabled at time to NOW()
EnableSMSOTP() database.Change
// DisableSMSOTP clears the enabled at time
DisableSMSOTP() database.Change
}
type humanTOTPChanges interface {
// SetTOTP changes the TOTP secret based on the verification
// * [VerificationTypeInit] to initialize totp verification, previously verified totp remains verified
// * [VerificationTypeVerified] to mark totp as verified, a verification must exist
// * [VerificationTypeUpdate] to update totp verification, a verification must exist (e.g. resend code)
// * [VerificationTypeSkipped] to skip totp verification, existing verification is removed (e.g. admin set totp)
SetTOTP(verification VerificationType) database.Change
// SetTOTPCheck sets the TOTP check based on the check type
// * [CheckTypeFailed] increments failed attempts
// * [CheckTypeSucceeded] to mark the check as succeeded, removes the check and updates verified at time
// * [CheckTypeInit] is not allowed for TOTP
CheckTOTP(check CheckType) database.Change
// RemoveTOTP removes the TOTP
RemoveTOTP() database.Change
}
type identityProviderLinkChanges interface {
// AddIdentityProviderLink adds or updates an identity provider link
AddIdentityProviderLink(link *IdentityProviderLink) database.Change
UpdateIdentityProviderLink(changes ...database.Change) database.Change
// RemoveIdentityProviderLink removes an identity provider link based on the condition
RemoveIdentityProviderLink(providerID, providedUserID string) database.Change
// SetIdentityProviderLinkUsername sets the username for an identity provider link
SetIdentityProviderLinkUsername(providerID, providedUserID, username string) database.Change
// SetIdentityProviderLinkProvidedID sets the provided user ID for an identity provider link
SetIdentityProviderLinkProvidedID(providerID, currentProvidedUserID, newProvidedUserID string) database.Change
}
type humanColumns interface {
userColumns
FirstNameColumn() database.Column
LastNameColumn() database.Column
NicknameColumn() database.Column
DisplayNameColumn() database.Column
EmailColumn() database.Column
}
//go:generate mockgen -typed -package domainmock -destination ./mock/machine_user.mock.go . MachineUserRepository
type MachineUserRepository interface {
// Update updates the machine user
// It ensures that updates are only applied to machine users
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
machineConditions
machineChanges
machineColumns
}
type machineConditions interface {
userConditions
}
type machineChanges interface {
userChanges
// SetName sets the name field
SetName(name string) database.Change
// SetDescription sets the description field
SetDescription(description string) database.Change
// SetSecret sets the secret field
// nil will set the secret to NULL in the database
SetSecret(secret *string) database.Change
// SetAccessTokenType sets the personal access token type field
SetAccessTokenType(tokenType PersonalAccessTokenType) database.Change
// AddKey adds a key for the machine user
AddKey(key *MachineKey) database.Change
// RemoveKey removes a key for the machine user
RemoveKey(id string) database.Change
// AddPersonalAccessToken adds a personal access token for the machine user
AddPersonalAccessToken(pat *PersonalAccessToken) database.Change
// RemovePersonalAccessToken removes a personal access token for the machine user
RemovePersonalAccessToken(id string) database.Change
}
type machineColumns interface {
userColumns
}
type humanPasskeyConditions interface {
PasskeyIDCondition(passkeyID string) database.Condition
PasskeyKeyIDCondition(keyID string) database.Condition
PasskeyChallengeCondition(challenge string) database.Condition
}
type humanPasskeyChanges interface {
// AddPasskey adds a passkey for the user
AddPasskey(passkey *Passkey) database.Change
// UpdatePasskey updates a passkey for the user.
// The condition must specify which passkey to update, use [humanPasskeyConditions].
// Use [humanPasskeyFieldChanges] to specify the fields to update.
UpdatePasskey(condition database.Condition, changes ...database.Change) database.Change
// RemovePasskey removes a passkey for the user
// The condition must specify which passkey to update, use [humanPasskeyConditions].
RemovePasskey(condition database.Condition) database.Change
passkeyFieldChanges
}
type passkeyFieldChanges interface {
// SetPasskeyKeyID sets the key ID field
SetPasskeyKeyID(keyID []byte) database.Change
// SetPasskeyPublicKey sets the public key field
SetPasskeyPublicKey(publicKey []byte) database.Change
// SetPasskeyAttestationType sets the attestation type field
SetPasskeyAttestationType(attestationType string) database.Change
// SetPasskeyAuthenticatorAttestationGUID sets the AAGUID field
SetPasskeyAuthenticatorAttestationGUID(aaguid []byte) database.Change
// SetPasskeyName sets the name field
SetPasskeyName(name string) database.Change
// SetPasskeySignCount sets the sign count field
SetPasskeySignCount(signCount uint32) database.Change
// SetPasskeyUpdatedAt sets the updated at field
SetPasskeyUpdatedAt(updatedAt time.Time) database.Change
// SetPasskeyVerifiedAt sets the verified at field
// If verifiedAt is zero, it will be set to NOW()
SetPasskeyVerifiedAt(verifiedAt time.Time) database.Change
}