mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-16 12:58:00 +00:00
a07b2f4677
# 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
250 lines
7.1 KiB
Go
250 lines
7.1 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/passwap"
|
|
"github.com/zitadel/passwap/bcrypt"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
|
)
|
|
|
|
func mockEncryptedCode(code string, exp time.Duration) encrypedCodeFunc {
|
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) {
|
|
return &EncryptedCode{
|
|
Crypted: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte(code),
|
|
},
|
|
Plain: code,
|
|
Expiry: exp,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func mockEncryptedCodeWithDefault(code string, exp time.Duration) encryptedCodeWithDefaultFunc {
|
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, _ *crypto.GeneratorConfig) (*EncryptedCode, error) {
|
|
return &EncryptedCode{
|
|
Crypted: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte(code),
|
|
},
|
|
Plain: code,
|
|
Expiry: exp,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func mockHashedSecret(secret string) hashedSecretFunc {
|
|
return func(_ context.Context, _ preparation.FilterToQueryReducer) (encodedHash string, plain string, err error) {
|
|
return secret, secret, nil
|
|
}
|
|
}
|
|
|
|
var (
|
|
testGeneratorConfig = crypto.GeneratorConfig{
|
|
Length: 12,
|
|
Expiry: 60000000000,
|
|
IncludeLowerLetters: true,
|
|
IncludeUpperLetters: true,
|
|
IncludeDigits: true,
|
|
IncludeSymbols: true,
|
|
}
|
|
)
|
|
|
|
func testSecretGeneratorAddedEvent(typ domain.SecretGeneratorType) *instance.SecretGeneratorAddedEvent {
|
|
return instance.NewSecretGeneratorAddedEvent(context.Background(),
|
|
&instance.NewAggregate("inst1").Aggregate, typ,
|
|
testGeneratorConfig.Length,
|
|
testGeneratorConfig.Expiry,
|
|
testGeneratorConfig.IncludeLowerLetters,
|
|
testGeneratorConfig.IncludeUpperLetters,
|
|
testGeneratorConfig.IncludeDigits,
|
|
testGeneratorConfig.IncludeSymbols,
|
|
)
|
|
}
|
|
|
|
func Test_newCryptoCode(t *testing.T) {
|
|
type args struct {
|
|
typ domain.SecretGeneratorType
|
|
alg crypto.EncryptionAlgorithm
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
eventstore *eventstore.Eventstore
|
|
args args
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "filter config error",
|
|
eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
|
args: args{
|
|
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
|
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
eventstore: eventstoreExpect(t, expectFilter(
|
|
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
|
)),
|
|
args: args{
|
|
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
|
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := newEncryptedCode(context.Background(), tt.eventstore.Filter, tt.args.typ, tt.args.alg) //nolint:staticcheck
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
if tt.wantErr == nil {
|
|
require.NotNil(t, got)
|
|
assert.NotNil(t, got.Crypted)
|
|
assert.NotEmpty(t, got)
|
|
assert.Equal(t, testGeneratorConfig.Expiry, got.Expiry)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_cryptoCodeGenerator(t *testing.T) {
|
|
type args struct {
|
|
typ domain.SecretGeneratorType
|
|
alg crypto.EncryptionAlgorithm
|
|
defaultConfig *crypto.GeneratorConfig
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
eventsore *eventstore.Eventstore
|
|
args args
|
|
want crypto.Generator
|
|
wantConf *crypto.GeneratorConfig
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "filter config error",
|
|
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
|
args: args{
|
|
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
|
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
defaultConfig: emptyConfig,
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "encryption generator",
|
|
eventsore: eventstoreExpect(t, expectFilter(
|
|
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
|
)),
|
|
args: args{
|
|
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
|
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
defaultConfig: emptyConfig,
|
|
},
|
|
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
|
|
wantConf: &testGeneratorConfig,
|
|
},
|
|
{
|
|
name: "encryption generator with default config",
|
|
eventsore: eventstoreExpect(t, expectFilter()),
|
|
args: args{
|
|
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
|
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
defaultConfig: &testGeneratorConfig,
|
|
},
|
|
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
|
|
wantConf: &testGeneratorConfig,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, gotConf, err := encryptedCodeGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, tt.args.defaultConfig) //nolint:staticcheck
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
assert.IsType(t, tt.want, got)
|
|
assert.Equal(t, tt.wantConf, gotConf)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_newHashedSecretWithDefault(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
wantLen int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "filter error",
|
|
eventstore: expectEventstore(
|
|
expectFilterError(io.ErrClosedPipe),
|
|
),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "default config",
|
|
eventstore: expectEventstore(
|
|
expectFilter(),
|
|
),
|
|
wantLen: 32,
|
|
},
|
|
{
|
|
name: "instance config",
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
instance.NewSecretGeneratorAddedEvent(context.Background(),
|
|
&instance.NewAggregate("INSTANCE").Aggregate,
|
|
domain.SecretGeneratorTypeAppSecret,
|
|
24,
|
|
time.Hour*1,
|
|
true,
|
|
true,
|
|
true,
|
|
true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
wantLen: 24,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
hasher := &crypto.Hasher{
|
|
Swapper: passwap.NewSwapper(bcrypt.New(bcrypt.MinCost)),
|
|
}
|
|
defaultConfig := &crypto.GeneratorConfig{
|
|
Length: 32,
|
|
Expiry: time.Minute,
|
|
IncludeLowerLetters: true,
|
|
}
|
|
generate := newHashedSecretWithDefault(hasher, defaultConfig)
|
|
encodedHash, plain, err := generate(context.Background(), tt.eventstore(t).Filter) //nolint:staticcheck
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Len(t, plain, tt.wantLen)
|
|
_, err = hasher.Verify(encodedHash, plain)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|