mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-10 18:54:36 +00:00
277 lines
9.4 KiB
Go
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)
|
|
}
|