zitadel/internal/command/user_v3_password.go
2024-09-27 17:52:18 +02:00

277 lines
9.4 KiB
Go

package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type SetSchemaUserPassword struct {
ResourceOwner string
UserID string
Verification *SchemaUserPasswordVerification
Password *SchemaUserPassword
}
type SchemaUserPasswordVerification struct {
CurrentPassword string
Code string
}
type SchemaUserPassword struct {
Password string
EncodedPasswordHash string
ChangeRequired bool
}
func (p *SchemaUserPassword) Validate(hasher *crypto.Hasher) (err error) {
if p.EncodedPasswordHash != "" {
if !hasher.EncodingSupported(p.EncodedPasswordHash) {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-oz74onzvqr", "Errors.User.Password.NotSupported")
}
}
if p.Password == "" && p.EncodedPasswordHash == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")
}
return nil
}
func (c *Commands) SetSchemaUserPassword(ctx context.Context, set *SetSchemaUserPassword) (*domain.ObjectDetails, error) {
if set.UserID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")
}
if err := set.Password.Validate(c.userPasswordHasher); err != nil {
return nil, err
}
schemauser, err := existingSchemaUser(ctx, c, set.ResourceOwner, set.UserID)
if err != nil {
return nil, err
}
set.ResourceOwner = schemauser.ResourceOwner
_, err = existingSchema(ctx, c, "", schemauser.SchemaID)
if err != nil {
return nil, err
}
// TODO check for possible authenticators
writeModel, events, err := c.setSchemaUserPassword(ctx, set.ResourceOwner, set.UserID, set.Verification, set.Password)
if err != nil {
return nil, err
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) setSchemaUserPassword(ctx context.Context, resourceOwner, userID string, verification *SchemaUserPasswordVerification, set *SchemaUserPassword) (*PasswordV3WriteModel, []eventstore.Command, error) {
if set == nil {
return nil, nil, nil
}
schemaUser := &schemaUserPassword{
Set: true,
ResourceOwner: resourceOwner,
UserID: userID,
Verification: verification,
NewPassword: set,
}
writeModel, err := c.getSchemaUserPasswordWithVerification(ctx, schemaUser)
if err != nil {
return nil, nil, err
}
// If password is provided, let's check if is compliant with the policy.
// If only a encodedPassword is passed, we can skip this.
if set.Password != "" {
if err = c.checkPasswordComplexity(ctx, set.Password, writeModel.ResourceOwner); err != nil {
return nil, nil, err
}
}
encodedPassword := schemaUser.NewPassword.EncodedPasswordHash
if encodedPassword == "" && set.Password != "" {
encodedPassword, err = c.userPasswordHasher.Hash(set.Password)
if err = convertPasswapErr(err); err != nil {
return nil, nil, err
}
}
events, err := writeModel.NewCreate(ctx,
encodedPassword,
set.ChangeRequired,
)
if err != nil {
return nil, nil, err
}
return writeModel, events, nil
}
type RequestSchemaUserPasswordReset struct {
ResourceOwner string
UserID string
URLTemplate string
NotificationType domain.NotificationType
PlainCode *string
ReturnCode bool
}
func (c *Commands) RequestSchemaUserPasswordReset(ctx context.Context, user *RequestSchemaUserPasswordReset) (_ *domain.ObjectDetails, err error) {
writeModel, err := existsSchemaUserPasswordWithPermission(ctx, c, user.ResourceOwner, user.UserID)
if err != nil {
return nil, err
}
events, plainCode, err := writeModel.NewAddCode(ctx,
user.NotificationType,
user.URLTemplate,
user.ReturnCode,
func(ctx context.Context) (*EncryptedCode, error) {
return c.newEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption) //nolint:staticcheck
},
)
if err != nil {
return nil, err
}
if plainCode != "" {
user.PlainCode = &plainCode
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
func (c *Commands) DeleteSchemaUserPassword(ctx context.Context, resourceOwner, id string) (_ *domain.ObjectDetails, err error) {
writeModel, err := existsSchemaUserPasswordWithPermission(ctx, c, resourceOwner, id)
if err != nil {
return nil, err
}
events, err := writeModel.NewDelete(ctx)
if err != nil {
return nil, err
}
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
}
type schemaUserPassword struct {
Set bool
ResourceOwner string
UserID string
Verification *SchemaUserPasswordVerification
NewPassword *SchemaUserPassword
}
func (c *Commands) getSchemaUserPasswordWM(ctx context.Context, resourceOwner, id string) (*PasswordV3WriteModel, error) {
writeModel := NewPasswordV3WriteModel(resourceOwner, id, c.checkPermission)
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
return nil, err
}
return writeModel, nil
}
func existsSchemaUserPasswordWithPermission(ctx context.Context, c *Commands, resourceOwner, id string) (*PasswordV3WriteModel, error) {
writeModel, err := c.getSchemaUserPasswordWithVerification(ctx, &schemaUserPassword{ResourceOwner: resourceOwner, UserID: id})
if err != nil {
return nil, err
}
return writeModel, writeModel.Exists()
}
func (c *Commands) getSchemaUserPasswordWithVerification(ctx context.Context, user *schemaUserPassword) (*PasswordV3WriteModel, error) {
if user.UserID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing")
}
writeModel, err := c.getSchemaUserPasswordWM(ctx, user.ResourceOwner, user.UserID)
if err != nil {
return nil, err
}
if err := writeModel.Exists(); !user.Set && err != nil {
return nil, err
}
// if no verification is set, the user must have the permission to change the password
verification := c.setSchemaUserPasswordWithPermission(writeModel.UserID, writeModel.ResourceOwner)
if user.Verification != nil {
// otherwise check the password code...
if user.Verification.Code != "" {
verification = c.setSchemaUserPasswordWithVerifyCode(writeModel.CodeCreationDate, writeModel.CodeExpiry, writeModel.Code, user.Verification.Code)
}
// ...or old password
if user.Verification.CurrentPassword != "" {
verification = c.checkSchemaUserCurrentPassword(user.NewPassword.Password, user.NewPassword.EncodedPasswordHash, user.Verification.CurrentPassword, writeModel.EncodedHash)
}
}
if verification != nil {
newEncodedPassword, err := verification(ctx)
if err != nil {
return nil, err
}
// use the new hash from the verification in case there is one (e.g. existing pw check)
if newEncodedPassword != "" {
user.NewPassword.EncodedPasswordHash = newEncodedPassword
}
}
return writeModel, nil
}
// setSchemaUserPasswordWithPermission returns a permission check as [setPasswordVerification] implementation
func (c *Commands) setSchemaUserPasswordWithPermission(orgID, userID string) setPasswordVerification {
return func(ctx context.Context) (_ string, err error) {
return "", c.checkPermissionUpdateUser(ctx, orgID, userID)
}
}
// setSchemaUserPasswordWithVerifyCode returns a password code check as [setPasswordVerification] implementation
func (c *Commands) setSchemaUserPasswordWithVerifyCode(
passwordCodeCreationDate time.Time,
passwordCodeExpiry time.Duration,
passwordCode *crypto.CryptoValue,
code string,
) setPasswordVerification {
return func(ctx context.Context) (_ string, err error) {
if passwordCode == nil {
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-TODO", "Errors.User.Code.NotFound")
}
_, spanCrypto := tracing.NewNamedSpan(ctx, "crypto.VerifyCode")
defer func() {
spanCrypto.EndWithError(err)
}()
return "", crypto.VerifyCode(passwordCodeCreationDate, passwordCodeExpiry, passwordCode, code, c.userEncryption)
}
}
// checkSchemaUserCurrentPassword returns a password check as [setPasswordVerification] implementation
func (c *Commands) checkSchemaUserCurrentPassword(
newPassword, newEncodedPassword, currentPassword, currentEncodePassword string,
) setPasswordVerification {
// in case the new password is already encoded, we only need to verify the current
if newEncodedPassword != "" {
return func(ctx context.Context) (string, error) {
_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
_, err := c.userPasswordHasher.Verify(currentEncodePassword, currentPassword)
spanPasswap.EndWithError(err)
return "", convertPasswapErr(err)
}
}
// otherwise let's directly verify and return the new generate hash, so we can reuse it in the event
return func(ctx context.Context) (string, error) {
return c.verifyAndUpdateSchemaUserPassword(ctx, currentEncodePassword, currentPassword, newPassword)
}
}
// verifyAndUpdateSchemaUserPassword verify if the old password is correct with the encoded hash and
// returns the hash of the new password if so
func (c *Commands) verifyAndUpdateSchemaUserPassword(ctx context.Context, encodedHash, oldPassword, newPassword string) (string, error) {
if encodedHash == "" {
return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-TODO", "Errors.User.Password.NotSet")
}
_, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.Verify")
updated, err := c.userPasswordHasher.VerifyAndUpdate(encodedHash, oldPassword, newPassword)
spanPasswap.EndWithError(err)
return updated, convertPasswapErr(err)
}