zitadel/internal/command/user_v2_invite.go
Livio Spring 833f6279e1
fix: allow invite codes for users with verified mails (#9962)
# Which Problems Are Solved

Users who started the invitation code verification, but haven't set up
any authentication method, need to be able to do so. This might require
a new invitation code, which was currently not possible since creation
was prevented for users with verified emails.

# How the Problems Are Solved

- Allow creation of invitation emails for users with verified emails.
- Merged the creation and resend into a single method, defaulting the
urlTemplate, applicatioName and authRequestID from the previous code (if
one exists). On the user service API, the `ResendInviteCode` endpoint
has been deprecated in favor of the `CreateInviteCode`

# Additional Changes

None

# Additional Context

- Noticed while investigating something internally.
- requires backport to 2.x and 3.x
2025-05-26 13:59:20 +02:00

176 lines
5.8 KiB
Go

package command
import (
"context"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateUserInvite struct {
UserID string
URLTemplate string
ReturnCode bool
ApplicationName string
AuthRequestID string
}
func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) {
return c.sendInviteCode(ctx, invite, "", false)
}
// ResendInviteCode resends the invite mail with a new code and an optional authRequestID.
// It will reuse the applicationName from the previous code.
func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
details, _, err := c.sendInviteCode(
ctx,
&CreateUserInvite{
UserID: userID,
AuthRequestID: authRequestID,
},
resourceOwner,
true,
)
return details, err
}
func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, resourceOwner string, requireExisting bool) (details *domain.ObjectDetails, returnCode *string, err error) {
invite.UserID = strings.TrimSpace(invite.UserID)
if invite.UserID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing")
}
wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, resourceOwner)
if err != nil {
return nil, nil, err
}
if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, nil, err
}
if !wm.UserState.Exists() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound")
}
if !wm.CreationAllowed() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised")
}
if requireExisting && wm.InviteCode == nil || wm.CodeReturned {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
}
code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint
if err != nil {
return nil, nil, err
}
if invite.URLTemplate == "" {
invite.URLTemplate = wm.URLTemplate
}
if invite.ApplicationName == "" {
invite.ApplicationName = wm.ApplicationName
}
if invite.AuthRequestID == "" {
invite.AuthRequestID = wm.AuthRequestID
}
err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent(
ctx,
UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel),
code.Crypted,
code.Expiry,
invite.URLTemplate,
invite.ReturnCode,
invite.ApplicationName,
invite.AuthRequestID,
))
if err != nil {
return nil, nil, err
}
if invite.ReturnCode {
returnCode = &code.Plain
}
return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil
}
func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) {
if userID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing")
}
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if !existingCode.UserState.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-HN34a", "Errors.User.NotFound")
}
if existingCode.InviteCode == nil || existingCode.CodeReturned {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel)
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCodeSentEvent(ctx, userAgg))
return err
}
func (c *Commands) VerifyInviteCode(ctx context.Context, userID, code string) (details *domain.ObjectDetails, err error) {
return c.VerifyInviteCodeSetPassword(ctx, userID, code, "", "")
}
func (c *Commands) VerifyInviteCodeSetPassword(ctx context.Context, userID, code, password, userAgentID string) (details *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gk3f2", "Errors.User.UserIDMissing")
}
wm, err := c.userInviteCodeWriteModel(ctx, userID, "")
if err != nil {
return nil, err
}
if !wm.UserState.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-F5g2h", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel)
err = crypto.VerifyCode(wm.InviteCodeCreationDate, wm.InviteCodeExpiry, wm.InviteCode, code, c.userEncryption)
if err != nil {
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCheckFailedEvent(ctx, userAgg))
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInviteCheckFailedEvent push failed")
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Wgn4q", "Errors.User.Code.Invalid")
}
commands := []eventstore.Command{
user.NewHumanInviteCheckSucceededEvent(ctx, userAgg),
user.NewHumanEmailVerifiedEvent(ctx, userAgg),
}
if password != "" {
passwordCommand, err := c.setPasswordCommand(
ctx,
userAgg,
wm.UserState,
password,
"",
userAgentID,
false,
nil,
)
if err != nil {
return nil, err
}
commands = append(commands, passwordCommand)
}
err = c.pushAppendAndReduce(ctx, wm, commands...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) userInviteCodeWriteModel(ctx context.Context, userID, orgID string) (writeModel *UserV2InviteWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = newUserV2InviteWriteModel(userID, orgID)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}