From 3fc0ef25069dff0a8565278fff30fb4722ba6a81 Mon Sep 17 00:00:00 2001 From: Wim Van Laer Date: Tue, 27 Jan 2026 15:11:56 +0100 Subject: [PATCH] feat: added xoauth for smtp (#11239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Which Problems Are Solved - Some mail providers (e.g.: MS Exchange) have deprecated plain auth # How the Problems Are Solved Adds XOauth2 auth option to SMPT - SMTP config now can contain XOAuth2 auth config - Proto files are updated to have a oneof for configuration selection. Old fields for configuring plain auth are still available not to create a breaking change. These can be removed in the future. - Columns are added to the database to persist the config # Additional Changes none # Additional Context Replace this example with links to related issues, discussions, discord threads, or other sources with more context. Use the Closing #issue syntax for issues that are resolved with this PR. - Closes #8042 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tim Möhlmann --- .../events_testing/settings_instance_test.go | 2 + .../customize/notification-providers.mdx | 20 +- internal/api/grpc/admin/email_converters.go | 139 +- .../api/grpc/admin/email_converters_test.go | 373 ++++- .../api/grpc/admin/iam_settings_converter.go | 56 +- internal/api/grpc/admin/smtp.go | 30 +- .../v2/integration_test/query_test.go | 10 +- .../v2beta/integration_test/domain_test.go | 9 +- .../v2beta/integration_test/query_test.go | 8 +- .../org/v2beta/integration_test/org_test.go | 1300 ++++++++--------- internal/command/instance.go | 12 +- .../command/instance_smtp_config_model.go | 160 +- internal/command/smtp.go | 176 ++- internal/command/smtp_test.go | 733 +++++++++- internal/integration/feature.go | 35 +- .../notification/channels/smtp/channel.go | 22 +- internal/notification/channels/smtp/config.go | 16 +- .../channels/smtp/generic_auth.go | 41 + .../channels/smtp/plain_or_login_auth.go | 21 +- .../channels/smtp/xoauth2_auth.go | 96 ++ .../notification/handlers/config_email.go | 84 +- .../handlers/config_email_test.go | 24 +- .../handlers/notification_worker_test.go | 8 +- .../handlers/user_notifier_legacy_test.go | 8 +- internal/query/projection/smtp.go | 113 +- internal/query/projection/smtp_test.go | 67 +- internal/query/smtp.go | 125 +- internal/query/smtp_test.go | 152 +- internal/repository/instance/smtp_config.go | 80 +- proto/zitadel/admin.proto | 84 ++ proto/zitadel/settings.proto | 45 + 31 files changed, 2990 insertions(+), 1059 deletions(-) create mode 100644 internal/notification/channels/smtp/generic_auth.go create mode 100644 internal/notification/channels/smtp/xoauth2_auth.go diff --git a/backend/v3/storage/database/events_testing/settings_instance_test.go b/backend/v3/storage/database/events_testing/settings_instance_test.go index 9984a4393..474d577d5 100644 --- a/backend/v3/storage/database/events_testing/settings_instance_test.go +++ b/backend/v3/storage/database/events_testing/settings_instance_test.go @@ -1406,6 +1406,8 @@ func TestServer_TestInstanceSecretGeneratorSettingsReduces(t *testing.T) { ), ) require.NoError(collect, err) + require.NotNil(collect, setting) + // ClientSecret assert.Equal(collect, uint(64), *setting.ClientSecret.Length) assert.Equal(collect, true, *setting.ClientSecret.IncludeLowerLetters) diff --git a/docs/docs/guides/manage/customize/notification-providers.mdx b/docs/docs/guides/manage/customize/notification-providers.mdx index a33defcb9..0865366d1 100644 --- a/docs/docs/guides/manage/customize/notification-providers.mdx +++ b/docs/docs/guides/manage/customize/notification-providers.mdx @@ -16,14 +16,18 @@ ZITADEL integrates with Twilio as SMS provider. ## SMTP providers -Integration with most SMTP providers is possible through a generic SMTP provider template that allows you to configure custom SMTP providers. -Additionally, integration templates are available for: +For easy configuration, there are some integration templates available such as: - Amazon SES - Mailgun - Mailjet -- Postmark -- Sendgrid +- SendGrid +- ... + +Aside from those, it is also possible to configure a generic SMTP provider. With a generic SMTP provider you specify what +the SMTP server, username and what kind of auth method you want to use. Currently, the Console only supports plain auth. +However, with [the API's](https://zitadel.com/docs/apis/resources/admin/admin-service-add-email-provider-smtp) you can +configure XOAUTH2 as well. :::info Default Provider ZITADEL Cloud A default SMTP provider is configured for ZITADEL Cloud customers. @@ -46,7 +50,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w curl -L 'https://${CUSTOM_DOMAIN}/admin/v1/sms/http' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' \ + -H 'Authorization: Bearer ${TOKEN}' \ -d '{ "endpoint": "http://relay.example.com/provider", "description": "provider description" @@ -63,7 +67,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w curl -L 'https://${CUSTOM_DOMAIN}/admin/v1/sms/:id/_activate' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' \ + -H 'Authorization: Bearer ${TOKEN}' \ -d '{}' ``` @@ -80,7 +84,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w curl -L 'https://${CUSTOM_DOMAIN}/admin/v1/email/http' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' \ + -H 'Authorization: Bearer ${TOKEN}' \ -d '{ "endpoint": "http://relay.example.com/provider", "description": "provider description" @@ -97,7 +101,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w curl -L 'https://${CUSTOM_DOMAIN}/admin/v1/email/:id/_activate' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ - -H 'Authorization: Bearer ' \ + -H 'Authorization: Bearer ${TOKEN}' \ -d '{}' ``` diff --git a/internal/api/grpc/admin/email_converters.go b/internal/api/grpc/admin/email_converters.go index e574ad1b6..e7b5d2465 100644 --- a/internal/api/grpc/admin/email_converters.go +++ b/internal/api/grpc/admin/email_converters.go @@ -75,7 +75,7 @@ func httpToPb(http *query.HTTP) *settings_pb.EmailProvider_Http { } func smtpToPb(config *query.SMTP) *settings_pb.EmailProvider_Smtp { - return &settings_pb.EmailProvider_Smtp{ + ret := &settings_pb.EmailProvider_Smtp{ Smtp: &settings_pb.EmailProviderSMTP{ Tls: config.TLS, Host: config.Host, @@ -85,10 +85,34 @@ func smtpToPb(config *query.SMTP) *settings_pb.EmailProvider_Smtp { ReplyToAddress: config.ReplyToAddress, }, } + + if config.PlainAuth != nil { + ret.Smtp.Auth = &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + } + } + if config.XOAuth2Auth != nil { + xoauth2 := &settings_pb.EmailProviderSMTP_Xoauth2{ + Xoauth2: &settings_pb.SMTPXOAuth2Auth{ + TokenEndpoint: config.XOAuth2Auth.TokenEndpoint, + Scopes: config.XOAuth2Auth.Scopes, + }, + } + if config.XOAuth2Auth.ClientCredentials != nil { + xoauth2.Xoauth2.OAuth2Type = &settings_pb.SMTPXOAuth2Auth_ClientCredentials_{ + ClientCredentials: &settings_pb.SMTPXOAuth2Auth_ClientCredentials{ + ClientId: config.XOAuth2Auth.ClientCredentials.ClientId, + }, + } + } + ret.Smtp.Auth = xoauth2 + } + + return ret } func addEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.AddEmailProviderSMTPRequest) *command.AddSMTPConfig { - return &command.AddSMTPConfig{ + cmd := &command.AddSMTPConfig{ ResourceOwner: authz.GetInstance(ctx).InstanceID(), Description: req.Description, Tls: req.Tls, @@ -97,12 +121,41 @@ func addEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.AddEmailPro ReplyToAddress: req.ReplyToAddress, Host: req.Host, User: req.User, - Password: req.Password, } + + switch v := req.Auth.(type) { + case *admin_pb.AddEmailProviderSMTPRequest_None: + // Nothing to do, no auth is required + case *admin_pb.AddEmailProviderSMTPRequest_Plain: + cmd.PlainAuth = &command.PlainAuth{ + Password: v.Plain.Password, + } + case *admin_pb.AddEmailProviderSMTPRequest_Xoauth2: + if xoauth2, ok := v.Xoauth2.OAuth2Type.(*admin_pb.SMTPXOAuth2Auth_ClientCredentials_); ok { + cmd.XOAuth2Auth = &command.XOAuth2Auth{ + TokenEndpoint: v.Xoauth2.TokenEndpoint, + Scopes: v.Xoauth2.Scopes, + ClientCredentialsAuth: &command.OAuth2ClientCredentials{ + ClientId: xoauth2.ClientCredentials.ClientId, + ClientSecret: xoauth2.ClientCredentials.ClientSecret, + }, + } + } + default: + // ensure backwards compatibility + //nolint:staticcheck + if req.User != "" || req.Password != "" { + cmd.PlainAuth = &command.PlainAuth{ + Password: req.Password, + } + } + } + + return cmd } func updateEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.UpdateEmailProviderSMTPRequest) *command.ChangeSMTPConfig { - return &command.ChangeSMTPConfig{ + cmd := &command.ChangeSMTPConfig{ ResourceOwner: authz.GetInstance(ctx).InstanceID(), ID: req.Id, Description: req.Description, @@ -112,8 +165,41 @@ func updateEmailProviderSMTPToConfig(ctx context.Context, req *admin_pb.UpdateEm ReplyToAddress: req.ReplyToAddress, Host: req.Host, User: req.User, - Password: req.Password, } + + switch v := req.Auth.(type) { + case *admin_pb.UpdateEmailProviderSMTPRequest_None: + // Nothing to do, no auth is required + case *admin_pb.UpdateEmailProviderSMTPRequest_Plain: + cmd.PlainAuth = &command.PlainAuth{ + Password: v.Plain.Password, + } + case *admin_pb.UpdateEmailProviderSMTPRequest_Xoauth2: + cmd.XOAuth2Auth = xoauth2ToCmd(v) + default: + // ensure backwards compatibility + if req.User != "" || req.Password != "" { + cmd.PlainAuth = &command.PlainAuth{ + Password: req.Password, + } + } + } + + return cmd +} + +func xoauth2ToCmd(xoauth2 *admin_pb.UpdateEmailProviderSMTPRequest_Xoauth2) *command.XOAuth2Auth { + cmd := &command.XOAuth2Auth{ + TokenEndpoint: xoauth2.Xoauth2.TokenEndpoint, + Scopes: xoauth2.Xoauth2.Scopes, + } + if v, ok := xoauth2.Xoauth2.OAuth2Type.(*admin_pb.SMTPXOAuth2Auth_ClientCredentials_); ok { + cmd.ClientCredentialsAuth = &command.OAuth2ClientCredentials{ + ClientId: v.ClientCredentials.ClientId, + ClientSecret: v.ClientCredentials.ClientSecret, + } + } + return cmd } func addEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.AddEmailProviderHTTPRequest) *command.AddSMTPConfigHTTP { @@ -137,14 +223,49 @@ func updateEmailProviderHTTPToConfig(ctx context.Context, req *admin_pb.UpdateEm } func testEmailProviderSMTPToConfig(req *admin_pb.TestEmailProviderSMTPRequest) *smtp.Config { - return &smtp.Config{ + cfg := &smtp.Config{ Tls: req.Tls, From: req.SenderAddress, FromName: req.SenderName, SMTP: smtp.SMTP{ - Host: req.Host, - User: req.User, - Password: req.Password, + Host: req.Host, }, } + + switch v := req.Auth.(type) { + case *admin_pb.TestEmailProviderSMTPRequest_None: + // Nothing to do, no auth is required + case *admin_pb.TestEmailProviderSMTPRequest_Plain: + cfg.SMTP.PlainAuth = &smtp.PlainAuthConfig{ + User: req.User, + Password: v.Plain.Password, + } + case *admin_pb.TestEmailProviderSMTPRequest_Xoauth2: + cfg.SMTP.XOAuth2Auth = xoauth2ToSmtp(v, req.User) + default: + // ensure backwards compatibility + if req.User != "" || req.Password != "" { + cfg.SMTP.PlainAuth = &smtp.PlainAuthConfig{ + User: req.User, + Password: req.Password, + } + } + } + + return cfg +} + +func xoauth2ToSmtp(xoauth2 *admin_pb.TestEmailProviderSMTPRequest_Xoauth2, user string) *smtp.XOAuth2AuthConfig { + cfg := &smtp.XOAuth2AuthConfig{ + User: user, + TokenEndpoint: xoauth2.Xoauth2.TokenEndpoint, + Scopes: xoauth2.Xoauth2.Scopes, + } + if v, ok := xoauth2.Xoauth2.OAuth2Type.(*admin_pb.SMTPXOAuth2Auth_ClientCredentials_); ok { + cfg.ClientCredentialsAuth = &smtp.OAuth2ClientCredentials{ + ClientId: v.ClientCredentials.ClientId, + ClientSecret: v.ClientCredentials.ClientSecret, + } + } + return cfg } diff --git a/internal/api/grpc/admin/email_converters_test.go b/internal/api/grpc/admin/email_converters_test.go index 2cf6422fe..480c40d04 100644 --- a/internal/api/grpc/admin/email_converters_test.go +++ b/internal/api/grpc/admin/email_converters_test.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" object_pb "github.com/zitadel/zitadel/pkg/grpc/object" @@ -85,6 +86,7 @@ func Test_emailProvidersToPb(t *testing.T) { ReplyToAddress: "address", Host: "host", User: "user", + PlainAuth: &query.PlainAuth{}, }, HTTPConfig: nil, State: 1, @@ -125,6 +127,9 @@ func Test_emailProvidersToPb(t *testing.T) { Host: "host", User: "user", ReplyToAddress: "address", + Auth: &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + }, }, }, }, @@ -184,6 +189,7 @@ func Test_emailProviderToProviderPb(t *testing.T) { ReplyToAddress: "address", Host: "host", User: "user", + PlainAuth: &query.PlainAuth{}, }, HTTPConfig: &query.HTTP{ Endpoint: "endpoint", @@ -210,6 +216,9 @@ func Test_emailProviderToProviderPb(t *testing.T) { Host: "host", User: "user", ReplyToAddress: "address", + Auth: &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + }, }, }, }, @@ -254,7 +263,7 @@ func Test_emailProviderToProviderPb(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := emailProviderToProviderPb(tt.args.req) - assert.Equal(t, tt.res, got) + assert.EqualValues(t, tt.res, got) }) } } @@ -363,6 +372,7 @@ func Test_smtpToPb(t *testing.T) { TLS: true, Host: "host", User: "user", + PlainAuth: &query.PlainAuth{}, ReplyToAddress: "address", }, }, @@ -374,6 +384,74 @@ func Test_smtpToPb(t *testing.T) { Host: "host", User: "user", ReplyToAddress: "address", + Auth: &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + }, + }, + }, + }, + { + name: "legacy auth must still be supported", + args: args{ + req: &query.SMTP{ + User: "user", + PlainAuth: &query.PlainAuth{}, + }, + }, + res: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + User: "user", + Auth: &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + }, + }, + }, + }, + { + name: "plain auth must map to plain", + args: args{ + req: &query.SMTP{ + User: "plain-user", + PlainAuth: &query.PlainAuth{}, + }, + }, + res: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + User: "plain-user", + Auth: &settings_pb.EmailProviderSMTP_Plain{ + Plain: &settings_pb.SMTPPlainAuth{}, + }, + }, + }, + }, + { + name: "xoauth2 auth must map to xoauth2", + args: args{ + req: &query.SMTP{ + User: "xoauth2-user", + XOAuth2Auth: &query.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + ClientCredentials: &query.XOAuthClientCredentials{ + ClientId: "my-client", + }, + }, + }, + }, + res: &settings_pb.EmailProvider_Smtp{ + Smtp: &settings_pb.EmailProviderSMTP{ + User: "xoauth2-user", + Auth: &settings_pb.EmailProviderSMTP_Xoauth2{ + Xoauth2: &settings_pb.SMTPXOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + OAuth2Type: &settings_pb.SMTPXOAuth2Auth_ClientCredentials_{ + ClientCredentials: &settings_pb.SMTPXOAuth2Auth_ClientCredentials{ + ClientId: "my-client", + }, + }, + }, + }, }, }, }, @@ -412,17 +490,90 @@ func Test_addEmailProviderSMTPToConfig(t *testing.T) { }, }, res: &command.AddSMTPConfig{ - ResourceOwner: "instance", - Description: "description", - Host: "host", - User: "user", - Password: "password", + ResourceOwner: "instance", + Description: "description", + Host: "host", + User: "user", + PlainAuth: &command.PlainAuth{ + Password: "password", + }, Tls: true, From: "sender", FromName: "sendername", ReplyToAddress: "address", }, }, + { + name: "legacy auth (username password) should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddEmailProviderSMTPRequest{ + User: "user", + Password: "password", + }, + }, + res: &command.AddSMTPConfig{ + ResourceOwner: "instance", + User: "user", + PlainAuth: &command.PlainAuth{ + Password: "password", + }, + }, + }, + { + name: "plain auth should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddEmailProviderSMTPRequest{ + User: "plain-user", + Auth: &admin_pb.AddEmailProviderSMTPRequest_Plain{ + Plain: &admin_pb.SMTPPlainAuth{ + Password: "other_password", + }, + }, + }, + }, + res: &command.AddSMTPConfig{ + ResourceOwner: "instance", + User: "plain-user", + PlainAuth: &command.PlainAuth{ + Password: "other_password", + }, + }, + }, + { + name: "xoauth2 auth should map to xoauth2", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.AddEmailProviderSMTPRequest{ + User: "xoauth2-user", + Auth: &admin_pb.AddEmailProviderSMTPRequest_Xoauth2{ + Xoauth2: &admin_pb.SMTPXOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + OAuth2Type: &admin_pb.SMTPXOAuth2Auth_ClientCredentials_{ + ClientCredentials: &admin_pb.SMTPXOAuth2Auth_ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, + }, + }, + res: &command.AddSMTPConfig{ + ResourceOwner: "instance", + User: "xoauth2-user", + XOAuth2Auth: &command.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + ClientCredentialsAuth: &command.OAuth2ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -459,18 +610,97 @@ func Test_updateEmailProviderSMTPToConfig(t *testing.T) { }, }, res: &command.ChangeSMTPConfig{ - ResourceOwner: "instance", - ID: "id", - Description: "description", - Host: "host", - User: "user", - Password: "password", + ResourceOwner: "instance", + ID: "id", + Description: "description", + Host: "host", + User: "user", + PlainAuth: &command.PlainAuth{ + Password: "password", + }, Tls: true, From: "sender", FromName: "sendername", ReplyToAddress: "address", }, }, + { + name: "legacy auth (username password) should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateEmailProviderSMTPRequest{ + User: "user", + Password: "password", + Id: "id", + }, + }, + res: &command.ChangeSMTPConfig{ + ResourceOwner: "instance", + ID: "id", + User: "user", + PlainAuth: &command.PlainAuth{ + Password: "password", + }, + }, + }, + { + name: "plain auth should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateEmailProviderSMTPRequest{ + Id: "id", + User: "plain-user", + Auth: &admin_pb.UpdateEmailProviderSMTPRequest_Plain{ + Plain: &admin_pb.SMTPPlainAuth{ + Password: "other_password", + }, + }, + }, + }, + res: &command.ChangeSMTPConfig{ + ResourceOwner: "instance", + ID: "id", + User: "plain-user", + PlainAuth: &command.PlainAuth{ + Password: "other_password", + }, + }, + }, + { + name: "xoauth2 auth should map to xoauth2", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.UpdateEmailProviderSMTPRequest{ + Id: "id", + User: "xoauth2-user", + Auth: &admin_pb.UpdateEmailProviderSMTPRequest_Xoauth2{ + Xoauth2: &admin_pb.SMTPXOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + OAuth2Type: &admin_pb.SMTPXOAuth2Auth_ClientCredentials_{ + ClientCredentials: &admin_pb.SMTPXOAuth2Auth_ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, + }, + }, + res: &command.ChangeSMTPConfig{ + ResourceOwner: "instance", + ID: "id", + User: "xoauth2-user", + XOAuth2Auth: &command.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + ClientCredentialsAuth: &command.OAuth2ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -552,3 +782,122 @@ func Test_updateEmailProviderHTTPToConfig(t *testing.T) { }) } } + +func Test_testEmailProviderSMTPToConfig(t *testing.T) { + type args struct { + ctx context.Context + req *admin_pb.TestEmailProviderSMTPRequest + } + tests := []struct { + name string + args args + res *smtp.Config + }{ + { + name: "all fields filled", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.TestEmailProviderSMTPRequest{ + SenderAddress: "sender", + SenderName: "sendername", + Tls: true, + Host: "host", + User: "user", + Password: "password", + }, + }, + res: &smtp.Config{ + Tls: true, + From: "sender", + FromName: "sendername", + SMTP: smtp.SMTP{ + Host: "host", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "password", + }, + }, + }, + }, + { + name: "legacy auth (username password) should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.TestEmailProviderSMTPRequest{ + User: "user", + Password: "password", + }, + }, + res: &smtp.Config{ + SMTP: smtp.SMTP{ + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "password", + }, + }, + }, + }, + { + name: "plain auth should map to plain", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.TestEmailProviderSMTPRequest{ + User: "plain-user", + Auth: &admin_pb.TestEmailProviderSMTPRequest_Plain{ + Plain: &admin_pb.SMTPPlainAuth{ + Password: "other_password", + }, + }, + }, + }, + res: &smtp.Config{ + SMTP: smtp.SMTP{ + PlainAuth: &smtp.PlainAuthConfig{ + User: "plain-user", + Password: "other_password", + }, + }, + }, + }, + { + name: "xoauth2 auth should map to xoauth2", + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance"), + req: &admin_pb.TestEmailProviderSMTPRequest{ + User: "xoauth2-user", + Auth: &admin_pb.TestEmailProviderSMTPRequest_Xoauth2{ + Xoauth2: &admin_pb.SMTPXOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + OAuth2Type: &admin_pb.SMTPXOAuth2Auth_ClientCredentials_{ + ClientCredentials: &admin_pb.SMTPXOAuth2Auth_ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, + }, + }, + res: &smtp.Config{ + SMTP: smtp.SMTP{ + XOAuth2Auth: &smtp.XOAuth2AuthConfig{ + User: "xoauth2-user", + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scopes"}, + ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{ + ClientId: "my-client", + ClientSecret: "some-secret", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := testEmailProviderSMTPToConfig(tt.args.req) + assert.Equal(t, tt.res, got) + }) + } +} diff --git a/internal/api/grpc/admin/iam_settings_converter.go b/internal/api/grpc/admin/iam_settings_converter.go index bfdebe35f..f3ce470a2 100644 --- a/internal/api/grpc/admin/iam_settings_converter.go +++ b/internal/api/grpc/admin/iam_settings_converter.go @@ -133,8 +133,9 @@ func SecretGeneratorTypeToDomain(generatorType settings_pb.SecretGeneratorType) } } +// Deprecated: use [addEmailProviderSMTPToConfig] instead func addSMTPToConfig(ctx context.Context, req *admin_pb.AddSMTPConfigRequest) *command.AddSMTPConfig { - return &command.AddSMTPConfig{ + cmd := &command.AddSMTPConfig{ ResourceOwner: authz.GetInstance(ctx).InstanceID(), Description: req.Description, Tls: req.Tls, @@ -143,12 +144,20 @@ func addSMTPToConfig(ctx context.Context, req *admin_pb.AddSMTPConfigRequest) *c ReplyToAddress: req.ReplyToAddress, Host: req.Host, User: req.User, - Password: req.Password, } + + if req.User != "" || req.Password != "" { + cmd.PlainAuth = &command.PlainAuth{ + Password: req.Password, + } + } + + return cmd } +// Deprecated: use [updateEmailProviderSMTPToConfig] instead func updateSMTPToConfig(ctx context.Context, req *admin_pb.UpdateSMTPConfigRequest) *command.ChangeSMTPConfig { - return &command.ChangeSMTPConfig{ + cmd := &command.ChangeSMTPConfig{ ResourceOwner: authz.GetInstance(ctx).InstanceID(), ID: req.Id, Description: req.Description, @@ -158,26 +167,35 @@ func updateSMTPToConfig(ctx context.Context, req *admin_pb.UpdateSMTPConfigReque ReplyToAddress: req.ReplyToAddress, Host: req.Host, User: req.User, - Password: req.Password, } -} -func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig { - if smtp.SMTPConfig != nil { - return &settings_pb.SMTPConfig{ - Description: smtp.Description, - Tls: smtp.SMTPConfig.TLS, - SenderAddress: smtp.SMTPConfig.SenderAddress, - SenderName: smtp.SMTPConfig.SenderName, - ReplyToAddress: smtp.SMTPConfig.ReplyToAddress, - Host: smtp.SMTPConfig.Host, - User: smtp.SMTPConfig.User, - Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.ResourceOwner), - Id: smtp.ID, - State: settings_pb.SMTPConfigState(smtp.State), + if req.User != "" || req.Password != "" { + cmd.PlainAuth = &command.PlainAuth{ + Password: req.Password, } } - return nil + + return cmd +} + +// Deprecated: use [smtpToPb] instead. +func SMTPConfigToPb(smtp *query.SMTPConfig) *settings_pb.SMTPConfig { + if smtp.SMTPConfig == nil { + return nil + } + + return &settings_pb.SMTPConfig{ + Description: smtp.Description, + Tls: smtp.SMTPConfig.TLS, + SenderAddress: smtp.SMTPConfig.SenderAddress, + SenderName: smtp.SMTPConfig.SenderName, + ReplyToAddress: smtp.SMTPConfig.ReplyToAddress, + Host: smtp.SMTPConfig.Host, + Details: obj_grpc.ToViewDetailsPb(smtp.Sequence, smtp.CreationDate, smtp.ChangeDate, smtp.ResourceOwner), + Id: smtp.ID, + State: settings_pb.SMTPConfigState(smtp.State), + User: smtp.SMTPConfig.User, + } } func SecurityPolicyToPb(policy *query.SecurityPolicy) *settings_pb.SecurityPolicy { diff --git a/internal/api/grpc/admin/smtp.go b/internal/api/grpc/admin/smtp.go index b695e7022..4107eb73f 100644 --- a/internal/api/grpc/admin/smtp.go +++ b/internal/api/grpc/admin/smtp.go @@ -9,6 +9,7 @@ import ( admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) +// Deprecated: use [GetEmailProvider] instead. func (s *Server) GetSMTPConfig(ctx context.Context, req *admin_pb.GetSMTPConfigRequest) (*admin_pb.GetSMTPConfigResponse, error) { smtp, err := s.query.SMTPConfigActive(ctx, authz.GetInstance(ctx).InstanceID()) if err != nil { @@ -19,6 +20,7 @@ func (s *Server) GetSMTPConfig(ctx context.Context, req *admin_pb.GetSMTPConfigR }, nil } +// Deprecated: use [GetEmailProviderById] instead. func (s *Server) GetSMTPConfigById(ctx context.Context, req *admin_pb.GetSMTPConfigByIdRequest) (*admin_pb.GetSMTPConfigByIdResponse, error) { smtp, err := s.query.SMTPConfigByID(ctx, authz.GetInstance(ctx).InstanceID(), req.Id) if err != nil { @@ -29,6 +31,7 @@ func (s *Server) GetSMTPConfigById(ctx context.Context, req *admin_pb.GetSMTPCon }, nil } +// Deprecated: use [AddEmailProviderSMTP] instead. func (s *Server) AddSMTPConfig(ctx context.Context, req *admin_pb.AddSMTPConfigRequest) (*admin_pb.AddSMTPConfigResponse, error) { config := addSMTPToConfig(ctx, req) if err := s.command.AddSMTPConfig(ctx, config); err != nil { @@ -40,6 +43,7 @@ func (s *Server) AddSMTPConfig(ctx context.Context, req *admin_pb.AddSMTPConfigR }, nil } +// Deprecated: use [UpdateEmailProviderSMTP] instead. func (s *Server) UpdateSMTPConfig(ctx context.Context, req *admin_pb.UpdateSMTPConfigRequest) (*admin_pb.UpdateSMTPConfigResponse, error) { config := updateSMTPToConfig(ctx, req) if err := s.command.ChangeSMTPConfig(ctx, config); err != nil { @@ -50,6 +54,7 @@ func (s *Server) UpdateSMTPConfig(ctx context.Context, req *admin_pb.UpdateSMTPC }, nil } +// Deprecated: use [RemoveEmailProviderSMTP] instead. func (s *Server) RemoveSMTPConfig(ctx context.Context, req *admin_pb.RemoveSMTPConfigRequest) (*admin_pb.RemoveSMTPConfigResponse, error) { details, err := s.command.RemoveSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id) if err != nil { @@ -60,6 +65,7 @@ func (s *Server) RemoveSMTPConfig(ctx context.Context, req *admin_pb.RemoveSMTPC }, nil } +// Deprecated: use [UpdateEmailProviderSMTPPassword] instead. func (s *Server) UpdateSMTPConfigPassword(ctx context.Context, req *admin_pb.UpdateSMTPConfigPasswordRequest) (*admin_pb.UpdateSMTPConfigPasswordResponse, error) { details, err := s.command.ChangeSMTPConfigPassword(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.Password) if err != nil { @@ -70,6 +76,7 @@ func (s *Server) UpdateSMTPConfigPassword(ctx context.Context, req *admin_pb.Upd }, nil } +// Deprecated: use [ListEmailProviders] instead. func (s *Server) ListSMTPConfigs(ctx context.Context, req *admin_pb.ListSMTPConfigsRequest) (*admin_pb.ListSMTPConfigsResponse, error) { queries, err := listSMTPConfigsToModel(req) if err != nil { @@ -85,6 +92,7 @@ func (s *Server) ListSMTPConfigs(ctx context.Context, req *admin_pb.ListSMTPConf }, nil } +// Deprecated: use [ActivateEmailProvider] instead. func (s *Server) ActivateSMTPConfig(ctx context.Context, req *admin_pb.ActivateSMTPConfigRequest) (*admin_pb.ActivateSMTPConfigResponse, error) { result, err := s.command.ActivateSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id) if err != nil { @@ -96,6 +104,7 @@ func (s *Server) ActivateSMTPConfig(ctx context.Context, req *admin_pb.ActivateS }, nil } +// Deprecated: use [DeactivateEmailProvider] instead. func (s *Server) DeactivateSMTPConfig(ctx context.Context, req *admin_pb.DeactivateSMTPConfigRequest) (*admin_pb.DeactivateSMTPConfigResponse, error) { result, err := s.command.DeactivateSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id) if err != nil { @@ -107,6 +116,7 @@ func (s *Server) DeactivateSMTPConfig(ctx context.Context, req *admin_pb.Deactiv }, nil } +// Deprecated: use [TestEmailProviderSMTPById] instead. func (s *Server) TestSMTPConfigById(ctx context.Context, req *admin_pb.TestSMTPConfigByIdRequest) (*admin_pb.TestSMTPConfigByIdResponse, error) { err := s.command.TestSMTPConfigById(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.ReceiverAddress) if err != nil { @@ -116,14 +126,20 @@ func (s *Server) TestSMTPConfigById(ctx context.Context, req *admin_pb.TestSMTPC return &admin_pb.TestSMTPConfigByIdResponse{}, nil } +// Deprecated: use [TestEmailProviderSMTP] instead. func (s *Server) TestSMTPConfig(ctx context.Context, req *admin_pb.TestSMTPConfigRequest) (*admin_pb.TestSMTPConfigResponse, error) { - config := smtp.Config{} - config.Tls = req.Tls - config.From = req.SenderAddress - config.FromName = req.SenderName - config.SMTP.Host = req.Host - config.SMTP.User = req.User - config.SMTP.Password = req.Password + config := smtp.Config{ + Tls: req.Tls, + From: req.SenderAddress, + FromName: req.SenderName, + SMTP: smtp.SMTP{ + Host: req.Host, + PlainAuth: &smtp.PlainAuthConfig{ + User: req.User, + Password: req.Password, + }, + }, + } err := s.command.TestSMTPConfig(ctx, authz.GetInstance(ctx).InstanceID(), req.Id, req.ReceiverAddress, &config) if err != nil { diff --git a/internal/api/grpc/instance/v2/integration_test/query_test.go b/internal/api/grpc/instance/v2/integration_test/query_test.go index dad3c6a24..381c543b8 100644 --- a/internal/api/grpc/instance/v2/integration_test/query_test.go +++ b/internal/api/grpc/instance/v2/integration_test/query_test.go @@ -165,7 +165,7 @@ func TestListInstances(t *testing.T) { orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner) - relTableState := integration.RelationalTablesEnableMatrix() + relTableState := integration.RelationalTablesEnableMatrix(t, ctx, ctxWithSysAuthZ) tt := []struct { testName string @@ -222,16 +222,13 @@ func TestListInstances(t *testing.T) { } for _, stateCase := range relTableState { - integration.EnsureInstanceFeature(t, ctx, inst, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) for _, tc := range tt { // TODO(IAM-Marco): Fix this test for relational case when permission checks are in place (see https://github.com/zitadel/zitadel/issues/10917) - if tc.testName == "when unauthZ context should return unauthZ error" && stateCase.State == "when relational tables are enabled" { + if tc.testName == "when unauthZ context should return unauthZ error" && stateCase.Name == "when relational tables are enabled" { continue } - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tc.testName), func(t *testing.T) { + t.Run(fmt.Sprintf("%s - %s", stateCase.Name, tc.testName), func(t *testing.T) { // Test res, err := inst.Client.InstanceV2.ListInstances(tc.inputContext, tc.inputRequest) @@ -251,7 +248,6 @@ func TestListInstances(t *testing.T) { }) } } - } func TestListCustomDomains(t *testing.T) { diff --git a/internal/api/grpc/instance/v2beta/integration_test/domain_test.go b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go index 6370d1db1..8cd24fa7a 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/domain_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go @@ -34,9 +34,12 @@ func TestAddCustomDomain(t *testing.T) { relationalInst := integration.NewInstance(ctxWithSysAuthZ) iamOwnerRelationalCtx := relationalInst.WithAuthorizationToken(context.Background(), integration.UserTypeIAMOwner) - integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, relationalInst, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.True(tCollect, got.EnableRelationalTables.GetEnabled()) - }) + integration.EnsureInstanceFeature(t, ctxWithSysAuthZ, relationalInst, + &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, + func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { + assert.True(tCollect, got.EnableRelationalTables.GetEnabled()) + }, + ) t.Cleanup(func() { inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go index f648a7ed9..f777ade4d 100644 --- a/internal/api/grpc/instance/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -140,7 +140,7 @@ func TestListInstances(t *testing.T) { orgOwnerCtx := inst.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner) - relTableState := integration.RelationalTablesEnableMatrix() + relTableState := integration.RelationalTablesEnableMatrix(t, ctx, ctxWithSysAuthZ) tt := []struct { testName string @@ -196,12 +196,8 @@ func TestListInstances(t *testing.T) { } for _, stateCase := range relTableState { - integration.EnsureInstanceFeature(t, ctx, inst, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - for _, tc := range tt { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tc.testName), func(t *testing.T) { + t.Run(fmt.Sprintf("%s - %s", stateCase.Name, tc.testName), func(t *testing.T) { // Test res, err := inst.Client.InstanceV2Beta.ListInstances(tc.inputContext, tc.inputRequest) diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index b61ba43af..31d27e138 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -5,7 +5,6 @@ package org_test import ( "context" "errors" - "fmt" "os" "slices" "strings" @@ -18,8 +17,8 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/instance/v2" metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" @@ -301,536 +300,470 @@ func TestServer_CreateOrganization(t *testing.T) { } func TestServer_UpdateOrganization(t *testing.T) { - ctx := Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(ctx, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) + sysAuthZ := integration.WithSystemAuthorization(ctx) - relTableState := integration.RelationalTablesEnableMatrix() + for _, relTableCase := range integration.RelationalTablesEnableMatrix(t, ctx, sysAuthZ) { + t.Run(relTableCase.Name, func(tt *testing.T) { + inst := relTableCase.Inst + client := inst.Client.OrgV2beta + instOwner := relTableCase.InstOwner - cases := len(relTableState) - orgs, orgNames, _ := createOrgs(CTX, t, Client, 2*cases) + tt.Cleanup(func() { + _, err := inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + assert.NoError(tt, err) + }) - for i, stateCase := range relTableState { - integration.EnsureInstanceFeature(t, ctx, Instance, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - tests := []struct { - name string - ctx context.Context - req *v2beta_org.UpdateOrganizationRequest - want *v2beta_org.UpdateOrganizationResponse - wantErr bool - }{ - { - name: "update org with new name", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.UpdateOrganizationRequest{ - Id: orgs[0+(i*cases)].GetId(), - Name: fmt.Sprintf("new org name %d", i), - }, - }, - { - name: "update org with same name", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.UpdateOrganizationRequest{ - Id: orgs[1+(i*cases)].GetId(), - Name: orgNames[1+(i*cases)], - }, - wantErr: true, - }, - { - name: "update org with non existent org id", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.UpdateOrganizationRequest{ - Id: "non existent org id", + tt.Run("happy/new name", func(ttt *testing.T) { + // given + organisation, orgName, _ := createOrg(sysAuthZ, ttt, client) + + // when + _, err := client.UpdateOrganization(instOwner, &v2beta_org.UpdateOrganizationRequest{ + Id: organisation.GetId(), + Name: "Not " + orgName, + }) + + // then + assert.NoError(ttt, err) + }) + + tt.Run("unhappy: same name", func(ttt *testing.T) { + // given + organisation, orgName, _ := createOrg(sysAuthZ, ttt, client) + + // when + _, err := client.UpdateOrganization(instOwner, &v2beta_org.UpdateOrganizationRequest{ + Id: organisation.GetId(), + Name: orgName, + }) + + // then + assert.Error(ttt, err) + }) + + tt.Run("unhappy: non existent org id", func(ttt *testing.T) { + // when + _, err := client.UpdateOrganization(instOwner, &v2beta_org.UpdateOrganizationRequest{ + Id: "non existing org id", Name: "new name", - }, - wantErr: true, - }, - { - name: "update org with no id", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.UpdateOrganizationRequest{ + }) + + // then + assert.Error(ttt, err) + }) + + tt.Run("unhappy: no org id", func(ttt *testing.T) { + // given + createOrg(sysAuthZ, ttt, client) + + // when + _, err := client.UpdateOrganization(instOwner, &v2beta_org.UpdateOrganizationRequest{ Id: " ", Name: "new name", - }, - wantErr: true, - }, - { - name: "no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), - req: &v2beta_org.UpdateOrganizationRequest{ - Id: orgs[1+(i*cases)].GetId(), - Name: integration.OrganizationName(), - }, - wantErr: !stateCase.FeatureSet.GetEnableRelationalTables(), //TODO: implement for relational tables - }, - } - for _, tt := range tests { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tt.name), func(t1 *testing.T) { - got, err := Client.UpdateOrganization(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t1, err) - return - } - require.NoError(t1, err) + }) - // check details - gotCD := got.GetChangeDate().AsTime() - now := time.Now() - assert.WithinRange(t1, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + // then + assert.Error(ttt, err) }) - } + + tt.Run("unhappy: no permission", func(ttt *testing.T) { + // given + organisation, orgName, _ := createOrg(sysAuthZ, ttt, client) + + // when + _, err := client.UpdateOrganization( + inst.WithAuthorizationToken(ctx, integration.UserTypeOrgOwner), + &v2beta_org.UpdateOrganizationRequest{ + Id: organisation.Id, + Name: orgName, + }, + ) + + // then + assert.Error(ttt, err) + }) + }) } } func TestServer_ListOrganizations(t *testing.T) { - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(CTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + sysAuthZ := integration.WithSystemAuthorization(ctx) testStartTimestamp := time.Now() - listOrgInstance := integration.NewInstance(CTX) - listOrgIAmOwnerCtx := listOrgInstance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) - listOrgClient := listOrgInstance.Client.OrgV2beta + for _, relTestCase := range integration.RelationalTablesEnableMatrix(t, ctx, sysAuthZ) { + t.Run(relTestCase.Name, func(tt *testing.T) { + inst := relTestCase.Inst + client := inst.Client.OrgV2beta + instOwner := relTestCase.InstOwner - noOfOrgs := 3 - orgs, orgsName, orgsDomain := createOrgs(listOrgIAmOwnerCtx, t, listOrgClient, noOfOrgs) - - // deactivate org[1] - _, err := listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ - Id: orgs[1].Id, - }) - require.NoError(t, err) - - relTableState := integration.RelationalTablesEnableMatrix() - - tests := []struct { - name string - ctx context.Context - query []*v2beta_org.OrganizationSearchFilter - want *v2beta_org.ListOrganizationsResponse - err error - }{ - // TODO: re-enable when permission model is implemented in relational tables - //{ - // name: "list organizations, without required permissions", - // ctx: listOrgInstance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), - // want: &v2beta_org.ListOrganizationsResponse{ - // Pagination: &filter.PaginationResponse{ - // TotalResult: 4, - // }, - // }, - //}, - { - name: "list organizations happy path, no filter", - ctx: listOrgIAmOwnerCtx, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 4, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: listOrgInstance.DefaultOrg.Id, - Name: listOrgInstance.DefaultOrg.Name, - }, - { - Id: orgs[0].Id, - Name: orgsName[0], - }, - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - { - Id: orgs[2].Id, - Name: orgsName[2], - }, - }, - }, - }, - { - name: "list organizations by id happy path", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ - IdFilter: &v2beta_org.OrgIDFilter{ - Id: orgs[1].Id, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations by state active", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ - StateFilter: &v2beta_org.OrgStateFilter{ - State: v2beta_org.OrgState_ORG_STATE_ACTIVE, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 3, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: listOrgInstance.DefaultOrg.Id, - Name: listOrgInstance.DefaultOrg.Name, - }, - { - Id: orgs[0].Id, - Name: orgsName[0], - }, - { - Id: orgs[2].Id, - Name: orgsName[2], - }, - }, - }, - }, - { - name: "list organizations by state inactive", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ - StateFilter: &v2beta_org.OrgStateFilter{ - State: v2beta_org.OrgState_ORG_STATE_INACTIVE, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations by id bad id", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ - IdFilter: &v2beta_org.OrgIDFilter{ - Id: "bad id", - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 0, - }, - Organizations: nil, - }, - }, - { - name: "list organizations specify org name equals", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ - NameFilter: &v2beta_org.OrgNameFilter{ - Name: orgsName[1], - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations specify org name contains", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ - NameFilter: &v2beta_org.OrgNameFilter{ - Name: func() string { - return orgsName[1][1 : len(orgsName[1])-2] - }(), - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations specify org name contains IGNORE CASE", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ - NameFilter: &v2beta_org.OrgNameFilter{ - Name: func() string { - return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) - }(), - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations specify domain name equals", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &v2beta_org.OrgDomainFilter{ - Domain: orgsDomain[1], - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations specify domain name contains", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &v2beta_org.OrgDomainFilter{ - Domain: orgsDomain[1][1 : len(orgsDomain[1])-2], - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - { - name: "list organizations specify org name contains IGNORE CASE", - ctx: listOrgIAmOwnerCtx, - query: []*v2beta_org.OrganizationSearchFilter{ - { - Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ - DomainFilter: &v2beta_org.OrgDomainFilter{ - Domain: strings.ToUpper(orgsDomain[1][1 : len(orgsDomain[1])-2]), - Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, - }, - }, - }, - }, - want: &v2beta_org.ListOrganizationsResponse{ - Pagination: &filter.PaginationResponse{ - TotalResult: 1, - }, - Organizations: []*v2beta_org.Organization{ - { - Id: orgs[1].Id, - Name: orgsName[1], - }, - }, - }, - }, - } - - for _, stateCase := range relTableState { - integration.EnsureInstanceFeature(t, listOrgIAmOwnerCtx, listOrgInstance, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - - for _, tt := range tests { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tt.name), func(t *testing.T) { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 20*time.Second) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ - Filter: tt.query, - Pagination: &filter.PaginationRequest{ - Asc: true, - }, - SortingColumn: v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE, - }) - if tt.err != nil { - require.ErrorContains(ttt, err, tt.err.Error()) - return - } - require.NoError(ttt, err) - - require.Equal(ttt, tt.want.GetPagination(), got.GetPagination()) - - require.Len(ttt, got.Organizations, len(tt.want.Organizations)) - - for i, got := range got.Organizations { - // created/chagned date - gotCD := got.GetCreationDate().AsTime() - now := time.Now() - assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) - gotCD = got.GetChangedDate().AsTime() - assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) - - assert.Equal(ttt, tt.want.Organizations[i].Id, got.Id) - assert.Equal(ttt, tt.want.Organizations[i].Name, got.Name) - } - }, retryDuration, tick, "timeout waiting for expected organizations being created") + tt.Cleanup(func() { + _, err := inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + assert.NoError(tt, err) }) - } + + orgs := make([]struct { + organisation *v2beta_org.CreateOrganizationResponse + name string + domain string + }, 3) + + for i := range orgs { + orgs[i].organisation, orgs[i].name, orgs[i].domain = createOrg(instOwner, tt, client) + } + + // deactivate org[1] + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].organisation.Id, + }) + require.NoError(t, err) + + // TODO: create test for when permission model is implemented in relational tables + + happyTestCases := []struct { + name string + query []*v2beta_org.OrganizationSearchFilter + want *v2beta_org.ListOrganizationsResponse + }{ + { + name: "list organizations happy path, no filter", + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + }, + Organizations: []*v2beta_org.Organization{ + {Id: inst.DefaultOrg.Id, Name: inst.DefaultOrg.Name}, + {Id: orgs[0].organisation.Id, Name: orgs[0].name}, + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + {Id: orgs[2].organisation.Id, Name: orgs[2].name}, + }, + }, + }, + { + name: "list organizations by id happy path", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].organisation.Id, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations by state active", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + }, + Organizations: []*v2beta_org.Organization{ + {Id: inst.DefaultOrg.Id, Name: inst.DefaultOrg.Name}, + {Id: orgs[0].organisation.Id, Name: orgs[0].name}, + {Id: orgs[2].organisation.Id, Name: orgs[2].name}, + }, + }, + }, + { + name: "list organizations by state inactive", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations by id bad id", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + }, + Organizations: nil, + }, + }, + { + name: "list organizations specify org name equals", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgs[1].name, + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations specify org name contains", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgs[1].name[1 : len(orgs[1].name)-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgs[1].name[1 : len(orgs[1].name)-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations specify domain name equals", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: orgs[1].domain, + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations specify domain name contains", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: orgs[1].domain[1 : len(orgs[1].domain)-2], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: strings.ToUpper(orgs[1].domain[1 : len(orgs[1].domain)-2]), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: &v2beta_org.ListOrganizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + }, + Organizations: []*v2beta_org.Organization{ + {Id: orgs[1].organisation.Id, Name: orgs[1].name}, + }, + }, + }, + } + + for _, happyTestCase := range happyTestCases { + tt.Run(happyTestCase.name, func(ttt *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 20*time.Second) + require.EventuallyWithT(ttt, func(tttt *assert.CollectT) { + got, err := client.ListOrganizations(instOwner, &v2beta_org.ListOrganizationsRequest{ + Filter: happyTestCase.query, + Pagination: &filter.PaginationRequest{Asc: true}, + SortingColumn: v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE, + }) + require.NoError(tttt, err) + + require.Equal(tttt, happyTestCase.want.GetPagination(), got.GetPagination()) + + require.Len(tttt, got.Organizations, len(happyTestCase.want.Organizations)) + + for i, got := range got.Organizations { + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(tttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(tttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + + assert.Equal(tttt, happyTestCase.want.Organizations[i].Id, got.Id) + assert.Equal(tttt, happyTestCase.want.Organizations[i].Name, got.Name) + } + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } + }) } } func TestServer_DeleteOrganization(t *testing.T) { - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(CTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() - relTableState := integration.RelationalTablesEnableMatrix() - var orgs []*v2beta_org.CreateOrganizationResponse - orgsNumPerCase := 3 - orgs, _, _ = createOrgs(CTX, t, Client, orgsNumPerCase*len(relTableState)) - require.NotNil(t, orgs) - require.NotEmpty(t, orgs) + sysAuthZ := integration.WithSystemAuthorization(ctx) - for i, stateCase := range relTableState { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - deleteRes, err := Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[2+(orgsNumPerCase*i)].GetId()}) - assert.Nil(ttt, err) - assert.NotNil(ttt, deleteRes) - assert.NotZero(ttt, deleteRes.GetDeletionDate()) - }, retryDuration, tick) + for _, relTableTestCase := range integration.RelationalTablesEnableMatrix(t, ctx, sysAuthZ) { + t.Run(relTableTestCase.Name, func(tt *testing.T) { + inst := relTableTestCase.Inst + client := inst.Client.OrgV2beta + instOwner := relTableTestCase.InstOwner - tests := []struct { - name string - ctx context.Context - req *v2beta_org.DeleteOrganizationRequest - want *v2beta_org.DeleteOrganizationResponse - dontCheckTime bool - err error - }{ - { - name: "delete org no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), - req: &v2beta_org.DeleteOrganizationRequest{ - Id: orgs[0+(orgsNumPerCase*i)].GetId(), - }, - err: errors.New("membership not found"), - }, - { - name: "delete org happy path", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.DeleteOrganizationRequest{ - Id: orgs[1+(orgsNumPerCase*i)].GetId(), - }, - }, - { - name: "delete already deleted org", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.DeleteOrganizationRequest{Id: orgs[2+(orgsNumPerCase*i)].GetId()}, - dontCheckTime: true, - }, - { - name: "delete non existent org", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner), - req: &v2beta_org.DeleteOrganizationRequest{ - Id: "non existent org id", - }, - dontCheckTime: true, - }, - } - integration.EnsureInstanceFeature(t, CTX, Instance, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - for _, tt := range tests { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tt.name), func(t *testing.T) { - got, err := Client.DeleteOrganization(tt.ctx, tt.req) - if tt.err != nil { - require.Contains(t, err.Error(), tt.err.Error()) - return + tt.Cleanup(func() { + _, err := inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + assert.NoError(tt, err) + }) + + tt.Run("happy path", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + + // when + got, err := client.DeleteOrganization(instOwner, &v2beta_org.DeleteOrganizationRequest{Id: organisation.Id}) + + // then + assert.NoError(ttt, err) + deletionTime := got.GetDeletionDate().AsTime() + now := time.Now() + assert.WithinRange(t, deletionTime, now.Add(-time.Minute), now.Add(time.Minute)) + }) + + tt.Run("unhappy: no permission", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + usersWithoutPermissions := []integration.UserType{ + integration.UserTypeOrgOwner, } - require.NoError(t, err) - // check details - gotCD := got.GetDeletionDate().AsTime() - if !tt.dontCheckTime { - now := time.Now() - assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + for _, userType := range usersWithoutPermissions { + ttt.Run(userType.String(), func(tttt *testing.T) { + u := inst.WithAuthorizationToken(ctx, userType) + + // when + _, err := client.DeleteOrganization(u, &v2beta_org.DeleteOrganizationRequest{Id: organisation.Id}) + + // then + assert.ErrorContains(tttt, err, "membership not found") + }) } }) - } + + tt.Run("happy: already deleted", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + _, err := client.DeleteOrganization(instOwner, &v2beta_org.DeleteOrganizationRequest{Id: organisation.Id}) + require.NoError(ttt, err) + + // when + _, err = client.DeleteOrganization(instOwner, &v2beta_org.DeleteOrganizationRequest{Id: organisation.Id}) + + // then + require.NoError(ttt, err) + }) + + tt.Run("happy: non existing", func(ttt *testing.T) { + // when + _, err := client.DeleteOrganization(instOwner, &v2beta_org.DeleteOrganizationRequest{Id: "non existing org id"}) + + // then + require.NoError(ttt, err) + }) + }) } } @@ -851,176 +784,151 @@ func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { } func TestServer_ActivateOrganization(t *testing.T) { - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(CTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) - relTableState := integration.RelationalTablesEnableMatrix() + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() - orgsNumPerCase := 2 - orgs, _, _ := createOrgs(CTX, t, Client, orgsNumPerCase*len(relTableState)) - require.NotNil(t, orgs) - require.NotEmpty(t, orgs) + sysAuthZ := integration.WithSystemAuthorization(ctx) - for i, stateCase := range relTableState { - deactivatedOrgID := orgs[1+(orgsNumPerCase*i)].GetId() - deactivateRes, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ - Id: deactivatedOrgID, - }) - require.NoError(t, err) - now := time.Now() - assert.WithinRange(t, deactivateRes.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + for _, testCase := range integration.RelationalTablesEnableMatrix(t, ctx, sysAuthZ) { + t.Run(testCase.Name, func(tt *testing.T) { + inst := testCase.Inst + client := inst.Client.OrgV2beta + instOwner := testCase.InstOwner - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 20*time.Second) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ - Filter: []*v2beta_org.OrganizationSearchFilter{ - {Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{IdFilter: &v2beta_org.OrgIDFilter{Id: deactivatedOrgID}}}, - }, + tt.Cleanup(func() { + _, err := inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + assert.NoError(tt, err) }) - assert.NoError(ttt, err) - assert.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) - }, retryDuration, tick, "timeout waiting for expected organizations being created") - tt := []struct { - name string - ctx context.Context - inputOrgID string - err error - }{ - { - name: "Activate, happy path", - ctx: CTX, - inputOrgID: deactivatedOrgID, - }, - { - name: "Activate, no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), - inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - err: func() error { - //TODO: implement permissions for relational tables - if stateCase.FeatureSet.GetEnableRelationalTables() { - return errors.New("Organisation is already active") - } - return errors.New("membership not found") - }(), - }, - { - name: "Activate, not existing", - ctx: CTX, - inputOrgID: "non-existing-org-id", - err: errors.New("Organisation not found"), - }, - { - name: "Activate, already activated", - ctx: CTX, - inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - err: errors.New("Organisation is already active"), - }, - } + tt.Run("Happy path", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) - integration.EnsureInstanceFeature(t, CTX, Instance, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - for _, tc := range tt { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tc.name), func(t *testing.T) { - _, err := Client.ActivateOrganization(tc.ctx, &v2beta_org.ActivateOrganizationRequest{ - Id: tc.inputOrgID, - }) - if tc.err != nil { - require.Contains(t, err.Error(), tc.err.Error()) - } else { - require.NoError(t, err) + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + require.NoError(ttt, err) + + // when + _, err = client.ActivateOrganization(instOwner, &v2beta_org.ActivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.NoError(ttt, err) + }) + + tt.Run("Unhappy: no permission", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + usersWithoutPermissions := []integration.UserType{ + integration.UserTypeOrgOwner, + } + + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + assert.NoError(ttt, err) + + for _, userType := range usersWithoutPermissions { + ttt.Run(userType.String(), func(tttt *testing.T) { + u := inst.WithAuthorizationToken(ctx, userType) + + // when + _, err = client.ActivateOrganization(u, &v2beta_org.ActivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.ErrorContains(tttt, err, "membership not found") + }) } }) - } + + tt.Run("Unhappy: unknown org", func(ttt *testing.T) { + // when + _, err := client.ActivateOrganization(instOwner, &v2beta_org.ActivateOrganizationRequest{Id: "does not exist"}) + + // then + assert.ErrorContains(ttt, err, "Organisation not found") + }) + + tt.Run("Unhappy: already activated", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + + // when + _, err := client.ActivateOrganization(instOwner, &v2beta_org.ActivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.ErrorContains(ttt, err, "Organisation is already active") + }) + }) } } func TestServer_DeactivateOrganization(t *testing.T) { - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(CTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) - relTableState := integration.RelationalTablesEnableMatrix() + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() - orgsNumPerCase := 2 - orgs, _, _ := createOrgs(CTX, t, Client, orgsNumPerCase*len(relTableState)) - require.NotNil(t, orgs) - require.NotEmpty(t, orgs) + sysAuthZ := integration.WithSystemAuthorization(ctx) + for _, testCase := range integration.RelationalTablesEnableMatrix(t, ctx, sysAuthZ) { + t.Run(testCase.Name, func(tt *testing.T) { + inst := testCase.Inst + client := inst.Client.OrgV2beta + instOwner := testCase.InstOwner - for i, stateCase := range relTableState { - deactivatedOrgID := orgs[1+(orgsNumPerCase*i)].GetId() - deactivateRes, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ - Id: deactivatedOrgID, - }) - require.NoError(t, err) - now := time.Now() - assert.WithinRange(t, deactivateRes.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 20*time.Second) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ - Filter: []*v2beta_org.OrganizationSearchFilter{ - {Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{IdFilter: &v2beta_org.OrgIDFilter{Id: deactivatedOrgID}}}, - }, + tt.Cleanup(func() { + _, err := inst.Client.InstanceV2.DeleteInstance(sysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + require.NoError(tt, err) }) - assert.NoError(ttt, err) - assert.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) - }, retryDuration, tick, "timeout waiting for expected organizations being created") - tt := []struct { - name string - ctx context.Context - inputOrgID string - err error - }{ - { - name: "Deactivate, happy path", - ctx: CTX, - inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - }, - { - name: "Deactivate, no permission", - ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), - inputOrgID: orgs[0+(orgsNumPerCase*i)].GetId(), - err: func() error { - //TODO: implement for relational tables - if stateCase.FeatureSet.GetEnableRelationalTables() { - return errors.New("Organisation is already deactivated") - } - return errors.New("membership not found") - }(), - }, - { - name: "Deactivate, not existing", - ctx: CTX, - inputOrgID: "non-existing-org-id", - err: errors.New("Organisation not found"), - }, - { - name: "Deactivate, already deactivated", - ctx: CTX, - inputOrgID: deactivatedOrgID, - err: errors.New("Organisation is already deactivated"), - }, - } + tt.Run("Happy path", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) - integration.EnsureInstanceFeature(t, CTX, Instance, stateCase.FeatureSet, func(tCollect *assert.CollectT, got *feature.GetInstanceFeaturesResponse) { - assert.Equal(tCollect, stateCase.FeatureSet.GetEnableRelationalTables(), got.EnableRelationalTables.GetEnabled()) - }) - for _, tc := range tt { - t.Run(fmt.Sprintf("%s - %s", stateCase.State, tc.name), func(t *testing.T) { - _, err := Client.DeactivateOrganization(tc.ctx, &v2beta_org.DeactivateOrganizationRequest{ - Id: tc.inputOrgID, - }) - if tc.err != nil { - require.Contains(t, err.Error(), tc.err.Error()) - } else { - require.NoError(t, err) + // when + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.NoError(ttt, err) + }) + + tt.Run("Unhappy: no permission", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + usersWithoutPermissions := []integration.UserType{ + integration.UserTypeOrgOwner, + } + + for _, userType := range usersWithoutPermissions { + ttt.Run(userType.String(), func(tttt *testing.T) { + u := inst.WithAuthorizationToken(ctx, userType) + + // when + _, err := client.DeactivateOrganization(u, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.ErrorContains(tttt, err, "membership not found") + }) } }) - } + + tt.Run("Unhappy: unknown org", func(ttt *testing.T) { + // when + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: "does not exist"}) + + // then + assert.ErrorContains(ttt, err, "Organisation not found") + }) + + tt.Run("Unhappy: already deactivated", func(ttt *testing.T) { + // given + organisation, _, _ := createOrg(instOwner, ttt, client) + _, err := client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + require.NoError(ttt, err) + + // when + _, err = client.DeactivateOrganization(instOwner, &v2beta_org.DeactivateOrganizationRequest{Id: organisation.Id}) + + // then + assert.ErrorContains(ttt, err, "Organisation is already deactivated") + }) + }) } } @@ -1037,9 +945,8 @@ func TestServer_AddOrganizationDomain(t *testing.T) { ctx: CTX, domain: integration.DomainName(), testFunc: func() string { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id - return orgId + organisation, _, _ := createOrg(CTX, t, Client) + return organisation.Id }, }, { @@ -1047,9 +954,8 @@ func TestServer_AddOrganizationDomain(t *testing.T) { ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), domain: integration.DomainName(), testFunc: func() string { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id - return orgId + organisation, _, _ := createOrg(CTX, t, Client) + return organisation.Id }, err: errors.New("membership not found"), }, @@ -1060,8 +966,8 @@ func TestServer_AddOrganizationDomain(t *testing.T) { testFunc: func() string { t.Helper() // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id domain := integration.DomainName() // 2. add domain @@ -1194,8 +1100,8 @@ func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) { func TestServer_ListOrganizationDomains(t *testing.T) { domain := integration.DomainName() - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id var primaryDomain string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second) @@ -1360,8 +1266,8 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { domain: domain, testFunc: func() string { // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id // 2. add domain addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ @@ -1395,8 +1301,8 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { domain: integration.DomainName(), testFunc: func() string { // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id domain := integration.DomainName() // 2. add domain @@ -1452,8 +1358,8 @@ func TestServer_DeleteOrganizationDomain(t *testing.T) { domain: domain, testFunc: func() string { // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id // 2. add domain addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ @@ -1511,8 +1417,8 @@ func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { name: "add org domain, re-add org domain", testFunc: func() { // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id domain := integration.DomainName() // 2. add domain @@ -1560,8 +1466,8 @@ func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { name: "add org domain, delete org domain, re-delete org domain", testFunc: func() { // 1. create organization - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id domain := integration.DomainName() // 2. add domain @@ -1639,8 +1545,8 @@ func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { } func TestServer_ValidateOrganizationDomain(t *testing.T) { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id _, err := Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ ValidateOrgDomains: true, @@ -1739,8 +1645,8 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) { } func TestServer_SetOrganizationMetadata(t *testing.T) { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id tests := []struct { name string @@ -1864,8 +1770,8 @@ func TestServer_SetOrganizationMetadata(t *testing.T) { } func TestServer_ListOrganizationMetadata(t *testing.T) { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id setRespoonse, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ OrganizationId: orgId, Metadata: []*v2beta_org.Metadata{ @@ -2034,8 +1940,8 @@ func TestServer_ListOrganizationMetadata(t *testing.T) { } func TestServer_DeleteOrganizationMetadata(t *testing.T) { - orgs, _, _ := createOrgs(CTX, t, Client, 1) - orgId := orgs[0].Id + organisation, _, _ := createOrg(CTX, t, Client) + orgId := organisation.Id _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ OrganizationId: orgId, @@ -2185,6 +2091,34 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) { } } +func createOrg(ctx context.Context, t *testing.T, client v2beta_org.OrganizationServiceClient) (organisation *v2beta_org.CreateOrganizationResponse, orgName string, orgDomain string) { + var err error + orgName = integration.OrganizationName() + organisation, err = client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{Name: orgName}) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + listOrgRes, err := client.ListOrganizations(ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: organisation.Id, + }, + }, + }, + }, + }) + require.NoError(collect, err) + require.Len(collect, listOrgRes.Organizations, 1) + + orgDomain = listOrgRes.Organizations[0].PrimaryDomain + }, retryDuration, tick, "timeout waiting for org creation") + + return organisation, orgName, orgDomain +} + func createOrgs(ctx context.Context, t *testing.T, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, []string) { var err error orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) diff --git a/internal/command/instance.go b/internal/command/instance.go index 6f83f6061..47ff8f3ba 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -520,6 +520,14 @@ func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation if smtpConfig == nil { return } + var username string + var pwd []byte + if smtpConfig.SMTP.PlainAuth != nil { + username = smtpConfig.SMTP.PlainAuth.User + if smtpConfig.SMTP.PlainAuth != nil { + pwd = []byte(smtpConfig.SMTP.PlainAuth.Password) + } + } *validations = append(*validations, commands.prepareAddAndActivateSMTPConfig( instanceAgg, @@ -528,8 +536,8 @@ func setupSMTPSettings(commands *Commands, validations *[]preparation.Validation smtpConfig.FromName, smtpConfig.ReplyToAddress, smtpConfig.SMTP.Host, - smtpConfig.SMTP.User, - []byte(smtpConfig.SMTP.Password), + username, + pwd, smtpConfig.Tls, ), ) diff --git a/internal/command/instance_smtp_config_model.go b/internal/command/instance_smtp_config_model.go index d396716ce..65146ccde 100644 --- a/internal/command/instance_smtp_config_model.go +++ b/internal/command/instance_smtp_config_model.go @@ -2,6 +2,7 @@ package command import ( "context" + "slices" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -29,10 +30,11 @@ type SMTPConfig struct { TLS bool Host string User string - Password *crypto.CryptoValue SenderAddress string SenderName string ReplyToAddress string + PlainAuth *instance.PlainAuth + XOAuth2Auth *instance.XOAuth2Auth } func NewIAMSMTPConfigWriteModel(instanceID, id, domain string) *IAMSMTPConfigWriteModel { @@ -85,7 +87,11 @@ func (wm *IAMSMTPConfigWriteModel) Reduce() error { continue } if e.Password != nil { - wm.SMTPConfig.Password = e.Password + if wm.SMTPConfig.PlainAuth == nil { + wm.SMTPConfig.PlainAuth = &instance.PlainAuth{Password: e.Password} + } else { + wm.SMTPConfig.PlainAuth.Password = e.Password + } } case *instance.SMTPConfigHTTPAddedEvent: if wm.ID != e.ID { @@ -157,7 +163,19 @@ func (wm *IAMSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder { Builder() } -func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, description string, tls bool, fromAddress, fromName, replyToAddress, smtpHost, smtpUser string, smtpPassword *crypto.CryptoValue) (*instance.SMTPConfigChangedEvent, bool, error) { +func (wm *IAMSMTPConfigWriteModel) NewChangedEvent( + ctx context.Context, aggregate *eventstore.Aggregate, + id, + description string, + tls bool, + fromAddress, + fromName, + replyToAddress, + smtpHost string, + smtpUser string, + plainAuth *instance.PlainAuth, + xoauth2Auth *instance.XOAuth2Auth, +) (*instance.SMTPConfigChangedEvent, bool, error) { changes := make([]instance.SMTPConfigChanges, 0) var err error if wm.SMTPConfig == nil { @@ -188,9 +206,13 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat if wm.SMTPConfig.User != smtpUser { changes = append(changes, instance.ChangeSMTPConfigSMTPUser(smtpUser)) } - if smtpPassword != nil { - changes = append(changes, instance.ChangeSMTPConfigSMTPPassword(smtpPassword)) + if plainAuth != nil { + changes = append(changes, smtpPlainAuthChanges(wm.SMTPConfig.PlainAuth, *plainAuth)...) } + if xoauth2Auth != nil { + changes = append(changes, smtpXOAuthChanges(wm.SMTPConfig.XOAuth2Auth, *xoauth2Auth)...) + } + if len(changes) == 0 { return nil, false, nil } @@ -201,6 +223,77 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat return changeEvent, true, nil } +func smtpPlainAuthChanges(wm *instance.PlainAuth, auth instance.PlainAuth) []instance.SMTPConfigChanges { + // if no auth is yet present, set both + if wm == nil { + return []instance.SMTPConfigChanges{ + instance.ChangeSMTPConfigSMTPPassword(auth.Password), + } + } + + // if auth is already present, add changes for the changed values + var changes []instance.SMTPConfigChanges + + if auth.Password != nil { + changes = append(changes, instance.ChangeSMTPConfigSMTPPassword(auth.Password)) + } + + return changes +} + +func smtpXOAuthChanges(wm *instance.XOAuth2Auth, auth instance.XOAuth2Auth) []instance.SMTPConfigChanges { + // if no auth is yet present, set all properties + if wm == nil { + return []instance.SMTPConfigChanges{ + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientId(auth.ClientCredentials.ClientId), + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientSecret(auth.ClientCredentials.ClientSecret), + instance.ChangeSMTPConfigXOAuth2TokenEndpoint(auth.TokenEndpoint), + instance.ChangeSMTPConfigXOAuth2Scopes(auth.Scopes), + } + } + + // if auth is already present, add changes for the changed values + var changes []instance.SMTPConfigChanges + + if wm.TokenEndpoint != auth.TokenEndpoint { + changes = append(changes, instance.ChangeSMTPConfigXOAuth2TokenEndpoint(auth.TokenEndpoint)) + } + if len(wm.Scopes) != len(auth.Scopes) { + changes = append(changes, instance.ChangeSMTPConfigXOAuth2Scopes(auth.Scopes)) + } else { + for _, s := range auth.Scopes { + if !slices.Contains(wm.Scopes, s) { + changes = append(changes, instance.ChangeSMTPConfigXOAuth2Scopes(auth.Scopes)) + break + } + } + } + + if auth.ClientCredentials != nil { + changes = append(changes, smtpXOAuthClientCredentialChanges(auth.ClientCredentials, *auth.ClientCredentials)...) + } + + return changes +} + +func smtpXOAuthClientCredentialChanges(wm *instance.XOAuth2ClientCredentials, cc instance.XOAuth2ClientCredentials) []instance.SMTPConfigChanges { + if wm == nil { + return []instance.SMTPConfigChanges{ + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientId(cc.ClientId), + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientSecret(cc.ClientSecret), + } + } + + var changes []instance.SMTPConfigChanges + if wm.ClientId != cc.ClientId { + changes = append(changes, instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientId(cc.ClientId)) + } + if wm.ClientSecret != cc.ClientSecret { + changes = append(changes, instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientSecret(cc.ClientSecret)) + } + return changes +} + func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -242,11 +335,30 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigAddedEvent(e *instance.SMTPCo TLS: e.TLS, Host: e.Host, User: e.User, - Password: e.Password, SenderName: e.SenderName, SenderAddress: e.SenderAddress, ReplyToAddress: e.ReplyToAddress, } + + if e.PlainAuth != nil { + wm.SMTPConfig.PlainAuth = &instance.PlainAuth{ + Password: e.PlainAuth.Password, + } + } + if e.XOAuth2Auth != nil { + wm.SMTPConfig.XOAuth2Auth = &instance.XOAuth2Auth{ + TokenEndpoint: e.XOAuth2Auth.TokenEndpoint, + Scopes: e.XOAuth2Auth.Scopes, + } + + if e.XOAuth2Auth.ClientCredentials != nil { + wm.SMTPConfig.XOAuth2Auth.ClientCredentials = &instance.XOAuth2ClientCredentials{ + ClientId: e.XOAuth2Auth.ClientCredentials.ClientId, + ClientSecret: e.XOAuth2Auth.ClientCredentials.ClientSecret, + } + } + } + wm.State = domain.SMTPConfigStateInactive // 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 @@ -290,9 +402,6 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigChangedEvent(e *instance.SMTP if e.User != nil { wm.SMTPConfig.User = *e.User } - if e.Password != nil { - wm.SMTPConfig.Password = e.Password - } if e.FromAddress != nil { wm.SMTPConfig.SenderAddress = *e.FromAddress } @@ -303,6 +412,39 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigChangedEvent(e *instance.SMTP wm.SMTPConfig.ReplyToAddress = *e.ReplyToAddress } + if !e.PlainAuth.IsEmpty() { + if wm.SMTPConfig.PlainAuth == nil { + wm.SMTPConfig.PlainAuth = &instance.PlainAuth{} + wm.SMTPConfig.XOAuth2Auth = nil + } + if e.PlainAuth.Password != nil { + wm.SMTPConfig.PlainAuth.Password = e.PlainAuth.Password + } else if e.Password != nil { + wm.SMTPConfig.PlainAuth.Password = e.Password + } + } + + if !e.XOAuth2Auth.IsEmpty() { + if wm.SMTPConfig.XOAuth2Auth == nil { + wm.SMTPConfig.XOAuth2Auth = &instance.XOAuth2Auth{} + wm.SMTPConfig.PlainAuth = nil + } + if e.XOAuth2Auth.TokenEndpoint != nil { + wm.SMTPConfig.XOAuth2Auth.TokenEndpoint = *e.XOAuth2Auth.TokenEndpoint + } + if e.XOAuth2Auth.Scopes != nil { + wm.SMTPConfig.XOAuth2Auth.Scopes = e.XOAuth2Auth.Scopes + } + if wm.SMTPConfig.XOAuth2Auth.ClientCredentials != nil && !e.XOAuth2Auth.ClientCredentials.IsEmpty() { + if e.XOAuth2Auth.ClientCredentials.ClientId != nil { + wm.SMTPConfig.XOAuth2Auth.ClientCredentials.ClientId = *e.XOAuth2Auth.ClientCredentials.ClientId + } + if e.XOAuth2Auth.ClientCredentials.ClientSecret != nil { + wm.SMTPConfig.XOAuth2Auth.ClientCredentials.ClientSecret = e.XOAuth2Auth.ClientCredentials.ClientSecret + } + } + } + // 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 if e.ID == "" { diff --git a/internal/command/smtp.go b/internal/command/smtp.go index 0dd917e6c..eace3ba00 100644 --- a/internal/command/smtp.go +++ b/internal/command/smtp.go @@ -23,11 +23,25 @@ type AddSMTPConfig struct { Description string Host string User string - Password string Tls bool From string FromName string ReplyToAddress string + PlainAuth *PlainAuth + XOAuth2Auth *XOAuth2Auth +} + +type PlainAuth struct { + Password string +} +type XOAuth2Auth struct { + TokenEndpoint string + Scopes []string + ClientCredentialsAuth *OAuth2ClientCredentials +} +type OAuth2ClientCredentials struct { + ClientId string + ClientSecret string } func (c *Commands) AddSMTPConfig(ctx context.Context, config *AddSMTPConfig) (err error) { @@ -55,14 +69,6 @@ func (c *Commands) AddSMTPConfig(ctx context.Context, config *AddSMTPConfig) (er return zerrors.ThrowInvalidArgument(nil, "COMMAND-EvAtufIinh", "Errors.Invalid.Argument") } - var smtpPassword *crypto.CryptoValue - if config.Password != "" { - smtpPassword, err = crypto.Encrypt([]byte(config.Password), c.smtpEncryption) - if err != nil { - return err - } - } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, senderDomain) if err != nil { return err @@ -73,6 +79,36 @@ func (c *Commands) AddSMTPConfig(ctx context.Context, config *AddSMTPConfig) (er return err } + var plainAuth *instance.PlainAuth + var xoauth2Auth *instance.XOAuth2Auth + + if config.XOAuth2Auth != nil { + xoauth2Auth = &instance.XOAuth2Auth{ + TokenEndpoint: config.XOAuth2Auth.TokenEndpoint, + Scopes: config.XOAuth2Auth.Scopes, + } + + if config.XOAuth2Auth.ClientCredentialsAuth != nil { + clientSecret, err := crypto.Encrypt([]byte(config.XOAuth2Auth.ClientCredentialsAuth.ClientSecret), c.smtpEncryption) + if err != nil { + return err + } + xoauth2Auth.ClientCredentials = &instance.XOAuth2ClientCredentials{ + ClientId: config.XOAuth2Auth.ClientCredentialsAuth.ClientId, + ClientSecret: clientSecret, + } + } + } + if config.PlainAuth != nil { + plainAuth = &instance.PlainAuth{} + if config.PlainAuth.Password != "" { + plainAuth.Password, err = crypto.Encrypt([]byte(config.PlainAuth.Password), c.smtpEncryption) + if err != nil { + return err + } + } + } + err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, instance.NewSMTPConfigAddedEvent( @@ -86,12 +122,14 @@ func (c *Commands) AddSMTPConfig(ctx context.Context, config *AddSMTPConfig) (er replyTo, hostAndPort, config.User, - smtpPassword, + plainAuth, + xoauth2Auth, ), ) if err != nil { return err } + config.Details = writeModelToObjectDetails(&smtpConfigWriteModel.WriteModel) return nil } @@ -104,11 +142,12 @@ type ChangeSMTPConfig struct { Description string Host string User string - Password string Tls bool From string FromName string ReplyToAddress string + PlainAuth *PlainAuth + XOAuth2Auth *XOAuth2Auth } func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *ChangeSMTPConfig) error { @@ -132,15 +171,6 @@ func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *ChangeSMTPConfi return zerrors.ThrowInvalidArgument(nil, "COMMAND-kZNVkuL32L", "Errors.Invalid.Argument") } - var smtpPassword *crypto.CryptoValue - var err error - if config.Password != "" { - smtpPassword, err = crypto.Encrypt([]byte(config.Password), c.smtpEncryption) - if err != nil { - return err - } - } - smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, senderDomain) if err != nil { return err @@ -155,6 +185,35 @@ func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *ChangeSMTPConfi return err } + var plainAuth *instance.PlainAuth + var xoauth2Auth *instance.XOAuth2Auth + + if config.PlainAuth != nil { + plainAuth = &instance.PlainAuth{} + if config.PlainAuth.Password != "" { + plainAuth.Password, err = crypto.Encrypt([]byte(config.PlainAuth.Password), c.smtpEncryption) + if err != nil { + return err + } + } + } + if config.XOAuth2Auth != nil { + xoauth2Auth = &instance.XOAuth2Auth{ + TokenEndpoint: config.XOAuth2Auth.TokenEndpoint, + Scopes: config.XOAuth2Auth.Scopes, + } + if config.XOAuth2Auth.ClientCredentialsAuth != nil { + xoauth2Auth.ClientCredentials = &instance.XOAuth2ClientCredentials{ + ClientId: config.XOAuth2Auth.ClientCredentialsAuth.ClientId, + } + if config.XOAuth2Auth.ClientCredentialsAuth.ClientSecret != "" { + xoauth2Auth.ClientCredentials.ClientSecret, err = crypto.Encrypt([]byte(config.XOAuth2Auth.ClientCredentialsAuth.ClientSecret), c.smtpEncryption) + if err != nil { + return err + } + } + } + } changedEvent, hasChanged, err := smtpConfigWriteModel.NewChangedEvent( ctx, InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel), @@ -166,7 +225,8 @@ func (c *Commands) ChangeSMTPConfig(ctx context.Context, config *ChangeSMTPConfi replyTo, hostAndPort, config.User, - smtpPassword, + plainAuth, + xoauth2Auth, ) if err != nil { return err @@ -430,34 +490,50 @@ func (c *Commands) RemoveSMTPConfig(ctx context.Context, resourceOwner, id strin } func (c *Commands) TestSMTPConfig(ctx context.Context, instanceID, id, email string, config *smtp.Config) error { - password := config.SMTP.Password if email == "" { return zerrors.ThrowInvalidArgument(nil, "SMTP-p9uy", "Errors.SMTPConfig.TestEmailNotFound") } - if id == "" && password == "" { + if id == "" && config.SMTP.PlainAuth != nil && config.SMTP.PlainAuth.Password == "" { return zerrors.ThrowInvalidArgument(nil, "SMTP-p9kj", "Errors.SMTPConfig.TestPassword") } - // If the password is not sent it'd mean that the password hasn't been changed for - // the stored configuration identified by its id so we can try to retrieve it - if id != "" && password == "" { + if id == "" && config.SMTP.XOAuth2Auth != nil && + config.SMTP.XOAuth2Auth.ClientCredentialsAuth != nil && + config.SMTP.XOAuth2Auth.ClientCredentialsAuth.ClientSecret == "" { + return zerrors.ThrowInvalidArgument(nil, "SMTP-9OP96s", "Errors.SMTPConfig.TestClientSecret") + } + + if config.SMTP.PlainAuth != nil && config.SMTP.PlainAuth.Password == "" { smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") if err != nil { return err } - if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.SMTPConfig == nil { + if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.SMTPConfig == nil || smtpConfigWriteModel.SMTPConfig.PlainAuth == nil { return zerrors.ThrowNotFound(nil, "SMTP-p9cc", "Errors.SMTPConfig.NotFound") } - password, err = crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.Password, c.smtpEncryption) + config.SMTP.PlainAuth.Password, err = crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.PlainAuth.Password, c.smtpEncryption) if err != nil { return err } } - config.SMTP.Password = password + if config.SMTP.XOAuth2Auth != nil && config.SMTP.XOAuth2Auth.ClientCredentialsAuth != nil && config.SMTP.XOAuth2Auth.ClientCredentialsAuth.ClientSecret == "" { + smtpConfigWriteModel, err := c.getSMTPConfig(ctx, instanceID, id, "") + if err != nil { + return err + } + if !smtpConfigWriteModel.State.Exists() || smtpConfigWriteModel.SMTPConfig == nil || smtpConfigWriteModel.SMTPConfig.XOAuth2Auth == nil { + return zerrors.ThrowNotFound(nil, "SMTP-p9c2", "Errors.SMTPConfig.NotFound") + } + + config.SMTP.XOAuth2Auth.ClientCredentialsAuth.ClientSecret, err = crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.ClientCredentials.ClientSecret, c.smtpEncryption) + if err != nil { + return err + } + } // Try to send an email err := smtp.TestConfiguration(config, email) @@ -486,9 +562,36 @@ func (c *Commands) TestSMTPConfigById(ctx context.Context, instanceID, id, email return zerrors.ThrowNotFound(nil, "SMTP-99klw", "Errors.SMTPConfig.NotFound") } - password, err := crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.Password, c.smtpEncryption) - if err != nil { - return err + var plainAuth *smtp.PlainAuthConfig + var xoauth2Auth *smtp.XOAuth2AuthConfig + + if smtpConfigWriteModel.SMTPConfig.PlainAuth != nil { + password, err := crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.PlainAuth.Password, c.smtpEncryption) + if err != nil { + return err + } + plainAuth = &smtp.PlainAuthConfig{ + User: smtpConfigWriteModel.SMTPConfig.User, + Password: password, + } + } + + if smtpConfigWriteModel.SMTPConfig.XOAuth2Auth != nil { + xoauth2Auth = &smtp.XOAuth2AuthConfig{ + User: smtpConfigWriteModel.SMTPConfig.User, + TokenEndpoint: smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.TokenEndpoint, + Scopes: smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.Scopes, + } + if smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.ClientCredentials != nil { + clientSecret, err := crypto.DecryptString(smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.ClientCredentials.ClientSecret, c.smtpEncryption) + if err != nil { + return err + } + xoauth2Auth.ClientCredentialsAuth = &smtp.OAuth2ClientCredentials{ + ClientId: smtpConfigWriteModel.SMTPConfig.XOAuth2Auth.ClientCredentials.ClientId, + ClientSecret: clientSecret, + } + } } smtpConfig := &smtp.Config{ @@ -496,9 +599,9 @@ func (c *Commands) TestSMTPConfigById(ctx context.Context, instanceID, id, email From: smtpConfigWriteModel.SMTPConfig.SenderAddress, FromName: smtpConfigWriteModel.SMTPConfig.SenderName, SMTP: smtp.SMTP{ - Host: smtpConfigWriteModel.SMTPConfig.Host, - User: smtpConfigWriteModel.SMTPConfig.User, - Password: password, + Host: smtpConfigWriteModel.SMTPConfig.Host, + PlainAuth: plainAuth, + XOAuth2Auth: xoauth2Auth, }, } @@ -582,7 +685,8 @@ func (c *Commands) prepareAddAndActivateSMTPConfig(a *instance.Aggregate, descri replyTo, hostAndPort, user, - smtpPassword, + &instance.PlainAuth{Password: smtpPassword}, + nil, ), instance.NewSMTPConfigActivatedEvent( ctx, diff --git a/internal/command/smtp_test.go b/internal/command/smtp_test.go index e86419eb3..7200660d2 100644 --- a/internal/command/smtp_test.go +++ b/internal/command/smtp_test.go @@ -78,7 +78,9 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { From: "from@domain.ch", Host: "host:587", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -136,12 +138,15 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("password"), + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, }, + nil, ), ), ), @@ -157,7 +162,9 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { FromName: "name", Host: "host:587", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -197,12 +204,15 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { "replyto@domain.ch", "host:587", "user", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("password"), + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, }, + nil, ), ), ), @@ -219,7 +229,9 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { ReplyToAddress: "replyto@domain.ch", Host: "host:587", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -244,7 +256,9 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { FromName: "name", Host: "host", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -269,7 +283,9 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { FromName: "name", Host: " ", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -309,12 +325,15 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { "", "[2001:db8::1]:2525", "user", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("password"), + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, }, + nil, ), ), ), @@ -330,7 +349,200 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { FromName: "name", Host: "[2001:db8::1]:2525", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "add smtp config with plain auth but no password, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, true, false, + ), + ), + ), + expectPush( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "configid", + "test", + true, + "from@domain.ch", + "name", + "replyto@domain.ch", + "host:587", + "user", + &instance.PlainAuth{}, + nil, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + ReplyToAddress: "replyto@domain.ch", + Host: "host:587", + User: "user", + PlainAuth: &PlainAuth{}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "add smtp config without auth, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, true, false, + ), + ), + ), + expectPush( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "configid", + "test", + true, + "from@domain.ch", + "name", + "replyto@domain.ch", + "host:587", + "", + nil, + nil, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + ReplyToAddress: "replyto@domain.ch", + Host: "host:587", + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "add smtp config with xoauth2 auth, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, true, false, + ), + ), + ), + expectPush( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "configid", + "test", + true, + "from@domain.ch", + "name", + "replyto@domain.ch", + "host:587", + "user", + nil, + &instance.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentials: &instance.XOAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, + }, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + smtp: &AddSMTPConfig{ + ResourceOwner: "INSTANCE", + Description: "test", + Tls: true, + From: "from@domain.ch", + FromName: "name", + ReplyToAddress: "replyto@domain.ch", + Host: "host:587", + User: "user", + XOAuth2Auth: &XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentialsAuth: &OAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: "password", + }, + }, }, }, res: res{ @@ -365,6 +577,7 @@ func TestCommandSide_AddSMTPConfig(t *testing.T) { func TestCommandSide_ChangeSMTPConfig(t *testing.T) { type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore + alg crypto.EncryptionAlgorithm } type args struct { smtp *ChangeSMTPConfig @@ -443,6 +656,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { FromName: "name", Host: "host:587", User: "user", + PlainAuth: &PlainAuth{}, }, }, res: res{ @@ -481,7 +695,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -497,6 +714,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { FromName: "name", Host: "host:587", User: "user", + PlainAuth: &PlainAuth{}, }, }, res: res{ @@ -533,7 +751,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -549,6 +770,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { FromName: "name", Host: "host:587", User: "user", + PlainAuth: &PlainAuth{}, }, }, res: res{ @@ -587,7 +809,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -602,6 +827,8 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "replyto@domain.ch", "host2:587", "user2", + &instance.PlainAuth{}, + nil, ), ), ), @@ -617,6 +844,7 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ReplyToAddress: "replyto@domain.ch", Host: "host2:587", User: "user2", + PlainAuth: &PlainAuth{}, }, }, res: res{ @@ -640,7 +868,9 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { FromName: "name", Host: "host", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -664,7 +894,9 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { FromName: "name", Host: " ", User: "user", - Password: "password", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -703,7 +935,10 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -718,6 +953,8 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { "replyto@domain.ch", "[2001:db8::1]:2525", "user2", + &instance.PlainAuth{}, + nil, ), ), ), @@ -733,6 +970,193 @@ func TestCommandSide_ChangeSMTPConfig(t *testing.T) { ReplyToAddress: "replyto@domain.ch", Host: "[2001:db8::1]:2525", User: "user2", + PlainAuth: &PlainAuth{}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "change auth from plain to xoauth2, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, true, true, + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "", + true, + "from@domain.ch", + "name", + "", + "host:587", + "user", + &instance.PlainAuth{}, + nil, + ), + ), + ), + expectPush( + newSMTPConfigChangedEvent( + context.Background(), + "ID", + "test", + false, + "from2@domain.ch", + "name2", + "replyto@domain.ch", + "host2:587", + "user2", + nil, + &instance.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentials: &instance.XOAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, + }, + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: false, + From: "from2@domain.ch", + FromName: "name2", + ReplyToAddress: "replyto@domain.ch", + Host: "host2:587", + User: "user2", + XOAuth2Auth: &XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentialsAuth: &OAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: "password", + }, + }, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + { + name: "change auth from xoauth2 to plain, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + eventFromEventPusher( + instance.NewDomainPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + true, true, true, + ), + ), + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "", + true, + "from@domain.ch", + "name", + "", + "host:587", + "user2", + nil, + &instance.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentials: &instance.XOAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("client-secret"), + }, + }, + }, + ), + ), + ), + expectPush( + newSMTPConfigChangedEvent( + context.Background(), + "ID", + "test", + false, + "from2@domain.ch", + "name2", + "replyto@domain.ch", + "host2:587", + "user", + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, + nil, + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + smtp: &ChangeSMTPConfig{ + ResourceOwner: "INSTANCE", + ID: "ID", + Description: "test", + Tls: false, + From: "from2@domain.ch", + FromName: "name2", + ReplyToAddress: "replyto@domain.ch", + Host: "host2:587", + User: "user", + PlainAuth: &PlainAuth{ + Password: "password", + }, }, }, res: res{ @@ -745,7 +1169,8 @@ func TestCommandSide_ChangeSMTPConfig(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), + smtpEncryption: tt.fields.alg, } err := r.ChangeSMTPConfig(context.Background(), tt.args.smtp) if tt.res.err == nil { @@ -842,7 +1267,10 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), eventFromEventPusher( @@ -1261,7 +1689,10 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -1302,7 +1733,10 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), eventFromEventPusher( @@ -1466,7 +1900,10 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), eventFromEventPusher( @@ -1513,7 +1950,10 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), eventFromEventPusher( @@ -1692,7 +2132,10 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) { "", "host:587", "user", - &crypto.CryptoValue{}, + &instance.PlainAuth{ + Password: &crypto.CryptoValue{}, + }, + nil, ), ), ), @@ -1837,9 +2280,11 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { From: "test@example,com", FromName: "Test", SMTP: smtp.SMTP{ - User: "user", - Password: "", - Host: "example.com:2525", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "", + }, + Host: "example.com:2525", }, }, }, @@ -1863,9 +2308,11 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { From: "test@example,com", FromName: "Test", SMTP: smtp.SMTP{ - User: "user", - Password: "", - Host: "example.com:2525", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "", + }, + Host: "example.com:2525", }, }, }, @@ -1874,7 +2321,7 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { }, }, { - name: "valid new smtp config, wrong auth, ok", + name: "valid new smtp config, wrong plain auth, ok", fields: fields{ eventstore: expectEventstore(), alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), @@ -1887,9 +2334,11 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { From: "test@example.com", FromName: "Test", SMTP: smtp.SMTP{ - User: "user", - Password: "password", - Host: "mail.smtp2go.com:2525", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "password", + }, + Host: "mail.smtp2go.com:2525", }, }, }, @@ -1914,11 +2363,103 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { "", "mail.smtp2go.com:2525", "user", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("password"), + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, + nil, + ), + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + email: "email", + id: "ID", + config: smtp.Config{ + From: "test@example.com", + FromName: "Test", + SMTP: smtp.SMTP{ + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "", + }, + Host: "mail.smtp2go.com:2525", + }, + }, + }, + res: res{ + err: zerrors.IsInternal, + }, + }, + { + name: "valid new smtp config, wrong xoauth2 auth, ok", + fields: fields{ + eventstore: expectEventstore(), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + email: "email", + config: smtp.Config{ + From: "test@example.com", + FromName: "Test", + SMTP: smtp.SMTP{ + XOAuth2Auth: &smtp.XOAuth2AuthConfig{ + User: "user", + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: "client-secret", + }, + }, + Host: "mail.smtp2go.com:2525", + }, + }, + }, + res: res{ + err: zerrors.IsInternal, + }, + }, + { + name: "valid smtp config using stored password, wrong auth, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + true, + "from", + "name", + "", + "mail.smtp2go.com:2525", + "user", + nil, + &instance.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentials: &instance.XOAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, }, ), ), @@ -1935,9 +2476,15 @@ func TestCommandSide_TestSMTPConfig(t *testing.T) { From: "test@example.com", FromName: "Test", SMTP: smtp.SMTP{ - User: "user", - Password: "", - Host: "mail.smtp2go.com:2525", + XOAuth2Auth: &smtp.XOAuth2AuthConfig{ + User: "user", + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{ + ClientId: "client-id", + }, + }, + Host: "mail.smtp2go.com:2525", }, }, }, @@ -2032,7 +2579,7 @@ func TestCommandSide_TestSMTPConfigById(t *testing.T) { }, }, { - name: "valid smtp config, wrong auth, ok", + name: "valid smtp config, wrong plain auth, ok", fields: fields{ eventstore: eventstoreExpect( t, @@ -2049,11 +2596,62 @@ func TestCommandSide_TestSMTPConfigById(t *testing.T) { "", "mail.smtp2go.com:2525", "user", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("password"), + &instance.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("password"), + }, + }, + nil, + ), + ), + ), + ), + alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + id: "ID", + instanceID: "INSTANCE", + email: "test@example.com", + }, + res: res{ + err: zerrors.IsInternal, + }, + }, + { + name: "valid smtp config, wrong xoauth2 auth, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSMTPConfigAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "ID", + "test", + true, + "from", + "name", + "", + "mail.smtp2go.com:2525", + "user", + nil, + &instance.XOAuth2Auth{ + TokenEndpoint: "auth.example.com/token", + Scopes: []string{"scope"}, + ClientCredentials: &instance.XOAuth2ClientCredentials{ + ClientId: "client-id", + ClientSecret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("client-secret"), + }, + }, }, ), ), @@ -2089,7 +2687,7 @@ func TestCommandSide_TestSMTPConfigById(t *testing.T) { } } -func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls bool, fromAddress, fromName, replyTo, host, user string) *instance.SMTPConfigChangedEvent { +func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls bool, fromAddress, fromName, replyTo, host, user string, plainAuth *instance.PlainAuth, xoauth2Auth *instance.XOAuth2Auth) *instance.SMTPConfigChangedEvent { changes := []instance.SMTPConfigChanges{ instance.ChangeSMTPConfigDescription(description), instance.ChangeSMTPConfigTLS(tls), @@ -2099,6 +2697,23 @@ func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls instance.ChangeSMTPConfigSMTPHost(host), instance.ChangeSMTPConfigSMTPUser(user), } + if plainAuth != nil { + changes = append(changes, + instance.ChangeSMTPConfigSMTPPassword(plainAuth.Password), + ) + } + if xoauth2Auth != nil { + changes = append(changes, + instance.ChangeSMTPConfigXOAuth2TokenEndpoint(xoauth2Auth.TokenEndpoint), + instance.ChangeSMTPConfigXOAuth2Scopes(xoauth2Auth.Scopes), + ) + if xoauth2Auth.ClientCredentials != nil { + changes = append(changes, + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientId(xoauth2Auth.ClientCredentials.ClientId), + instance.ChangeSMTPConfigXOAuth2ClientCredentialsClientSecret(xoauth2Auth.ClientCredentials.ClientSecret), + ) + } + } event, _ := instance.NewSMTPConfigChangeEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, id, diff --git a/internal/integration/feature.go b/internal/integration/feature.go index f27173b2c..6a3f6d4d8 100644 --- a/internal/integration/feature.go +++ b/internal/integration/feature.go @@ -31,20 +31,33 @@ func EnsureInstanceFeature(t *testing.T, ctx context.Context, instance *Instance } type RelationalTableFeatureMatrix struct { - State string - FeatureSet *feature.SetInstanceFeaturesRequest + Name string + Inst *Instance + InstOwner context.Context } // TODO(IAM-Marco): Once we have gotten rid of eventstore, this can be removed -func RelationalTablesEnableMatrix() []RelationalTableFeatureMatrix { +func RelationalTablesEnableMatrix(t *testing.T, ctx context.Context, sysAuthZ context.Context) []RelationalTableFeatureMatrix { return []RelationalTableFeatureMatrix{ - { - State: "when relational tables are disabled", - FeatureSet: &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(false)}, - }, - { - State: "when relational tables are enabled", - FeatureSet: &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(true)}, - }, + func() RelationalTableFeatureMatrix { + inst := NewInstance(sysAuthZ) + instOwner := inst.WithAuthorizationToken(ctx, UserTypeIAMOwner) + return RelationalTableFeatureMatrix{ + Name: "when relational tables are disabled", + Inst: inst, + InstOwner: instOwner, + } + }(), + func() RelationalTableFeatureMatrix { + inst := NewInstance(sysAuthZ) + instOwner := inst.WithAuthorizationToken(ctx, UserTypeIAMOwner) + _, err := inst.Client.FeatureV2.SetInstanceFeatures(instOwner, &feature.SetInstanceFeaturesRequest{EnableRelationalTables: gu.Ptr(false)}) + require.NoError(t, err) + return RelationalTableFeatureMatrix{ + Name: "when relational tables are enabled", + Inst: inst, + InstOwner: instOwner, + } + }(), } } diff --git a/internal/notification/channels/smtp/channel.go b/internal/notification/channels/smtp/channel.go index 0fa446287..d8655b361 100644 --- a/internal/notification/channels/smtp/channel.go +++ b/internal/notification/channels/smtp/channel.go @@ -148,14 +148,22 @@ func (smtpConfig SMTP) getSMTPClientWithStartTls(host string) (*smtp.Client, err } func (smtpConfig SMTP) smtpAuth(client *smtp.Client, host string) error { - if !smtpConfig.HasAuth() { + if smtpConfig.XOAuth2Auth != nil { + err := client.Auth(XOAuth2Auth(*smtpConfig.XOAuth2Auth, host)) + if err != nil { + return zerrors.ThrowInternal(err, "EMAIL-av88SB", "Errors.SMTP.CouldNotXOAuth2") + } return nil } - // Auth - err := client.Auth(PlainOrLoginAuth(smtpConfig.User, smtpConfig.Password, host)) - if err != nil { - return zerrors.ThrowInternal(err, "EMAIL-s9kfs", "Errors.SMTP.CouldNotAuth") + + if smtpConfig.PlainAuth != nil { + err := client.Auth(PlainOrLoginAuth(*smtpConfig.PlainAuth, host)) + if err != nil { + return zerrors.ThrowInternal(err, "EMAIL-s9kfs", "Errors.SMTP.CouldNotAuth") + } + return nil } + return nil } @@ -176,11 +184,11 @@ func TestConfiguration(cfg *Config, testEmail string) error { } if err := client.Mail(cfg.From); err != nil { - return zerrors.ThrowInternal(err, "EMAIL-s3is3", "Errors.SMTP.CouldNotSetSender") + return zerrors.ThrowInternal(err, "EMAIL-s5is5", "Errors.SMTP.CouldNotSetSender") } if err := client.Rcpt(testEmail); err != nil { - return zerrors.ThrowInternal(err, "EMAIL-s4is4", "Errors.SMTP.CouldNotSetRecipient") + return zerrors.ThrowInternal(err, "EMAIL-s6is6", "Errors.SMTP.CouldNotSetRecipient") } // Open data connection diff --git a/internal/notification/channels/smtp/config.go b/internal/notification/channels/smtp/config.go index 46d6eab0d..3f03edac6 100644 --- a/internal/notification/channels/smtp/config.go +++ b/internal/notification/channels/smtp/config.go @@ -9,19 +9,9 @@ type Config struct { } type SMTP struct { - Host string - User string - Password string -} - -// HasAuth reports whether SMTP authentication should be attempted. -// -// It returns true when a User is configured, regardless of whether Password -// is set. This behavior is intentional to support SMTP servers that allow -// authentication without a password. For servers that require a password, -// this means authentication may still be attempted with an empty password. -func (smtp *SMTP) HasAuth() bool { - return smtp.User != "" + Host string + PlainAuth *PlainAuthConfig + XOAuth2Auth *XOAuth2AuthConfig } type ConfigHTTP struct { diff --git a/internal/notification/channels/smtp/generic_auth.go b/internal/notification/channels/smtp/generic_auth.go new file mode 100644 index 000000000..58c73dc5a --- /dev/null +++ b/internal/notification/channels/smtp/generic_auth.go @@ -0,0 +1,41 @@ +package smtp + +import ( + "net/smtp" + "slices" + + "github.com/zitadel/zitadel/internal/zerrors" +) + +type GenericAuth struct { + Host string + PlainOrLogin *plainOrLoginAuth + XOAuth2 *xoauth2Auth + selectedMethod smtp.Auth +} + +func (a *GenericAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + if server.Name != a.Host { + return "", nil, zerrors.ThrowInternal(nil, "SMTP-BtXgkX", "wrong host name") + } + + switch { + case slices.Contains(server.Auth, "XOAUTH2") && a.XOAuth2 != nil: + a.selectedMethod = a.XOAuth2 + case slices.Contains(server.Auth, "PLAIN") && a.PlainOrLogin != nil: + a.selectedMethod = a.PlainOrLogin + case slices.Contains(server.Auth, "LOGIN") && a.PlainOrLogin != nil: + a.selectedMethod = a.PlainOrLogin + default: + return "", nil, zerrors.ThrowInternalf(nil, "SMTP-3mBHTz", "no supported auth method found (server supports: %v)", server.Auth) + } + return a.selectedMethod.Start(server) +} + +func (a *GenericAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if a.selectedMethod == nil { + return nil, zerrors.ThrowInternal(nil, "SMTP-G99DUr", "no auth method selected") + } + + return a.selectedMethod.Next(fromServer, more) +} diff --git a/internal/notification/channels/smtp/plain_or_login_auth.go b/internal/notification/channels/smtp/plain_or_login_auth.go index 8e9ca56e0..a0ca1db5a 100644 --- a/internal/notification/channels/smtp/plain_or_login_auth.go +++ b/internal/notification/channels/smtp/plain_or_login_auth.go @@ -11,17 +11,24 @@ import ( // golang net/smtp SMTP AUTH LOGIN or PLAIN Auth Handler // Reference: https://gist.github.com/andelf/5118732?permalink_comment_id=4825669#gistcomment-4825669 -func PlainOrLoginAuth(username, password, host string) smtp.Auth { - return &plainOrLoginAuth{username: username, password: password, host: host} +func PlainOrLoginAuth(config PlainAuthConfig, host string) smtp.Auth { + return &plainOrLoginAuth{ + host: host, + config: config, + } } type plainOrLoginAuth struct { - username string - password string host string + config PlainAuthConfig authMethod string } +type PlainAuthConfig struct { + User string + Password string +} + func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { if server.Name != a.host { return "", nil, zerrors.ThrowInternal(nil, "SMTP-RRi75", "wrong host name") @@ -31,7 +38,7 @@ func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error return a.authMethod, nil, nil } else { a.authMethod = "PLAIN" - resp := []byte("\x00" + a.username + "\x00" + a.password) + resp := []byte("\x00" + a.config.User + "\x00" + a.config.Password) return a.authMethod, resp, nil } } @@ -48,9 +55,9 @@ func (a *plainOrLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) { switch { case bytes.Equal(fromServer, []byte("Username:")): - return []byte(a.username), nil + return []byte(a.config.User), nil case bytes.Equal(fromServer, []byte("Password:")): - return []byte(a.password), nil + return []byte(a.config.Password), nil default: return nil, zerrors.ThrowInternal(nil, "SMTP-HjW21", "unexpected server challenge") } diff --git a/internal/notification/channels/smtp/xoauth2_auth.go b/internal/notification/channels/smtp/xoauth2_auth.go new file mode 100644 index 000000000..c68d1a9d5 --- /dev/null +++ b/internal/notification/channels/smtp/xoauth2_auth.go @@ -0,0 +1,96 @@ +package smtp + +import ( + "context" + "hash/fnv" + "net/smtp" + + lru "github.com/hashicorp/golang-lru/v2" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + + "github.com/zitadel/zitadel/internal/zerrors" +) + +var tokenSourceCache *lru.TwoQueueCache[string, oauth2.TokenSource] + +func init() { + var err error + tokenSourceCache, err = lru.New2Q[string, oauth2.TokenSource](100) + if err != nil { + panic(err) + } +} + +type XOAuth2AuthConfig struct { + User string + TokenEndpoint string + Scopes []string + ClientCredentialsAuth *OAuth2ClientCredentials +} + +type OAuth2ClientCredentials struct { + ClientId string + ClientSecret string +} + +func (cfg XOAuth2AuthConfig) Hash() string { + hash := fnv.New128() + hash.Write([]byte(cfg.User)) + hash.Write([]byte(cfg.TokenEndpoint)) + for _, s := range cfg.Scopes { + hash.Write([]byte(s)) + } + if cfg.ClientCredentialsAuth != nil { + hash.Write([]byte(cfg.ClientCredentialsAuth.ClientId)) + hash.Write([]byte(cfg.ClientCredentialsAuth.ClientSecret)) + } + return string(hash.Sum(nil)) +} + +type xoauth2Auth struct { + host string + config XOAuth2AuthConfig +} + +func XOAuth2Auth(config XOAuth2AuthConfig, host string) smtp.Auth { + return &xoauth2Auth{ + host: host, + config: config, + } +} +func (a *xoauth2Auth) Start(server *smtp.ServerInfo) (string, []byte, error) { + if server.Name != a.host { + return "", nil, zerrors.ThrowInternal(nil, "SMTP-eRJLyi", "wrong host name") + } + + hash := a.config.Hash() + tokenSource, ok := tokenSourceCache.Get(hash) + if !ok { + if a.config.ClientCredentialsAuth != nil { + config := &clientcredentials.Config{ + ClientID: a.config.ClientCredentialsAuth.ClientId, + ClientSecret: a.config.ClientCredentialsAuth.ClientSecret, + Scopes: a.config.Scopes, + TokenURL: a.config.TokenEndpoint, + } + tokenSource = config.TokenSource(context.Background()) + tokenSourceCache.Add(hash, tokenSource) + } + } + + token, err := tokenSource.Token() + if err != nil { + return "", nil, zerrors.ThrowInternal(err, "SMTP-fjHcJu", "Failed to get token to connect with smtp server") + } + + resp := []byte("user=" + a.config.User + "\x01" + "auth=Bearer " + token.AccessToken + "\x01\x01") + return "XOAUTH2", resp, nil +} + +func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) { + if !more { + return nil, nil + } + return nil, zerrors.ThrowInternal(nil, "SMTP-Pqsrj9", "unexpected server challenge for XOAUTH2 auth method") +} diff --git a/internal/notification/handlers/config_email.go b/internal/notification/handlers/config_email.go index 42bbca9b7..5b78b39b3 100644 --- a/internal/notification/handlers/config_email.go +++ b/internal/notification/handlers/config_email.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/notification/channels/email" "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/notification/channels/webhook" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -22,30 +23,11 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email. ID: config.ID, Description: config.Description, } - if config.SMTPConfig != nil { - var password string - if config.SMTPConfig.Password != nil { - password, err = crypto.DecryptString(config.SMTPConfig.Password, n.SMTPPasswordCrypto) - if err != nil { - return nil, err - } - } - return &email.Config{ - ProviderConfig: provider, - SMTPConfig: &smtp.Config{ - From: config.SMTPConfig.SenderAddress, - FromName: config.SMTPConfig.SenderName, - ReplyToAddress: config.SMTPConfig.ReplyToAddress, - Tls: config.SMTPConfig.TLS, - SMTP: smtp.SMTP{ - Host: config.SMTPConfig.Host, - User: config.SMTPConfig.User, - Password: password, - }, - }, - }, nil + if config.SMTPConfig != nil { + return smtpToEmailConfig(config.SMTPConfig, provider, n.SMTPPasswordCrypto) } + if config.HTTPConfig != nil { return &email.Config{ ProviderConfig: provider, @@ -59,3 +41,61 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email. } return nil, zerrors.ThrowNotFound(err, "QUERY-KPQleOckOV", "Errors.SMTPConfig.NotFound") } + +func smtpToEmailConfig(qs *query.SMTP, provider *email.Provider, passCrypto crypto.EncryptionAlgorithm) (*email.Config, error) { + config := &email.Config{ + ProviderConfig: provider, + SMTPConfig: &smtp.Config{ + From: qs.SenderAddress, + FromName: qs.SenderName, + ReplyToAddress: qs.ReplyToAddress, + Tls: qs.TLS, + SMTP: smtp.SMTP{ + Host: qs.Host, + }, + }, + } + + if qs.XOAuth2Auth != nil { + config.SMTPConfig.SMTP.XOAuth2Auth = &smtp.XOAuth2AuthConfig{ + User: qs.User, + TokenEndpoint: qs.XOAuth2Auth.TokenEndpoint, + Scopes: qs.XOAuth2Auth.Scopes, + } + if qs.XOAuth2Auth.ClientCredentials != nil { + clientSecret, err := crypto.DecryptString(qs.XOAuth2Auth.ClientCredentials.ClientSecret, passCrypto) + if err != nil { + return nil, err + } + config.SMTPConfig.SMTP.XOAuth2Auth.ClientCredentialsAuth = &smtp.OAuth2ClientCredentials{ + ClientId: qs.XOAuth2Auth.ClientCredentials.ClientId, + ClientSecret: clientSecret, + } + } + } + + if qs.PlainAuth != nil { + config.SMTPConfig.SMTP.PlainAuth = &smtp.PlainAuthConfig{ + User: qs.User, + } + + if qs.PlainAuth.Password != nil { + password, err := crypto.DecryptString(qs.PlainAuth.Password, passCrypto) + if err != nil { + return nil, err + } + config.SMTPConfig.SMTP.PlainAuth.Password = password + } + } + + // if no auth is configured but there is a user, use plain auth without a password + if qs.User != "" && + config.SMTPConfig.SMTP.PlainAuth == nil && + config.SMTPConfig.SMTP.XOAuth2Auth == nil { + config.SMTPConfig.SMTP.PlainAuth = &smtp.PlainAuthConfig{ + User: qs.User, + } + } + + return config, nil +} diff --git a/internal/notification/handlers/config_email_test.go b/internal/notification/handlers/config_email_test.go index c389dda3b..7da0c1c12 100644 --- a/internal/notification/handlers/config_email_test.go +++ b/internal/notification/handlers/config_email_test.go @@ -45,11 +45,13 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) { ReplyToAddress: "reply@example.com", Host: "mail.com", User: "mail-user", - Password: &crypto.CryptoValue{ - CryptoType: 0, - Algorithm: cryptAlg, - KeyID: keyId, - Crypted: encryptedPwd, + PlainAuth: &query.PlainAuth{ + Password: &crypto.CryptoValue{ + CryptoType: 0, + Algorithm: cryptAlg, + KeyID: keyId, + Crypted: encryptedPwd, + }, }, }, }, @@ -60,9 +62,11 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) { }, SMTPConfig: &smtp.Config{ SMTP: smtp.SMTP{ - Host: "mail.com", - User: "mail-user", - Password: pwd, + Host: "mail.com", + PlainAuth: &smtp.PlainAuthConfig{ + User: "mail-user", + Password: pwd, + }, }, Tls: true, From: "sender@example.com", @@ -92,7 +96,9 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) { SMTPConfig: &smtp.Config{ SMTP: smtp.SMTP{ Host: "mail.com", - User: "mail-user", + PlainAuth: &smtp.PlainAuthConfig{ + User: "mail-user", + }, }, Tls: true, From: "sender@example.com", diff --git a/internal/notification/handlers/notification_worker_test.go b/internal/notification/handlers/notification_worker_test.go index 90c6de51f..cefb8d7a0 100644 --- a/internal/notification/handlers/notification_worker_test.go +++ b/internal/notification/handlers/notification_worker_test.go @@ -443,9 +443,11 @@ func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock. }, SMTPConfig: &smtp.Config{ SMTP: smtp.SMTP{ - Host: "host", - User: "user", - Password: "password", + Host: "host", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "password", + }, }, Tls: true, From: "from", diff --git a/internal/notification/handlers/user_notifier_legacy_test.go b/internal/notification/handlers/user_notifier_legacy_test.go index 2088b3c21..18335b936 100644 --- a/internal/notification/handlers/user_notifier_legacy_test.go +++ b/internal/notification/handlers/user_notifier_legacy_test.go @@ -1982,9 +1982,11 @@ func newUserNotifierLegacy(t *testing.T, ctrl *gomock.Controller, queries *mock. }, SMTPConfig: &smtp.Config{ SMTP: smtp.SMTP{ - Host: "host", - User: "user", - Password: "password", + Host: "host", + PlainAuth: &smtp.PlainAuthConfig{ + User: "user", + Password: "password", + }, }, Tls: true, From: "from", diff --git a/internal/query/projection/smtp.go b/internal/query/projection/smtp.go index 997f4ac8c..9b559c1fb 100644 --- a/internal/query/projection/smtp.go +++ b/internal/query/projection/smtp.go @@ -11,7 +11,7 @@ import ( ) const ( - SMTPConfigProjectionTable = "projections.smtp_configs5" + SMTPConfigProjectionTable = "projections.smtp_configs6" SMTPConfigTable = SMTPConfigProjectionTable + "_" + smtpConfigSMTPTableSuffix SMTPConfigHTTPTable = SMTPConfigProjectionTable + "_" + smtpConfigHTTPTableSuffix @@ -25,16 +25,20 @@ const ( SMTPConfigColumnState = "state" SMTPConfigColumnDescription = "description" - smtpConfigSMTPTableSuffix = "smtp" - SMTPConfigSMTPColumnInstanceID = "instance_id" - SMTPConfigSMTPColumnID = "id" - SMTPConfigSMTPColumnTLS = "tls" - SMTPConfigSMTPColumnSenderAddress = "sender_address" - SMTPConfigSMTPColumnSenderName = "sender_name" - SMTPConfigSMTPColumnReplyToAddress = "reply_to_address" - SMTPConfigSMTPColumnHost = "host" - SMTPConfigSMTPColumnUser = "username" - SMTPConfigSMTPColumnPassword = "password" + smtpConfigSMTPTableSuffix = "smtp" + SMTPConfigSMTPColumnInstanceID = "instance_id" + SMTPConfigSMTPColumnID = "id" + SMTPConfigSMTPColumnTLS = "tls" + SMTPConfigSMTPColumnSenderAddress = "sender_address" + SMTPConfigSMTPColumnSenderName = "sender_name" + SMTPConfigSMTPColumnReplyToAddress = "reply_to_address" + SMTPConfigSMTPColumnHost = "host" + SMTPConfigSMTPColumnUser = "username" + SMTPConfigSMTPColumnPlainAuthPassword = "password" + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId = "xoauth2auth_client_credentials_client_id" + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret = "xoauth2auth_client_credentials_client_secret" + SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint = "xoauth2auth_token_endpoint" + SMTPConfigSMTPColumnXOAuth2AuthScope = "xoauth2auth_scope" smtpConfigHTTPTableSuffix = "http" SMTPConfigHTTPColumnInstanceID = "instance_id" @@ -77,7 +81,11 @@ func (*smtpConfigProjection) Init() *old_handler.Check { handler.NewColumn(SMTPConfigSMTPColumnReplyToAddress, handler.ColumnTypeText), handler.NewColumn(SMTPConfigSMTPColumnHost, handler.ColumnTypeText), handler.NewColumn(SMTPConfigSMTPColumnUser, handler.ColumnTypeText), - handler.NewColumn(SMTPConfigSMTPColumnPassword, handler.ColumnTypeJSONB, handler.Nullable()), + handler.NewColumn(SMTPConfigSMTPColumnPlainAuthPassword, handler.ColumnTypeJSONB, handler.Nullable()), + handler.NewColumn(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret, handler.ColumnTypeJSONB, handler.Nullable()), + handler.NewColumn(SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SMTPConfigSMTPColumnXOAuth2AuthScope, handler.ColumnTypeTextArray, handler.Nullable()), }, handler.NewPrimaryKey(SMTPConfigSMTPColumnInstanceID, SMTPConfigSMTPColumnID), smtpConfigSMTPTableSuffix, @@ -155,6 +163,39 @@ func (p *smtpConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*h state = domain.SMTPConfigStateActive } + columns := []handler.Column{ + handler.NewCol(SMTPConfigSMTPColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SMTPConfigSMTPColumnID, getSMTPConfigID(e.ID, e.Aggregate())), + handler.NewCol(SMTPConfigSMTPColumnTLS, e.TLS), + handler.NewCol(SMTPConfigSMTPColumnSenderAddress, e.SenderAddress), + handler.NewCol(SMTPConfigSMTPColumnSenderName, e.SenderName), + handler.NewCol(SMTPConfigSMTPColumnReplyToAddress, e.ReplyToAddress), + handler.NewCol(SMTPConfigSMTPColumnHost, e.Host), + handler.NewCol(SMTPConfigSMTPColumnUser, e.User), + } + + if e.PlainAuth != nil { + columns = append(columns, removeXoauth()...) + columns = append(columns, handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, e.PlainAuth.Password)) + } else if e.Password != nil { + columns = append(columns, removeXoauth()...) + columns = append(columns, handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, e.Password)) + } + if e.XOAuth2Auth != nil { + columns = append(columns, removePlainAuth()...) + columns = append(columns, + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint, e.XOAuth2Auth.TokenEndpoint), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthScope, e.XOAuth2Auth.Scopes), + ) + + if e.XOAuth2Auth.ClientCredentials != nil { + columns = append(columns, + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId, e.XOAuth2Auth.ClientCredentials.ClientId), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret, e.XOAuth2Auth.ClientCredentials.ClientSecret), + ) + } + } + return handler.NewMultiStatement( e, handler.AddCreateStatement( @@ -171,22 +212,24 @@ func (p *smtpConfigProjection) reduceSMTPConfigAdded(event eventstore.Event) (*h }, ), handler.AddCreateStatement( - []handler.Column{ - handler.NewCol(SMTPConfigSMTPColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SMTPConfigSMTPColumnID, getSMTPConfigID(e.ID, e.Aggregate())), - handler.NewCol(SMTPConfigSMTPColumnTLS, e.TLS), - handler.NewCol(SMTPConfigSMTPColumnSenderAddress, e.SenderAddress), - handler.NewCol(SMTPConfigSMTPColumnSenderName, e.SenderName), - handler.NewCol(SMTPConfigSMTPColumnReplyToAddress, e.ReplyToAddress), - handler.NewCol(SMTPConfigSMTPColumnHost, e.Host), - handler.NewCol(SMTPConfigSMTPColumnUser, e.User), - handler.NewCol(SMTPConfigSMTPColumnPassword, e.Password), - }, + columns, handler.WithTableSuffix(smtpConfigSMTPTableSuffix), ), ), nil } +func removeXoauth() []handler.Column { + return []handler.Column{ + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint, nil), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthScope, nil), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId, nil), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret, nil), + } +} +func removePlainAuth() []handler.Column { + return []handler.Column{handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, nil)} +} + func (p *smtpConfigProjection) reduceSMTPConfigHTTPAdded(event eventstore.Event) (*handler.Statement, error) { e, err := assertEvent[*instance.SMTPConfigHTTPAddedEvent](event) if err != nil { @@ -307,7 +350,27 @@ func (p *smtpConfigProjection) reduceSMTPConfigChanged(event eventstore.Event) ( smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigSMTPColumnUser, *e.User)) } if e.Password != nil { - smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigSMTPColumnPassword, *e.Password)) + smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, *e.Password)) + } + + if !e.PlainAuth.IsEmpty() { + smtpColumns = append(smtpColumns, + handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, e.PlainAuth.Password), + ) + } + + if !e.XOAuth2Auth.IsEmpty() { + smtpColumns = append(smtpColumns, + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint, e.XOAuth2Auth.TokenEndpoint), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthScope, e.XOAuth2Auth.Scopes), + ) + + if !e.XOAuth2Auth.ClientCredentials.IsEmpty() { + smtpColumns = append(smtpColumns, + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId, e.XOAuth2Auth.ClientCredentials.ClientId), + handler.NewCol(SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret, e.XOAuth2Auth.ClientCredentials.ClientSecret), + ) + } } if len(smtpColumns) > 0 { stmts = append(stmts, handler.AddUpdateStatement( @@ -333,7 +396,7 @@ func (p *smtpConfigProjection) reduceSMTPConfigPasswordChanged(event eventstore. e, handler.AddUpdateStatement( []handler.Column{ - handler.NewCol(SMTPConfigSMTPColumnPassword, e.Password), + handler.NewCol(SMTPConfigSMTPColumnPlainAuthPassword, e.Password), }, []handler.Condition{ handler.NewCond(SMTPConfigSMTPColumnID, getSMTPConfigID(e.ID, e.Aggregate())), diff --git a/internal/query/projection/smtp_test.go b/internal/query/projection/smtp_test.go index 1529a5d2b..110be22a5 100644 --- a/internal/query/projection/smtp_test.go +++ b/internal/query/projection/smtp_test.go @@ -49,7 +49,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -59,7 +59,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_smtp SET (tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.smtp_configs6_smtp SET (tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ true, "sender", @@ -105,7 +105,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -115,7 +115,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_smtp SET (tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", + expectedStmt: "UPDATE projections.smtp_configs6_smtp SET (tls, sender_address, sender_name, reply_to_address, host, username) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (instance_id = $8)", expectedArgs: []interface{}{ true, "sender", @@ -155,7 +155,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -192,7 +192,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -201,7 +201,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_smtp SET sender_address = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.smtp_configs6_smtp SET sender_address = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "sender", "config-id", @@ -238,7 +238,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -248,7 +248,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_http SET (endpoint, signing_key) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.smtp_configs6_http SET (endpoint, signing_key) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "endpoint", anyArg{}, @@ -284,7 +284,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, description) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -321,7 +321,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -330,7 +330,7 @@ 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_configs6_http SET endpoint = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "endpoint", "config-id", @@ -340,7 +340,8 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, }, - }, { + }, + { name: "reduceSMTPConfigHTTPChanged, signing key", args: args{ event: getEvent( @@ -364,7 +365,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -373,7 +374,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5_http SET signing_key = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.smtp_configs6_http SET signing_key = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ anyArg{}, "config-id", @@ -416,7 +417,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.smtp_configs5 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.smtp_configs6 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -430,7 +431,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.smtp_configs5_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.smtp_configs6_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, xoauth2auth_token_endpoint, xoauth2auth_scope, xoauth2auth_client_credentials_client_id, xoauth2auth_client_credentials_client_secret, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", expectedArgs: []interface{}{ "instance-id", "ro-id", @@ -440,6 +441,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "reply-to", "host", "user", + nil, nil, nil, nil, anyArg{}, }, }, @@ -481,7 +483,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.smtp_configs5 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.smtp_configs6 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -495,7 +497,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.smtp_configs5_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.smtp_configs6_smtp (instance_id, id, tls, sender_address, sender_name, reply_to_address, host, username, xoauth2auth_token_endpoint, xoauth2auth_scope, xoauth2auth_client_credentials_client_id, xoauth2auth_client_credentials_client_secret, password) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", expectedArgs: []interface{}{ "instance-id", "config-id", @@ -505,6 +507,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { "reply-to", "host", "user", + nil, nil, nil, nil, anyArg{}, }, }, @@ -538,7 +541,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.smtp_configs5 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + expectedStmt: "INSERT INTO projections.smtp_configs6 (creation_date, change_date, instance_id, resource_owner, aggregate_id, id, sequence, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -552,7 +555,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "INSERT INTO projections.smtp_configs5_http (instance_id, id, endpoint, signing_key) VALUES ($1, $2, $3, $4)", + expectedStmt: "INSERT INTO projections.smtp_configs6_http (instance_id, id, endpoint, signing_key) VALUES ($1, $2, $3, $4)", expectedArgs: []interface{}{ "instance-id", "config-id", @@ -585,7 +588,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -596,7 +599,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -629,7 +632,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (NOT (id = $4)) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -640,7 +643,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -674,7 +677,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -707,7 +710,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence, state) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -747,7 +750,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.smtp_configs5_smtp SET password = $1 WHERE (id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.smtp_configs6_smtp SET password = $1 WHERE (id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ anyArg{}, "config-id", @@ -755,7 +758,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { }, }, { - expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", + expectedStmt: "UPDATE projections.smtp_configs6 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -788,7 +791,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.smtp_configs5 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.smtp_configs6 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "config-id", "instance-id", @@ -818,7 +821,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.smtp_configs5 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.smtp_configs6 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "ro-id", "instance-id", @@ -845,7 +848,7 @@ func TestSMTPConfigProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.smtp_configs5 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.smtp_configs6 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/smtp.go b/internal/query/smtp.go index 21320872b..7e215cc10 100644 --- a/internal/query/smtp.go +++ b/internal/query/smtp.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -100,8 +101,24 @@ var ( name: projection.SMTPConfigSMTPColumnUser, table: smtpConfigsSMTPTable, } - SMTPConfigSMTPColumnPassword = Column{ - name: projection.SMTPConfigSMTPColumnPassword, + SMTPConfigSMTPColumnPlainAuthPassword = Column{ + name: projection.SMTPConfigSMTPColumnPlainAuthPassword, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId = Column{ + name: projection.SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret = Column{ + name: projection.SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint = Column{ + name: projection.SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint, + table: smtpConfigsSMTPTable, + } + SMTPConfigSMTPColumnXOAuth2AuthScope = Column{ + name: projection.SMTPConfigSMTPColumnXOAuth2AuthScope, table: smtpConfigsSMTPTable, } @@ -156,7 +173,35 @@ type SMTP struct { ReplyToAddress string Host string User string - Password *crypto.CryptoValue + PlainAuth *PlainAuth + XOAuth2Auth *XOAuth2Auth +} + +type PlainAuth struct { + Password *crypto.CryptoValue +} + +func (a PlainAuth) isEmpty() bool { + return a.Password == nil +} + +type XOAuth2Auth struct { + TokenEndpoint string + Scopes []string + ClientCredentials *XOAuthClientCredentials +} + +func (a XOAuth2Auth) isEmpty() bool { + return a.ClientCredentials == nil && a.TokenEndpoint == "" && len(a.Scopes) == 0 +} + +type XOAuthClientCredentials struct { + ClientId string + ClientSecret *crypto.CryptoValue +} + +func (a XOAuthClientCredentials) isEmpty() bool { + return a.ClientId == "" && a.ClientSecret == nil } func (q *Queries) SMTPConfigActive(ctx context.Context, resourceOwner string) (config *SMTPConfig, err error) { @@ -229,7 +274,11 @@ func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, er SMTPConfigSMTPColumnReplyToAddress.identifier(), SMTPConfigSMTPColumnHost.identifier(), SMTPConfigSMTPColumnUser.identifier(), - SMTPConfigSMTPColumnPassword.identifier(), + SMTPConfigSMTPColumnPlainAuthPassword.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthScope.identifier(), SMTPConfigHTTPColumnID.identifier(), SMTPConfigHTTPColumnEndpoint.identifier(), @@ -260,7 +309,11 @@ func prepareSMTPConfigQuery() (sq.SelectBuilder, func(*sql.Row) (*SMTPConfig, er &smtpConfig.replyToAddress, &smtpConfig.host, &smtpConfig.user, - &smtpConfig.password, + &smtpConfig.plainAuthPassword, + &smtpConfig.xoauth2AuthClientCredentialsClientId, + &smtpConfig.xoauth2AuthClientClientCredentialsSecret, + &smtpConfig.xoauth2AuthTokenEndpoint, + &smtpConfig.xoauth2AuthScope, &httpConfig.id, &httpConfig.endpoint, &httpConfig.signingKey, @@ -296,7 +349,11 @@ func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, SMTPConfigSMTPColumnReplyToAddress.identifier(), SMTPConfigSMTPColumnHost.identifier(), SMTPConfigSMTPColumnUser.identifier(), - SMTPConfigSMTPColumnPassword.identifier(), + SMTPConfigSMTPColumnPlainAuthPassword.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientId.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthClientCredentialsClientSecret.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthTokenEndpoint.identifier(), + SMTPConfigSMTPColumnXOAuth2AuthScope.identifier(), SMTPConfigHTTPColumnID.identifier(), SMTPConfigHTTPColumnEndpoint.identifier(), @@ -330,7 +387,11 @@ func prepareSMTPConfigsQuery() (sq.SelectBuilder, func(*sql.Rows) (*SMTPConfigs, &smtpConfig.replyToAddress, &smtpConfig.host, &smtpConfig.user, - &smtpConfig.password, + &smtpConfig.plainAuthPassword, + &smtpConfig.xoauth2AuthClientCredentialsClientId, + &smtpConfig.xoauth2AuthClientClientCredentialsSecret, + &smtpConfig.xoauth2AuthTokenEndpoint, + &smtpConfig.xoauth2AuthScope, &httpConfig.id, &httpConfig.endpoint, &httpConfig.signingKey, @@ -379,20 +440,51 @@ func (q *Queries) SearchSMTPConfigs(ctx context.Context, queries *SMTPConfigsSea } type sqlSmtpConfig struct { - id sql.NullString - tls sql.NullBool - senderAddress sql.NullString - senderName sql.NullString - replyToAddress sql.NullString - host sql.NullString - user sql.NullString - password *crypto.CryptoValue + id sql.NullString + tls sql.NullBool + senderAddress sql.NullString + senderName sql.NullString + replyToAddress sql.NullString + host sql.NullString + user sql.NullString + plainAuthPassword *crypto.CryptoValue + xoauth2AuthClientCredentialsClientId sql.NullString + xoauth2AuthClientClientCredentialsSecret *crypto.CryptoValue + xoauth2AuthTokenEndpoint sql.NullString + xoauth2AuthScope sql.NullString } func (c sqlSmtpConfig) set(smtpConfig *SMTPConfig) { if !c.id.Valid { return } + + plainAuth := &PlainAuth{ + Password: c.plainAuthPassword, + } + if plainAuth.isEmpty() { + plainAuth = nil + } + + scopes := strings.Split(c.xoauth2AuthScope.String, " ") + if len(scopes) == 1 && scopes[0] == "" { + scopes = nil + } + xoauth2Auth := &XOAuth2Auth{ + TokenEndpoint: c.xoauth2AuthTokenEndpoint.String, + Scopes: scopes, + ClientCredentials: &XOAuthClientCredentials{ + ClientId: c.xoauth2AuthClientCredentialsClientId.String, + ClientSecret: c.xoauth2AuthClientClientCredentialsSecret, + }, + } + if xoauth2Auth.ClientCredentials.isEmpty() { + xoauth2Auth.ClientCredentials = nil + } + if xoauth2Auth.isEmpty() { + xoauth2Auth = nil + } + smtpConfig.SMTPConfig = &SMTP{ TLS: c.tls.Bool, SenderAddress: c.senderAddress.String, @@ -400,7 +492,8 @@ func (c sqlSmtpConfig) set(smtpConfig *SMTPConfig) { ReplyToAddress: c.replyToAddress.String, Host: c.host.String, User: c.user.String, - Password: c.password, + PlainAuth: plainAuth, + XOAuth2Auth: xoauth2Auth, } } diff --git a/internal/query/smtp_test.go b/internal/query/smtp_test.go index f94a88230..a28db4dde 100644 --- a/internal/query/smtp_test.go +++ b/internal/query/smtp_test.go @@ -14,49 +14,57 @@ import ( ) var ( - prepareSMTPConfigStmt = `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` + - ` 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,` + + prepareSMTPConfigStmt = `SELECT projections.smtp_configs6.creation_date,` + + ` projections.smtp_configs6.change_date,` + + ` projections.smtp_configs6.resource_owner,` + + ` projections.smtp_configs6.sequence,` + + ` projections.smtp_configs6.id,` + + ` projections.smtp_configs6.state,` + + ` projections.smtp_configs6.description,` + + ` projections.smtp_configs6_smtp.id,` + + ` projections.smtp_configs6_smtp.tls,` + + ` projections.smtp_configs6_smtp.sender_address,` + + ` projections.smtp_configs6_smtp.sender_name,` + + ` projections.smtp_configs6_smtp.reply_to_address,` + + ` projections.smtp_configs6_smtp.host,` + + ` projections.smtp_configs6_smtp.username,` + + ` projections.smtp_configs6_smtp.password,` + + ` projections.smtp_configs6_smtp.xoauth2auth_client_credentials_client_id,` + + ` projections.smtp_configs6_smtp.xoauth2auth_client_credentials_client_secret,` + + ` projections.smtp_configs6_smtp.xoauth2auth_token_endpoint,` + + ` projections.smtp_configs6_smtp.xoauth2auth_scope,` + + ` projections.smtp_configs6_http.id,` + + ` projections.smtp_configs6_http.endpoint,` + + ` projections.smtp_configs6_http.signing_key` + + ` FROM projections.smtp_configs6` + + ` LEFT JOIN projections.smtp_configs6_smtp ON projections.smtp_configs6.id = projections.smtp_configs6_smtp.id AND projections.smtp_configs6.instance_id = projections.smtp_configs6_smtp.instance_id` + + ` LEFT JOIN projections.smtp_configs6_http ON projections.smtp_configs6.id = projections.smtp_configs6_http.id AND projections.smtp_configs6.instance_id = projections.smtp_configs6_http.instance_id` + prepareSMTPConfigsStmt = `SELECT projections.smtp_configs6.creation_date,` + + ` projections.smtp_configs6.change_date,` + + ` projections.smtp_configs6.resource_owner,` + + ` projections.smtp_configs6.sequence,` + + ` projections.smtp_configs6.id,` + + ` projections.smtp_configs6.state,` + + ` projections.smtp_configs6.description,` + + ` projections.smtp_configs6_smtp.id,` + + ` projections.smtp_configs6_smtp.tls,` + + ` projections.smtp_configs6_smtp.sender_address,` + + ` projections.smtp_configs6_smtp.sender_name,` + + ` projections.smtp_configs6_smtp.reply_to_address,` + + ` projections.smtp_configs6_smtp.host,` + + ` projections.smtp_configs6_smtp.username,` + + ` projections.smtp_configs6_smtp.password,` + + ` projections.smtp_configs6_smtp.xoauth2auth_client_credentials_client_id,` + + ` projections.smtp_configs6_smtp.xoauth2auth_client_credentials_client_secret,` + + ` projections.smtp_configs6_smtp.xoauth2auth_token_endpoint,` + + ` projections.smtp_configs6_smtp.xoauth2auth_scope,` + + ` projections.smtp_configs6_http.id,` + + ` projections.smtp_configs6_http.endpoint,` + + ` projections.smtp_configs6_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` + ` FROM projections.smtp_configs6` + + ` LEFT JOIN projections.smtp_configs6_smtp ON projections.smtp_configs6.id = projections.smtp_configs6_smtp.id AND projections.smtp_configs6.instance_id = projections.smtp_configs6_smtp.instance_id` + + ` LEFT JOIN projections.smtp_configs6_http ON projections.smtp_configs6.id = projections.smtp_configs6_http.id AND projections.smtp_configs6.instance_id = projections.smtp_configs6_http.instance_id` prepareSMTPConfigCols = []string{ "creation_date", @@ -74,6 +82,10 @@ var ( "smtp_host", "smtp_user", "smtp_password", + "xoauth2auth_client_credentials_client_id", + "xoauth2auth_client_credentials_client_secret", + "xoauth2auth_token_endpoint", + "xoauth2auth_scope", "id", "endpoint", "signing_key", @@ -136,6 +148,10 @@ func Test_SMTPConfigPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, ), }, @@ -151,7 +167,9 @@ func Test_SMTPConfigPrepares(t *testing.T) { ReplyToAddress: "reply-to", Host: "host", User: "user", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "2232323", State: domain.SMTPConfigStateActive, @@ -181,6 +199,10 @@ func Test_SMTPConfigPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, "2232323", "endpoint", &crypto.CryptoValue{ @@ -237,6 +259,10 @@ func Test_SMTPConfigPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, ), }, @@ -252,7 +278,9 @@ func Test_SMTPConfigPrepares(t *testing.T) { ReplyToAddress: "reply-to2", Host: "host2", User: "user2", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "44442323", State: domain.SMTPConfigStateInactive, @@ -285,6 +313,10 @@ func Test_SMTPConfigPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, ), }, @@ -300,7 +332,9 @@ func Test_SMTPConfigPrepares(t *testing.T) { ReplyToAddress: "reply-to3", Host: "host3", User: "user3", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "23234444", State: domain.SMTPConfigStateInactive, @@ -383,6 +417,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, }, ), @@ -404,7 +442,9 @@ func Test_SMTPConfigsPrepares(t *testing.T) { ReplyToAddress: "reply-to", Host: "host", User: "user", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "2232323", State: domain.SMTPConfigStateActive, @@ -437,6 +477,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, "2232323", "endpoint", &crypto.CryptoValue{ @@ -465,6 +509,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, { testNow, @@ -485,6 +533,10 @@ func Test_SMTPConfigsPrepares(t *testing.T) { nil, nil, nil, + nil, + nil, + nil, + nil, }, }, ), @@ -524,7 +576,9 @@ func Test_SMTPConfigsPrepares(t *testing.T) { ReplyToAddress: "reply-to2", Host: "host2", User: "user2", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "44442323", State: domain.SMTPConfigStateInactive, @@ -542,7 +596,9 @@ func Test_SMTPConfigsPrepares(t *testing.T) { ReplyToAddress: "reply-to3", Host: "host3", User: "user3", - Password: &crypto.CryptoValue{}, + PlainAuth: &PlainAuth{ + Password: &crypto.CryptoValue{}, + }, }, ID: "23234444", State: domain.SMTPConfigStateInactive, diff --git a/internal/repository/instance/smtp_config.go b/internal/repository/instance/smtp_config.go index 78c14ed4e..089e723e4 100644 --- a/internal/repository/instance/smtp_config.go +++ b/internal/repository/instance/smtp_config.go @@ -33,6 +33,23 @@ type SMTPConfigAddedEvent struct { Host string `json:"host,omitempty"` User string `json:"user,omitempty"` Password *crypto.CryptoValue `json:"password,omitempty"` + PlainAuth *PlainAuth `json:"plainAuth,omitempty"` + XOAuth2Auth *XOAuth2Auth `json:"xoauth2Auth,omitempty"` +} + +type XOAuth2Auth struct { + TokenEndpoint string `json:"tokenEndpoint,omitempty"` + Scopes []string `json:"scopes,omitempty"` + ClientCredentials *XOAuth2ClientCredentials `json:"clientCredentials,omitempty"` +} + +type XOAuth2ClientCredentials struct { + ClientId string `json:"clientId,omitempty"` + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` +} + +type PlainAuth struct { + Password *crypto.CryptoValue `json:"password,omitempty"` } func NewSMTPConfigAddedEvent( @@ -43,9 +60,10 @@ func NewSMTPConfigAddedEvent( senderAddress, senderName, replyToAddress, - host, + host string, user string, - password *crypto.CryptoValue, + plainAuth *PlainAuth, + xoauth2Auth *XOAuth2Auth, ) *SMTPConfigAddedEvent { return &SMTPConfigAddedEvent{ BaseEvent: eventstore.NewBaseEventForPush( @@ -61,9 +79,11 @@ func NewSMTPConfigAddedEvent( ReplyToAddress: replyToAddress, Host: host, User: user, - Password: password, + PlainAuth: plainAuth, + XOAuth2Auth: xoauth2Auth, } } + func (e *SMTPConfigAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = event } @@ -87,6 +107,35 @@ type SMTPConfigChangedEvent struct { Host *string `json:"host,omitempty"` User *string `json:"user,omitempty"` Password *crypto.CryptoValue `json:"password,omitempty"` + PlainAuth PlainAuthChanged `json:"plainAuth,omitempty"` + XOAuth2Auth XOAuth2AuthChanged `json:"xoauth2Auth,omitempty"` +} + +type XOAuth2AuthChanged struct { + TokenEndpoint *string `json:"tokenEndpoint,omitempty"` + Scopes []string `json:"scopes,omitempty"` + ClientCredentials XOAuth2ClientCredentialsChanged `json:"clientCredentials,omitempty"` +} + +func (c XOAuth2AuthChanged) IsEmpty() bool { + return c.ClientCredentials.IsEmpty() && c.TokenEndpoint == nil && len(c.Scopes) == 0 +} + +type XOAuth2ClientCredentialsChanged struct { + ClientId *string `json:"clientId,omitempty"` + ClientSecret *crypto.CryptoValue `json:"clientSecret,omitempty"` +} + +func (c XOAuth2ClientCredentialsChanged) IsEmpty() bool { + return c.ClientId == nil && c.ClientSecret == nil +} + +type PlainAuthChanged struct { + Password *crypto.CryptoValue `json:"password,omitempty"` +} + +func (c PlainAuthChanged) IsEmpty() bool { + return c.Password == nil } func (e *SMTPConfigChangedEvent) SetBaseEvent(event *eventstore.BaseEvent) { @@ -177,6 +226,31 @@ func ChangeSMTPConfigSMTPUser(smtpUser string) func(event *SMTPConfigChangedEven func ChangeSMTPConfigSMTPPassword(password *crypto.CryptoValue) func(event *SMTPConfigChangedEvent) { return func(e *SMTPConfigChangedEvent) { e.Password = password + e.PlainAuth.Password = password + } +} + +func ChangeSMTPConfigXOAuth2ClientCredentialsClientId(clientId string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.XOAuth2Auth.ClientCredentials.ClientId = &clientId + } +} + +func ChangeSMTPConfigXOAuth2ClientCredentialsClientSecret(clientSecret *crypto.CryptoValue) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.XOAuth2Auth.ClientCredentials.ClientSecret = clientSecret + } +} + +func ChangeSMTPConfigXOAuth2TokenEndpoint(tokenEndpoint string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.XOAuth2Auth.TokenEndpoint = &tokenEndpoint + } +} + +func ChangeSMTPConfigXOAuth2Scopes(scopes []string) func(event *SMTPConfigChangedEvent) { + return func(e *SMTPConfigChangedEvent) { + e.XOAuth2Auth.Scopes = scopes } } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index d491857f8..677f04bb3 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4944,6 +4944,7 @@ message AddEmailProviderSMTPRequest { example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; } ]; + // Deprecated: use [Auth] oneof instead. string password = 6 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"this-is-my-password\""; @@ -4965,6 +4966,78 @@ message AddEmailProviderSMTPRequest { max_length: 200; } ]; + oneof Auth { + SMTPNoAuth none = 9; + SMTPPlainAuth plain = 10; + SMTPXOAuth2Auth xoauth2 = 11; + } +} + +// SMTPNoAuth can be used when no authentication is required for the SMTP connection. +message SMTPNoAuth {} + +// SMTPPlainAuth adds plain authentication to the smtp connection. The username of the parent configuration is used in +// combination with the password in this message. +message SMTPPlainAuth { + string password = 1 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-client-secret\""; + } + ]; +} + +// SMTPXOAuth2Auth adds XOAut2 authentication to the smtp connection. The username of the parent configuration is used +// in combination with the other parameters of this message. +message SMTPXOAuth2Auth { + // token_endpoint is the endpoint from which a token will be requested according to the oauth2 specification. + string token_endpoint = 1 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://auth.example.com/token\""; + min_length: 0; + max_length: 200; + } + ]; + // scopes is a list of scopes which should be requested from the authorization service. To authorize a token to + // use the smtp functionality, the server requires a specific scope. These scopes can be found in the documentation + // of the provider. + repeated string scopes = 2 [ + (google.api.field_behavior) = REQUIRED, + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://mail.example.com\"]"; + } + ]; + // OAuth2Type is a oneof used to switch between the different types of oauth2 authorization flows. + oneof OAuth2Type { + ClientCredentials client_credentials = 3; + } + + // ClientCredentials authenticates the SMTP client using Oauth 2.0 Client Credentials grant. + // https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + message ClientCredentials { + string client_id = 1 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + min_length: 0; + max_length: 200; + } + ]; + string client_secret = 2 [ + (validate.rules).string = {min_len: 0, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"this-is-my-client-secret\""; + min_length: 0; + max_length: 200; + } + ]; + } } message AddEmailProviderSMTPResponse { @@ -5029,6 +5102,11 @@ message UpdateEmailProviderSMTPRequest { } ]; string id = 9 [(validate.rules).string = {min_len: 1, max_len: 100}]; + oneof Auth { + SMTPNoAuth none = 10; + SMTPPlainAuth plain = 11; + SMTPXOAuth2Auth xoauth2 = 12; + } } message UpdateEmailProviderSMTPResponse { @@ -5221,6 +5299,12 @@ message TestEmailProviderSMTPRequest { example: "\"267191369515139464\""; } ]; + oneof Auth { + option (validate.required) = true; + SMTPNoAuth none = 9; + SMTPPlainAuth plain = 10; + SMTPXOAuth2Auth xoauth2 = 11; + } } // This is an empty response diff --git a/proto/zitadel/settings.proto b/proto/zitadel/settings.proto index 9e1f43c34..68d7ac1af 100644 --- a/proto/zitadel/settings.proto +++ b/proto/zitadel/settings.proto @@ -140,6 +140,51 @@ message EmailProviderSMTP { example: "\"replyto@m.zitadel.cloud\""; } ]; + oneof Auth { + SMTPNoAuth none = 9; + SMTPPlainAuth plain = 10; + SMTPXOAuth2Auth xoauth2 = 11; + } +} + +// SMTPNoAuth can be used when no authentication is required for the SMTP connection. +message SMTPNoAuth {} + +// SMTPPlainAuth adds plain authentication to the smtp connection. The username of the parent configuration is used in +// combination with the password in this message. +message SMTPPlainAuth {} + +// SMTPXOAuth2Auth adds XOAut2 authentication to the smtp connection. The username of the parent configuration is used +// in combination with the other parameters of this message. +message SMTPXOAuth2Auth { + // token_endpoint is the endpoint from which a token will be requested according to the oauth2 specification. + string token_endpoint = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"http://auth.example.com/token\""; + } + ]; + // scopes is a list of scopes which should be requested from the authorization service. To authorize a token to + // use the smtp functionality, the server requires a specific scope. These scopes can be found in the documentation + // of the provider. + repeated string scopes = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://mail.example.com\"]"; + } + ]; + // OAuth2Type is a oneof used to switch between the different types of oauth2 authorization flows. + oneof OAuth2Type { + ClientCredentials client_credentials = 3; + } + + // ClientCredentials authenticates the SMTP client using Oauth 2.0 Client Credentials grant. + // https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 + message ClientCredentials { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"197f0117-529e-443d-bf6c-0292dd9a02b7\""; + } + ]; + } } message EmailProviderHTTP {