Skip to content

Commit

Permalink
Merge pull request #403 from canonical/IAM-978-user-invite-initiation
Browse files Browse the repository at this point in the history
IAM 978 User invite - send email for identity creation with recovery code and link
  • Loading branch information
shipperizer committed Sep 10, 2024
2 parents 5442373 + f4d0ca9 commit 24391a4
Show file tree
Hide file tree
Showing 17 changed files with 580 additions and 33 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,20 @@ This is the Admin UI for the Canonical Identity Platform.
middleware is enabled default to `true`
- `AUTHENTICATION_ENABLED`: flag defining if the OAuth authentication middleware
is enabled, default to `false`
- `OIDC_ISSUER`: URL of the OIDC provider
- `OIDC_ISSUER`: URL of the OIDC provider
- `OAUTH2_CLIENT_ID`: OAuth2 client ID used for authentication purposes
- `OAUTH2_CLIENT_SECRET`: OAuth2 client secret used for authentication purposes
- `OAUTH2_REDIRECT_URI`: URI used by the Oauth2 provider for the redirecting callback
- `OAUTH2_CODEGRANT_SCOPES`: OAuth2 scopes, defaults to `openid,offline_access`
- `OAUTH2_AUTH_COOKIES_ENCRYPTION_KEY`: 32 bytes string used for encrypting cookies
- `ACCESS_TOKEN_VERIFICATION_STRATEGY`: OAuth2 verification startegy, one of `jwks` or `userinfo``
- `MAIL_HOST`: host of the mail server (required)
- `MAIL_PORT`: port exposed by the mail server (required)
- `MAIL_USERNAME`: username to use for the simple authentication on the mail server (if present, both username and
password are used)
- `MAIL_PASSWORD`: password to use for the simple authentication on the mail server
- `MAIL_FROM_ADDRESS`: email address sending the email (required)
- `MAIL_SEND_TIMEOUT_SECONDS`: timeout used to send emails (defaults to 15 seconds)

## Development setup

Expand Down
5 changes: 4 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
k8s "github.com/canonical/identity-platform-admin-ui/internal/k8s"
ik "github.com/canonical/identity-platform-admin-ui/internal/kratos"
"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/mail"
"github.com/canonical/identity-platform-admin-ui/internal/monitoring/prometheus"
io "github.com/canonical/identity-platform-admin-ui/internal/oathkeeper"
"github.com/canonical/identity-platform-admin-ui/internal/openfga"
Expand Down Expand Up @@ -164,9 +165,11 @@ func serve() {
hydraAdminClient,
)

mailConfig := mail.NewConfig(specs.MailHost, specs.MailPort, specs.MailUsername, specs.MailPassword, specs.MailFromAddress, specs.MailSendTimeoutSeconds)

ollyConfig := web.NewO11yConfig(tracer, monitor, logger)

routerConfig := web.NewRouterConfig(specs.ContextPath, specs.PayloadValidationEnabled, idpConfig, schemasConfig, rulesConfig, uiConfig, externalConfig, oauth2Config, ollyConfig)
routerConfig := web.NewRouterConfig(specs.ContextPath, specs.PayloadValidationEnabled, idpConfig, schemasConfig, rulesConfig, uiConfig, externalConfig, oauth2Config, mailConfig, ollyConfig)

router := web.NewRouter(routerConfig, wpool)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ require (
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/wneessen/go-mail v0.4.4 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJ
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wneessen/go-mail v0.4.4 h1:rI8wJzPYymUpUth87vFV3k313bmnid4v+FwhBAYYLFM=
github.com/wneessen/go-mail v0.4.4/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
7 changes: 7 additions & 0 deletions internal/config/specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,11 @@ type EnvSpec struct {
PayloadValidationEnabled bool `envconfig:"payload_validation_enabled" default:"true"`

OpenFGAWorkersTotal int `envconfig:"openfga_workers_total" default:"150"`

MailHost string `envconfig:"MAIL_HOST" required:"true"`
MailPort int `envconfig:"MAIL_PORT" required:"true"`
MailUsername string `envconfig:"MAIL_USERNAME"`
MailPassword string `envconfig:"MAIL_PASSWORD"`
MailFromAddress string `envconfig:"MAIL_FROM_ADDRESS" required:"true"`
MailSendTimeoutSeconds int `envconfig:"MAIL_SEND_TIMEOUT_SECONDS" default:"15"`
}
85 changes: 85 additions & 0 deletions internal/mail/html/user-invite.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!-- Copyright 2024 Canonical Ltd. -->
<!-- SPDX-License-Identifier: AGPL-3.0 -->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Verify Your Account</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f6f6f6;
color: #333;
margin: 0;
padding: 0;
}

.container {
width: 100%;
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.header {
text-align: center;
padding: 20px 0;
}

.header h1 {
color: #007BFF;
margin: 0;
}

.content {
text-align: center;
}

.content p {
font-size: 16px;
line-height: 1.5;
margin: 20px 0;
}

.verify-button {
display: inline-block;
background-color: #007BFF;
color: #ffffff;
text-decoration: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 18px;
margin-top: 20px;
}

.footer {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Verify Your Account</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Your account with the email address <strong>{{ .Email }}</strong> was recently created. To complete your
registration, click the button below:</p>
<p>Verification code <span><strong>{{ .RecoveryCode }}</strong></span></p>
<a class="verify-button" href="{{ .InviteUrl }}">Verify Account</a>
</div>
<div class="footer">
<p>&copy;Copyright 2024 Canonical Ltd.</p>
</div>
</div>
</body>
</html>
19 changes: 19 additions & 0 deletions internal/mail/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 Canonical Ltd.
// SPDX-License-Identifier: AGPL-3.0

package mail

import (
"context"
"html/template"

mail2 "github.com/wneessen/go-mail"
)

type EmailServiceInterface interface {
Send(context.Context, string, string, *template.Template, any) error
}

type MailClientInterface interface {
DialAndSendWithContext(context.Context, ...*mail2.Msg) error
}
105 changes: 105 additions & 0 deletions internal/mail/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 Canonical Ltd.
// SPDX-License-Identifier: AGPL-3.0

package mail

import (
"context"
"html/template"
"time"

"github.com/wneessen/go-mail"
"go.opentelemetry.io/otel/trace"

"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/monitoring"
)

type Config struct {
Host string `validate:"required"`
Port int `validate:"required"`
Username string
Password string
FromAddress string `validate:"required"`
SendTimeout time.Duration
}

func NewConfig(host string, port int, username, password, from string, sendTimeout int) *Config {
c := new(Config)

c.Host = host
c.Port = port
c.Username = username
c.Password = password
c.FromAddress = from
c.SendTimeout = time.Duration(sendTimeout) * time.Second

return c
}

type EmailService struct {
from string
client MailClientInterface

tracer trace.Tracer
monitor monitoring.MonitorInterface
logger logging.LoggerInterface
}

func (e *EmailService) Send(ctx context.Context, to, subject string, template *template.Template, templateArgs any) error {
ctx, span := e.tracer.Start(ctx, "mail.EmailService.Send")
defer span.End()

msg := mail.NewMsg()

if err := msg.From(e.from); err != nil {
return err
}

if err := msg.SetBodyHTMLTemplate(template, templateArgs); err != nil {
return err
}

if err := msg.To(to); err != nil {
return err
}

msg.Subject(subject)

return e.client.DialAndSendWithContext(ctx, msg)
}

func NewEmailService(config *Config, tracer trace.Tracer, monitor monitoring.MonitorInterface, logger logging.LoggerInterface) *EmailService {
s := new(EmailService)
s.from = config.FromAddress

var err error
mailOpts := []mail.Option{
mail.WithPort(config.Port),
mail.WithTLSPolicy(mail.TLSOpportunistic),
mail.WithTimeout(config.SendTimeout),
}

// treat smtp connection as authenticated only if username is passed
if config.Username != "" {
mailOpts = append(
mailOpts,
[]mail.Option{mail.WithSMTPAuth(mail.SMTPAuthPlain), mail.WithUsername(config.Username), mail.WithPassword(config.Password)}...,
)
}

s.client, err = mail.NewClient(
config.Host,
mailOpts...,
)

if err != nil {
logger.Fatalf("failed to create email client: %s", err)
}

s.monitor = monitor
s.tracer = tracer
s.logger = logger

return s
}
Loading

0 comments on commit 24391a4

Please sign in to comment.