forked from mystiq/dex
commit
095aff647b
12 changed files with 99 additions and 8 deletions
|
@ -16,7 +16,6 @@ func TestNewEmailConfigFromReader(t *testing.T) {
|
||||||
{
|
{
|
||||||
json: `{"type":"mailgun","id":"mg","privateAPIKey":"private","publicAPIKey":"public","domain":"example.com"}`,
|
json: `{"type":"mailgun","id":"mg","privateAPIKey":"private","publicAPIKey":"public","domain":"example.com"}`,
|
||||||
want: MailgunEmailerConfig{
|
want: MailgunEmailerConfig{
|
||||||
ID: "mg",
|
|
||||||
PrivateAPIKey: "private",
|
PrivateAPIKey: "private",
|
||||||
PublicAPIKey: "public",
|
PublicAPIKey: "public",
|
||||||
Domain: "example.com",
|
Domain: "example.com",
|
||||||
|
|
|
@ -36,6 +36,11 @@ type TemplatizedEmailer struct {
|
||||||
textTemplates *template.Template
|
textTemplates *template.Template
|
||||||
htmlTemplates *htmltemplate.Template
|
htmlTemplates *htmltemplate.Template
|
||||||
emailer Emailer
|
emailer Emailer
|
||||||
|
globalCtx map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TemplatizedEmailer) SetGlobalContext(ctx map[string]interface{}) {
|
||||||
|
t.globalCtx = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMail queues an email to be sent to a recipient.
|
// SendMail queues an email to be sent to a recipient.
|
||||||
|
@ -59,6 +64,10 @@ func (t *TemplatizedEmailer) SendMail(from, subject, tplName string, data map[st
|
||||||
data["from"] = from
|
data["from"] = from
|
||||||
data["subject"] = subject
|
data["subject"] = subject
|
||||||
|
|
||||||
|
for k, v := range t.globalCtx {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
var textBuffer bytes.Buffer
|
var textBuffer bytes.Buffer
|
||||||
if textTpl != nil {
|
if textTpl != nil {
|
||||||
err := textTpl.Execute(&textBuffer, data)
|
err := textTpl.Execute(&textBuffer, data)
|
||||||
|
|
|
@ -9,9 +9,11 @@ import (
|
||||||
const (
|
const (
|
||||||
textTemplateString = `{{define "T1.txt"}}{{.gift}} from {{.from}} to {{.to}}.{{end}}
|
textTemplateString = `{{define "T1.txt"}}{{.gift}} from {{.from}} to {{.to}}.{{end}}
|
||||||
{{define "T3.txt"}}Hello there, {{.name}}!{{end}}
|
{{define "T3.txt"}}Hello there, {{.name}}!{{end}}
|
||||||
|
{{define "T4.txt"}}Hello there, {{.name}}! Welcome to {{.planet}}!{{end}}
|
||||||
`
|
`
|
||||||
htmlTemplateString = `{{define "T1.html"}}<html><body>{{.gift}} from {{.from}} to {{.to}}.</body></html>{{end}}
|
htmlTemplateString = `{{define "T1.html"}}<html><body>{{.gift}} from {{.from}} to {{.to}}.</body></html>{{end}}
|
||||||
{{define "T2.html"}}<html><body>Hello, {{.name}}!</body></html>{{end}}
|
{{define "T2.html"}}<html><body>Hello, {{.name}}!</body></html>{{end}}
|
||||||
|
{{define "T4.html"}}<html><body>Hello there, {{.name}}! Welcome to {{.planet}}!</body></html>{{end}}
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,6 +53,7 @@ func TestTemplatizedEmailSendMail(t *testing.T) {
|
||||||
wantText string
|
wantText string
|
||||||
wantHtml string
|
wantHtml string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
ctx map[string]interface{}
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
tplName: "T1",
|
tplName: "T1",
|
||||||
|
@ -97,11 +100,29 @@ func TestTemplatizedEmailSendMail(t *testing.T) {
|
||||||
wantText: "",
|
wantText: "",
|
||||||
wantHtml: htmlStart + "Hello, Alice<script>alert('hacked!')</script>!" + htmlEnd,
|
wantHtml: htmlStart + "Hello, Alice<script>alert('hacked!')</script>!" + htmlEnd,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tplName: "T4",
|
||||||
|
from: "bob@example.com",
|
||||||
|
to: "alice@example.com",
|
||||||
|
subject: "hello there",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"name": "Alice",
|
||||||
|
},
|
||||||
|
wantText: "Hello there, Alice! Welcome to Mars!",
|
||||||
|
ctx: map[string]interface{}{
|
||||||
|
"planet": "Mars",
|
||||||
|
},
|
||||||
|
wantHtml: "<html><body>Hello there, Alice! Welcome to Mars!</body></html>",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
emailer := &testEmailer{}
|
emailer := &testEmailer{}
|
||||||
templatizer := NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
|
templatizer := NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
|
||||||
|
if tt.ctx != nil {
|
||||||
|
templatizer.SetGlobalContext(tt.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
err := templatizer.SendMail(tt.from, tt.subject, tt.tplName, tt.data, tt.to)
|
err := templatizer.SendMail(tt.from, tt.subject, tt.tplName, tt.data, tt.to)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -519,6 +519,7 @@ func TestCreateUser(t *testing.T) {
|
||||||
cantEmail: tt.cantEmail,
|
cantEmail: tt.cantEmail,
|
||||||
lastEmail: tt.req.User.Email,
|
lastEmail: tt.req.User.Email,
|
||||||
lastClientID: "XXX",
|
lastClientID: "XXX",
|
||||||
|
lastWasInvite: true,
|
||||||
lastRedirectURL: *urlParsed,
|
lastRedirectURL: *urlParsed,
|
||||||
}
|
}
|
||||||
if diff := pretty.Compare(wantEmalier, f.emailer); diff != "" {
|
if diff := pretty.Compare(wantEmalier, f.emailer); diff != "" {
|
||||||
|
@ -578,6 +579,7 @@ type testEmailer struct {
|
||||||
lastEmail string
|
lastEmail string
|
||||||
lastClientID string
|
lastClientID string
|
||||||
lastRedirectURL url.URL
|
lastRedirectURL url.URL
|
||||||
|
lastWasInvite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
|
// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
|
||||||
|
@ -585,6 +587,20 @@ func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
|
||||||
t.lastEmail = email
|
t.lastEmail = email
|
||||||
t.lastRedirectURL = redirectURL
|
t.lastRedirectURL = redirectURL
|
||||||
t.lastClientID = clientID
|
t.lastClientID = clientID
|
||||||
|
t.lastWasInvite = false
|
||||||
|
|
||||||
|
var retURL *url.URL
|
||||||
|
if t.cantEmail {
|
||||||
|
retURL = &testResetPasswordURL
|
||||||
|
}
|
||||||
|
return retURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
||||||
|
t.lastEmail = email
|
||||||
|
t.lastRedirectURL = redirectURL
|
||||||
|
t.lastClientID = clientID
|
||||||
|
t.lastWasInvite = true
|
||||||
|
|
||||||
var retURL *url.URL
|
var retURL *url.URL
|
||||||
if t.cantEmail {
|
if t.cantEmail {
|
||||||
|
|
|
@ -85,7 +85,7 @@ func (cfg *ServerConfig) Server() (*Server, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setEmailer(&srv, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
|
err = setEmailer(&srv, cfg.IssuerName, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ func setTemplates(srv *Server, tpls *template.Template) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setEmailer(srv *Server, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {
|
func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {
|
||||||
|
|
||||||
cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile)
|
cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -290,6 +290,9 @@ func setEmailer(srv *Server, fromAddress, emailerConfigFile string, emailTemplat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
|
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
|
||||||
|
tMailer.SetGlobalContext(map[string]interface{}{
|
||||||
|
"issuer_name": issuerName,
|
||||||
|
})
|
||||||
|
|
||||||
ue := useremail.NewUserEmailer(srv.UserRepo,
|
ue := useremail.NewUserEmailer(srv.UserRepo,
|
||||||
srv.PasswordInfoRepo,
|
srv.PasswordInfoRepo,
|
||||||
|
|
|
@ -102,7 +102,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
|
||||||
wantEmailer: &testEmailer{
|
wantEmailer: &testEmailer{
|
||||||
to: str("Email-1@example.com"),
|
to: str("Email-1@example.com"),
|
||||||
from: "noreply@example.com",
|
from: "noreply@example.com",
|
||||||
subject: "Reset your password.",
|
subject: "Reset Your Password",
|
||||||
},
|
},
|
||||||
wantPRUserID: "ID-1",
|
wantPRUserID: "ID-1",
|
||||||
wantPRRedirect: &testRedirectURL,
|
wantPRRedirect: &testRedirectURL,
|
||||||
|
@ -138,7 +138,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
|
||||||
wantEmailer: &testEmailer{
|
wantEmailer: &testEmailer{
|
||||||
to: str("Email-1@example.com"),
|
to: str("Email-1@example.com"),
|
||||||
from: "noreply@example.com",
|
from: "noreply@example.com",
|
||||||
subject: "Reset your password.",
|
subject: "Reset Your Password",
|
||||||
},
|
},
|
||||||
wantPRPassword: "password",
|
wantPRPassword: "password",
|
||||||
wantPRUserID: "ID-1",
|
wantPRUserID: "ID-1",
|
||||||
|
|
7
static/email/invite.html
Normal file
7
static/email/invite.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
Welcome to Dex! Click below to set your password:
|
||||||
|
|
||||||
|
<a href="{{ .link }}">Set Password</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
static/email/invite.txt
Normal file
4
static/email/invite.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Welcome to Dex! Click below to set your password:
|
||||||
|
|
||||||
|
Link:
|
||||||
|
{{ .link }}
|
2
test
2
test
|
@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"}
|
||||||
|
|
||||||
source ./build
|
source ./build
|
||||||
|
|
||||||
TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user user/api"
|
TESTABLE="connector db integration pkg/crypto pkg/flag pkg/http pkg/net pkg/time pkg/html functional/repo server session user user/api email"
|
||||||
FORMATTABLE="$TESTABLE cmd/dexctl cmd/dex-worker cmd/dex-overlord examples/app functional pkg/log"
|
FORMATTABLE="$TESTABLE cmd/dexctl cmd/dex-worker cmd/dex-overlord examples/app functional pkg/log"
|
||||||
|
|
||||||
# user has not provided PKG override
|
# user has not provided PKG override
|
||||||
|
|
|
@ -89,6 +89,7 @@ type UsersAPI struct {
|
||||||
|
|
||||||
type Emailer interface {
|
type Emailer interface {
|
||||||
SendResetPasswordEmail(string, url.URL, string) (*url.URL, error)
|
SendResetPasswordEmail(string, url.URL, string) (*url.URL, error)
|
||||||
|
SendInviteEmail(string, url.URL, string) (*url.URL, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Creds struct {
|
type Creds struct {
|
||||||
|
@ -169,7 +170,7 @@ func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (s
|
||||||
|
|
||||||
usr = userToSchemaUser(userUser)
|
usr = userToSchemaUser(userUser)
|
||||||
|
|
||||||
url, err := u.emailer.SendResetPasswordEmail(usr.Email, validRedirURL, creds.ClientID)
|
url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID)
|
||||||
|
|
||||||
// An email is sent only if we don't get a link and there's no error.
|
// An email is sent only if we don't get a link and there's no error.
|
||||||
emailSent := err == nil && url == nil
|
emailSent := err == nil && url == nil
|
||||||
|
|
|
@ -20,13 +20,23 @@ type testEmailer struct {
|
||||||
lastEmail string
|
lastEmail string
|
||||||
lastClientID string
|
lastClientID string
|
||||||
lastRedirectURL url.URL
|
lastRedirectURL url.URL
|
||||||
|
lastWasInvite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
|
// SendResetPasswordEmail returns resetPasswordURL when it can't email, mimicking the behavior of the real UserEmailer.
|
||||||
func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
func (t *testEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
||||||
|
return t.sendEmail(email, redirectURL, clientID, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
||||||
|
return t.sendEmail(email, redirectURL, clientID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testEmailer) sendEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) {
|
||||||
t.lastEmail = email
|
t.lastEmail = email
|
||||||
t.lastRedirectURL = redirectURL
|
t.lastRedirectURL = redirectURL
|
||||||
t.lastClientID = clientID
|
t.lastClientID = clientID
|
||||||
|
t.lastWasInvite = invite
|
||||||
|
|
||||||
var retURL *url.URL
|
var retURL *url.URL
|
||||||
if t.cantEmail {
|
if t.cantEmail {
|
||||||
|
@ -369,6 +379,7 @@ func TestCreateUser(t *testing.T) {
|
||||||
lastEmail: tt.usr.Email,
|
lastEmail: tt.usr.Email,
|
||||||
lastClientID: tt.creds.ClientID,
|
lastClientID: tt.creds.ClientID,
|
||||||
lastRedirectURL: tt.redirURL,
|
lastRedirectURL: tt.redirURL,
|
||||||
|
lastWasInvite: true,
|
||||||
}
|
}
|
||||||
if diff := pretty.Compare(wantEmalier, emailer); diff != "" {
|
if diff := pretty.Compare(wantEmalier, emailer); diff != "" {
|
||||||
t.Errorf("case %d: Compare(want, got) = %v", i,
|
t.Errorf("case %d: Compare(want, got) = %v", i,
|
||||||
|
|
|
@ -53,6 +53,16 @@ func NewUserEmailer(ur user.UserRepo,
|
||||||
// This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so.
|
// This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so.
|
||||||
// If there is no emailer is configured, the URL of the aforementioned link is returned, otherwise nil is returned.
|
// If there is no emailer is configured, the URL of the aforementioned link is returned, otherwise nil is returned.
|
||||||
func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
||||||
|
return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendInviteEmail is exactly the same as SendResetPasswordEmail, except that it uses the invite template and subject name.
|
||||||
|
// In the near future, invite emails might diverge further.
|
||||||
|
func (u *UserEmailer) SendInviteEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
|
||||||
|
return u.sendResetPasswordOrInviteEmail(email, redirectURL, clientID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserEmailer) sendResetPasswordOrInviteEmail(email string, redirectURL url.URL, clientID string, invite bool) (*url.URL, error) {
|
||||||
usr, err := u.ur.GetByEmail(nil, email)
|
usr, err := u.ur.GetByEmail(nil, email)
|
||||||
if err == user.ErrorNotFound {
|
if err == user.ErrorNotFound {
|
||||||
log.Errorf("No Such user for email: %q", email)
|
log.Errorf("No Such user for email: %q", email)
|
||||||
|
@ -95,8 +105,17 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
|
||||||
q.Set("token", token)
|
q.Set("token", token)
|
||||||
resetURL.RawQuery = q.Encode()
|
resetURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
var tmplName, subj string
|
||||||
|
if invite {
|
||||||
|
tmplName = "invite"
|
||||||
|
subj = "Activate Your Account"
|
||||||
|
} else {
|
||||||
|
tmplName = "password-reset"
|
||||||
|
subj = "Reset Your Password"
|
||||||
|
}
|
||||||
|
|
||||||
if u.emailer != nil {
|
if u.emailer != nil {
|
||||||
err = u.emailer.SendMail(u.fromAddress, "Reset your password.", "password-reset",
|
err = u.emailer.SendMail(u.fromAddress, subj, tmplName,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"email": usr.Email,
|
"email": usr.Email,
|
||||||
"link": resetURL.String(),
|
"link": resetURL.String(),
|
||||||
|
@ -107,6 +126,7 @@ func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &resetURL, nil
|
return &resetURL, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEmailVerification sends an email to the user with the given userID containing a link which when visited marks the user as having had their email verified.
|
// SendEmailVerification sends an email to the user with the given userID containing a link which when visited marks the user as having had their email verified.
|
||||||
|
|
Loading…
Reference in a new issue