From e9087a1d1ce4e79bffdae21da261c037ba8c30df Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Tue, 8 Feb 2022 16:09:06 +0100 Subject: [PATCH] feat: courier template configs (#2156) It is now possible to override individual courier email templates using the configuration system! Closes #2054 --- README.md | 34 ++- courier/courier.go | 26 +- courier/courier_test.go | 6 +- courier/template/load_template.go | 93 ++++-- courier/template/load_template_test.go | 144 ++++++++- courier/template/recovery_invalid.go | 19 +- courier/template/recovery_invalid_test.go | 24 +- courier/template/recovery_valid.go | 19 +- courier/template/recovery_valid_test.go | 24 +- courier/template/stub.go | 19 +- courier/template/template.go | 17 ++ courier/template/testhelpers/testhelpers.go | 127 ++++++++ courier/template/verification_invalid.go | 19 +- courier/template/verification_invalid_test.go | 26 +- courier/template/verification_valid.go | 19 +- courier/template/verification_valid_test.go | 24 +- courier/templates.go | 19 +- courier/templates_test.go | 29 +- docs/docs/concepts/email-sms.md | 97 ++++++- docs/docs/reference/configuration.md | 274 ++++++++++++++++-- .../version-v0.8/concepts/email-sms.md | 109 ++++++- driver/config/config.go | 75 ++++- driver/config/config_test.go | 61 ++++ .../.kratos.courier.remote.invalid.body.yaml | 30 ++ ...kratos.courier.remote.invalid.subject.yaml | 30 ++ ...atos.courier.remote.partial.templates.yaml | 26 ++ .../.kratos.courier.remote.templates.yaml | 30 ++ driver/registry_default.go | 6 +- embedx/config.schema.json | 81 ++++++ selfservice/strategy/link/sender.go | 17 +- .../profiles/recovery/recovery/errors.spec.ts | 12 + .../verification/verify/errors.spec.ts | 10 + test/e2e/cypress/support/commands.ts | 32 ++ test/e2e/cypress/support/index.d.ts | 5 + test/e2e/run.sh | 2 +- .../courierTemplates.bodyHtmlMissing.yaml | 5 + .../courierTemplates.emailMissing.yaml | 4 + .../emailCourierTemplate.malformed.yaml | 4 + ...ngCourierTemplatesVerificationInvalid.yaml | 20 ++ .../courierTemplates.full.yaml | 4 + .../emailCourierTemplate.full.yaml | 4 + .../emailCourierTemplate.withMissingBody.yaml | 1 + ...ailCourierTemplate.withMissingSubject.yaml | 3 + .../root.courierTemplates.yaml | 18 ++ 44 files changed, 1454 insertions(+), 194 deletions(-) create mode 100644 courier/template/testhelpers/testhelpers.go create mode 100644 driver/config/stub/.kratos.courier.remote.invalid.body.yaml create mode 100644 driver/config/stub/.kratos.courier.remote.invalid.subject.yaml create mode 100644 driver/config/stub/.kratos.courier.remote.partial.templates.yaml create mode 100644 driver/config/stub/.kratos.courier.remote.templates.yaml create mode 100644 test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml create mode 100644 test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml create mode 100644 test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml create mode 100644 test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml create mode 100644 test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml create mode 100644 test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml create mode 100644 test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml create mode 100644 test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml create mode 100644 test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml diff --git a/README.md b/README.md index 844a4e568ff2..3318bc396319 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ that your company deserves a spot here, reach out to DataDetect Datadetect unifiedglobalarchiving.com/data-detect/ - + Adopter * Sainsbury's @@ -183,13 +183,13 @@ that your company deserves a spot here, reach out to Reyah Reyah reyah.eu - + Adopter * Zero Project Zero by Commit getzero.dev - + Adopter * Padis @@ -207,7 +207,7 @@ that your company deserves a spot here, reach out to Security Onion Solutions Security Onion Solutions securityonionsolutions.com - + Adopter * Factly @@ -231,7 +231,7 @@ that your company deserves a spot here, reach out to Spiri.bo Spiri.bo spiri.bo - + Sponsor Strivacity @@ -504,6 +504,30 @@ For more details, run: ./test/e2e/run.sh +**Run only a singular test** + +Add `.only` to the test you would like to run. + +For example: + +```ts +it.only('invalid remote recovery email template', () => { + ... +}) +``` + +**Run a subset of tests** + +This will require editing the `cypress.json` file located in the `test/e2e/` folder. + +Add the `testFiles` option and specify the test to run inside the `cypress/integration` folder. +As an example we will add only the `network` tests. +```json +"testFiles": ["profiles/network/*"], +``` + +Now start the tests again using the run script or makefile. + #### Build Docker You can build a development Docker Image using: diff --git a/courier/courier.go b/courier/courier.go index 3e800408e560..329a95a48d27 100644 --- a/courier/courier.go +++ b/courier/courier.go @@ -5,10 +5,14 @@ import ( "crypto/tls" "encoding/json" "fmt" - "net/url" "strconv" "time" + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httpx" + "github.com/cenkalti/backoff" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -21,20 +25,14 @@ import ( ) type ( - SMTPConfig interface { - CourierSMTPURL() *url.URL - CourierSMTPFrom() string - CourierSMTPFromName() string - CourierSMTPHeaders() map[string]string - CourierTemplatesRoot() string - } SMTPDependencies interface { PersistenceProvider x.LoggingProvider ConfigProvider + HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client } TemplateTyper func(t EmailTemplate) (TemplateType, error) - EmailTemplateFromMessage func(c SMTPConfig, msg Message) (EmailTemplate, error) + EmailTemplateFromMessage func(d SMTPDependencies, msg Message) (EmailTemplate, error) Courier struct { Dialer *gomail.Dialer d SMTPDependencies @@ -45,7 +43,7 @@ type ( Courier(ctx context.Context) *Courier } ConfigProvider interface { - CourierConfig(ctx context.Context) SMTPConfig + CourierConfig(ctx context.Context) config.CourierConfigs } ) @@ -101,12 +99,12 @@ func (m *Courier) QueueEmail(ctx context.Context, t EmailTemplate) (uuid.UUID, e return uuid.Nil, err } - subject, err := t.EmailSubject() + subject, err := t.EmailSubject(ctx) if err != nil { return uuid.Nil, err } - bodyPlaintext, err := t.EmailBodyPlaintext() + bodyPlaintext, err := t.EmailBodyPlaintext(ctx) if err != nil { return uuid.Nil, err } @@ -188,14 +186,14 @@ func (m *Courier) DispatchMessage(ctx context.Context, msg Message) error { gm.SetBody("text/plain", msg.Body) - tmpl, err := m.NewEmailTemplateFromMessage(m.d.CourierConfig(ctx), msg) + tmpl, err := m.NewEmailTemplateFromMessage(m.d, msg) if err != nil { m.d.Logger(). WithError(err). WithField("message_id", msg.ID). Error(`Unable to get email template from message.`) } else { - htmlBody, err := tmpl.EmailBody() + htmlBody, err := tmpl.EmailBody(ctx) if err != nil { m.d.Logger(). WithError(err). diff --git a/courier/courier_test.go b/courier/courier_test.go index 866a975f30cb..d47ba8e7fd41 100644 --- a/courier/courier_test.go +++ b/courier/courier_test.go @@ -85,7 +85,7 @@ func TestSMTP(t *testing.T) { ctx, cancel := context.WithCancel(ctx) defer cancel() - id, err := c.QueueEmail(ctx, templates.NewTestStub(conf, &templates.TestStubModel{ + id, err := c.QueueEmail(ctx, templates.NewTestStub(reg, &templates.TestStubModel{ To: "test-recipient-1@example.org", Subject: "test-subject-1", Body: "test-body-1", @@ -93,7 +93,7 @@ func TestSMTP(t *testing.T) { require.NoError(t, err) require.NotEqual(t, uuid.Nil, id) - id, err = c.QueueEmail(ctx, templates.NewTestStub(conf, &templates.TestStubModel{ + id, err = c.QueueEmail(ctx, templates.NewTestStub(reg, &templates.TestStubModel{ To: "test-recipient-2@example.org", Subject: "test-subject-2", Body: "test-body-2", @@ -107,7 +107,7 @@ func TestSMTP(t *testing.T) { conf.MustSet(config.ViperKeyCourierSMTPHeaders+".test-stub-header2", "bar") customerHeaders := conf.CourierSMTPHeaders() require.Len(t, customerHeaders, 2) - id, err = c.QueueEmail(ctx, templates.NewTestStub(conf, &templates.TestStubModel{ + id, err = c.QueueEmail(ctx, templates.NewTestStub(reg, &templates.TestStubModel{ To: "test-recipient-3@example.org", Subject: "test-subject-3", Body: "test-body-3", diff --git a/courier/template/load_template.go b/courier/template/load_template.go index cf24c4c140a9..242e67803ccc 100644 --- a/courier/template/load_template.go +++ b/courier/template/load_template.go @@ -2,6 +2,7 @@ package template import ( "bytes" + "context" "embed" htemplate "html/template" "io" @@ -9,6 +10,11 @@ import ( "path/filepath" "text/template" + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/fetcher" + "github.com/ory/x/httpx" + "github.com/Masterminds/sprig/v3" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" @@ -17,18 +23,22 @@ import ( //go:embed courier/builtin/templates/* var templates embed.FS -var cache, _ = lru.New(16) +var Cache, _ = lru.New(16) type Template interface { Execute(wr io.Writer, data interface{}) error } -func loadBuiltInTemplate(filesytem fs.FS, name string, html bool) (Template, error) { - if t, found := cache.Get(name); found { +type templateDependencies interface { + HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client +} + +func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, error) { + if t, found := Cache.Get(name); found { return t.(Template), nil } - file, err := filesytem.Open(name) + file, err := filesystem.Open(name) if err != nil { // try to fallback to bundled templates var fallbackErr error @@ -61,12 +71,45 @@ func loadBuiltInTemplate(filesytem fs.FS, name string, html bool) (Template, err tpl = t } - _ = cache.Add(name, tpl) + _ = Cache.Add(name, tpl) return tpl, nil } +func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, html bool) (Template, error) { + var b []byte + var err error + + // instead of creating a new request always we always cache the bytes.Buffer using the url as the key + if t, found := Cache.Get(url); found { + b = t.([]byte) + } else { + f := fetcher.NewFetcher(fetcher.WithClient(d.HTTPClient(ctx))) + bb, err := f.Fetch(url) + if err != nil { + return nil, errors.WithStack(err) + } + b = bb.Bytes() + _ = Cache.Add(url, b) + } + + var t Template + if html { + t, err = htemplate.New(url).Funcs(sprig.HtmlFuncMap()).Parse(string(b)) + if err != nil { + return nil, errors.WithStack(err) + } + } else { + t, err = template.New(url).Funcs(sprig.TxtFuncMap()).Parse(string(b)) + if err != nil { + return nil, errors.WithStack(err) + } + } + + return t, nil +} + func loadTemplate(filesystem fs.FS, name, pattern string, html bool) (Template, error) { - if t, found := cache.Get(name); found { + if t, found := Cache.Get(name); found { return t.(Template), nil } @@ -102,15 +145,23 @@ func loadTemplate(filesystem fs.FS, name, pattern string, html bool) (Template, tpl = t } - _ = cache.Add(name, tpl) + _ = Cache.Add(name, tpl) return tpl, nil } -func LoadTextTemplate(filesystem fs.FS, name, pattern string, model interface{}) (string, error) { - t, err := loadTemplate(filesystem, name, pattern, false) - - if err != nil { - return "", err +func LoadTextTemplate(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) { + var t Template + var err error + if remoteURL != "" { + t, err = loadRemoteTemplate(ctx, d, remoteURL, false) + if err != nil { + return "", err + } + } else { + t, err = loadTemplate(filesystem, name, pattern, false) + if err != nil { + return "", err + } } var b bytes.Buffer @@ -120,11 +171,19 @@ func LoadTextTemplate(filesystem fs.FS, name, pattern string, model interface{}) return b.String(), nil } -func LoadHTMLTemplate(filesystem fs.FS, name, pattern string, model interface{}) (string, error) { - t, err := loadTemplate(filesystem, name, pattern, true) - - if err != nil { - return "", err +func LoadHTMLTemplate(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) { + var t Template + var err error + if remoteURL != "" { + t, err = loadRemoteTemplate(ctx, d, remoteURL, true) + if err != nil { + return "", err + } + } else { + t, err = loadTemplate(filesystem, name, pattern, true) + if err != nil { + return "", err + } } var b bytes.Buffer diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go index d618b8cb861d..354dbd7e9793 100644 --- a/courier/template/load_template_test.go +++ b/courier/template/load_template_test.go @@ -1,9 +1,22 @@ -package template +package template_test import ( + "context" + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" + "time" + + "github.com/julienschmidt/httprouter" + + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/internal" + "github.com/ory/x/fetcher" lru "github.com/hashicorp/golang-lru" "github.com/stretchr/testify/assert" @@ -14,13 +27,17 @@ import ( func TestLoadTextTemplate(t *testing.T) { var executeTextTemplate = func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { - tp, err := LoadTextTemplate(os.DirFS(dir), name, pattern, model) + ctx := context.Background() + _, reg := internal.NewFastRegistryWithMocks(t) + tp, err := template.LoadTextTemplate(ctx, reg, os.DirFS(dir), name, pattern, model, "") require.NoError(t, err) return tp } var executeHTMLTemplate = func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { - tp, err := LoadHTMLTemplate(os.DirFS(dir), name, pattern, model) + ctx := context.Background() + _, reg := internal.NewFastRegistryWithMocks(t) + tp, err := template.LoadHTMLTemplate(ctx, reg, os.DirFS(dir), name, pattern, model, "") require.NoError(t, err) return tp } @@ -31,26 +48,26 @@ func TestLoadTextTemplate(t *testing.T) { }) t.Run("method=fallback to bundled", func(t *testing.T) { - cache, _ = lru.New(16) // prevent cache hit + template.Cache, _ = lru.New(16) // prevent Cache hit actual := executeTextTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl", "", nil) assert.Contains(t, actual, "stub email") }) t.Run("method=with Sprig functions", func(t *testing.T) { - cache, _ = lru.New(16) // prevent cache hit + template.Cache, _ = lru.New(16) // prevent Cache hit m := map[string]interface{}{"input": "hello world"} // create a simple model actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.sprig.gotmpl", "", m) assert.Contains(t, actual, "HelloWorld,HELLOWORLD") }) t.Run("method=html with nested templates", func(t *testing.T) { - cache, _ = lru.New(16) // prevent cache hit + template.Cache, _ = lru.New(16) // prevent Cache hit m := map[string]interface{}{"lang": "en_US"} // create a simple model actual := executeHTMLTemplate(t, "courier/builtin/templates/test_stub", "email.body.html.gotmpl", "email.body.html*", m) assert.Contains(t, actual, "lang=en_US") }) - t.Run("method=cache works", func(t *testing.T) { + t.Run("method=Cache works", func(t *testing.T) { dir := os.TempDir() name := x.NewUUID().String() + ".body.gotmpl" fp := filepath.Join(dir, name) @@ -61,4 +78,117 @@ func TestLoadTextTemplate(t *testing.T) { require.NoError(t, os.RemoveAll(fp)) assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body") }) + + t.Run("method=remote resource", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("case=base64 encoded data", func(t *testing.T) { + t.Run("html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + f, err := ioutil.ReadFile("courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + require.NoError(t, err) + b64 := base64.StdEncoding.EncodeToString(f) + tp, err := template.LoadHTMLTemplate(ctx, reg, nil, "", "", m, "base64://"+b64) + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + f, err := ioutil.ReadFile("courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + require.NoError(t, err) + + b64 := base64.StdEncoding.EncodeToString(f) + + tp, err := template.LoadTextTemplate(ctx, reg, nil, "", "", m, "base64://"+b64) + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + + }) + + t.Run("case=file resource", func(t *testing.T) { + t.Run("case=html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + tp, err := template.LoadHTMLTemplate(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + tp, err := template.LoadTextTemplate(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + }) + + t.Run("case=http resource", func(t *testing.T) { + router := httprouter.New() + router.Handle("GET", "/html", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { + http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") + }) + router.Handle("GET", "/plaintext", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { + http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") + }) + ts := httptest.NewServer(router) + defer ts.Close() + + t.Run("case=html template", func(t *testing.T) { + m := map[string]interface{}{"lang": "en_US"} + tp, err := template.LoadHTMLTemplate(ctx, reg, nil, "", "", m, ts.URL+"/html") + require.NoError(t, err) + assert.Contains(t, tp, "lang=en_US") + }) + + t.Run("case=plaintext", func(t *testing.T) { + m := map[string]interface{}{"Body": "something"} + tp, err := template.LoadTextTemplate(ctx, reg, nil, "", "", m, ts.URL+"/plaintext") + require.NoError(t, err) + assert.Contains(t, tp, "stub email body something") + }) + + }) + + t.Run("case=unsupported resource", func(t *testing.T) { + tp, err := template.LoadHTMLTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") + + require.ErrorIs(t, err, fetcher.ErrUnknownScheme) + require.Empty(t, tp) + + tp, err = template.LoadTextTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") + require.ErrorIs(t, err, fetcher.ErrUnknownScheme) + require.Empty(t, tp) + }) + + t.Run("case=disallowed resources", func(t *testing.T) { + require.NoError(t, reg.Config(ctx).Source().Set(config.ViperKeyClientHTTPNoPrivateIPRanges, true)) + reg.HTTPClient(ctx).RetryMax = 1 + reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond + + _, err := template.LoadHTMLTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") + + require.Error(t, err) + assert.Contains(t, err.Error(), "is in the") + + _, err = template.LoadTextTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") + require.Error(t, err) + assert.Contains(t, err.Error(), "is in the") + + }) + + t.Run("method=cache works", func(t *testing.T) { + tp1, err := template.LoadTextTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://e3sgJGwgOj0gY2F0ICJsYW5nPSIgLmxhbmcgfX0Ke3sgbm9zcGFjZSAkbCB9fQ==") + assert.NoError(t, err) + + tp2, err := template.LoadTextTemplate(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://c3R1YiBlbWFpbCBib2R5IHt7IC5Cb2R5IH19") + assert.NoError(t, err) + + require.NotEqualf(t, tp1, tp2, "Expected remote template 1 and remote template 2 to not be equal") + }) + + }) } diff --git a/courier/template/recovery_invalid.go b/courier/template/recovery_invalid.go index d442132c652c..a90995e3364e 100644 --- a/courier/template/recovery_invalid.go +++ b/courier/template/recovery_invalid.go @@ -1,13 +1,14 @@ package template import ( + "context" "encoding/json" "os" ) type ( RecoveryInvalid struct { - c TemplateConfig + d TemplateDependencies m *RecoveryInvalidModel } RecoveryInvalidModel struct { @@ -15,24 +16,24 @@ type ( } ) -func NewRecoveryInvalid(c TemplateConfig, m *RecoveryInvalidModel) *RecoveryInvalid { - return &RecoveryInvalid{c: c, m: m} +func NewRecoveryInvalid(d TemplateDependencies, m *RecoveryInvalidModel) *RecoveryInvalid { + return &RecoveryInvalid{d: d, m: m} } func (t *RecoveryInvalid) EmailRecipient() (string, error) { return t.m.To, nil } -func (t *RecoveryInvalid) EmailSubject() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/invalid/email.subject.gotmpl", "recovery/invalid/email.subject*", t.m) +func (t *RecoveryInvalid) EmailSubject(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/invalid/email.subject.gotmpl", "recovery/invalid/email.subject*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryInvalid().Subject) } -func (t *RecoveryInvalid) EmailBody() (string, error) { - return LoadHTMLTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/invalid/email.body.gotmpl", "recovery/invalid/email.body*", t.m) +func (t *RecoveryInvalid) EmailBody(ctx context.Context) (string, error) { + return LoadHTMLTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/invalid/email.body.gotmpl", "recovery/invalid/email.body*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryInvalid().Body.HTML) } -func (t *RecoveryInvalid) EmailBodyPlaintext() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/invalid/email.body.plaintext.gotmpl", "recovery/invalid/email.body.plaintext*", t.m) +func (t *RecoveryInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/invalid/email.body.plaintext.gotmpl", "recovery/invalid/email.body.plaintext*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryInvalid().Body.PlainText) } func (t *RecoveryInvalid) MarshalJSON() ([]byte, error) { diff --git a/courier/template/recovery_invalid_test.go b/courier/template/recovery_invalid_test.go index 021efc100a8e..aa5b1c83f0c2 100644 --- a/courier/template/recovery_invalid_test.go +++ b/courier/template/recovery_invalid_test.go @@ -1,24 +1,28 @@ package template_test import ( + "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/internal" ) func TestRecoverInvalid(t *testing.T) { - conf, _ := internal.NewFastRegistryWithMocks(t) - tpl := template.NewRecoveryInvalid(conf, &template.RecoveryInvalidModel{}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) - rendered, err := tpl.EmailBody() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := template.NewRecoveryInvalid(reg, &template.RecoveryInvalidModel{}) - rendered, err = tpl.EmailSubject() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("case=test remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "courier/builtin/templates/recovery/invalid", courier.TypeRecoveryInvalid) + }) } diff --git a/courier/template/recovery_valid.go b/courier/template/recovery_valid.go index 5a164bdc406b..ba7d1c0fe187 100644 --- a/courier/template/recovery_valid.go +++ b/courier/template/recovery_valid.go @@ -1,13 +1,14 @@ package template import ( + "context" "encoding/json" "os" ) type ( RecoveryValid struct { - c TemplateConfig + d TemplateDependencies m *RecoveryValidModel } RecoveryValidModel struct { @@ -17,24 +18,24 @@ type ( } ) -func NewRecoveryValid(c TemplateConfig, m *RecoveryValidModel) *RecoveryValid { - return &RecoveryValid{c: c, m: m} +func NewRecoveryValid(d TemplateDependencies, m *RecoveryValidModel) *RecoveryValid { + return &RecoveryValid{d: d, m: m} } func (t *RecoveryValid) EmailRecipient() (string, error) { return t.m.To, nil } -func (t *RecoveryValid) EmailSubject() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/valid/email.subject.gotmpl", "recovery/valid/email.subject*", t.m) +func (t *RecoveryValid) EmailSubject(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/valid/email.subject.gotmpl", "recovery/valid/email.subject*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryValid().Subject) } -func (t *RecoveryValid) EmailBody() (string, error) { - return LoadHTMLTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/valid/email.body.gotmpl", "recovery/valid/email.body*", t.m) +func (t *RecoveryValid) EmailBody(ctx context.Context) (string, error) { + return LoadHTMLTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/valid/email.body.gotmpl", "recovery/valid/email.body*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryValid().Body.HTML) } -func (t *RecoveryValid) EmailBodyPlaintext() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "recovery/valid/email.body.plaintext.gotmpl", "recovery/valid/email.body.plaintext*", t.m) +func (t *RecoveryValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "recovery/valid/email.body.plaintext.gotmpl", "recovery/valid/email.body.plaintext*", t.m, t.d.CourierConfig(ctx).CourierTemplatesRecoveryValid().Body.PlainText) } func (t *RecoveryValid) MarshalJSON() ([]byte, error) { diff --git a/courier/template/recovery_valid_test.go b/courier/template/recovery_valid_test.go index 09d355e14555..0de24aa4b9dc 100644 --- a/courier/template/recovery_valid_test.go +++ b/courier/template/recovery_valid_test.go @@ -1,24 +1,28 @@ package template_test import ( + "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/internal" ) func TestRecoverValid(t *testing.T) { - conf, _ := internal.NewFastRegistryWithMocks(t) - tpl := template.NewRecoveryValid(conf, &template.RecoveryValidModel{}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) - rendered, err := tpl.EmailBody() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := template.NewRecoveryValid(reg, &template.RecoveryValidModel{}) - rendered, err = tpl.EmailSubject() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "courier/builtin/templates/recovery/valid", courier.TypeRecoveryValid) + }) } diff --git a/courier/template/stub.go b/courier/template/stub.go index 64481ee23f51..58f95a376696 100644 --- a/courier/template/stub.go +++ b/courier/template/stub.go @@ -1,12 +1,13 @@ package template import ( + "context" "encoding/json" "os" ) type TestStub struct { - c TemplateConfig + d TemplateDependencies m *TestStubModel } @@ -16,24 +17,24 @@ type TestStubModel struct { Body string } -func NewTestStub(c TemplateConfig, m *TestStubModel) *TestStub { - return &TestStub{c: c, m: m} +func NewTestStub(d TemplateDependencies, m *TestStubModel) *TestStub { + return &TestStub{d: d, m: m} } func (t *TestStub) EmailRecipient() (string, error) { return t.m.To, nil } -func (t *TestStub) EmailSubject() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "test_stub/email.subject.gotmpl", "test_stub/email.subject*", t.m) +func (t *TestStub) EmailSubject(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "test_stub/email.subject.gotmpl", "test_stub/email.subject*", t.m, "") } -func (t *TestStub) EmailBody() (string, error) { - return LoadHTMLTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "test_stub/email.body.gotmpl", "test_stub/email.body*", t.m) +func (t *TestStub) EmailBody(ctx context.Context) (string, error) { + return LoadHTMLTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "test_stub/email.body.gotmpl", "test_stub/email.body*", t.m, "") } -func (t *TestStub) EmailBodyPlaintext() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "test_stub/email.body.plaintext.gotmpl", "test_stub/email.body.plaintext*", t.m) +func (t *TestStub) EmailBodyPlaintext(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "test_stub/email.body.plaintext.gotmpl", "test_stub/email.body.plaintext*", t.m, "") } func (t *TestStub) MarshalJSON() ([]byte, error) { diff --git a/courier/template/template.go b/courier/template/template.go index 0486356fb0e8..46e94b8e9c60 100644 --- a/courier/template/template.go +++ b/courier/template/template.go @@ -1,7 +1,24 @@ package template +import ( + "context" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/kratos/driver/config" + "github.com/ory/x/httpx" +) + type ( TemplateConfig interface { CourierTemplatesRoot() string + CourierTemplatesVerificationInvalid() *config.CourierEmailTemplate + CourierTemplatesVerificationValid() *config.CourierEmailTemplate + CourierTemplatesRecoveryInvalid() *config.CourierEmailTemplate + CourierTemplatesRecoveryValid() *config.CourierEmailTemplate + } + TemplateDependencies interface { + CourierConfig(ctx context.Context) config.CourierConfigs + HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client } ) diff --git a/courier/template/testhelpers/testhelpers.go b/courier/template/testhelpers/testhelpers.go new file mode 100644 index 000000000000..0e2a3a490827 --- /dev/null +++ b/courier/template/testhelpers/testhelpers.go @@ -0,0 +1,127 @@ +package testhelpers + +import ( + "context" + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" + "path" + "testing" + + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/internal" +) + +func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html string, subject string) *driver.RegistryDefault { + _, reg := internal.NewFastRegistryWithMocks(t) + require.NoError(t, reg.Config(ctx).Set(config.ViperKeyCourierTemplatesRecoveryInvalidEmail, &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: plaintext, + HTML: html, + }, + Subject: subject, + })) + return reg +} + +func TestRendered(t *testing.T, ctx context.Context, tpl interface { + EmailBody(context.Context) (string, error) + EmailSubject(context.Context) (string, error) +}) { + rendered, err := tpl.EmailBody(ctx) + require.NoError(t, err) + assert.NotEmpty(t, rendered) + + rendered, err = tpl.EmailSubject(ctx) + require.NoError(t, err) + assert.NotEmpty(t, rendered) +} + +func TestRemoteTemplates(t *testing.T, basePath string, tmplType courier.TemplateType) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + toBase64 := func(filePath string) string { + f, err := ioutil.ReadFile(filePath) + require.NoError(t, err) + return base64.StdEncoding.EncodeToString(f) + } + + getTemplate := func(tmpl courier.TemplateType, d template.TemplateDependencies) interface { + EmailBody(context.Context) (string, error) + EmailSubject(context.Context) (string, error) + } { + switch tmpl { + case courier.TypeRecoveryInvalid: + return template.NewRecoveryInvalid(d, &template.RecoveryInvalidModel{}) + case courier.TypeRecoveryValid: + return template.NewRecoveryValid(d, &template.RecoveryValidModel{}) + case courier.TypeTestStub: + return template.NewTestStub(d, &template.TestStubModel{}) + case courier.TypeVerificationInvalid: + return template.NewVerificationInvalid(d, &template.VerificationInvalidModel{}) + case courier.TypeVerificationValid: + return template.NewVerificationValid(d, &template.VerificationValidModel{}) + default: + return nil + } + } + + t.Run("case=http resource", func(t *testing.T) { + router := httprouter.New() + router.Handle("GET", "/:filename", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { + http.ServeFile(writer, request, path.Join(basePath, params.ByName("filename"))) + }) + ts := httptest.NewServer(router) + defer ts.Close() + + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + ts.URL+"/email.body.plaintext.gotmpl", + ts.URL+"/email.body.gotmpl", + ts.URL+"/email.subject.gotmpl")) + + TestRendered(t, ctx, tpl) + }) + + t.Run("case=base64 resource", func(t *testing.T) { + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl")))) + + TestRendered(t, ctx, tpl) + }) + + t.Run("case=file resource", func(t *testing.T) { + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "file://"+path.Join(basePath, "email.body.plaintext.gotmpl"), + "file://"+path.Join(basePath, "email.body.gotmpl"), + "file://"+path.Join(basePath, "email.subject.gotmpl"))) + + TestRendered(t, ctx, tpl) + }) + + t.Run("case=partial subject override", func(t *testing.T) { + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "", + "", + "base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl")))) + TestRendered(t, ctx, tpl) + }) + + t.Run("case=partial body override", func(t *testing.T) { + tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx, + "base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")), + "base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")), + "")) + TestRendered(t, ctx, tpl) + }) +} diff --git a/courier/template/verification_invalid.go b/courier/template/verification_invalid.go index fa457ee1ff07..e78ec3a106f1 100644 --- a/courier/template/verification_invalid.go +++ b/courier/template/verification_invalid.go @@ -1,13 +1,14 @@ package template import ( + "context" "encoding/json" "os" ) type ( VerificationInvalid struct { - c TemplateConfig + d TemplateDependencies m *VerificationInvalidModel } VerificationInvalidModel struct { @@ -15,24 +16,24 @@ type ( } ) -func NewVerificationInvalid(c TemplateConfig, m *VerificationInvalidModel) *VerificationInvalid { - return &VerificationInvalid{c: c, m: m} +func NewVerificationInvalid(d TemplateDependencies, m *VerificationInvalidModel) *VerificationInvalid { + return &VerificationInvalid{d: d, m: m} } func (t *VerificationInvalid) EmailRecipient() (string, error) { return t.m.To, nil } -func (t *VerificationInvalid) EmailSubject() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/invalid/email.subject.gotmpl", "verification/invalid/email.subject*", t.m) +func (t *VerificationInvalid) EmailSubject(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/invalid/email.subject.gotmpl", "verification/invalid/email.subject*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationInvalid().Subject) } -func (t *VerificationInvalid) EmailBody() (string, error) { - return LoadHTMLTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/invalid/email.body.gotmpl", "verification/invalid/email.body*", t.m) +func (t *VerificationInvalid) EmailBody(ctx context.Context) (string, error) { + return LoadHTMLTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/invalid/email.body.gotmpl", "verification/invalid/email.body*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationInvalid().Body.HTML) } -func (t *VerificationInvalid) EmailBodyPlaintext() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/invalid/email.body.plaintext.gotmpl", "verification/invalid/email.body.plaintext*", t.m) +func (t *VerificationInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/invalid/email.body.plaintext.gotmpl", "verification/invalid/email.body.plaintext*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationInvalid().Body.PlainText) } func (t *VerificationInvalid) MarshalJSON() ([]byte, error) { diff --git a/courier/template/verification_invalid_test.go b/courier/template/verification_invalid_test.go index 5d77cae21fb1..8bbb9972e582 100644 --- a/courier/template/verification_invalid_test.go +++ b/courier/template/verification_invalid_test.go @@ -1,24 +1,30 @@ package template_test import ( + "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/internal" ) func TestVerifyInvalid(t *testing.T) { - conf, _ := internal.NewFastRegistryWithMocks(t) - tpl := template.NewVerificationInvalid(conf, &template.VerificationInvalidModel{}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) - rendered, err := tpl.EmailBody() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := template.NewVerificationInvalid(reg, &template.VerificationInvalidModel{}) - rendered, err = tpl.EmailSubject() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "courier/builtin/templates/verification/invalid", courier.TypeVerificationInvalid) + }) + }) } diff --git a/courier/template/verification_valid.go b/courier/template/verification_valid.go index 9973bd681689..cdd6e25c6b85 100644 --- a/courier/template/verification_valid.go +++ b/courier/template/verification_valid.go @@ -1,13 +1,14 @@ package template import ( + "context" "encoding/json" "os" ) type ( VerificationValid struct { - c TemplateConfig + d TemplateDependencies m *VerificationValidModel } VerificationValidModel struct { @@ -17,24 +18,24 @@ type ( } ) -func NewVerificationValid(c TemplateConfig, m *VerificationValidModel) *VerificationValid { - return &VerificationValid{c: c, m: m} +func NewVerificationValid(d TemplateDependencies, m *VerificationValidModel) *VerificationValid { + return &VerificationValid{d: d, m: m} } func (t *VerificationValid) EmailRecipient() (string, error) { return t.m.To, nil } -func (t *VerificationValid) EmailSubject() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/valid/email.subject.gotmpl", "verification/valid/email.subject*", t.m) +func (t *VerificationValid) EmailSubject(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/valid/email.subject.gotmpl", "verification/valid/email.subject*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationValid().Subject) } -func (t *VerificationValid) EmailBody() (string, error) { - return LoadHTMLTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/valid/email.body.gotmpl", "verification/valid/email.body*", t.m) +func (t *VerificationValid) EmailBody(ctx context.Context) (string, error) { + return LoadHTMLTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/valid/email.body.gotmpl", "verification/valid/email.body*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationValid().Body.HTML) } -func (t *VerificationValid) EmailBodyPlaintext() (string, error) { - return LoadTextTemplate(os.DirFS(t.c.CourierTemplatesRoot()), "verification/valid/email.body.plaintext.gotmpl", "verification/valid/email.body.plaintext*", t.m) +func (t *VerificationValid) EmailBodyPlaintext(ctx context.Context) (string, error) { + return LoadTextTemplate(ctx, t.d, os.DirFS(t.d.CourierConfig(ctx).CourierTemplatesRoot()), "verification/valid/email.body.plaintext.gotmpl", "verification/valid/email.body.plaintext*", t.m, t.d.CourierConfig(ctx).CourierTemplatesVerificationValid().Body.PlainText) } func (t *VerificationValid) MarshalJSON() ([]byte, error) { diff --git a/courier/template/verification_valid_test.go b/courier/template/verification_valid_test.go index f80fbf4cec45..2313c74d0fe2 100644 --- a/courier/template/verification_valid_test.go +++ b/courier/template/verification_valid_test.go @@ -1,24 +1,28 @@ package template_test import ( + "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/ory/kratos/courier" + "github.com/ory/kratos/courier/template/testhelpers" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/internal" ) func TestVerifyValid(t *testing.T) { - conf, _ := internal.NewFastRegistryWithMocks(t) - tpl := template.NewVerificationValid(conf, &template.VerificationValidModel{}) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) - rendered, err := tpl.EmailBody() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + t.Run("test=with courier templates directory", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + tpl := template.NewVerificationValid(reg, &template.VerificationValidModel{}) - rendered, err = tpl.EmailSubject() - require.NoError(t, err) - assert.NotEmpty(t, rendered) + testhelpers.TestRendered(t, ctx, tpl) + }) + + t.Run("test=with remote resources", func(t *testing.T) { + testhelpers.TestRemoteTemplates(t, "courier/builtin/templates/verification/valid", courier.TypeVerificationValid) + }) } diff --git a/courier/templates.go b/courier/templates.go index e04da43e4cb3..cbadd5731062 100644 --- a/courier/templates.go +++ b/courier/templates.go @@ -1,6 +1,7 @@ package courier import ( + "context" "encoding/json" "github.com/pkg/errors" @@ -12,9 +13,9 @@ type ( TemplateType string EmailTemplate interface { json.Marshaler - EmailSubject() (string, error) - EmailBody() (string, error) - EmailBodyPlaintext() (string, error) + EmailSubject(context.Context) (string, error) + EmailBody(context.Context) (string, error) + EmailBodyPlaintext(context.Context) (string, error) EmailRecipient() (string, error) } ) @@ -44,38 +45,38 @@ func GetTemplateType(t EmailTemplate) (TemplateType, error) { } } -func NewEmailTemplateFromMessage(c SMTPConfig, msg Message) (EmailTemplate, error) { +func NewEmailTemplateFromMessage(d SMTPDependencies, msg Message) (EmailTemplate, error) { switch msg.TemplateType { case TypeRecoveryInvalid: var t template.RecoveryInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } - return template.NewRecoveryInvalid(c, &t), nil + return template.NewRecoveryInvalid(d, &t), nil case TypeRecoveryValid: var t template.RecoveryValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } - return template.NewRecoveryValid(c, &t), nil + return template.NewRecoveryValid(d, &t), nil case TypeVerificationInvalid: var t template.VerificationInvalidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } - return template.NewVerificationInvalid(c, &t), nil + return template.NewVerificationInvalid(d, &t), nil case TypeVerificationValid: var t template.VerificationValidModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } - return template.NewVerificationValid(c, &t), nil + return template.NewVerificationValid(d, &t), nil case TypeTestStub: var t template.TestStubModel if err := json.Unmarshal(msg.TemplateData, &t); err != nil { return nil, err } - return template.NewTestStub(c, &t), nil + return template.NewTestStub(d, &t), nil default: return nil, errors.Errorf("received unexpected message template type: %s", msg.TemplateType) } diff --git a/courier/templates_test.go b/courier/templates_test.go index ac6339482e72..e41fa0705bdc 100644 --- a/courier/templates_test.go +++ b/courier/templates_test.go @@ -1,6 +1,7 @@ package courier_test import ( + "context" "encoding/json" "fmt" "testing" @@ -31,20 +32,22 @@ func TestGetTemplateType(t *testing.T) { } func TestNewEmailTemplateFromMessage(t *testing.T) { - conf := internal.NewConfigurationWithDefaults(t) + _, reg := internal.NewFastRegistryWithMocks(t) + ctx := context.Background() + for tmplType, expectedTmpl := range map[courier.TemplateType]courier.EmailTemplate{ - courier.TypeRecoveryInvalid: template.NewRecoveryInvalid(conf, &template.RecoveryInvalidModel{To: "foo"}), - courier.TypeRecoveryValid: template.NewRecoveryValid(conf, &template.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}), - courier.TypeVerificationInvalid: template.NewVerificationInvalid(conf, &template.VerificationInvalidModel{To: "baz"}), - courier.TypeVerificationValid: template.NewVerificationValid(conf, &template.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}), - courier.TypeTestStub: template.NewTestStub(conf, &template.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), + courier.TypeRecoveryInvalid: template.NewRecoveryInvalid(reg, &template.RecoveryInvalidModel{To: "foo"}), + courier.TypeRecoveryValid: template.NewRecoveryValid(reg, &template.RecoveryValidModel{To: "bar", RecoveryURL: "http://foo.bar"}), + courier.TypeVerificationInvalid: template.NewVerificationInvalid(reg, &template.VerificationInvalidModel{To: "baz"}), + courier.TypeVerificationValid: template.NewVerificationValid(reg, &template.VerificationValidModel{To: "faz", VerificationURL: "http://bar.foo"}), + courier.TypeTestStub: template.NewTestStub(reg, &template.TestStubModel{To: "far", Subject: "test subject", Body: "test body"}), } { t.Run(fmt.Sprintf("case=%s", tmplType), func(t *testing.T) { tmplData, err := json.Marshal(expectedTmpl) require.NoError(t, err) m := courier.Message{TemplateType: tmplType, TemplateData: tmplData} - actualTmpl, err := courier.NewEmailTemplateFromMessage(conf, m) + actualTmpl, err := courier.NewEmailTemplateFromMessage(reg, m) require.NoError(t, err) require.IsType(t, expectedTmpl, actualTmpl) @@ -55,21 +58,21 @@ func TestNewEmailTemplateFromMessage(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedRecipient, actualRecipient) - expectedSubject, err := expectedTmpl.EmailSubject() + expectedSubject, err := expectedTmpl.EmailSubject(ctx) require.NoError(t, err) - actualSubject, err := actualTmpl.EmailSubject() + actualSubject, err := actualTmpl.EmailSubject(ctx) require.NoError(t, err) require.Equal(t, expectedSubject, actualSubject) - expectedBody, err := expectedTmpl.EmailBody() + expectedBody, err := expectedTmpl.EmailBody(ctx) require.NoError(t, err) - actualBody, err := actualTmpl.EmailBody() + actualBody, err := actualTmpl.EmailBody(ctx) require.NoError(t, err) require.Equal(t, expectedBody, actualBody) - expectedBodyPlaintext, err := expectedTmpl.EmailBodyPlaintext() + expectedBodyPlaintext, err := expectedTmpl.EmailBodyPlaintext(ctx) require.NoError(t, err) - actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext() + actualBodyPlaintext, err := actualTmpl.EmailBodyPlaintext(ctx) require.NoError(t, err) require.Equal(t, expectedBodyPlaintext, actualBodyPlaintext) diff --git a/docs/docs/concepts/email-sms.md b/docs/docs/concepts/email-sms.md index 1ceed215ab02..63ee4d7656d2 100644 --- a/docs/docs/concepts/email-sms.md +++ b/docs/docs/concepts/email-sms.md @@ -92,12 +92,67 @@ courier: # > set COURIER_TEMPLATE_OVERRIDE_PATH= # template_override_path: /conf/courier-templates + + ## Override with remote templates ## + # + # You can specify specific template values to override or the whole template + # + # Supported templates are: + # - verification + # - valid + # - invalid + # - recovery + # - valid + # - invalid + # + # Each template supports the following layout. A singular key can be specified under `email` to override the defaults. + # When specifying `body`, however, Kratos expects `html` and `plaintext` to be set. + # email: + # subject: http(s)://, file://, base64:// + # body: + # html: http(s)://, file://, base64:// + # plaintext: http(s)://, file://, base64:// + templates: + # we can specify here + verification: + valid: + email: + body: + # plaintext and html are required when overriding the body + html: https://some-remote-resource/gotmpl + plaintext: base64://SGV5IHlvdSBkZWNvZGVkIG1lIDop + # optional + subject: file://some-file/subject.gotmpl + # we can also omit the `invalid` field here if you wish to use the default built-in templates + # or template_override_path + invalid: + # same configuration structure as valid + # this is also optional and can be omitted in preference for the default built-in templates + # or template_override_path + recovery: + # the configuration structure is the same as the verification ``` Ory Kratos comes with built-in templates. If you wish to define your own, custom -templates, you should define `template_override_path`, as shown above, to -indicate where your custom templates are located. This will become the -`` for your custom templates, as indicated below. +templates, you can use two methods. + +1. Define each template individually through `templates` as shown above for + `recovery.invalid`, `recovery.valid`, `verification.invalid` and + `verification.valid`. None of the configurations listed are mandatory and + will always fallback to the build-in templates or what is defined by + `template_override_path`. +2. Define `template_override_path`, as shown above, to indicate where your + custom templates are located. This will become the `` for your + custom templates, as indicated below. + +### Remote Templates + +Templates can be added through `http://`, `file://` and `base64://` URIs in the +configurations. The only mandatory fields are `plaintext` and `html` when +defining the `body` key. All other keys are optional and will always fallback to +the built-in templates or the `template_override_path`. + +### Template Override Path `email.subject.gotmpl`, `email.body.gotmpl` and `email.body.plaintext.gotmpl` are common template file names expected in the sub directories of the root @@ -107,6 +162,8 @@ as [alternatives](https://github.com/ory/kratos/blob/871ee0475a27771dd6395aad617f41a22ccc3b9a/courier/courier.go#L205) for fallback. +### Creating Templates + > Templates use the golang template engine in the `text/template` package for > rendering the `email.subject.gotmpl` and `email.body.plaintext.gotmpl` > templates, and the `html/template` package for rendering the @@ -201,6 +258,40 @@ the following pattern: `email.body*`. You can also see that the `Identity` of the user is available in all templates, and that you can use Sprig functions also in the nested templates. +### Nested templates with remote templates + +When remote templates are used in Kratos, the dynamics of loading nested +templates change. The templates cannot reference templates outside itself as +with templates loaded from a singular directory. + +The template will need to contain the nested templates in the same file. See +below for an example. + +```yaml title="path/to/my/kratos/config.yml" +courier: + templates: + verify: + email: + body: + plaintext: https://some-remote-template/tmp.gotmpl + html: https://some-remote-template/tmp.gotmpl +``` + +**Our template:** + +```gotmpl title="https://some-remote-template/tmp.gotmpl" + +{{define "en_US"}} +{{ $l := cat "lang=" .lang }} +{{ nospace $l }} +{{end}} + +{{- if eq .lang "en_US" -}} +{{ template "en_US" . }} +{{- end -}} + +``` + ### Custom Headers You can configure custom SMTP headers. For example, if integrating with AWS SES diff --git a/docs/docs/reference/configuration.md b/docs/docs/reference/configuration.md index 192153a19786..e8bedc5b1b96 100644 --- a/docs/docs/reference/configuration.md +++ b/docs/docs/reference/configuration.md @@ -37,24 +37,7 @@ section. ## identity ## # identity: - ## JSON Schema URL for default identity traits ## - # - # URL for JSON Schema which describes a default identity's traits. Can be a file path, a https URL, or a base64 encoded string. Will have ID: "default" - # - # Examples: - # - file://path/to/identity.traits.schema.json - # - https://foo.bar.com/path/to/identity.traits.schema.json - # - base64://ewogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInR5cGUiOiAib2JqZWN0IiwKICAicHJvcGVydGllcyI6IHsKICAgICJiYXIiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJiYXIiCiAgXQp9 - # - # Set this value using environment variables on - # - Linux/macOS: - # $ export IDENTITY_DEFAULT_SCHEMA_URL= - # - Windows Command Line (CMD): - # > set IDENTITY_DEFAULT_SCHEMA_URL= - # - default_schema_url: file://path/to/identity.traits.schema.json - - ## Additional JSON Schemas for Identity Traits ## + ## All JSON Schemas for Identity Traits ## # # Examples: # - - id: customer @@ -78,6 +61,20 @@ identity: - id: employee-v2 url: file://path/to/employee.v2.traits.schema.json + ## The default Identity Schema ## + # + # This Identity Schema will be used as the default for self-service flows. Its ID needs to exist in the "schemas" list. + # + # Default value: default + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export IDENTITY_DEFAULT_SCHEMA_ID= + # - Windows Command Line (CMD): + # > set IDENTITY_DEFAULT_SCHEMA_ID= + # + default_schema_id: '' + ## Data Source Name ## # # DSN is used to specify the database credentials as a connection URI. @@ -2558,4 +2555,245 @@ courier: # > set COURIER_TEMPLATE_OVERRIDE_PATH= # template_override_path: /conf/courier-templates + + ## templates ## + # + templates: + ## verification ## + # + verification: + ## valid ## + # + valid: + ## email ## + # + email: + ## subject ## + # + # Examples: + # - file://path/to/subject.gotmpl + # - https://foo.bar.com/path/to/subject.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_SUBJECT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_SUBJECT= + # + subject: file://path/to/subject.gotmpl + + ## body ## + # + body: + ## plaintext ## + # + # The fallback template for email clients that do not support html. + # + # Examples: + # - file://path/to/body.plaintext.gotmpl + # - https://foo.bar.com/path/to/body.plaintext.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_BODY_PLAINTEXT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_BODY_PLAINTEXT= + # + plaintext: file://path/to/body.plaintext.gotmpl + + ## html ## + # + # The default template used for sending out emails. The template can contain HTML + # + # Examples: + # - file://path/to/body.html.gotmpl + # - https://foo.bar.com/path/to/body.html.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_BODY_HTML= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_VALID_EMAIL_BODY_HTML= + # + html: file://path/to/body.html.gotmpl + + ## invalid ## + # + invalid: + ## email ## + # + email: + ## subject ## + # + # Examples: + # - file://path/to/subject.gotmpl + # - https://foo.bar.com/path/to/subject.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_SUBJECT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_SUBJECT= + # + subject: file://path/to/subject.gotmpl + + ## body ## + # + body: + ## plaintext ## + # + # The fallback template for email clients that do not support html. + # + # Examples: + # - file://path/to/body.plaintext.gotmpl + # - https://foo.bar.com/path/to/body.plaintext.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_BODY_PLAINTEXT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_BODY_PLAINTEXT= + # + plaintext: file://path/to/body.plaintext.gotmpl + + ## html ## + # + # The default template used for sending out emails. The template can contain HTML + # + # Examples: + # - file://path/to/body.html.gotmpl + # - https://foo.bar.com/path/to/body.html.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_BODY_HTML= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_VERIFICATION_INVALID_EMAIL_BODY_HTML= + # + html: file://path/to/body.html.gotmpl + + ## recovery ## + # + recovery: + ## valid ## + # + valid: + ## email ## + # + email: + ## subject ## + # + # Examples: + # - file://path/to/subject.gotmpl + # - https://foo.bar.com/path/to/subject.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_SUBJECT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_SUBJECT= + # + subject: file://path/to/subject.gotmpl + + ## body ## + # + body: + ## plaintext ## + # + # The fallback template for email clients that do not support html. + # + # Examples: + # - file://path/to/body.plaintext.gotmpl + # - https://foo.bar.com/path/to/body.plaintext.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_BODY_PLAINTEXT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_BODY_PLAINTEXT= + # + plaintext: file://path/to/body.plaintext.gotmpl + + ## html ## + # + # The default template used for sending out emails. The template can contain HTML + # + # Examples: + # - file://path/to/body.html.gotmpl + # - https://foo.bar.com/path/to/body.html.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_BODY_HTML= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_VALID_EMAIL_BODY_HTML= + # + html: file://path/to/body.html.gotmpl + + ## invalid ## + # + invalid: + ## email ## + # + email: + ## subject ## + # + # Examples: + # - file://path/to/subject.gotmpl + # - https://foo.bar.com/path/to/subject.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_SUBJECT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_SUBJECT= + # + subject: file://path/to/subject.gotmpl + + ## body ## + # + body: + ## plaintext ## + # + # The fallback template for email clients that do not support html. + # + # Examples: + # - file://path/to/body.plaintext.gotmpl + # - https://foo.bar.com/path/to/body.plaintext.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_BODY_PLAINTEXT= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_BODY_PLAINTEXT= + # + plaintext: file://path/to/body.plaintext.gotmpl + + ## html ## + # + # The default template used for sending out emails. The template can contain HTML + # + # Examples: + # - file://path/to/body.html.gotmpl + # - https://foo.bar.com/path/to/body.html.gotmpl + # - base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4 + # + # Set this value using environment variables on + # - Linux/macOS: + # $ export COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_BODY_HTML= + # - Windows Command Line (CMD): + # > set COURIER_TEMPLATES_RECOVERY_INVALID_EMAIL_BODY_HTML= + # + html: file://path/to/body.html.gotmpl ``` diff --git a/docs/versioned_docs/version-v0.8/concepts/email-sms.md b/docs/versioned_docs/version-v0.8/concepts/email-sms.md index 3a2dc7498a5e..63ee4d7656d2 100644 --- a/docs/versioned_docs/version-v0.8/concepts/email-sms.md +++ b/docs/versioned_docs/version-v0.8/concepts/email-sms.md @@ -3,8 +3,8 @@ id: email-sms title: Out-of-band communication via E-Mail and SMS --- -Ory Kratos sends out-of-band messages via SMS or E-Mail. The following exemplary use cases require these messages: - +Ory Kratos sends out-of-band messages via SMS or E-Mail. The following exemplary +use cases require these messages: - Send an account activation email - Verify an E-Mail address or mobile phone number using SMS @@ -92,17 +92,77 @@ courier: # > set COURIER_TEMPLATE_OVERRIDE_PATH= # template_override_path: /conf/courier-templates + + ## Override with remote templates ## + # + # You can specify specific template values to override or the whole template + # + # Supported templates are: + # - verification + # - valid + # - invalid + # - recovery + # - valid + # - invalid + # + # Each template supports the following layout. A singular key can be specified under `email` to override the defaults. + # When specifying `body`, however, Kratos expects `html` and `plaintext` to be set. + # email: + # subject: http(s)://, file://, base64:// + # body: + # html: http(s)://, file://, base64:// + # plaintext: http(s)://, file://, base64:// + templates: + # we can specify here + verification: + valid: + email: + body: + # plaintext and html are required when overriding the body + html: https://some-remote-resource/gotmpl + plaintext: base64://SGV5IHlvdSBkZWNvZGVkIG1lIDop + # optional + subject: file://some-file/subject.gotmpl + # we can also omit the `invalid` field here if you wish to use the default built-in templates + # or template_override_path + invalid: + # same configuration structure as valid + # this is also optional and can be omitted in preference for the default built-in templates + # or template_override_path + recovery: + # the configuration structure is the same as the verification ``` Ory Kratos comes with built-in templates. If you wish to define your own, custom -templates, you should define `template_override_path`, as shown above, to -indicate where your custom templates are located. This will become the -`` for your custom templates, as indicated below. +templates, you can use two methods. + +1. Define each template individually through `templates` as shown above for + `recovery.invalid`, `recovery.valid`, `verification.invalid` and + `verification.valid`. None of the configurations listed are mandatory and + will always fallback to the build-in templates or what is defined by + `template_override_path`. +2. Define `template_override_path`, as shown above, to indicate where your + custom templates are located. This will become the `` for your + custom templates, as indicated below. + +### Remote Templates + +Templates can be added through `http://`, `file://` and `base64://` URIs in the +configurations. The only mandatory fields are `plaintext` and `html` when +defining the `body` key. All other keys are optional and will always fallback to +the built-in templates or the `template_override_path`. + +### Template Override Path `email.subject.gotmpl`, `email.body.gotmpl` and `email.body.plaintext.gotmpl` are common template file names expected in the sub directories of the root directory, corresponding to the respective methods for filling e-mail subject -and body. +and body. Both plain text and HTML templates are required. The courier uses them +as +[alternatives](https://github.com/ory/kratos/blob/871ee0475a27771dd6395aad617f41a22ccc3b9a/courier/courier.go#L205) +for fallback. + +### Creating Templates > Templates use the golang template engine in the `text/template` package for > rendering the `email.subject.gotmpl` and `email.body.plaintext.gotmpl` @@ -144,6 +204,9 @@ Hi, please verify your account by clicking the following link: Hi, please verify your account by clicking the following link: {{ .VerificationURL }} ``` +If you're running multiple instances of Kratos and separate courier job, make +sure to provide templates to all instances (both Kratos and courier). + ### The Identity attribute To be able to customize the content of templates based on the identity of the @@ -195,6 +258,40 @@ the following pattern: `email.body*`. You can also see that the `Identity` of the user is available in all templates, and that you can use Sprig functions also in the nested templates. +### Nested templates with remote templates + +When remote templates are used in Kratos, the dynamics of loading nested +templates change. The templates cannot reference templates outside itself as +with templates loaded from a singular directory. + +The template will need to contain the nested templates in the same file. See +below for an example. + +```yaml title="path/to/my/kratos/config.yml" +courier: + templates: + verify: + email: + body: + plaintext: https://some-remote-template/tmp.gotmpl + html: https://some-remote-template/tmp.gotmpl +``` + +**Our template:** + +```gotmpl title="https://some-remote-template/tmp.gotmpl" + +{{define "en_US"}} +{{ $l := cat "lang=" .lang }} +{{ nospace $l }} +{{end}} + +{{- if eq .lang "en_US" -}} +{{ template "en_US" . }} +{{- end -}} + +``` + ### Custom Headers You can configure custom SMTP headers. For example, if integrating with AWS SES diff --git a/driver/config/config.go b/driver/config/config.go index 0d985ac68281..1f728668b328 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -62,6 +62,10 @@ const ( ViperKeyDSN = "dsn" ViperKeyCourierSMTPURL = "courier.smtp.connection_uri" ViperKeyCourierTemplatesPath = "courier.template_override_path" + ViperKeyCourierTemplatesRecoveryInvalidEmail = "courier.templates.recovery.invalid.email" + ViperKeyCourierTemplatesRecoveryValidEmail = "courier.templates.recovery.valid.email" + ViperKeyCourierTemplatesVerificationInvalidEmail = "courier.templates.verification.invalid.email" + ViperKeyCourierTemplatesVerificationValidEmail = "courier.templates.verification.valid.email" ViperKeyCourierSMTPFrom = "courier.smtp.from_address" ViperKeyCourierSMTPFromName = "courier.smtp.from_name" ViperKeyCourierSMTPHeaders = "courier.smtp.headers" @@ -208,17 +212,35 @@ type ( MinPasswordLength uint `json:"min_password_length"` IdentifierSimilarityCheckEnabled bool `json:"identifier_similarity_check_enabled"` } - Schemas []Schema - Config struct { + Schemas []Schema + CourierEmailBodyTemplate struct { + PlainText string `json:"plaintext"` + HTML string `json:"html"` + } + CourierEmailTemplate struct { + Body *CourierEmailBodyTemplate `json:"body"` + Subject string `json:"subject"` + } + Config struct { l *logrusx.Logger p *configx.Provider identitySchema *jsonschema.Schema stdOutOrErr io.Writer } - Provider interface { Config(ctx context.Context) *Config } + CourierConfigs interface { + CourierSMTPURL() *url.URL + CourierSMTPFrom() string + CourierSMTPFromName() string + CourierSMTPHeaders() map[string]string + CourierTemplatesRoot() string + CourierTemplatesVerificationInvalid() *CourierEmailTemplate + CourierTemplatesVerificationValid() *CourierEmailTemplate + CourierTemplatesRecoveryInvalid() *CourierEmailTemplate + CourierTemplatesRecoveryValid() *CourierEmailTemplate + } ) func (c *Argon2) MarshalJSON() ([]byte, error) { @@ -846,6 +868,53 @@ func (p *Config) CourierTemplatesRoot() string { return p.p.StringF(ViperKeyCourierTemplatesPath, "courier/builtin/templates") } +func (p *Config) CourierTemplatesHelper(key string) *CourierEmailTemplate { + courierTemplate := &CourierEmailTemplate{ + Body: &CourierEmailBodyTemplate{ + PlainText: "", + HTML: "", + }, + Subject: "", + } + + if !p.p.Exists(key) { + return courierTemplate + } + + out, err := p.p.Marshal(kjson.Parser()) + if err != nil { + p.l.WithError(err).Fatalf("Unable to dencode values from %s.", key) + return courierTemplate + } + + config := gjson.GetBytes(out, key).Raw + if len(config) == 0 { + return courierTemplate + } + + if err := json.NewDecoder(bytes.NewBufferString(config)).Decode(&courierTemplate); err != nil { + p.l.WithError(err).Fatalf("Unable to encode values from %s.", key) + return courierTemplate + } + return courierTemplate +} + +func (p *Config) CourierTemplatesVerificationInvalid() *CourierEmailTemplate { + return p.CourierTemplatesHelper(ViperKeyCourierTemplatesVerificationInvalidEmail) +} + +func (p *Config) CourierTemplatesVerificationValid() *CourierEmailTemplate { + return p.CourierTemplatesHelper(ViperKeyCourierTemplatesVerificationValidEmail) +} + +func (p *Config) CourierTemplatesRecoveryInvalid() *CourierEmailTemplate { + return p.CourierTemplatesHelper(ViperKeyCourierTemplatesRecoveryInvalidEmail) +} + +func (p *Config) CourierTemplatesRecoveryValid() *CourierEmailTemplate { + return p.CourierTemplatesHelper(ViperKeyCourierTemplatesRecoveryValidEmail) +} + func (p *Config) CourierSMTPHeaders() map[string]string { return p.p.StringMap(ViperKeyCourierSMTPHeaders) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 35cc56d02e2d..fb674635fa19 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -1045,3 +1045,64 @@ func TestChangeMinPasswordLength(t *testing.T) { assert.NoError(t, err) }) } + +func TestCourierTemplatesConfig(t *testing.T) { + ctx := context.Background() + + t.Run("case=partial template update allowed", func(t *testing.T) { + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml")) + assert.NoError(t, err) + }) + + t.Run("case=missing required body plaintext on invalid recovery template", func(t *testing.T) { + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + configx.WithConfigFiles("stub/.kratos.courier.remote.invalid.body.yaml")) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing properties: \"plaintext\"") + }) + + t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) { + _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) + assert.NoError(t, err) + }) + + t.Run("case=courier template helper", func(t *testing.T) { + c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, + configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml")) + + assert.NoError(t, err) + + courierTemplateConfig := &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "", + HTML: "", + }, + Subject: "", + } + + assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(config.ViperKeyCourierTemplatesVerificationInvalidEmail)) + assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(config.ViperKeyCourierTemplatesVerificationValidEmail)) + // this should return an empty courierEmailTemplate as the key does not exist + assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper("a_random_key")) + + courierTemplateConfig = &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu", + HTML: "base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu", + }, + Subject: "base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk", + } + assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(config.ViperKeyCourierTemplatesRecoveryInvalidEmail)) + + courierTemplateConfig = &config.CourierEmailTemplate{ + Body: &config.CourierEmailBodyTemplate{ + PlainText: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K", + HTML: "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=", + }, + Subject: "base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50", + } + assert.Equal(t, courierTemplateConfig, c.CourierTemplatesHelper(config.ViperKeyCourierTemplatesRecoveryValidEmail)) + }) +} diff --git a/driver/config/stub/.kratos.courier.remote.invalid.body.yaml b/driver/config/stub/.kratos.courier.remote.invalid.body.yaml new file mode 100644 index 000000000000..c971e6a2d179 --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.invalid.body.yaml @@ -0,0 +1,30 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + # omit plaintext to verify it is validated + subject: Account Access Attempted + valid: + email: + body: + plaintext: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + html: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4= + subject: Recover access to your account + # omit verification here to test templates override fallback + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml b/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml new file mode 100644 index 000000000000..4eb0a5a64ba7 --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.invalid.subject.yaml @@ -0,0 +1,30 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + # omit subject to verify it is validated + valid: + # omit template_root as it is not required on valid + email: + body: + plaintext: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + html: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4= + subject: base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50 + # omit verification here to test templates override fallback + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.remote.partial.templates.yaml b/driver/config/stub/.kratos.courier.remote.partial.templates.yaml new file mode 100644 index 000000000000..bafadb283a3b --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.partial.templates.yaml @@ -0,0 +1,26 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + plaintext: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + verification: + valid: + email: + subject: base64://VmVyaWZpY2F0aW9uIEVtYWls + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/config/stub/.kratos.courier.remote.templates.yaml b/driver/config/stub/.kratos.courier.remote.templates.yaml new file mode 100644 index 000000000000..23d35b6210be --- /dev/null +++ b/driver/config/stub/.kratos.courier.remote.templates.yaml @@ -0,0 +1,30 @@ +dsn: sqlite://foo.db?mode=memory&_fk=true + +selfservice: + default_browser_return_url: http://return-to-3-test.ory.sh/ + +identity: + default_schema_id: default + schemas: + - id: default + url: base64://ewogICIkaWQiOiAib3J5Oi8vaWRlbnRpdHktdGVzdC1zY2hlbWEiLAogICIkc2NoZW1hIjogImh0dHA6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQtMDcvc2NoZW1hIyIsCiAgInRpdGxlIjogIklkZW50aXR5U2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgInRyYWl0cyI6IHsKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaXJzdCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJsYXN0IjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfSwKICAgICAgInJlcXVpcmVkIjogWwogICAgICAgICJuYW1lIgogICAgICBdLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9CiAgfQp9 + +courier: + smtp: + connection_uri: smtp://stub-url + templates: + recovery: + invalid: + email: + body: + plaintext: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + html: base64://SGksCgp5b3UgKG9yIHNvbWVvbmUgZWxzZSkgZW50ZXJlZCB0aGlzIGVtYWlsIGFkZHJlc3Mgd2hlbiB0cnlpbmcgdG8gcmVjb3ZlciBhY2Nlc3MgdG8gYW4gYWNjb3VudC4KCkhvd2V2ZXIsIHRoaXMgZW1haWwgYWRkcmVzcyBpcyBub3Qgb24gb3VyIGRhdGFiYXNlIG9mIHJlZ2lzdGVyZWQgdXNlcnMgYW5kIHRoZXJlZm9yZSB0aGUgYXR0ZW1wdCBoYXMgZmFpbGVkLgoKSWYgdGhpcyB3YXMgeW91LCBjaGVjayBpZiB5b3Ugc2lnbmVkIHVwIHVzaW5nIGEgZGlmZmVyZW50IGFkZHJlc3MuCgpJZiB0aGlzIHdhcyBub3QgeW91LCBwbGVhc2UgaWdub3JlIHRoaXMgZW1haWwu + subject: base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk + valid: + email: + body: + plaintext: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K + html: base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4= + subject: base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50 + # omit verification here to test templates override fallback + template_override_path: "../../courier/template/courier/builtin/templates" diff --git a/driver/registry_default.go b/driver/registry_default.go index 4152bd1a3100..874dcff710de 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -263,11 +263,7 @@ func (m *RegistryDefault) Config(ctx context.Context) *config.Config { return corp.ContextualizeConfig(ctx, m.c) } -func (m *RegistryDefault) CourierConfig(ctx context.Context) courier.SMTPConfig { - return m.Config(ctx) -} - -func (m *RegistryDefault) SMTPConfig(ctx context.Context) courier.SMTPConfig { +func (m *RegistryDefault) CourierConfig(ctx context.Context) config.CourierConfigs { return m.Config(ctx) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 94cdb97da948..d0310b7ca866 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -783,6 +783,76 @@ ] } } + }, + "courierTemplates": { + "type": "object", + "properties": { + "invalid": { + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + }, + "valid": { + "type": "object", + "properties": { + "email": { + "$ref": "#/definitions/emailCourierTemplate" + } + }, + "required": [ + "email" + ] + } + } + }, + "emailCourierTemplate": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "plaintext": { + "type": "string", + "description": "The fallback template for email clients that do not support html.", + "format": "uri", + "examples": [ + "file://path/to/body.plaintext.gotmpl", + "https://foo.bar.com/path/to/body.plaintext.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQp7eyAuUmVjb3ZlcnlVUkwgfX0K" + ] + }, + "html": { + "type": "string", + "description": "The default template used for sending out emails. The template can contain HTML ", + "format": "uri", + "examples": [ + "file://path/to/body.html.gotmpl", + "https://foo.bar.com/path/to/body.html.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4" + ] + } + }, + "required": [ + "plaintext", + "html" + ] + }, + "subject": { + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/subject.gotmpl", + "https://foo.bar.com/path/to/subject.gotmpl", + "base64://e3sgZGVmaW5lIGFmLVpBIH19CkhhbGxvLAoKSGVyc3RlbCBqb3UgcmVrZW5pbmcgZGV1ciBoaWVyZGllIHNrYWtlbCB0ZSB2b2xnOgp7ey0gZW5kIC19fQoKe3sgZGVmaW5lIGVuLVVTIH19CkhpLAoKcGxlYXNlIHJlY292ZXIgYWNjZXNzIHRvIHlvdXIgYWNjb3VudCBieSBjbGlja2luZyB0aGUgZm9sbG93aW5nIGxpbms6Cnt7LSBlbmQgLX19Cgp7ey0gaWYgZXEgLmxhbmcgImFmLVpBIiAtfX0KCnt7IHRlbXBsYXRlICJhZi1aQSIgLiB9fQoKe3stIGVsc2UgLX19Cgp7eyB0ZW1wbGF0ZSAiZW4tVVMiIH19Cgp7ey0gZW5kIC19fQo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4" + ] + } + } } }, "properties": { @@ -1305,6 +1375,17 @@ "title": "Courier configuration", "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", "properties": { + "templates": { + "type": "object", + "properties": { + "recovery": { + "$ref": "#/definitions/courierTemplates" + }, + "verification": { + "$ref": "#/definitions/courierTemplates" + } + } + }, "template_override_path": { "type": "string", "title": "Override message templates", diff --git a/selfservice/strategy/link/sender.go b/selfservice/strategy/link/sender.go index 77aaee20c518..f6567467f7f0 100644 --- a/selfservice/strategy/link/sender.go +++ b/selfservice/strategy/link/sender.go @@ -5,6 +5,10 @@ import ( "net/http" "net/url" + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + "github.com/pkg/errors" "github.com/ory/x/errorsx" @@ -23,6 +27,8 @@ import ( type ( senderDependencies interface { courier.Provider + courier.ConfigProvider + identity.PoolProvider identity.ManagementProvider identity.PrivilegedPoolProvider @@ -31,8 +37,9 @@ type ( VerificationTokenPersistenceProvider RecoveryTokenPersistenceProvider - } + HTTPClient(ctx context.Context, opts ...httpx.ResilientOptions) *retryablehttp.Client + } SenderProvider interface { LinkSender() *Sender } @@ -59,7 +66,7 @@ func (s *Sender) SendRecoveryLink(ctx context.Context, r *http.Request, f *recov address, err := s.r.IdentityPool().FindRecoveryAddressByValue(ctx, identity.RecoveryAddressTypeEmail, to) if err != nil { - if err := s.send(ctx, string(via), templates.NewRecoveryInvalid(s.r.Config(ctx), &templates.RecoveryInvalidModel{To: to})); err != nil { + if err := s.send(ctx, string(via), templates.NewRecoveryInvalid(s.r, &templates.RecoveryInvalidModel{To: to})); err != nil { return err } return errors.Cause(ErrUnknownAddress) @@ -99,7 +106,7 @@ func (s *Sender) SendVerificationLink(ctx context.Context, f *verification.Flow, WithField("via", via). WithSensitiveField("email_address", address). Info("Sending out invalid verification email because address is unknown.") - if err := s.send(ctx, string(via), templates.NewVerificationInvalid(s.r.Config(ctx), &templates.VerificationInvalidModel{To: to})); err != nil { + if err := s.send(ctx, string(via), templates.NewVerificationInvalid(s.r, &templates.VerificationInvalidModel{To: to})); err != nil { return err } return errors.Cause(ErrUnknownAddress) @@ -138,7 +145,7 @@ func (s *Sender) SendRecoveryTokenTo(ctx context.Context, f *recovery.Flow, i *i return err } - return s.send(ctx, string(address.Via), templates.NewRecoveryValid(s.r.Config(ctx), + return s.send(ctx, string(address.Via), templates.NewRecoveryValid(s.r, &templates.RecoveryValidModel{To: address.Value, RecoveryURL: urlx.CopyWithQuery( urlx.AppendPaths(s.r.Config(ctx).SelfServiceLinkMethodBaseURL(), recovery.RouteSubmitFlow), url.Values{ @@ -161,7 +168,7 @@ func (s *Sender) SendVerificationTokenTo(ctx context.Context, f *verification.Fl return err } - if err := s.send(ctx, string(address.Via), templates.NewVerificationValid(s.r.Config(ctx), + if err := s.send(ctx, string(address.Via), templates.NewVerificationValid(s.r, &templates.VerificationValidModel{To: address.Value, VerificationURL: urlx.CopyWithQuery( urlx.AppendPaths(s.r.Config(ctx).SelfServiceLinkMethodBaseURL(), verification.RouteSubmitFlow), url.Values{ diff --git a/test/e2e/cypress/integration/profiles/recovery/recovery/errors.spec.ts b/test/e2e/cypress/integration/profiles/recovery/recovery/errors.spec.ts index 3b4889005aab..73a74bbfca6e 100644 --- a/test/e2e/cypress/integration/profiles/recovery/recovery/errors.spec.ts +++ b/test/e2e/cypress/integration/profiles/recovery/recovery/errors.spec.ts @@ -122,6 +122,18 @@ context('Account Recovery Errors', () => { cy.noSession() }) }) + + it('invalid remote recovery email template', () => { + cy.remoteCourierRecoveryTemplates() + const identity = gen.identityWithWebsite() + cy.recoverApi({ email: identity.email }) + + cy.getMail().then((mail) => { + expect(mail.body).to.include( + 'this is a remote invalid recovery template' + ) + }) + }) }) }) }) diff --git a/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts b/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts index 74c6c1ec40d6..e5148647a6c9 100644 --- a/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts +++ b/test/e2e/cypress/integration/profiles/verification/verify/errors.spec.ts @@ -85,6 +85,16 @@ context('Account Verification Error', () => { ) }) }) + + it('unable to verify non-existent account', async () => { + cy.get('input[name="email"]').type(gen.identity().email) + cy.get('button[value="link"]').click() + cy.getMail().then((mail) => { + expect(mail.subject).eq( + 'Someone tried to verify this email address (remote)' + ) + }) + }) }) }) }) diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts index f61ad4ca5680..ba8334df42a0 100644 --- a/test/e2e/cypress/support/commands.ts +++ b/test/e2e/cypress/support/commands.ts @@ -462,6 +462,38 @@ Cypress.Commands.add('browserReturnUrlOry', ({} = {}) => { }) }) +Cypress.Commands.add('remoteCourierRecoveryTemplates', ({} = {}) => { + updateConfigFile((config) => { + config.courier.templates = { + recovery: { + invalid: { + email: { + body: { + html: + 'base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=', + plaintext: + 'base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIGludmFsaWQgcmVjb3ZlcnkgdGVtcGxhdGU=' + }, + subject: 'base64://QWNjb3VudCBBY2Nlc3MgQXR0ZW1wdGVk' + } + }, + valid: { + email: { + body: { + html: + 'base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgo8YSBocmVmPSJ7eyAuUmVjb3ZlcnlVUkwgfX0iPnt7IC5SZWNvdmVyeVVSTCB9fTwvYT4=', + plaintext: + 'base64://SGksCgp0aGlzIGlzIGEgcmVtb3RlIHRlbXBsYXRlCnBsZWFzZSByZWNvdmVyIGFjY2VzcyB0byB5b3VyIGFjY291bnQgYnkgY2xpY2tpbmcgdGhlIGZvbGxvd2luZyBsaW5rOgp7eyAuUmVjb3ZlcnlVUkwgfX0=' + }, + subject: 'base64://UmVjb3ZlciBhY2Nlc3MgdG8geW91ciBhY2NvdW50' + } + } + } + } + return config + }) +}) + Cypress.Commands.add( 'loginOidc', ({ expectSession = true, url = APP_URL + '/login' }) => { diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts index 8815665f394e..e3b90adf23da 100644 --- a/test/e2e/cypress/support/index.d.ts +++ b/test/e2e/cypress/support/index.d.ts @@ -133,6 +133,11 @@ declare global { */ browserReturnUrlOry(): Chainable + /** + * Change the courier recovery invalid and valid templates to remote base64 strings + */ + remoteCourierRecoveryTemplates(): Chainable + /** * Changes the config so that the registration flow lifespan is very short. * diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 425931e4a15b..f023dd632156 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -108,7 +108,7 @@ prepare() { ( cd "$rn_ui_dir" - npm i -g expo-cli + npm i expo-cli WEB_PORT=4457 KRATOS_URL=http://localhost:4433 npm run web -- --non-interactive \ >"${base}/test/e2e/rn-profile-app.e2e.log" 2>&1 & ) diff --git a/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml b/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml new file mode 100644 index 000000000000..8a8f9c54c1c1 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/courierTemplates.bodyHtmlMissing.yaml @@ -0,0 +1,5 @@ +valid: + email: + body: + plaintext: "https://some-url" + subject: "https://some-url" diff --git a/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml b/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml new file mode 100644 index 000000000000..9ed67f474d88 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/courierTemplates.emailMissing.yaml @@ -0,0 +1,4 @@ +invalid: + another-courier: +valid: + email: diff --git a/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml b/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml new file mode 100644 index 000000000000..b9ac44d6c452 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/emailCourierTemplate.malformed.yaml @@ -0,0 +1,4 @@ +body: + plaintext: "http://call-something" + html: "malformed uri" +subject: "something" diff --git a/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml b/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml new file mode 100644 index 000000000000..2f8b1b0ac4be --- /dev/null +++ b/test/schema/fixtures/config.schema.test.failure/root.missingCourierTemplatesVerificationInvalid.yaml @@ -0,0 +1,20 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +courier: + template_override_path: foo + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + templates: + recovery: "#/definitions/courierTemplates" + verification: + valid: + email: "#/definitions/emailCourierTemplate" diff --git a/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml b/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml new file mode 100644 index 000000000000..8f0ae4803845 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/courierTemplates.full.yaml @@ -0,0 +1,4 @@ +invalid: + email: "#/definitions/emailCourierTemplate" +valid: + email: "#/definitions/emailCourierTemplate" diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml new file mode 100644 index 000000000000..02298718a29b --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.full.yaml @@ -0,0 +1,4 @@ +body: + plaintext: "base64://" + html: "base64://" +subject: "http://" diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml new file mode 100644 index 000000000000..1de8bac6c149 --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingBody.yaml @@ -0,0 +1 @@ +subject: "http://" diff --git a/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml new file mode 100644 index 000000000000..7a802c86697d --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/emailCourierTemplate.withMissingSubject.yaml @@ -0,0 +1,3 @@ +body: + plaintext: "base64://" + html: "base64://" diff --git a/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml b/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml new file mode 100644 index 000000000000..026d96c32ceb --- /dev/null +++ b/test/schema/fixtures/config.schema.test.success/root.courierTemplates.yaml @@ -0,0 +1,18 @@ +selfservice: + default_browser_return_url: "#/definitions/defaultReturnTo" + +dsn: foo + +identity: + schemas: + - id: default + url: https://example.com + +courier: + template_override_path: foo + smtp: + connection_uri: smtps://foo:bar@my-mailserver:1234/ + from_address: no-reply@ory.kratos.sh + templates: + recovery: "#/definitions/courierTemplates" + verification: "#/definitions/courierTemplates"