feat: invite user link (#8578)

# Which Problems Are Solved

As an administrator I want to be able to invite users to my application
with the API V2, some user data I will already prefil, the user should
add the authentication method themself (password, passkey, sso).

# How the Problems Are Solved

- A user can now be created with a email explicitly set to false.
- If a user has no verified email and no authentication method, an
`InviteCode` can be created through the User V2 API.
  - the code can be returned or sent through email
- additionally `URLTemplate` and an `ApplicatioName` can provided for
the email
- The code can be resent and verified through the User V2 API
- The V1 login allows users to verify and resend the code and set a
password (analog user initialization)
- The message text for the user invitation can be customized

# Additional Changes

- `verifyUserPasskeyCode` directly uses `crypto.VerifyCode` (instead of
`verifyEncryptedCode`)
- `verifyEncryptedCode` is removed (unnecessarily queried for the code
generator)

# Additional Context

- closes #8310
- TODO: login V2 will have to implement invite flow:
https://github.com/zitadel/typescript/issues/166
This commit is contained in:
Livio Spring
2024-09-11 12:53:55 +02:00
committed by GitHub
parent 02c78a19c6
commit a07b2f4677
114 changed files with 3898 additions and 293 deletions

View File

@@ -19,6 +19,7 @@ type Commands interface {
HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error
PasswordChangeSent(ctx context.Context, orgID, userID string) error
HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) error
InviteCodeSent(ctx context.Context, orgID, userID string) error
UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error
MilestonePushed(ctx context.Context, msType milestone.Type, endpoints []string, primaryDomain string) error
}

View File

@@ -18,30 +18,30 @@ import (
gomock "go.uber.org/mock/gomock"
)
// MockCommands is a mock of Commands interface.
// MockCommands is a mock of Commands interface
type MockCommands struct {
ctrl *gomock.Controller
recorder *MockCommandsMockRecorder
}
// MockCommandsMockRecorder is the mock recorder for MockCommands.
// MockCommandsMockRecorder is the mock recorder for MockCommands
type MockCommandsMockRecorder struct {
mock *MockCommands
}
// NewMockCommands creates a new mock instance.
// NewMockCommands creates a new mock instance
func NewMockCommands(ctrl *gomock.Controller) *MockCommands {
mock := &MockCommands{ctrl: ctrl}
mock.recorder = &MockCommandsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCommands) EXPECT() *MockCommandsMockRecorder {
return m.recorder
}
// HumanEmailVerificationCodeSent mocks base method.
// HumanEmailVerificationCodeSent mocks base method
func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2)
@@ -49,13 +49,13 @@ func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1
return ret0
}
// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent.
func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent
func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2)
}
// HumanInitCodeSent mocks base method.
// HumanInitCodeSent mocks base method
func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2)
@@ -63,13 +63,13 @@ func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string
return ret0
}
// HumanInitCodeSent indicates an expected call of HumanInitCodeSent.
func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanInitCodeSent indicates an expected call of HumanInitCodeSent
func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2)
}
// HumanOTPEmailCodeSent mocks base method.
// HumanOTPEmailCodeSent mocks base method
func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2)
@@ -77,13 +77,13 @@ func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 st
return ret0
}
// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent.
func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent
func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2)
}
// HumanOTPSMSCodeSent mocks base method.
// HumanOTPSMSCodeSent mocks base method
func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2)
@@ -91,13 +91,13 @@ func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 stri
return ret0
}
// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent.
func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent
func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2)
}
// HumanPasswordlessInitCodeSent mocks base method.
// HumanPasswordlessInitCodeSent mocks base method
func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3)
@@ -105,13 +105,13 @@ func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1,
return ret0
}
// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent.
func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call {
// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent
func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3)
}
// HumanPhoneVerificationCodeSent mocks base method.
// HumanPhoneVerificationCodeSent mocks base method
func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2)
@@ -119,13 +119,27 @@ func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1
return ret0
}
// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent.
func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent
func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2)
}
// MilestonePushed mocks base method.
// InviteCodeSent mocks base method
func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// InviteCodeSent indicates an expected call of InviteCodeSent
func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2)
}
// MilestonePushed mocks base method
func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type, arg2 []string, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3)
@@ -133,13 +147,13 @@ func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type
return ret0
}
// MilestonePushed indicates an expected call of MilestonePushed.
func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call {
// MilestonePushed indicates an expected call of MilestonePushed
func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3)
}
// OTPEmailSent mocks base method.
// OTPEmailSent mocks base method
func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2)
@@ -147,13 +161,13 @@ func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) err
return ret0
}
// OTPEmailSent indicates an expected call of OTPEmailSent.
func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call {
// OTPEmailSent indicates an expected call of OTPEmailSent
func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2)
}
// OTPSMSSent mocks base method.
// OTPSMSSent mocks base method
func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2)
@@ -161,13 +175,13 @@ func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error
return ret0
}
// OTPSMSSent indicates an expected call of OTPSMSSent.
func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 any) *gomock.Call {
// OTPSMSSent indicates an expected call of OTPSMSSent
func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2)
}
// PasswordChangeSent mocks base method.
// PasswordChangeSent mocks base method
func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2)
@@ -175,13 +189,13 @@ func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 strin
return ret0
}
// PasswordChangeSent indicates an expected call of PasswordChangeSent.
func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call {
// PasswordChangeSent indicates an expected call of PasswordChangeSent
func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2)
}
// PasswordCodeSent mocks base method.
// PasswordCodeSent mocks base method
func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2)
@@ -189,13 +203,13 @@ func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string)
return ret0
}
// PasswordCodeSent indicates an expected call of PasswordCodeSent.
func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// PasswordCodeSent indicates an expected call of PasswordCodeSent
func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2)
}
// UsageNotificationSent mocks base method.
// UsageNotificationSent mocks base method
func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1)
@@ -203,13 +217,13 @@ func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.N
return ret0
}
// UsageNotificationSent indicates an expected call of UsageNotificationSent.
func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call {
// UsageNotificationSent indicates an expected call of UsageNotificationSent
func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1)
}
// UserDomainClaimedSent mocks base method.
// UserDomainClaimedSent mocks base method
func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2)
@@ -217,8 +231,8 @@ func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 st
return ret0
}
// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent.
func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call {
// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent
func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2)
}

View File

@@ -106,6 +106,10 @@ func (u *userNotifier) Reducers() []handler.AggregateReducer {
Event: user.HumanOTPEmailCodeAddedType,
Reduce: u.reduceOTPEmailCodeAdded,
},
{
Event: user.HumanInviteCodeAddedType,
Reduce: u.reduceInviteCodeAdded,
},
},
},
{
@@ -718,6 +722,61 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
}), nil
}
func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanInviteCodeAddedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanInviteCodeAddedType)
}
if e.CodeReturned {
return handler.NewNoOpStatement(e), nil
}
return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error {
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.HumanInviteCodeAddedType, user.HumanInviteCodeSentType)
if err != nil {
return err
}
if alreadyHandled {
return nil
}
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
if err != nil {
return err
}
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return err
}
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return err
}
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
if err != nil {
return err
}
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InviteUserMessageType)
if err != nil {
return err
}
ctx, err = u.queries.Origin(ctx, e)
if err != nil {
return err
}
notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e)
err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID)
if err != nil {
return err
}
return u.commands.InviteCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
}), nil
}
func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
if event.CreatedAt().Add(expiry).Before(time.Now().UTC()) {
return true, nil