diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 6c66c36caa1..6baa1b88fb9 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -149,6 +149,8 @@ func projections( keys.OIDC, keys.SAML, keys.Target, + keys.SMS, + keys.SMTP, config.InternalAuthZ.RolePermissionMappings, sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { diff --git a/cmd/setup/62.go b/cmd/setup/62.go new file mode 100644 index 00000000000..9927a293531 --- /dev/null +++ b/cmd/setup/62.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 62.sql + addHTTPProviderSigningKey string +) + +type HTTPProviderAddSigningKey struct { + dbClient *database.DB +} + +func (mig *HTTPProviderAddSigningKey) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, addHTTPProviderSigningKey) + return err +} + +func (mig *HTTPProviderAddSigningKey) String() string { + return "62_http_provider_add_signing_key" +} diff --git a/cmd/setup/62.sql b/cmd/setup/62.sql new file mode 100644 index 00000000000..8f43a32c515 --- /dev/null +++ b/cmd/setup/62.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS projections.sms_configs3_http ADD COLUMN IF NOT EXISTS signing_key TEXT NULL; +ALTER TABLE IF EXISTS projections.smtp_configs5_http ADD COLUMN IF NOT EXISTS signing_key TEXT NULL; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 2a7f4cf41c1..cf6c43efc24 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -158,6 +158,7 @@ type Steps struct { s59SetupWebkeys *SetupWebkeys s60GenerateSystemID *GenerateSystemID s61IDPTemplate6SAMLSignatureAlgorithm *IDPTemplate6SAMLSignatureAlgorithm + s62HTTPProviderAddSigningKey *HTTPProviderAddSigningKey } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index fd4cbda285e..7cc80d3724c 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -219,6 +219,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient} steps.s60GenerateSystemID = &GenerateSystemID{eventstore: eventstoreClient} steps.s61IDPTemplate6SAMLSignatureAlgorithm = &IDPTemplate6SAMLSignatureAlgorithm{dbClient: dbClient} + steps.s62HTTPProviderAddSigningKey = &HTTPProviderAddSigningKey{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -268,6 +269,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s58ReplaceLoginNames3View, steps.s60GenerateSystemID, steps.s61IDPTemplate6SAMLSignatureAlgorithm, + steps.s62HTTPProviderAddSigningKey, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -468,6 +470,8 @@ func startCommandsQueries( keys.OIDC, keys.SAML, keys.Target, + keys.SMS, + keys.SMTP, config.InternalAuthZ.RolePermissionMappings, sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { diff --git a/cmd/start/start.go b/cmd/start/start.go index 01fe78b7ab6..2adaf3ebc1e 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -205,6 +205,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server keys.OIDC, keys.SAML, keys.Target, + keys.SMS, + keys.SMTP, config.InternalAuthZ.RolePermissionMappings, sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { diff --git a/internal/api/grpc/admin/email.go b/internal/api/grpc/admin/email.go index c207970000b..c46e235dedb 100644 --- a/internal/api/grpc/admin/email.go +++ b/internal/api/grpc/admin/email.go @@ -55,8 +55,9 @@ func (s *Server) AddEmailProviderHTTP(ctx context.Context, req *admin_pb.AddEmai return nil, err } return &admin_pb.AddEmailProviderHTTPResponse{ - Details: object.DomainToChangeDetailsPb(config.Details), - Id: config.ID, + Details: object.DomainToChangeDetailsPb(config.Details), + Id: config.ID, + SigningKey: config.SigningKey, }, nil } @@ -66,7 +67,8 @@ func (s *Server) UpdateEmailProviderHTTP(ctx context.Context, req *admin_pb.Upda return nil, err } return &admin_pb.UpdateEmailProviderHTTPResponse{ - Details: object.DomainToChangeDetailsPb(config.Details), + Details: object.DomainToChangeDetailsPb(config.Details), + SigningKey: config.SigningKey, }, nil } diff --git a/internal/api/grpc/admin/email_converters.go b/internal/api/grpc/admin/email_converters.go index d1f566ec700..e574ad1b64a 100644 --- a/internal/api/grpc/admin/email_converters.go +++ b/internal/api/grpc/admin/email_converters.go @@ -68,7 +68,8 @@ func emailProviderToPb(config *query.SMTPConfig) settings_pb.EmailConfig { func httpToPb(http *query.HTTP) *settings_pb.EmailProvider_Http { return &settings_pb.EmailProvider_Http{ Http: &settings_pb.EmailProviderHTTP{ - Endpoint: http.Endpoint, + Endpoint: http.Endpoint, + SigningKey: http.SigningKey, }, } } @@ -76,11 +77,12 @@ func httpToPb(http *query.HTTP) *settings_pb.EmailProvider_Http { func smtpToPb(config *query.SMTP) *settings_pb.EmailProvider_Smtp { return &settings_pb.EmailProvider_Smtp{ Smtp: &settings_pb.EmailProviderSMTP{ - Tls: config.TLS, - Host: config.Host, - User: config.User, - SenderAddress: config.SenderAddress, - SenderName: config.SenderName, + Tls: config.TLS, + Host: config.Host, + User: config.User, + SenderAddress: config.SenderAddress, + SenderName: config.SenderName, + ReplyToAddress: config.ReplyToAddress, }, } } @@ -123,11 +125,14 @@ func addEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.AddEmailPro } func updateEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.UpdateEmailProviderHTTPRequest) *command.ChangeSMTPConfigHTTP { + // TODO handle expiration, currently only immediate expiration is supported + expirationSigningKey := req.GetExpirationSigningKey() != nil return &command.ChangeSMTPConfigHTTP{ - ResourceOwner: authz.GetInstance(ctx).InstanceID(), - ID: req.Id, - Description: req.Description, - Endpoint: req.Endpoint, + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + ID: req.Id, + Description: req.Description, + Endpoint: req.Endpoint, + ExpirationSigningKey: expirationSigningKey, } } diff --git a/internal/api/grpc/admin/email_converters_test.go b/internal/api/grpc/admin/email_converters_test.go new file mode 100644 index 00000000000..2cf6422fed7 --- /dev/null +++ b/internal/api/grpc/admin/email_converters_test.go @@ -0,0 +1,554 @@ +package admin + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object" + settings_pb "github.com/zitadel/zitadel/pkg/grpc/settings" +) + +func Test_listEmailProvidersToModel(t *testing.T) { + type args struct { + req *admin_pb.ListEmailProvidersRequest + } + tests := []struct { + name string + args args + res *query.SMTPConfigsSearchQueries + }{ + { + name: "all fields filled", + args: args{ + req: &admin_pb.ListEmailProvidersRequest{ + Query: &object_pb.ListQuery{ + Offset: 100, + Limit: 100, + Asc: true, + }, + }, + }, + res: &query.SMTPConfigsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 100, + Limit: 100, + Asc: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := listEmailProvidersToModel(tt.args.req) + require.NoError(t, err) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_emailProvidersToPb(t *testing.T) { + type args struct { + req []*query.SMTPConfig + } + tests := []struct { + name string + args args + res []*settings_pb.EmailProvider + }{ + { + name: "all fields filled", + args: args{ + req: []*query.SMTPConfig{ + { + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + SMTPConfig: &query.SMTP{ + TLS: true, + SenderAddress: "sender", + SenderName: "sendername", + ReplyToAddress: "address", + Host: "host", + User: "user", + }, + HTTPConfig: nil, + State: 1, + }, + { + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + SMTPConfig: nil, + HTTPConfig: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + State: 1, + }, + }, + }, + res: []*settings_pb.EmailProvider{ + { + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + ReplyToAddress: "address", + }, + }, + }, + { + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.EmailProvider_Http{ + Http: &settings_pb.EmailProviderHTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := emailProvidersToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_emailProviderToProviderPb(t *testing.T) { + type args struct { + req *query.SMTPConfig + } + tests := []struct { + name string + args args + res *settings_pb.EmailProvider + }{ + { + name: "all fields filled, smtp", + args: args{ + req: &query.SMTPConfig{ + + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + SMTPConfig: &query.SMTP{ + TLS: true, + SenderAddress: "sender", + SenderName: "sendername", + ReplyToAddress: "address", + Host: "host", + User: "user", + }, + HTTPConfig: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + State: 1, + }, + }, + res: &settings_pb.EmailProvider{ + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + ReplyToAddress: "address", + }, + }, + }, + }, + { + name: "all fields filled, http", + args: args{ + req: &query.SMTPConfig{ + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + HTTPConfig: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + State: 1, + }, + }, + res: &settings_pb.EmailProvider{ + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.EmailProvider_Http{ + Http: &settings_pb.EmailProviderHTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := emailProviderToProviderPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_emailProviderStateToPb(t *testing.T) { + type args struct { + req domain.SMTPConfigState + } + tests := []struct { + name string + args args + res settings_pb.EmailProviderState + }{ + { + name: "unspecified", + args: args{ + req: domain.SMTPConfigStateUnspecified, + }, + res: settings_pb.EmailProviderState_EMAIL_PROVIDER_STATE_UNSPECIFIED, + }, + { + name: "removed", + args: args{ + req: domain.SMTPConfigStateRemoved, + }, + res: settings_pb.EmailProviderState_EMAIL_PROVIDER_STATE_UNSPECIFIED, + }, + { + name: "active", + args: args{ + req: domain.SMTPConfigStateActive, + }, + res: settings_pb.EmailProviderState_EMAIL_PROVIDER_ACTIVE, + }, + { + name: "inactive", + args: args{ + req: domain.SMTPConfigStateInactive, + }, + res: settings_pb.EmailProviderState_EMAIL_PROVIDER_INACTIVE, + }, + { + name: "default", + args: args{ + req: 100, + }, + res: settings_pb.EmailProviderState_EMAIL_PROVIDER_STATE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := emailProviderStateToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_httpToPb(t *testing.T) { + type args struct { + req *query.HTTP + } + tests := []struct { + name string + args args + res *settings_pb.EmailProvider_Http + }{ + { + name: "all fields filled", + args: args{ + req: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + res: &settings_pb.EmailProvider_Http{ + Http: &settings_pb.EmailProviderHTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := httpToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_smtpToPb(t *testing.T) { + type args struct { + req *query.SMTP + } + tests := []struct { + name string + args args + res *settings_pb.EmailProvider_Smtp + }{ + { + name: "all fields filled", + args: args{ + req: &query.SMTP{ + SenderAddress: "sender", + SenderName: "sendername", + TLS: true, + Host: "host", + User: "user", + ReplyToAddress: "address", + }, + }, + res: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + ReplyToAddress: "address", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := smtpToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_addEmailProviderSMTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.AddEmailProviderSMTPRequest + } + tests := []struct { + name string + args args + res *command.AddSMTPConfig + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddEmailProviderSMTPRequest{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + Password: "password", + ReplyToAddress: "address", + Description: "description", + }, + }, + res: &command.AddSMTPConfig{ + ResourceOwner: "instance", + Description: "description", + Host: "host", + User: "user", + Password: "password", + Tls: true, + From: "sender", + FromName: "sendername", + ReplyToAddress: "address", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addEmailProviderSMTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_updateEmailProviderSMTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.UpdateEmailProviderSMTPRequest + } + tests := []struct { + name string + args args + res *command.ChangeSMTPConfig + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateEmailProviderSMTPRequest{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + ReplyToAddress: "address", + Password: "password", + Description: "description", + Id: "id", + }, + }, + res: &command.ChangeSMTPConfig{ + ResourceOwner: "instance", + ID: "id", + Description: "description", + Host: "host", + User: "user", + Password: "password", + Tls: true, + From: "sender", + FromName: "sendername", + ReplyToAddress: "address", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateEmailProviderSMTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_addEmailProviderHTTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.AddEmailProviderHTTPRequest + } + tests := []struct { + name string + args args + res *command.AddSMTPConfigHTTP + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddEmailProviderHTTPRequest{ + Endpoint: "endpoint", + Description: "description", + }, + }, + res: &command.AddSMTPConfigHTTP{ + ResourceOwner: "instance", + ID: "", + Description: "description", + Endpoint: "endpoint", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addEmailProviderHTTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_updateEmailProviderHTTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.UpdateEmailProviderHTTPRequest + } + tests := []struct { + name string + args args + res *command.ChangeSMTPConfigHTTP + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateEmailProviderHTTPRequest{ + Id: "id", + Endpoint: "endpoint", + Description: "description", + ExpirationSigningKey: durationpb.New(time.Second), + }, + }, + res: &command.ChangeSMTPConfigHTTP{ + ResourceOwner: "instance", + ID: "id", + Description: "description", + Endpoint: "endpoint", + ExpirationSigningKey: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateEmailProviderHTTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} diff --git a/internal/api/grpc/admin/sms.go b/internal/api/grpc/admin/sms.go index 8517a8ff2d1..ac54ddf81f8 100644 --- a/internal/api/grpc/admin/sms.go +++ b/internal/api/grpc/admin/sms.go @@ -71,8 +71,9 @@ func (s *Server) AddSMSProviderHTTP(ctx context.Context, req *admin_pb.AddSMSPro return nil, err } return &admin_pb.AddSMSProviderHTTPResponse{ - Details: object.DomainToAddDetailsPb(smsConfig.Details), - Id: smsConfig.ID, + Details: object.DomainToAddDetailsPb(smsConfig.Details), + Id: smsConfig.ID, + SigningKey: smsConfig.SigningKey, }, nil } @@ -82,7 +83,8 @@ func (s *Server) UpdateSMSProviderHTTP(ctx context.Context, req *admin_pb.Update return nil, err } return &admin_pb.UpdateSMSProviderHTTPResponse{ - Details: object.DomainToChangeDetailsPb(smsConfig.Details), + Details: object.DomainToChangeDetailsPb(smsConfig.Details), + SigningKey: smsConfig.SigningKey, }, nil } diff --git a/internal/api/grpc/admin/sms_converter.go b/internal/api/grpc/admin/sms_converter.go index 72f33cb548a..c17176f3114 100644 --- a/internal/api/grpc/admin/sms_converter.go +++ b/internal/api/grpc/admin/sms_converter.go @@ -56,7 +56,8 @@ func SMSConfigToPb(config *query.SMSConfig) settings_pb.SMSConfig { func HTTPConfigToPb(http *query.HTTP) *settings_pb.SMSProvider_Http { return &settings_pb.SMSProvider_Http{ Http: &settings_pb.HTTPConfig{ - Endpoint: http.Endpoint, + Endpoint: http.Endpoint, + SigningKey: http.SigningKey, }, } } @@ -73,6 +74,8 @@ func TwilioConfigToPb(twilio *query.Twilio) *settings_pb.SMSProvider_Twilio { func smsStateToPb(state domain.SMSConfigState) settings_pb.SMSProviderConfigState { switch state { + case domain.SMSConfigStateUnspecified, domain.SMSConfigStateRemoved: + return settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE case domain.SMSConfigStateInactive: return settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE case domain.SMSConfigStateActive: @@ -113,10 +116,13 @@ func addSMSConfigHTTPToConfig(ctx context.Context, req *admin_pb.AddSMSProviderH } func updateSMSConfigHTTPToConfig(ctx context.Context, req *admin_pb.UpdateSMSProviderHTTPRequest) *command.ChangeSMSHTTP { + // TODO handle expiration, currently only immediate expiration is supported + expirationSigningKey := req.GetExpirationSigningKey() != nil return &command.ChangeSMSHTTP{ - ResourceOwner: authz.GetInstance(ctx).InstanceID(), - ID: req.Id, - Description: gu.Ptr(req.Description), - Endpoint: gu.Ptr(req.Endpoint), + ResourceOwner: authz.GetInstance(ctx).InstanceID(), + ID: req.Id, + Description: gu.Ptr(req.Description), + Endpoint: gu.Ptr(req.Endpoint), + ExpirationSigningKey: expirationSigningKey, } } diff --git a/internal/api/grpc/admin/sms_converters_test.go b/internal/api/grpc/admin/sms_converters_test.go new file mode 100644 index 00000000000..fcf343bdd39 --- /dev/null +++ b/internal/api/grpc/admin/sms_converters_test.go @@ -0,0 +1,520 @@ +package admin + +import ( + "context" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object" + settings_pb "github.com/zitadel/zitadel/pkg/grpc/settings" +) + +func Test_listSMSConfigsToModel(t *testing.T) { + type args struct { + req *admin_pb.ListSMSProvidersRequest + } + tests := []struct { + name string + args args + res *query.SMSConfigsSearchQueries + }{ + { + name: "all fields filled", + args: args{ + req: &admin_pb.ListSMSProvidersRequest{ + Query: &object_pb.ListQuery{ + Offset: 100, + Limit: 100, + Asc: true, + }, + }, + }, + res: &query.SMSConfigsSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 100, + Limit: 100, + Asc: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := listSMSConfigsToModel(tt.args.req) + require.NoError(t, err) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_SMSConfigsToPb(t *testing.T) { + type args struct { + req []*query.SMSConfig + } + tests := []struct { + name string + args args + res []*settings_pb.SMSProvider + }{ + { + name: "all fields filled", + args: args{ + req: []*query.SMSConfig{ + { + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + TwilioConfig: &query.Twilio{ + SID: "sid", + Token: nil, + SenderNumber: "sender", + VerifyServiceSID: "verify", + }, + State: 1, + }, + { + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + HTTPConfig: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + State: 1, + }, + }, + }, + res: []*settings_pb.SMSProvider{ + { + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.SMSProvider_Twilio{ + Twilio: &settings_pb.TwilioConfig{ + Sid: "sid", + SenderNumber: "sender", + VerifyServiceSid: "verify", + }, + }, + }, + { + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.SMSProvider_Http{ + Http: &settings_pb.HTTPConfig{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SMSConfigsToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_SMSConfigToProviderPb(t *testing.T) { + type args struct { + req *query.SMSConfig + } + tests := []struct { + name string + args args + res *settings_pb.SMSProvider + }{ + { + name: "all fields filled, twilio", + args: args{ + req: &query.SMSConfig{ + + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + TwilioConfig: &query.Twilio{ + SID: "sid", + Token: nil, + SenderNumber: "sender", + VerifyServiceSID: "verify", + }, + State: 1, + }, + }, + res: &settings_pb.SMSProvider{ + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.SMSProvider_Twilio{ + Twilio: &settings_pb.TwilioConfig{ + Sid: "sid", + SenderNumber: "sender", + VerifyServiceSid: "verify", + }, + }, + }, + }, + { + name: "all fields filled, http", + args: args{ + req: &query.SMSConfig{ + CreationDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ChangeDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + ResourceOwner: "resourceowner", + AggregateID: "agg", + ID: "id", + Sequence: 1, + Description: "description", + HTTPConfig: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + State: 1, + }, + }, + res: &settings_pb.SMSProvider{ + Details: &object_pb.ObjectDetails{ + Sequence: 1, + CreationDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ChangeDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ResourceOwner: "resourceowner", + }, + Id: "id", + State: 1, + Description: "description", + Config: &settings_pb.SMSProvider_Http{ + Http: &settings_pb.HTTPConfig{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SMSConfigToProviderPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_smsStateToPb(t *testing.T) { + type args struct { + req domain.SMSConfigState + } + tests := []struct { + name string + args args + res settings_pb.SMSProviderConfigState + }{ + { + name: "unspecified", + args: args{ + req: domain.SMSConfigStateUnspecified, + }, + res: settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE, + }, + { + name: "removed", + args: args{ + req: domain.SMSConfigStateRemoved, + }, + res: settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE, + }, + { + name: "active", + args: args{ + req: domain.SMSConfigStateActive, + }, + res: settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_ACTIVE, + }, + { + name: "inactive", + args: args{ + req: domain.SMSConfigStateInactive, + }, + res: settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE, + }, + { + name: "default", + args: args{ + req: 100, + }, + res: settings_pb.SMSProviderConfigState_SMS_PROVIDER_CONFIG_INACTIVE, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := smsStateToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_HTTPConfigToPb(t *testing.T) { + type args struct { + req *query.HTTP + } + tests := []struct { + name string + args args + res *settings_pb.SMSProvider_Http + }{ + { + name: "all fields filled", + args: args{ + req: &query.HTTP{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + res: &settings_pb.SMSProvider_Http{ + Http: &settings_pb.HTTPConfig{ + Endpoint: "endpoint", + SigningKey: "key", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := HTTPConfigToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_TwilioConfigToPb(t *testing.T) { + type args struct { + req *query.Twilio + } + tests := []struct { + name string + args args + res *settings_pb.SMSProvider_Twilio + }{ + { + name: "all fields filled", + args: args{ + req: &query.Twilio{ + SID: "sid", + SenderNumber: "sender", + VerifyServiceSID: "verify", + }, + }, + res: &settings_pb.SMSProvider_Twilio{ + Twilio: &settings_pb.TwilioConfig{ + Sid: "sid", + SenderNumber: "sender", + VerifyServiceSid: "verify", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := TwilioConfigToPb(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_addSMSConfigTwilioToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.AddSMSProviderTwilioRequest + } + tests := []struct { + name string + args args + res *command.AddTwilioConfig + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddSMSProviderTwilioRequest{ + Sid: "sid", + Token: "token", + SenderNumber: "sender", + Description: "description", + VerifyServiceSid: "verify", + }, + }, + res: &command.AddTwilioConfig{ + ResourceOwner: "instance", + ID: "", + Description: "description", + SID: "sid", + Token: "token", + SenderNumber: "sender", + VerifyServiceSID: "verify", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addSMSConfigTwilioToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_updateSMSConfigTwilioToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.UpdateSMSProviderTwilioRequest + } + tests := []struct { + name string + args args + res *command.ChangeTwilioConfig + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateSMSProviderTwilioRequest{ + Id: "id", + Sid: "sid", + SenderNumber: "sender", + Description: "description", + VerifyServiceSid: "verify", + }, + }, + res: &command.ChangeTwilioConfig{ + ResourceOwner: "instance", + ID: "id", + Description: gu.Ptr("description"), + SID: gu.Ptr("sid"), + SenderNumber: gu.Ptr("sender"), + VerifyServiceSID: gu.Ptr("verify"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateSMSConfigTwilioToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_addSMSConfigHTTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.AddSMSProviderHTTPRequest + } + tests := []struct { + name string + args args + res *command.AddSMSHTTP + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddSMSProviderHTTPRequest{ + Endpoint: "endpoint", + Description: "description", + }, + }, + res: &command.AddSMSHTTP{ + ResourceOwner: "instance", + ID: "", + Description: "description", + Endpoint: "endpoint", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := addSMSConfigHTTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} + +func Test_updateSMSConfigHTTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.UpdateSMSProviderHTTPRequest + } + tests := []struct { + name string + args args + res *command.ChangeSMSHTTP + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateSMSProviderHTTPRequest{ + Id: "id", + Endpoint: "endpoint", + Description: "description", + ExpirationSigningKey: durationpb.New(time.Second), + }, + }, + res: &command.ChangeSMSHTTP{ + ResourceOwner: "instance", + ID: "id", + Description: gu.Ptr("description"), + Endpoint: gu.Ptr("endpoint"), + ExpirationSigningKey: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateSMSConfigHTTPToConfig(tt.args.ctx, tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} diff --git a/internal/api/grpc/object/converter_test.go b/internal/api/grpc/object/converter_test.go new file mode 100644 index 00000000000..006c5d38563 --- /dev/null +++ b/internal/api/grpc/object/converter_test.go @@ -0,0 +1,63 @@ +package object + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + object_pb "github.com/zitadel/zitadel/pkg/grpc/object" +) + +func Test_ListQueryToModel(t *testing.T) { + type args struct { + req *object_pb.ListQuery + } + type res struct { + offset, limit uint64 + asc bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "all fields filled", + args: args{ + req: &object_pb.ListQuery{ + Offset: 100, + Limit: 100, + Asc: true, + }, + }, + res: res{ + offset: 100, + limit: 100, + asc: true, + }, + }, + { + name: "all fields empty", + args: args{ + req: &object_pb.ListQuery{ + Offset: 0, + Limit: 0, + Asc: false, + }, + }, + res: res{ + offset: 0, + limit: 0, + asc: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + offset, limit, asc := ListQueryToModel(tt.args.req) + assert.Equal(t, tt.res.offset, offset) + assert.Equal(t, tt.res.limit, limit) + assert.Equal(t, tt.res.asc, asc) + }) + } +} diff --git a/internal/command/instance_smtp_config_model.go b/internal/command/instance_smtp_config_model.go index 187ccdc0ee9..d396716ce7a 100644 --- a/internal/command/instance_smtp_config_model.go +++ b/internal/command/instance_smtp_config_model.go @@ -201,7 +201,12 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat return changeEvent, true, nil } -func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, description, endpoint string) (*instance.SMTPConfigHTTPChangedEvent, bool, error) { +func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id, description, endpoint string, + signingKey *crypto.CryptoValue, +) (*instance.SMTPConfigHTTPChangedEvent, bool, error) { changes := make([]instance.SMTPConfigHTTPChanges, 0) var err error if wm.HTTPConfig == nil { @@ -217,6 +222,10 @@ func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggr if wm.HTTPConfig.Endpoint != endpoint { changes = append(changes, instance.ChangeSMTPConfigHTTPEndpoint(endpoint)) } + // if signingkey is set, update it as it is encrypted + if signingKey != nil { + changes = append(changes, instance.ChangeSMTPConfigHTTPSigningKey(signingKey)) + } if len(changes) == 0 { return nil, false, nil } @@ -251,7 +260,8 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigAddedEvent(e *instance.SMTPCo func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPAddedEvent(e *instance.SMTPConfigHTTPAddedEvent) { wm.Description = e.Description wm.HTTPConfig = &HTTPConfig{ - Endpoint: e.Endpoint, + Endpoint: e.Endpoint, + SigningKey: e.SigningKey, } wm.State = domain.SMTPConfigStateInactive // If ID has empty value we're dealing with the old and unique smtp settings @@ -313,6 +323,9 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPChangedEvent(e *instance. if e.Endpoint != nil { wm.HTTPConfig.Endpoint = *e.Endpoint } + if e.SigningKey != nil { + wm.HTTPConfig.SigningKey = e.SigningKey + } // If ID has empty value we're dealing with the old and unique smtp settings // These would be the default values for ID and State diff --git a/internal/command/sms_config.go b/internal/command/sms_config.go index bc16380217a..3fa0b2b32a4 100644 --- a/internal/command/sms_config.go +++ b/internal/command/sms_config.go @@ -157,6 +157,7 @@ type AddSMSHTTP struct { Description string Endpoint string + SigningKey string } func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (err error) { @@ -174,6 +175,12 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er return err } + code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smsEncryption) //nolint + if err != nil { + return err + } + config.SigningKey = code.PlainCode() + err = c.pushAppendAndReduce(ctx, smsConfigWriteModel, instance.NewSMSConfigHTTPAddedEvent( @@ -182,6 +189,7 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er config.ID, config.Description, config.Endpoint, + code.Crypted, ), ) if err != nil { @@ -196,8 +204,10 @@ type ChangeSMSHTTP struct { ResourceOwner string ID string - Description *string - Endpoint *string + Description *string + Endpoint *string + ExpirationSigningKey bool + SigningKey *string } func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTTP) (err error) { @@ -214,12 +224,24 @@ func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTT if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.HTTP == nil { return zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound") } + + var changedSigningKey *crypto.CryptoValue + if config.ExpirationSigningKey { + code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint + if err != nil { + return err + } + changedSigningKey = code.Crypted + config.SigningKey = &code.Plain + } + changedEvent, hasChanged, err := smsConfigWriteModel.NewHTTPChangedEvent( ctx, InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel), config.ID, config.Description, - config.Endpoint) + config.Endpoint, + changedSigningKey) if err != nil { return err } diff --git a/internal/command/sms_config_model.go b/internal/command/sms_config_model.go index 922c43ada5c..088ae96415f 100644 --- a/internal/command/sms_config_model.go +++ b/internal/command/sms_config_model.go @@ -27,7 +27,8 @@ type TwilioConfig struct { } type HTTPConfig struct { - Endpoint string + Endpoint string + SigningKey *crypto.CryptoValue } func NewIAMSMSConfigWriteModel(instanceID, id string) *IAMSMSConfigWriteModel { @@ -82,7 +83,8 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { continue } wm.HTTP = &HTTPConfig{ - Endpoint: e.Endpoint, + Endpoint: e.Endpoint, + SigningKey: e.SigningKey, } wm.Description = e.Description wm.State = domain.SMSConfigStateInactive @@ -96,6 +98,9 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error { if e.Endpoint != nil { wm.HTTP.Endpoint = *e.Endpoint } + if e.SigningKey != nil { + wm.HTTP.SigningKey = e.SigningKey + } case *instance.SMSConfigTwilioActivatedEvent: if wm.ID != e.ID { wm.State = domain.SMSConfigStateInactive @@ -189,7 +194,13 @@ func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, agg return changeEvent, true, nil } -func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, endpoint *string) (*instance.SMSConfigHTTPChangedEvent, bool, error) { +func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + id string, + description, endpoint *string, + signingKey *crypto.CryptoValue, +) (*instance.SMSConfigHTTPChangedEvent, bool, error) { changes := make([]instance.SMSConfigHTTPChanges, 0) var err error @@ -203,6 +214,10 @@ func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggre if endpoint != nil && wm.HTTP.Endpoint != *endpoint { changes = append(changes, instance.ChangeSMSConfigHTTPEndpoint(*endpoint)) } + // if signingkey is set, update it as it is encrypted + if signingKey != nil { + changes = append(changes, instance.ChangeSMSConfigHTTPSigningKey(signingKey)) + } if len(changes) == 0 { return nil, false, nil diff --git a/internal/command/sms_config_test.go b/internal/command/sms_config_test.go index 12fcd8661c8..e1a81d957a0 100644 --- a/internal/command/sms_config_test.go +++ b/internal/command/sms_config_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" @@ -306,9 +307,11 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) { func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - idGenerator id.Generator - alg crypto.EncryptionAlgorithm + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators + alg crypto.EncryptionAlgorithm } type args struct { ctx context.Context @@ -351,10 +354,18 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, }, args: args{ ctx: context.Background(), @@ -374,9 +385,11 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - idGenerator: tt.fields.idGenerator, - smsEncryption: tt.fields.alg, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, + smsEncryption: tt.fields.alg, } err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http) if tt.res.err == nil { @@ -394,7 +407,9 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) { func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { ctx context.Context @@ -474,6 +489,12 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -505,6 +526,12 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -514,17 +541,26 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { "providerid", "endpoint2", "description2", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("87654321"), + }, ), ), ), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, }, args: args{ ctx: context.Background(), http: &ChangeSMSHTTP{ - ResourceOwner: "INSTANCE", - ID: "providerid", - Description: gu.Ptr("description2"), - Endpoint: gu.Ptr("endpoint2"), + ResourceOwner: "INSTANCE", + ID: "providerid", + Description: gu.Ptr("description2"), + Endpoint: gu.Ptr("endpoint2"), + ExpirationSigningKey: true, }, }, res: res{ @@ -537,7 +573,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http) if tt.res.err == nil { @@ -707,6 +745,12 @@ func TestCommandSide_ActivateSMSConfig(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -917,6 +961,12 @@ func TestCommandSide_DeactivateSMSConfig(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), eventFromEventPusher( @@ -1083,6 +1133,12 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) { "providerid", "description", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -1141,10 +1197,11 @@ func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, de return event } -func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string) *instance.SMSConfigHTTPChangedEvent { +func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string, signingKey *crypto.CryptoValue) *instance.SMSConfigHTTPChangedEvent { changes := []instance.SMSConfigHTTPChanges{ instance.ChangeSMSConfigHTTPEndpoint(endpoint), instance.ChangeSMSConfigHTTPDescription(description), + instance.ChangeSMSConfigHTTPSigningKey(signingKey), } event, _ := instance.NewSMSConfigHTTPChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, diff --git a/internal/command/smtp.go b/internal/command/smtp.go index d856b9a3dac..0dd917e6c32 100644 --- a/internal/command/smtp.go +++ b/internal/command/smtp.go @@ -230,6 +230,7 @@ type AddSMTPConfigHTTP struct { Description string Endpoint string + SigningKey string } func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigHTTP) (err error) { @@ -248,12 +249,19 @@ func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigH return err } + code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint + if err != nil { + return err + } + config.SigningKey = code.PlainCode() + err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, instance.NewSMTPConfigHTTPAddedEvent( ctx, InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), config.ID, config.Description, config.Endpoint, + code.Crypted, )) if err != nil { return err @@ -267,8 +275,10 @@ type ChangeSMTPConfigHTTP struct { ResourceOwner string ID string - Description string - Endpoint string + Description string + Endpoint string + ExpirationSigningKey bool + SigningKey *string } func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPConfigHTTP) (err error) { @@ -288,12 +298,23 @@ func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPC return zerrors.ThrowNotFound(nil, "COMMAND-xIrdledqv4", "Errors.SMTPConfig.NotFound") } + var changedSigningKey *crypto.CryptoValue + if config.ExpirationSigningKey { + code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint + if err != nil { + return err + } + changedSigningKey = code.Crypted + config.SigningKey = &code.Plain + } + changedEvent, hasChanged, err := smtpConfigWriteModel.NewHTTPChangedEvent( ctx, InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), config.ID, config.Description, config.Endpoint, + changedSigningKey, ) if err != nil { return err diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go index 7aa224f2518..e86419eb33a 100644 --- a/internal/command/smtp_test.go +++ b/internal/command/smtp_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -902,8 +903,10 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) { type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators + idGenerator id.Generator } type args struct { http *AddSMTPConfigHTTP @@ -944,10 +947,18 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) { "configid", "test", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, }, args: args{ http: &AddSMTPConfigHTTP{ @@ -966,8 +977,10 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.AddSMTPConfigHTTP(context.Background(), tt.args.http) if tt.res.err == nil { @@ -986,7 +999,9 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) { func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc + defaultSecretGenerators *SecretGenerators } type args struct { http *ChangeSMTPConfigHTTP @@ -1063,6 +1078,12 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { "ID", "test", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -1094,6 +1115,12 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { "ID", "", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -1103,16 +1130,25 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { "ID", "test", "endpoint2", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("87654321"), + }, ), ), ), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, }, args: args{ http: &ChangeSMTPConfigHTTP{ - ResourceOwner: "INSTANCE", - ID: "ID", - Description: "test", - Endpoint: "endpoint2", + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Endpoint: "endpoint2", + ExpirationSigningKey: true, }, }, res: res{ @@ -1125,7 +1161,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, + defaultSecretGenerators: tt.fields.defaultSecretGenerators, } err := r.ChangeSMTPConfigHTTP(context.Background(), tt.args.http) if tt.res.err == nil { @@ -1300,6 +1338,12 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { "ID", "test", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -1511,6 +1555,12 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { "ID", "test", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), eventFromEventPusher( @@ -1677,6 +1727,12 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { "ID", "test", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("12345678"), + }, ), ), ), @@ -2051,10 +2107,11 @@ func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls return event } -func newSMTPConfigHTTPChangedEvent(ctx context.Context, id, description, endpoint string) *instance.SMTPConfigHTTPChangedEvent { +func newSMTPConfigHTTPChangedEvent(ctx context.Context, id, description, endpoint string, signingKey *crypto.CryptoValue) *instance.SMTPConfigHTTPChangedEvent { changes := []instance.SMTPConfigHTTPChanges{ instance.ChangeSMTPConfigHTTPDescription(description), instance.ChangeSMTPConfigHTTPEndpoint(endpoint), + instance.ChangeSMTPConfigHTTPSigningKey(signingKey), } event, _ := instance.NewSMTPConfigHTTPChangeEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, diff --git a/internal/notification/channels/webhook/channel.go b/internal/notification/channels/webhook/channel.go index 4065767d14e..d532ac6cdc9 100644 --- a/internal/notification/channels/webhook/channel.go +++ b/internal/notification/channels/webhook/channel.go @@ -12,6 +12,11 @@ import ( "github.com/zitadel/zitadel/internal/notification/channels" "github.com/zitadel/zitadel/internal/notification/messages" "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/actions" +) + +const ( + SigningHeader = "ZITADEL-Signature" ) func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel, error) { @@ -39,6 +44,10 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel, req.Header = cfg.Headers } req.Header.Set("Content-Type", "application/json") + if cfg.SigningKey != "" { + req.Header.Set(SigningHeader, actions.ComputeSignatureHeader(time.Now(), []byte(payload), cfg.SigningKey)) + } + resp, err := http.DefaultClient.Do(req) if err != nil { return err diff --git a/internal/notification/channels/webhook/config.go b/internal/notification/channels/webhook/config.go index 9772151d88c..5372cb7636b 100644 --- a/internal/notification/channels/webhook/config.go +++ b/internal/notification/channels/webhook/config.go @@ -6,9 +6,10 @@ import ( ) type Config struct { - CallURL string - Method string - Headers http.Header + CallURL string + Method string + Headers http.Header + SigningKey string } func (w *Config) Validate() error { diff --git a/internal/notification/handlers/config_email.go b/internal/notification/handlers/config_email.go index 3e6eaa27a17..a1c9ca344eb 100644 --- a/internal/notification/handlers/config_email.go +++ b/internal/notification/handlers/config_email.go @@ -49,9 +49,10 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email. return &email.Config{ ProviderConfig: provider, WebhookConfig: &webhook.Config{ - CallURL: config.HTTPConfig.Endpoint, - Method: http.MethodPost, - Headers: nil, + CallURL: config.HTTPConfig.Endpoint, + Method: http.MethodPost, + Headers: nil, + SigningKey: config.HTTPConfig.SigningKey, }, }, nil } diff --git a/internal/notification/handlers/config_sms.go b/internal/notification/handlers/config_sms.go index fd733b37313..bdbc7f162e2 100644 --- a/internal/notification/handlers/config_sms.go +++ b/internal/notification/handlers/config_sms.go @@ -45,9 +45,10 @@ func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Conf return &sms.Config{ ProviderConfig: provider, WebhookConfig: &webhook.Config{ - CallURL: config.HTTPConfig.Endpoint, - Method: http.MethodPost, - Headers: nil, + CallURL: config.HTTPConfig.Endpoint, + Method: http.MethodPost, + Headers: nil, + SigningKey: config.HTTPConfig.SigningKey, }, }, nil } diff --git a/internal/query/projection/sms.go b/internal/query/projection/sms.go index eb54d7afac6..4b8e875fcf3 100644 --- a/internal/query/projection/sms.go +++ b/internal/query/projection/sms.go @@ -37,6 +37,7 @@ const ( SMSHTTPColumnSMSID = "sms_id" SMSHTTPColumnInstanceID = "instance_id" SMSHTTPColumnEndpoint = "endpoint" + SMSHTTPColumnSigningKey = "signing_key" ) type smsConfigProjection struct{} @@ -80,6 +81,7 @@ func (*smsConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMSHTTPColumnSMSID, handler.ColumnTypeText), handler.NewColumn(SMSHTTPColumnInstanceID, handler.ColumnTypeText), handler.NewColumn(SMSHTTPColumnEndpoint, handler.ColumnTypeText), + handler.NewColumn(SMSHTTPColumnSigningKey, handler.ColumnTypeJSONB, handler.Nullable()), }, handler.NewPrimaryKey(SMSHTTPColumnInstanceID, SMSHTTPColumnSMSID), smsHTTPTableSuffix, @@ -286,6 +288,7 @@ func (p *smsConfigProjection) reduceSMSConfigHTTPAdded(event eventstore.Event) ( handler.NewCol(SMSHTTPColumnSMSID, e.ID), handler.NewCol(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), handler.NewCol(SMSHTTPColumnEndpoint, e.Endpoint), + handler.NewCol(SMSHTTPColumnSigningKey, e.SigningKey), }, handler.WithTableSuffix(smsHTTPTableSuffix), ), @@ -306,21 +309,24 @@ func (p *smsConfigProjection) reduceSMSConfigHTTPChanged(event eventstore.Event) if e.Description != nil { columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description)) } - if len(columns) > 0 { - stmts = append(stmts, handler.AddUpdateStatement( - columns, - []handler.Condition{ - handler.NewCond(SMSColumnID, e.ID), - handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), - }, - )) - } + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMSColumnID, e.ID), + handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID), + }, + )) + httpColumns := make([]handler.Column, 0) + if e.SigningKey != nil { + httpColumns = append(httpColumns, handler.NewCol(SMSHTTPColumnSigningKey, e.SigningKey)) + } if e.Endpoint != nil { + httpColumns = append(httpColumns, handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint)) + } + if len(httpColumns) > 0 { stmts = append(stmts, handler.AddUpdateStatement( - []handler.Column{ - handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint), - }, + httpColumns, []handler.Condition{ handler.NewCond(SMSHTTPColumnSMSID, e.ID), handler.NewCond(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID), diff --git a/internal/query/projection/sms_test.go b/internal/query/projection/sms_test.go index 7a083c2234f..953ccb55534 100644 --- a/internal/query/projection/sms_test.go +++ b/internal/query/projection/sms_test.go @@ -302,7 +302,8 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id", "description": "description", - "endpoint": "endpoint" + "endpoint": "endpoint", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } }`), ), eventstore.GenericEventMapper[instance.SMSConfigHTTPAddedEvent]), }, @@ -327,11 +328,12 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint) VALUES ($1, $2, $3)", + expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint, signing_key) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "id", "instance-id", "endpoint", + anyArg{}, }, }, }, @@ -348,7 +350,8 @@ func TestSMSProjection_reduces(t *testing.T) { []byte(`{ "id": "id", "endpoint": "endpoint", - "description": "description" + "description": "description", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } }`), ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), }, @@ -369,8 +372,9 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.sms_configs3_http SET (signing_key, endpoint) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ + anyArg{}, "endpoint", "id", "instance-id", @@ -452,6 +456,46 @@ func TestSMSProjection_reduces(t *testing.T) { }, }, }, + { + name: "instance reduceSMSConfigHTTPChanged, only signing key", + args: args{ + event: getEvent( + testEvent( + instance.SMSConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "id": "id", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } + }`), + ), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]), + }, + reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.sms_configs3_http SET signing_key = $1 WHERE (sms_id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + "id", + "instance-id", + }, + }, + }, + }, + }, + }, { name: "instance reduceSMSConfigTwilioActivated", args: args{ diff --git a/internal/query/projection/smtp.go b/internal/query/projection/smtp.go index c350d3d2f62..997f4ac8c98 100644 --- a/internal/query/projection/smtp.go +++ b/internal/query/projection/smtp.go @@ -40,6 +40,7 @@ const ( SMTPConfigHTTPColumnInstanceID = "instance_id" SMTPConfigHTTPColumnID = "id" SMTPConfigHTTPColumnEndpoint = "endpoint" + SMTPConfigHTTPColumnSigningKey = "signing_key" ) type smtpConfigProjection struct{} @@ -86,6 +87,7 @@ func (*smtpConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMTPConfigHTTPColumnID, handler.ColumnTypeText), handler.NewColumn(SMTPConfigHTTPColumnInstanceID, handler.ColumnTypeText), handler.NewColumn(SMTPConfigHTTPColumnEndpoint, handler.ColumnTypeText), + handler.NewColumn(SMTPConfigHTTPColumnSigningKey, handler.ColumnTypeJSONB, handler.Nullable()), }, handler.NewPrimaryKey(SMTPConfigHTTPColumnInstanceID, SMTPConfigHTTPColumnID), smtpConfigHTTPTableSuffix, @@ -211,6 +213,7 @@ func (p *smtpConfigProjection) reduceSMTPConfigHTTPAdded(event eventstore.Event) handler.NewCol(SMTPConfigHTTPColumnInstanceID, e.Aggregate().InstanceID), handler.NewCol(SMTPConfigHTTPColumnID, getSMTPConfigID(e.ID, e.Aggregate())), handler.NewCol(SMTPConfigHTTPColumnEndpoint, e.Endpoint), + handler.NewCol(SMTPConfigHTTPColumnSigningKey, e.SigningKey), }, handler.WithTableSuffix(smtpConfigHTTPTableSuffix), ), @@ -231,20 +234,21 @@ func (p *smtpConfigProjection) reduceSMTPConfigHTTPChanged(event eventstore.Even if e.Description != nil { columns = append(columns, handler.NewCol(SMTPConfigColumnDescription, *e.Description)) } - if len(columns) > 0 { - stmts = append(stmts, handler.AddUpdateStatement( - columns, - []handler.Condition{ - handler.NewCond(SMTPConfigColumnID, getSMTPConfigID(e.ID, e.Aggregate())), - handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), - }, - )) - } + stmts = append(stmts, handler.AddUpdateStatement( + columns, + []handler.Condition{ + handler.NewCond(SMTPConfigColumnID, getSMTPConfigID(e.ID, e.Aggregate())), + handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID), + }, + )) smtpColumns := make([]handler.Column, 0, 1) if e.Endpoint != nil { smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigHTTPColumnEndpoint, *e.Endpoint)) } + if e.SigningKey != nil { + smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigHTTPColumnSigningKey, e.SigningKey)) + } if len(smtpColumns) > 0 { stmts = append(stmts, handler.AddUpdateStatement( smtpColumns, diff --git a/internal/query/projection/smtp_test.go b/internal/query/projection/smtp_test.go index a897fff680b..1529a5d2b6d 100644 --- a/internal/query/projection/smtp_test.go +++ b/internal/query/projection/smtp_test.go @@ -225,7 +225,8 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "aggregate_id": "agg-id", "id": "config-id", "description": "test", - "endpoint": "endpoint" + "endpoint": "endpoint", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } }`, ), ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]), @@ -247,9 +248,10 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_http SET endpoint = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.smtp_configs5_http SET (endpoint, signing_key) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "endpoint", + anyArg{}, "config-id", "instance-id", }, @@ -338,6 +340,49 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, }, + }, { + name: "reduceSMTPConfigHTTPChanged, signing key", + args: args{ + event: getEvent( + testEvent( + instance.SMTPConfigHTTPChangedEventType, + instance.AggregateType, + []byte(`{ + "instance_id": "instance-id", + "resource_owner": "ro-id", + "aggregate_id": "agg-id", + "id": "config-id", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } + }`, + ), + ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]), + }, + reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "config-id", + "instance-id", + }, + }, + { + expectedStmt: "UPDATE projections.smtp_configs5_http SET signing_key = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedArgs: []interface{}{ + anyArg{}, + "config-id", + "instance-id", + }, + }, + }, + }, + }, }, { name: "reduceSMTPConfigAdded (no id)", @@ -481,7 +526,8 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "id": "config-id", "description": "test", "senderAddress": "sender", - "endpoint": "endpoint" + "endpoint": "endpoint", + "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" } }`), ), eventstore.GenericEventMapper[instance.SMTPConfigHTTPAddedEvent]), }, @@ -506,11 +552,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.smtp_configs5_http (instance_id, id, endpoint) VALUES ($1, $2, $3)", + expectedStmt: "INSERT INTO projections.smtp_configs5_http (instance_id, id, endpoint, signing_key) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "instance-id", "config-id", "endpoint", + anyArg{}, }, }, }, diff --git a/internal/query/query.go b/internal/query/query.go index 7be38c6bda5..da539442cee 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -32,6 +32,8 @@ type Queries struct { keyEncryptionAlgorithm crypto.EncryptionAlgorithm idpConfigEncryption crypto.EncryptionAlgorithm targetEncryptionAlgorithm crypto.EncryptionAlgorithm + smtpEncryptionAlgorithm crypto.EncryptionAlgorithm + smsEncryptionAlgorithm crypto.EncryptionAlgorithm sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error) checkPermission domain.PermissionCheck @@ -53,7 +55,7 @@ func StartQueries( cacheConnectors connector.Connectors, projections projection.Config, defaults sd.SystemDefaults, - idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm, targetEncryptionAlgorithm crypto.EncryptionAlgorithm, + idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm, targetEncryptionAlgorithm, smsEncryptionAlgorithm, smtpEncryptionAlgorithm crypto.EncryptionAlgorithm, zitadelRoles []authz.RoleMapping, sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error), permissionCheck func(q *Queries) domain.PermissionCheck, @@ -72,6 +74,8 @@ func StartQueries( keyEncryptionAlgorithm: keyEncryptionAlgorithm, idpConfigEncryption: idpConfigEncryption, targetEncryptionAlgorithm: targetEncryptionAlgorithm, + smsEncryptionAlgorithm: smsEncryptionAlgorithm, + smtpEncryptionAlgorithm: smtpEncryptionAlgorithm, sessionTokenVerifier: sessionTokenVerifier, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ diff --git a/internal/query/sms.go b/internal/query/sms.go index 3659f05dafe..b0a54e7efe0 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -35,6 +35,13 @@ type SMSConfig struct { HTTPConfig *HTTP } +func (h *SMSConfig) decryptSigningKey(alg crypto.EncryptionAlgorithm) error { + if h.HTTPConfig == nil { + return nil + } + return h.HTTPConfig.decryptSigningKey(alg) +} + type Twilio struct { SID string Token *crypto.CryptoValue @@ -43,7 +50,21 @@ type Twilio struct { } type HTTP struct { - Endpoint string + Endpoint string + signingKey *crypto.CryptoValue + SigningKey string +} + +func (h *HTTP) decryptSigningKey(alg crypto.EncryptionAlgorithm) error { + if h.signingKey == nil { + return nil + } + keyValue, err := crypto.DecryptString(h.signingKey, alg) + if err != nil { + return zerrors.ThrowInternal(err, "QUERY-bxovy3YXwy", "Errors.Internal") + } + h.SigningKey = keyValue + return nil } type SMSConfigsSearchQueries struct { @@ -142,6 +163,10 @@ var ( name: projection.SMSHTTPColumnEndpoint, table: smsHTTPTable, } + SMSHTTPColumnSigningKey = Column{ + name: projection.SMSHTTPColumnSigningKey, + table: smsHTTPTable, + } ) func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config *SMSConfig, err error) { @@ -163,7 +188,13 @@ func (q *Queries) SMSProviderConfigByID(ctx context.Context, id string) (config config, err = scan(row) return err }, stmt, args...) - return config, err + if err != nil { + return nil, err + } + if err := config.decryptSigningKey(q.smsEncryptionAlgorithm); err != nil { + return nil, err + } + return config, nil } func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string) (config *SMSConfig, err error) { @@ -185,7 +216,13 @@ func (q *Queries) SMSProviderConfigActive(ctx context.Context, instanceID string config, err = scan(row) return err }, stmt, args...) - return config, err + if err != nil { + return nil, err + } + if err := config.decryptSigningKey(q.smsEncryptionAlgorithm); err != nil { + return nil, err + } + return config, nil } func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearchQueries) (configs *SMSConfigs, err error) { @@ -208,8 +245,13 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-l4bxm", "Errors.Internal") } + for i := range configs.Configs { + if err := configs.Configs[i].decryptSigningKey(q.smsEncryptionAlgorithm); err != nil { + return nil, err + } + } configs.State, err = q.latestState(ctx, smsConfigsTable) - return configs, err + return configs, nil } func NewSMSProviderStateQuery(state domain.SMSConfigState) (SearchQuery, error) { @@ -235,6 +277,7 @@ func prepareSMSConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, erro SMSHTTPColumnSMSID.identifier(), SMSHTTPColumnEndpoint.identifier(), + SMSHTTPColumnSigningKey.identifier(), ).From(smsConfigsTable.identifier()). LeftJoin(join(SMSTwilioColumnSMSID, SMSColumnID)). LeftJoin(join(SMSHTTPColumnSMSID, SMSColumnID)). @@ -264,6 +307,7 @@ func prepareSMSConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMSConfig, erro &httpConfig.id, &httpConfig.endpoint, + &httpConfig.signingKey, ) if err != nil { @@ -299,6 +343,7 @@ func prepareSMSConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, e SMSHTTPColumnSMSID.identifier(), SMSHTTPColumnEndpoint.identifier(), + SMSHTTPColumnSigningKey.identifier(), countColumn.identifier(), ).From(smsConfigsTable.identifier()). @@ -332,6 +377,7 @@ func prepareSMSConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMSConfigs, e &httpConfig.id, &httpConfig.endpoint, + &httpConfig.signingKey, &configs.Count, ) @@ -371,8 +417,9 @@ func (c sqlTwilioConfig) set(smsConfig *SMSConfig) { } type sqlHTTPConfig struct { - id sql.NullString - endpoint sql.NullString + id sql.NullString + endpoint sql.NullString + signingKey *crypto.CryptoValue } func (c sqlHTTPConfig) setSMS(smsConfig *SMSConfig) { @@ -380,6 +427,7 @@ func (c sqlHTTPConfig) setSMS(smsConfig *SMSConfig) { return } smsConfig.HTTPConfig = &HTTP{ - Endpoint: c.endpoint.String, + Endpoint: c.endpoint.String, + signingKey: c.signingKey, } } diff --git a/internal/query/sms_test.go b/internal/query/sms_test.go index e6e79d72bcc..545e54f612d 100644 --- a/internal/query/sms_test.go +++ b/internal/query/sms_test.go @@ -32,7 +32,8 @@ var ( // http config ` projections.sms_configs3_http.sms_id,` + - ` projections.sms_configs3_http.endpoint` + + ` projections.sms_configs3_http.endpoint,` + + ` projections.sms_configs3_http.signing_key` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + ` LEFT JOIN projections.sms_configs3_http ON projections.sms_configs3.id = projections.sms_configs3_http.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_http.instance_id`) @@ -55,6 +56,7 @@ var ( // http config ` projections.sms_configs3_http.sms_id,` + ` projections.sms_configs3_http.endpoint,` + + ` projections.sms_configs3_http.signing_key,` + ` COUNT(*) OVER ()` + ` FROM projections.sms_configs3` + ` LEFT JOIN projections.sms_configs3_twilio ON projections.sms_configs3.id = projections.sms_configs3_twilio.sms_id AND projections.sms_configs3.instance_id = projections.sms_configs3_twilio.instance_id` + @@ -78,6 +80,7 @@ var ( // http config "sms_id", "endpoint", + "signing_key", } smsConfigsCols = append(smsConfigCols, "count") ) @@ -131,6 +134,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { // http config nil, nil, + nil, }, }, ), @@ -185,6 +189,12 @@ func Test_SMSConfigsPrepare(t *testing.T) { // http config "sms-id", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, }, ), @@ -205,6 +215,12 @@ func Test_SMSConfigsPrepare(t *testing.T) { Description: "description", HTTPConfig: &HTTP{ Endpoint: "endpoint", + signingKey: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, }, }, @@ -236,6 +252,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { // http config nil, nil, + nil, }, { "sms-id2", @@ -255,6 +272,7 @@ func Test_SMSConfigsPrepare(t *testing.T) { // http config nil, nil, + nil, }, { "sms-id3", @@ -274,6 +292,12 @@ func Test_SMSConfigsPrepare(t *testing.T) { // http config "sms-id3", "endpoint3", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, }, ), @@ -326,6 +350,12 @@ func Test_SMSConfigsPrepare(t *testing.T) { Description: "description", HTTPConfig: &HTTP{ Endpoint: "endpoint3", + signingKey: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, }, }, @@ -410,6 +440,7 @@ func Test_SMSConfigPrepare(t *testing.T) { // http config nil, nil, + nil, }, ), }, @@ -455,6 +486,12 @@ func Test_SMSConfigPrepare(t *testing.T) { // http config "sms-id", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, ), }, @@ -469,6 +506,12 @@ func Test_SMSConfigPrepare(t *testing.T) { Description: "description", HTTPConfig: &HTTP{ Endpoint: "endpoint", + signingKey: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, }, }, diff --git a/internal/query/smtp.go b/internal/query/smtp.go index 4238ec121ed..21320872b6f 100644 --- a/internal/query/smtp.go +++ b/internal/query/smtp.go @@ -121,6 +121,10 @@ var ( name: projection.SMTPConfigHTTPColumnEndpoint, table: smtpConfigsHTTPTable, } + SMTPConfigHTTPColumnSigningKey = Column{ + name: projection.SMTPConfigHTTPColumnSigningKey, + table: smtpConfigsHTTPTable, + } ) type SMTPConfig struct { @@ -138,6 +142,13 @@ type SMTPConfig struct { State domain.SMTPConfigState } +func (h *SMTPConfig) decryptSigningKey(alg crypto.EncryptionAlgorithm) error { + if h.HTTPConfig == nil { + return nil + } + return h.HTTPConfig.decryptSigningKey(alg) +} + type SMTP struct { TLS bool SenderAddress string @@ -166,7 +177,13 @@ func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (c config, err = scan(row) return err }, query, args...) - return config, err + if err != nil { + return nil, err + } + if err := config.decryptSigningKey(q.smtpEncryptionAlgorithm); err != nil { + return nil, err + } + return config, nil } func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (config *SMTPConfig, err error) { @@ -186,12 +203,16 @@ func (q *Queries) SMTPConfigByID(ctx context.Context, instanceID, id string) (co config, err = scan(row) return err }, query, args...) + if err != nil { + return nil, err + } + if err := config.decryptSigningKey(q.smtpEncryptionAlgorithm); err != nil { + return nil, err + } return config, err } func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, error)) { - password := new(crypto.CryptoValue) - return sq.Select( SMTPConfigColumnCreationDate.identifier(), SMTPConfigColumnChangeDate.identifier(), @@ -211,7 +232,9 @@ func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, er SMTPConfigSMTPColumnPassword.identifier(), SMTPConfigHTTPColumnID.identifier(), - SMTPConfigHTTPColumnEndpoint.identifier()). + SMTPConfigHTTPColumnEndpoint.identifier(), + SMTPConfigHTTPColumnSigningKey.identifier(), + ). From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). LeftJoin(join(SMTPConfigHTTPColumnID, SMTPConfigColumnID)). @@ -237,9 +260,10 @@ func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, er &smtpConfig.replyToAddress, &smtpConfig.host, &smtpConfig.user, - &password, + &smtpConfig.password, &httpConfig.id, &httpConfig.endpoint, + &httpConfig.signingKey, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -247,9 +271,10 @@ func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, er } return nil, zerrors.ThrowInternal(err, "QUERY-9k87F", "Errors.Internal") } - smtpConfig.password = password + smtpConfig.set(config) httpConfig.setSMTP(config) + return config, nil } } @@ -275,6 +300,8 @@ func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, SMTPConfigHTTPColumnID.identifier(), SMTPConfigHTTPColumnEndpoint.identifier(), + SMTPConfigHTTPColumnSigningKey.identifier(), + countColumn.identifier(), ).From(smtpConfigsTable.identifier()). LeftJoin(join(SMTPConfigSMTPColumnID, SMTPConfigColumnID)). @@ -284,7 +311,6 @@ func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, configs := &SMTPConfigs{Configs: []*SMTPConfig{}} for rows.Next() { config := new(SMTPConfig) - password := new(crypto.CryptoValue) var ( smtpConfig = sqlSmtpConfig{} httpConfig = sqlHTTPConfig{} @@ -304,20 +330,19 @@ func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, &smtpConfig.replyToAddress, &smtpConfig.host, &smtpConfig.user, - &password, + &smtpConfig.password, &httpConfig.id, &httpConfig.endpoint, + &httpConfig.signingKey, &configs.Count, ) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-fwofw", "Errors.SMTPConfig.NotFound") - } return nil, zerrors.ThrowInternal(err, "QUERY-9k87F", "Errors.Internal") } - smtpConfig.password = password + smtpConfig.set(config) httpConfig.setSMTP(config) + configs.Configs = append(configs.Configs, config) } return configs, nil @@ -344,6 +369,11 @@ func (q *Queries) SearchSMTPConfigs(ctx context.Context, queries *SMTPConfigsSea if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-tOpKN", "Errors.Internal") } + for i := range configs.Configs { + if err := configs.Configs[i].decryptSigningKey(q.smtpEncryptionAlgorithm); err != nil { + return nil, err + } + } configs.State, err = q.latestState(ctx, smsConfigsTable) return configs, err } @@ -379,6 +409,7 @@ func (c sqlHTTPConfig) setSMTP(smtpConfig *SMTPConfig) { return } smtpConfig.HTTPConfig = &HTTP{ - Endpoint: c.endpoint.String, + Endpoint: c.endpoint.String, + signingKey: c.signingKey, } } diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go index 68ace249aac..f94a88230ac 100644 --- a/internal/query/smtp_test.go +++ b/internal/query/smtp_test.go @@ -30,10 +30,34 @@ var ( ` projections.smtp_configs5_smtp.username,` + ` projections.smtp_configs5_smtp.password,` + ` projections.smtp_configs5_http.id,` + - ` projections.smtp_configs5_http.endpoint` + + ` projections.smtp_configs5_http.endpoint,` + + ` projections.smtp_configs5_http.signing_key` + ` FROM projections.smtp_configs5` + ` LEFT JOIN projections.smtp_configs5_smtp ON projections.smtp_configs5.id = projections.smtp_configs5_smtp.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_smtp.instance_id` + ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` + prepareSMTPConfigsStmt = `SELECT projections.smtp_configs5.creation_date,` + + ` projections.smtp_configs5.change_date,` + + ` projections.smtp_configs5.resource_owner,` + + ` projections.smtp_configs5.sequence,` + + ` projections.smtp_configs5.id,` + + ` projections.smtp_configs5.state,` + + ` projections.smtp_configs5.description,` + + ` projections.smtp_configs5_smtp.id,` + + ` projections.smtp_configs5_smtp.tls,` + + ` projections.smtp_configs5_smtp.sender_address,` + + ` projections.smtp_configs5_smtp.sender_name,` + + ` projections.smtp_configs5_smtp.reply_to_address,` + + ` projections.smtp_configs5_smtp.host,` + + ` projections.smtp_configs5_smtp.username,` + + ` projections.smtp_configs5_smtp.password,` + + ` projections.smtp_configs5_http.id,` + + ` projections.smtp_configs5_http.endpoint,` + + ` projections.smtp_configs5_http.signing_key,` + + ` COUNT(*) OVER ()` + + ` FROM projections.smtp_configs5` + + ` LEFT JOIN projections.smtp_configs5_smtp ON projections.smtp_configs5.id = projections.smtp_configs5_smtp.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_smtp.instance_id` + + ` LEFT JOIN projections.smtp_configs5_http ON projections.smtp_configs5.id = projections.smtp_configs5_http.id AND projections.smtp_configs5.instance_id = projections.smtp_configs5_http.instance_id` + prepareSMTPConfigCols = []string{ "creation_date", "change_date", @@ -52,10 +76,12 @@ var ( "smtp_password", "id", "endpoint", + "signing_key", } + prepareSMTPConfigsCols = append(prepareSMTPConfigCols, "count") ) -func Test_SMTPConfigsPrepares(t *testing.T) { +func Test_SMTPConfigPrepares(t *testing.T) { type want struct { sqlExpectations sqlExpectation err checkErr @@ -109,6 +135,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) { &crypto.CryptoValue{}, nil, nil, + nil, }, ), }, @@ -156,6 +183,12 @@ func Test_SMTPConfigsPrepares(t *testing.T) { nil, "2232323", "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, ), }, @@ -166,6 +199,12 @@ func Test_SMTPConfigsPrepares(t *testing.T) { Sequence: 20211108, HTTPConfig: &HTTP{ Endpoint: "endpoint", + signingKey: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, }, ID: "2232323", State: domain.SMTPConfigStateActive, @@ -197,6 +236,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) { &crypto.CryptoValue{}, nil, nil, + nil, }, ), }, @@ -244,6 +284,7 @@ func Test_SMTPConfigsPrepares(t *testing.T) { &crypto.CryptoValue{}, nil, nil, + nil, }, ), }, @@ -290,3 +331,247 @@ func Test_SMTPConfigsPrepares(t *testing.T) { }) } } + +func Test_SMTPConfigsPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + + { + name: "prepareSMTPConfigsQuery no result", + prepare: prepareSMTPConfigsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareSMTPConfigsStmt), + nil, + nil, + ), + }, + object: &SMTPConfigs{Configs: []*SMTPConfig{}}, + }, + { + name: "prepareSMTPConfigsQuery found", + prepare: prepareSMTPConfigsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareSMTPConfigsStmt), + prepareSMTPConfigsCols, + [][]driver.Value{ + { + testNow, + testNow, + "ro", + uint64(20211108), + "2232323", + domain.SMTPConfigStateActive, + "test", + "2232323", + true, + "sender", + "name", + "reply-to", + "host", + "user", + &crypto.CryptoValue{}, + nil, + nil, + nil, + }, + }, + ), + }, + object: &SMTPConfigs{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Configs: []*SMTPConfig{ + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender", + SenderName: "name", + ReplyToAddress: "reply-to", + Host: "host", + User: "user", + Password: &crypto.CryptoValue{}, + }, + ID: "2232323", + State: domain.SMTPConfigStateActive, + Description: "test", + }, + }, + }, + }, + { + name: "prepareSMTPConfigsQuery found, multi", + prepare: prepareSMTPConfigsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareSMTPConfigsStmt), + prepareSMTPConfigsCols, + [][]driver.Value{ + { + testNow, + testNow, + "ro", + uint64(20211108), + "2232323", + domain.SMTPConfigStateActive, + "test", + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + "2232323", + "endpoint", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + }, + { + testNow, + testNow, + "ro", + uint64(20211109), + "44442323", + domain.SMTPConfigStateInactive, + "test2", + "44442323", + true, + "sender2", + "name2", + "reply-to2", + "host2", + "user2", + &crypto.CryptoValue{}, + nil, + nil, + nil, + }, + { + testNow, + testNow, + "ro", + uint64(20211109), + "23234444", + domain.SMTPConfigStateInactive, + "test3", + "23234444", + true, + "sender3", + "name3", + "reply-to3", + "host3", + "user3", + &crypto.CryptoValue{}, + nil, + nil, + nil, + }, + }, + ), + }, + object: &SMTPConfigs{ + SearchResponse: SearchResponse{ + Count: 3, + }, + Configs: []*SMTPConfig{ + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211108, + HTTPConfig: &HTTP{ + Endpoint: "endpoint", + signingKey: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "alg", + KeyID: "encKey", + Crypted: []byte("crypted"), + }, + }, + ID: "2232323", + State: domain.SMTPConfigStateActive, + Description: "test", + }, + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender2", + SenderName: "name2", + ReplyToAddress: "reply-to2", + Host: "host2", + User: "user2", + Password: &crypto.CryptoValue{}, + }, + ID: "44442323", + State: domain.SMTPConfigStateInactive, + Description: "test2", + }, + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + SMTPConfig: &SMTP{ + TLS: true, + SenderAddress: "sender3", + SenderName: "name3", + ReplyToAddress: "reply-to3", + Host: "host3", + User: "user3", + Password: &crypto.CryptoValue{}, + }, + ID: "23234444", + State: domain.SMTPConfigStateInactive, + Description: "test3", + }, + }, + }, + }, + { + name: "prepareSMTPConfigsQuery sql err", + prepare: prepareSMTPConfigsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareSMTPConfigsStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*SMTPConfigs)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/repository/instance/sms.go b/internal/repository/instance/sms.go index 7a402e67fee..ad755444893 100644 --- a/internal/repository/instance/sms.go +++ b/internal/repository/instance/sms.go @@ -183,9 +183,10 @@ func (e *SMSConfigTwilioTokenChangedEvent) UniqueConstraints() []*eventstore.Uni type SMSConfigHTTPAddedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - Endpoint string `json:"endpoint,omitempty"` + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + SigningKey *crypto.CryptoValue `json:"signingKey"` } func NewSMSConfigHTTPAddedEvent( @@ -194,6 +195,7 @@ func NewSMSConfigHTTPAddedEvent( id, description, endpoint string, + signingKey *crypto.CryptoValue, ) *SMSConfigHTTPAddedEvent { return &SMSConfigHTTPAddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -204,6 +206,7 @@ func NewSMSConfigHTTPAddedEvent( ID: id, Description: description, Endpoint: endpoint, + SigningKey: signingKey, } } @@ -222,9 +225,10 @@ func (e *SMSConfigHTTPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstr type SMSConfigHTTPChangedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description *string `json:"description,omitempty"` - Endpoint *string `json:"endpoint,omitempty"` + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` + SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"` } func NewSMSConfigHTTPChangedEvent( @@ -262,6 +266,11 @@ func ChangeSMSConfigHTTPEndpoint(endpoint string) func(event *SMSConfigHTTPChang e.Endpoint = &endpoint } } +func ChangeSMSConfigHTTPSigningKey(signingKey *crypto.CryptoValue) func(event *SMSConfigHTTPChangedEvent) { + return func(e *SMSConfigHTTPChangedEvent) { + e.SigningKey = signingKey + } +} func (e *SMSConfigHTTPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = event diff --git a/internal/repository/instance/smtp_config.go b/internal/repository/instance/smtp_config.go index 403cc569a32..78c14ed4e04 100644 --- a/internal/repository/instance/smtp_config.go +++ b/internal/repository/instance/smtp_config.go @@ -217,9 +217,10 @@ func (e *SMTPConfigPasswordChangedEvent) UniqueConstraints() []*eventstore.Uniqu type SMTPConfigHTTPAddedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - Endpoint string `json:"endpoint,omitempty"` + ID string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + SigningKey *crypto.CryptoValue `json:"signingKey"` } func NewSMTPConfigHTTPAddedEvent( @@ -227,6 +228,7 @@ func NewSMTPConfigHTTPAddedEvent( aggregate *eventstore.Aggregate, id, description string, endpoint string, + signingKey *crypto.CryptoValue, ) *SMTPConfigHTTPAddedEvent { return &SMTPConfigHTTPAddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -237,6 +239,7 @@ func NewSMTPConfigHTTPAddedEvent( ID: id, Description: description, Endpoint: endpoint, + SigningKey: signingKey, } } @@ -254,9 +257,10 @@ func (e *SMTPConfigHTTPAddedEvent) UniqueConstraints() []*eventstore.UniqueConst type SMTPConfigHTTPChangedEvent struct { *eventstore.BaseEvent `json:"-"` - ID string `json:"id,omitempty"` - Description *string `json:"description,omitempty"` - Endpoint *string `json:"endpoint,omitempty"` + ID string `json:"id,omitempty"` + Description *string `json:"description,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` + SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"` } func (e *SMTPConfigHTTPChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -314,6 +318,12 @@ func ChangeSMTPConfigHTTPEndpoint(endpoint string) func(event *SMTPConfigHTTPCha } } +func ChangeSMTPConfigHTTPSigningKey(signingKey *crypto.CryptoValue) func(event *SMTPConfigHTTPChangedEvent) { + return func(e *SMTPConfigHTTPChangedEvent) { + e.SigningKey = signingKey + } +} + type SMTPConfigActivatedEvent struct { *eventstore.BaseEvent `json:"-"` ID string `json:"id,omitempty"` diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index ff5e1e5f846..1a9bd8558b6 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -5069,6 +5069,12 @@ message AddEmailProviderHTTPRequest { message AddEmailProviderHTTPResponse { zitadel.v1.ObjectDetails details = 1; string id = 2; + // Key used to sign and check payload sent to the HTTP provider. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } message UpdateEmailProviderHTTPRequest { @@ -5090,10 +5096,31 @@ message UpdateEmailProviderHTTPRequest { max_length: 200; } ]; + // Regenerate the key used for signing and checking the payload sent to the HTTP provider. + // Set the graceful period for the existing key. During that time, the previous + // signing key and the new one will be used to sign the request to allow you a smooth + // transition onf your API. + // + // Note that we currently only allow an immediate rotation ("0s") and will support + // longer expirations in the future. + optional google.protobuf.Duration expiration_signing_key = 4 [ + (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0s\"" + minimum: 0 + maximum: 0 + } + ]; } message UpdateEmailProviderHTTPResponse { zitadel.v1.ObjectDetails details = 1; + // Key used to sign and check payload sent to the HTTP provider. + optional string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } message ActivateEmailProviderRequest { @@ -5336,6 +5363,12 @@ message AddSMSProviderHTTPRequest { message AddSMSProviderHTTPResponse { zitadel.v1.ObjectDetails details = 1; string id = 2; + // Key used to sign and check payload sent to the HTTP provider. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } message UpdateSMSProviderHTTPRequest { @@ -5357,10 +5390,30 @@ message UpdateSMSProviderHTTPRequest { max_length: 200; } ]; + // Regenerate the key used for signing and checking the payload sent to the HTTP provider. + // Set the graceful period for the existing key. During that time, the previous + // signing key and the new one will be used to sign the request to allow you a smooth + // transition onf your API. + // + // Note that we currently only allow an immediate rotation ("0s") and will support + // longer expirations in the future. + optional google.protobuf.Duration expiration_signing_key = 4 [ + (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0s\"" + minimum: 0 + maximum: 0 + } + ]; } message UpdateSMSProviderHTTPResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1;// Key used to sign and check payload sent to the HTTP provider. + optional string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } message ActivateSMSProviderRequest { diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index c761e1c841a..661a0ac9a5c 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -144,6 +144,11 @@ message EmailProviderSMTP { message EmailProviderHTTP { string endpoint = 1; + string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } message SMSProvider { @@ -166,6 +171,11 @@ message TwilioConfig { message HTTPConfig { string endpoint = 1; + string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; } enum SMSProviderConfigState {