mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 08:47:32 +00:00
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
(cherry picked from commit 833f6279e1
)
This commit is contained in:
@@ -19,14 +19,34 @@ type CreateUserInvite struct {
|
||||
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, "")
|
||||
wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -39,10 +59,22 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit
|
||||
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),
|
||||
@@ -51,7 +83,7 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit
|
||||
invite.URLTemplate,
|
||||
invite.ReturnCode,
|
||||
invite.ApplicationName,
|
||||
"",
|
||||
invite.AuthRequestID,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -62,53 +94,6 @@ func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvit
|
||||
return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if userID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.checkPermissionUpdateUser(ctx, existingCode.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existingCode.UserState.Exists() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound")
|
||||
}
|
||||
if !existingCode.CreationAllowed() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised")
|
||||
}
|
||||
if existingCode.InviteCode == nil || existingCode.CodeReturned {
|
||||
return 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, err
|
||||
}
|
||||
if authRequestID == "" {
|
||||
authRequestID = existingCode.AuthRequestID
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, existingCode,
|
||||
user.NewHumanInviteCodeAddedEvent(
|
||||
ctx,
|
||||
UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel),
|
||||
code.Crypted,
|
||||
code.Expiry,
|
||||
existingCode.URLTemplate,
|
||||
false,
|
||||
existingCode.ApplicationName,
|
||||
authRequestID,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) {
|
||||
if userID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing")
|
||||
|
@@ -28,7 +28,7 @@ type UserV2InviteWriteModel struct {
|
||||
}
|
||||
|
||||
func (wm *UserV2InviteWriteModel) CreationAllowed() bool {
|
||||
return !wm.EmailVerified && !wm.AuthMethodSet
|
||||
return !wm.AuthMethodSet
|
||||
}
|
||||
|
||||
func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel {
|
||||
|
@@ -11,7 +11,6 @@ import (
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
@@ -316,7 +315,7 @@ func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
userID: "",
|
||||
},
|
||||
want{
|
||||
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing"),
|
||||
err: zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -348,7 +347,7 @@ func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
userID: "unknown",
|
||||
},
|
||||
want{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound"),
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -566,76 +565,6 @@ func TestCommands_ResendInviteCode(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"resend with own user ok",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName",
|
||||
"lastName",
|
||||
"nickName",
|
||||
"displayName",
|
||||
language.Afrikaans,
|
||||
domain.GenderUnspecified,
|
||||
"email",
|
||||
false,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"authRequestID",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInviteCodeAddedEvent(authz.NewMockContext("instanceID", "org1", "userID"),
|
||||
&user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
time.Hour,
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"authRequestID2",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(), // user does not have permission, is allowed in the own context
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: authz.NewMockContext("instanceID", "org1", "userID"),
|
||||
userID: "userID",
|
||||
authRequestID: "authRequestID2",
|
||||
},
|
||||
want{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
ID: "userID",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user