3
0

Implement optional email invitation to newly created users via CLI

This commit is contained in:
Mumbi Francis
2023-04-17 13:34:34 +03:00
committed by Mumbi Francis
parent 4b234516f5
commit 5d88d1e5ea
19 changed files with 387 additions and 96 deletions

View File

@@ -240,6 +240,38 @@
<b-form-input v-model="settings['auth.mail.from-name']" />
</b-input-group>
</b-form-group>
<hr>
<h5>
{{ $t('internal.send-user-invite-email.title') }}
</h5>
<b-form-group
label-cols="2"
:description="$t('internal.send-user-invite-email.description')"
>
<b-form-checkbox
v-model="settings['auth.internal.send-user-invite-email.enabled']"
:value="true"
:unchecked-value="false"
>
{{ $t('internal.send-user-invite-email.enabled') }}
</b-form-checkbox>
</b-form-group>
<b-form-group
:label="$t('internal.send-user-invite-email.expires.label')"
:description="$t('internal.send-user-invite-email.expires.description')"
label-cols="2"
>
<b-input-group append="hours">
<b-form-input
v-model="settings['auth.internal.send-user-invite-email.expires']"
type="number"
placeholder="72"
/>
</b-input-group>
</b-form-group>
</b-form>
<template #header>

View File

@@ -0,0 +1,14 @@
template:
title: Accept Invite
form:
email:
label: E-mail
placeholder: email@domain.ltd
new-password:
label: Password
placeholder: password
buttons:
password: Set password
alert:
invalid-expired-invite-token: Invalid or expired user invite token, please request a password reset.
success: Password successfully updated

View File

@@ -29,6 +29,13 @@ editor:
label: Enable split-credentials check
profile-avatar:
enabled: Profile avatar enabled
send-user-invite-email:
title: Invite email
enabled: Send invite email on user creation
description: When enabled, this configuration sends an invite email to a newly created user via the CLI
expires:
label: Valid for
description: How long will the invite token be valid before it expires
mail:
title: Authentication email sender mail
from-address: Sender's address

View File

@@ -629,6 +629,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
SplitCredentialsCheck: current.Auth.Internal.SplitCredentialsCheck,
ExternalEnabled: current.Auth.External.Enabled,
ProfileAvatarEnabled: current.Auth.Internal.ProfileAvatar.Enabled,
SendUserInviteEmail: current.Auth.Internal.SendUserInviteEmail.Enabled,
MultiFactor: authSettings.MultiFactor{
TOTP: authSettings.TOTP{
Enabled: current.Auth.MultiFactor.TOTP.Enabled,

View File

@@ -0,0 +1,56 @@
{{ template "inc_header.html.tpl" . }}
<div class="card-body p-0">
<h4 class="card-title p-3 border-bottom">{{ tr "invite.template.title" }}</h4>
<form
method="POST"
action="{{ links.AcceptInvite }}"
class="p-3"
>
{{ .csrfField }}
{{ if .form.error }}
<div class="text-danger font-weight-bold p-3" role="alert">
{{ .form.error }}
</div>
{{ end }}
<div class="mb-3">
<label>
{{ tr "invite.template.form.email.label" }}
</label>
<input
data-test-id="input-email"
type="email"
class="form-control"
name="email"
readonly
placeholder="{{ tr "invite.template.form.email.placeholder" }}"
value="{{ .user.Email }}"
aria-label="{{ tr "invite.template.form.emaillabel" }}"
>
</div>
<div class="mb-3">
<label>
{{ tr "invite.template.form.new-password.label" }}
</label>
<input
data-test-id="input-new-password"
type="password"
required
class="form-control"
name="password"
autocomplete="new-password"
placeholder="{{ tr "invite.template.form.new-password.placeholder" }}"
aria-label="{{ tr "invite.template.form.new-passwordlabel" }}"
>
</div>
<div class="text-right">
<button
data-test-id="button-change-password"
class="btn btn-primary btn-block btn-lg"
type="submit"
>
{{ tr "invite.template.form.buttons.password" }}
</button>
</div>
</form>
</div>
{{ template "inc_footer.html.tpl" . }}

View File

@@ -0,0 +1,79 @@
package handlers
import (
"github.com/cortezaproject/corteza/server/auth/request"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
func (h *AuthHandlers) acceptInviteForm(req *request.AuthReq) (err error) {
h.Log.Debug("invite email password reset form")
req.Template = TmplInvite
// user not set, expecting valid token in URL
if token := req.Request.URL.Query().Get("token"); len(token) > 0 {
var user *types.User
user, err = h.AuthService.ValidateInviteEmailToken(req.Context(), token)
if err == nil {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false)
if req.AuthUser.PendingEmailOTP() {
// Email OTP enabled & pending
//
// If we're here it means user clicked on a link in an email;
// we are effectively confirming email OTP
req.AuthUser.CompleteEmailOTP()
}
// redirect back to self (but without token and with user in session)
h.Log.Debug("valid user invite password reset token found, refreshing page with stored user")
req.RedirectTo = GetLinks().AcceptInvite
req.AuthUser.Save(req.Session)
return nil
}
}
if req.AuthUser == nil || err != nil {
h.Log.Warn("invalid, user invite password reset token used", zap.Error(err))
req.RedirectTo = GetLinks().Login
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "warning",
Text: t("invite.alert.invalid-expired-invite-token"),
})
}
req.Data["form"] = req.PopKV()
return nil
}
func (h *AuthHandlers) acceptInviteProc(req *request.AuthReq) (err error) {
h.Log.Debug("password reset proc")
err = h.AuthService.SetPassword(req.Context(), req.AuthUser.User.ID, req.Request.PostFormValue("password"))
if err == nil {
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: t("invite.alert.success"),
})
req.RedirectTo = GetLinks().Profile
return nil
}
switch {
case service.AuthErrPasswordResetDisabledByConfig().Is(err):
h.passwordResetDisabledAlert(req)
return nil
default:
h.Log.Error("unhandled error", zap.Error(err))
return err
}
}

View File

@@ -53,6 +53,8 @@ type (
SendEmailOTP(ctx context.Context) (err error)
ConfigureEmailOTP(ctx context.Context, userID uint64, enable bool) (u *types.User, err error)
ValidateEmailOTP(ctx context.Context, code string) (err error)
SendInviteEmail(ctx context.Context, email string) (err error)
ValidateInviteEmailToken(ctx context.Context, token string) (user *types.User, err error)
}
credentialsService interface {
@@ -143,6 +145,7 @@ const (
TmplRequestPasswordReset = "request-password-reset.html.tpl"
TmplPasswordResetRequested = "password-reset-requested.html.tpl"
TmplResetPassword = "reset-password.html.tpl"
TmplInvite = "invite.html.tpl"
TmplSecurity = "security.html.tpl"
TmplProfile = "profile.html.tpl"
TmplSessions = "sessions.html.tpl"

View File

@@ -20,6 +20,7 @@ type (
RequestPasswordReset,
PasswordResetRequested,
ResetPassword,
AcceptInvite,
Sessions,
AuthorizedClients,
Logout,
@@ -74,6 +75,7 @@ func GetLinks() Links {
RequestPasswordReset: b + "auth/request-password-reset",
PasswordResetRequested: b + "auth/password-reset-requested",
ResetPassword: b + "auth/reset-password",
AcceptInvite: b + "auth/accept-invite",
Sessions: b + "auth/sessions",
AuthorizedClients: b + "auth/authorized-clients",
Logout: b + "auth/logout",

View File

@@ -101,6 +101,8 @@ type (
sendEmailOTP func(context.Context) (err error)
configureEmailOTP func(context.Context, uint64, bool) (u *types.User, err error)
validateEmailOTP func(context.Context, string) (err error)
sendInviteEmail func(context.Context, string) (err error)
validateEmailToken func(context.Context, string) (user *types.User, err error)
}
)
@@ -198,6 +200,14 @@ func (s authServiceMocked) ValidateEmailOTP(ctx context.Context, code string) (e
return s.validateEmailOTP(ctx, code)
}
func (s authServiceMocked) SendInviteEmail(ctx context.Context, email string) (err error) {
return s.sendInviteEmail(ctx, email)
}
func (s authServiceMocked) ValidateInviteEmailToken(ctx context.Context, token string) (user *types.User, err error) {
return s.validateEmailToken(ctx, token)
}
func (s authServiceMocked) LoadRoleMemberships(ctx context.Context, u *types.User) error {
// no-op for now
return nil
@@ -283,6 +293,15 @@ func (ma mockAuthService) ValidateTOTP(ctx context.Context, code string) (err er
return
}
func (ma mockAuthService) SendInviteEmail(ctx context.Context, email string) (err error) {
err = nil
return
}
func (ma mockAuthService) ValidateInviteEmailToken(ctx context.Context, token string) (*types.User, error) {
return &types.User{ID: 123}, nil
}
// Mocking notification service
func (m mockNotificationService) EmailConfirmation(ctx context.Context, emailAddress string, token string) error {
return nil
@@ -300,6 +319,10 @@ func (m mockNotificationService) EmailOTP(ctx context.Context, emailAddress stri
return nil
}
func (m mockNotificationService) InviteEmail(ctx context.Context, emailAddress string, token string) error {
return nil
}
// Mocking gorilla session
func (ms mockSession) Get(r *http.Request, name string) (*sessions.Session, error) {
return ms.get(r, name)

View File

@@ -77,6 +77,8 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
r.Get(tbp(l.PasswordResetRequested), h.handle(h.onlyIfPasswordResetEnabled(anonyOnly(h.passwordResetRequested))))
r.Get(tbp(l.ResetPassword), h.handle(h.onlyIfPasswordResetEnabled(h.resetPasswordForm)))
r.Post(tbp(l.ResetPassword), h.handle(h.onlyIfPasswordResetEnabled(authOnly(h.resetPasswordProc))))
r.Get(tbp(l.AcceptInvite), h.handle(h.acceptInviteForm))
r.Post(tbp(l.AcceptInvite), h.handle(h.acceptInviteProc))
r.Get(tbp(l.Security), h.handle(authOnly(h.securityForm)))
r.Post(tbp(l.Security), h.handle(authOnly(h.securityProc)))

View File

@@ -14,6 +14,7 @@ type (
MultiFactor MultiFactor
BackgroundUI BackgroundUI
ProfileAvatarEnabled bool
SendUserInviteEmail bool
}
SAML struct {

View File

@@ -51,3 +51,21 @@ templates:
<p>Hello,</p>
<p>Enter this code into your login form: <code>{{.Code}}</code></p>
{{template "email_general_footer" .}}
auth_email_user_invite_subject:
type: text/plain
meta:
short: User invite subject
template: Invitation to join our platform
auth_email_user_invite_content:
type: text/html
meta:
short: User invite content
template: |-
{{template "email_general_header" .}}
<h2 style="color: #568ba2;text-align: center;">Invitation to join our platform</h2>
<p>Hello,</p>
<p>To get started, please follow <a href="{{ .URL }}" style="color:#568ba2;">this link</a> to accept the invitation and set up your password</p>
<p>Once you accept the invitation, you will be able to logged in to your account.</p>
{{template "email_general_footer" .}}

View File

@@ -10,6 +10,7 @@ import (
type (
serviceInitializer interface {
InitServices(ctx context.Context) error
Activate(ctx context.Context) error
}
)
@@ -18,3 +19,9 @@ func commandPreRunInitService(app serviceInitializer) func(*cobra.Command, []str
return app.InitServices(cli.Context())
}
}
func commandPreRunInitActivate(app serviceInitializer) func(*cobra.Command, []string) error {
return func(_ *cobra.Command, _ []string) error {
return app.Activate(cli.Context())
}
}

View File

@@ -24,6 +24,7 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
flagPassword string
flagMakePasswordLink bool
flagRoles []string
flagSendEmail bool
)
// User management commands.
@@ -93,7 +94,7 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
Short: "Add new user",
Args: cobra.MinimumNArgs(1),
PreRunE: commandPreRunInitService(app),
PreRunE: commandPreRunInitActivate(app),
Run: func(cmd *cobra.Command, args []string) {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
@@ -148,6 +149,12 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
}
}
if flagSendEmail {
if err = authSvc.SendInviteEmail(ctx, user.Email); err != nil {
cli.HandleError(err)
}
}
fmt.Fprintf(cmd.OutOrStdout(), "User created [%d].\n", user.ID)
if flagMakePasswordLink && len(url) > 0 {
@@ -195,6 +202,12 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
nil,
"Add user to roles (use ID or handle, repeat for multiple roles)")
addCmd.Flags().BoolVar(
&flagSendEmail,
"send-invite",
false,
"Send invite email to user with accept invite link")
pwdCmd := &cobra.Command{
Use: "password [email]",
Short: "Change password for user",

View File

@@ -56,7 +56,6 @@ var (
// setEmail updates authActionProps's email
//
// This function is auto-generated.
//
func (p *authActionProps) setEmail(email string) *authActionProps {
p.email = email
return p
@@ -65,7 +64,6 @@ func (p *authActionProps) setEmail(email string) *authActionProps {
// setProvider updates authActionProps's provider
//
// This function is auto-generated.
//
func (p *authActionProps) setProvider(provider string) *authActionProps {
p.provider = provider
return p
@@ -74,7 +72,6 @@ func (p *authActionProps) setProvider(provider string) *authActionProps {
// setCredentials updates authActionProps's credentials
//
// This function is auto-generated.
//
func (p *authActionProps) setCredentials(credentials *types.Credential) *authActionProps {
p.credentials = credentials
return p
@@ -83,7 +80,6 @@ func (p *authActionProps) setCredentials(credentials *types.Credential) *authAct
// setRole updates authActionProps's role
//
// This function is auto-generated.
//
func (p *authActionProps) setRole(role *types.Role) *authActionProps {
p.role = role
return p
@@ -92,7 +88,6 @@ func (p *authActionProps) setRole(role *types.Role) *authActionProps {
// setUser updates authActionProps's user
//
// This function is auto-generated.
//
func (p *authActionProps) setUser(user *types.User) *authActionProps {
p.user = user
return p
@@ -101,7 +96,6 @@ func (p *authActionProps) setUser(user *types.User) *authActionProps {
// Serialize converts authActionProps to actionlog.Meta
//
// This function is auto-generated.
//
func (p authActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
@@ -134,7 +128,6 @@ func (p authActionProps) Serialize() actionlog.Meta {
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
//
func (p authActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
@@ -221,7 +214,6 @@ func (p authActionProps) Format(in string, err error) string {
// String returns loggable description as string
//
// This function is auto-generated.
//
func (a *authAction) String() string {
var props = &authActionProps{}
@@ -249,7 +241,6 @@ func (e *authAction) ToAction() *actionlog.Action {
// AuthActionAuthenticate returns "system:auth.authenticate" action
//
// This function is auto-generated.
//
func AuthActionAuthenticate(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -269,7 +260,6 @@ func AuthActionAuthenticate(props ...*authActionProps) *authAction {
// AuthActionIssueToken returns "system:auth.issueToken" action
//
// This function is auto-generated.
//
func AuthActionIssueToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -289,7 +279,6 @@ func AuthActionIssueToken(props ...*authActionProps) *authAction {
// AuthActionValidateToken returns "system:auth.validateToken" action
//
// This function is auto-generated.
//
func AuthActionValidateToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -309,7 +298,6 @@ func AuthActionValidateToken(props ...*authActionProps) *authAction {
// AuthActionChangePassword returns "system:auth.changePassword" action
//
// This function is auto-generated.
//
func AuthActionChangePassword(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -329,7 +317,6 @@ func AuthActionChangePassword(props ...*authActionProps) *authAction {
// AuthActionInternalSignup returns "system:auth.internalSignup" action
//
// This function is auto-generated.
//
func AuthActionInternalSignup(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -349,7 +336,6 @@ func AuthActionInternalSignup(props ...*authActionProps) *authAction {
// AuthActionConfirmEmail returns "system:auth.confirmEmail" action
//
// This function is auto-generated.
//
func AuthActionConfirmEmail(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -369,7 +355,6 @@ func AuthActionConfirmEmail(props ...*authActionProps) *authAction {
// AuthActionExternalSignup returns "system:auth.externalSignup" action
//
// This function is auto-generated.
//
func AuthActionExternalSignup(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -389,7 +374,6 @@ func AuthActionExternalSignup(props ...*authActionProps) *authAction {
// AuthActionSendEmailConfirmationToken returns "system:auth.sendEmailConfirmationToken" action
//
// This function is auto-generated.
//
func AuthActionSendEmailConfirmationToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -409,7 +393,6 @@ func AuthActionSendEmailConfirmationToken(props ...*authActionProps) *authAction
// AuthActionSendPasswordResetToken returns "system:auth.sendPasswordResetToken" action
//
// This function is auto-generated.
//
func AuthActionSendPasswordResetToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -429,7 +412,6 @@ func AuthActionSendPasswordResetToken(props ...*authActionProps) *authAction {
// AuthActionExchangePasswordResetToken returns "system:auth.exchangePasswordResetToken" action
//
// This function is auto-generated.
//
func AuthActionExchangePasswordResetToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -449,7 +431,6 @@ func AuthActionExchangePasswordResetToken(props ...*authActionProps) *authAction
// AuthActionGeneratePasswordCreateToken returns "system:auth.generatePasswordCreateToken" action
//
// This function is auto-generated.
//
func AuthActionGeneratePasswordCreateToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -469,7 +450,6 @@ func AuthActionGeneratePasswordCreateToken(props ...*authActionProps) *authActio
// AuthActionAutoPromote returns "system:auth.autoPromote" action
//
// This function is auto-generated.
//
func AuthActionAutoPromote(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -489,7 +469,6 @@ func AuthActionAutoPromote(props ...*authActionProps) *authAction {
// AuthActionUpdateCredentials returns "system:auth.updateCredentials" action
//
// This function is auto-generated.
//
func AuthActionUpdateCredentials(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -509,7 +488,6 @@ func AuthActionUpdateCredentials(props ...*authActionProps) *authAction {
// AuthActionCreateCredentials returns "system:auth.createCredentials" action
//
// This function is auto-generated.
//
func AuthActionCreateCredentials(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -529,7 +507,6 @@ func AuthActionCreateCredentials(props ...*authActionProps) *authAction {
// AuthActionImpersonate returns "system:auth.impersonate" action
//
// This function is auto-generated.
//
func AuthActionImpersonate(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -549,7 +526,6 @@ func AuthActionImpersonate(props ...*authActionProps) *authAction {
// AuthActionTotpConfigure returns "system:auth.totpConfigure" action
//
// This function is auto-generated.
//
func AuthActionTotpConfigure(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -569,7 +545,6 @@ func AuthActionTotpConfigure(props ...*authActionProps) *authAction {
// AuthActionTotpRemove returns "system:auth.totpRemove" action
//
// This function is auto-generated.
//
func AuthActionTotpRemove(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -589,7 +564,6 @@ func AuthActionTotpRemove(props ...*authActionProps) *authAction {
// AuthActionTotpValidate returns "system:auth.totpValidate" action
//
// This function is auto-generated.
//
func AuthActionTotpValidate(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -609,7 +583,6 @@ func AuthActionTotpValidate(props ...*authActionProps) *authAction {
// AuthActionEmailOtpVerify returns "system:auth.emailOtpVerify" action
//
// This function is auto-generated.
//
func AuthActionEmailOtpVerify(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -629,7 +602,6 @@ func AuthActionEmailOtpVerify(props ...*authActionProps) *authAction {
// AuthActionAccessTokensRemoved returns "system:auth.accessTokensRemoved" action
//
// This function is auto-generated.
//
func AuthActionAccessTokensRemoved(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
@@ -646,15 +618,32 @@ func AuthActionAccessTokensRemoved(props ...*authActionProps) *authAction {
return a
}
// AuthActionSendInviteEMail returns "system:auth.sendInviteEMail" action
//
// This function is auto-generated.
func AuthActionSendInviteEMail(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "sendInviteEMail",
log: "invite email sent to {{email}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// AuthErrGeneric returns "system:auth.generic" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrGeneric(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -688,9 +677,7 @@ func AuthErrGeneric(mm ...*authActionProps) *errors.Error {
// AuthErrInvalidCredentials returns "system:auth.invalidCredentials" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidCredentials(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -724,9 +711,7 @@ func AuthErrInvalidCredentials(mm ...*authActionProps) *errors.Error {
// AuthErrInvalidEmailFormat returns "system:auth.invalidEmailFormat" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidEmailFormat(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -758,9 +743,7 @@ func AuthErrInvalidEmailFormat(mm ...*authActionProps) *errors.Error {
// AuthErrInvalidHandle returns "system:auth.invalidHandle" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidHandle(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -795,7 +778,6 @@ func AuthErrInvalidHandle(mm ...*authActionProps) *errors.Error {
// Note: This error will be wrapped with safe (system:auth.invalidCredentials) error!
//
// This function is auto-generated.
//
func AuthErrFailedForUnknownUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -835,7 +817,6 @@ func AuthErrFailedForUnknownUser(mm ...*authActionProps) *errors.Error {
// Note: This error will be wrapped with safe (system:auth.invalidCredentials) error!
//
// This function is auto-generated.
//
func AuthErrFailedForDeletedUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -875,7 +856,6 @@ func AuthErrFailedForDeletedUser(mm ...*authActionProps) *errors.Error {
// Note: This error will be wrapped with safe (system:auth.invalidCredentials) error!
//
// This function is auto-generated.
//
func AuthErrFailedForSuspendedUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -915,7 +895,6 @@ func AuthErrFailedForSuspendedUser(mm ...*authActionProps) *errors.Error {
// Note: This error will be wrapped with safe (system:auth.invalidCredentials) error!
//
// This function is auto-generated.
//
func AuthErrFailedForSystemUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -952,9 +931,7 @@ func AuthErrFailedForSystemUser(mm ...*authActionProps) *errors.Error {
// AuthErrFailedUnconfirmedEmail returns "system:auth.failedUnconfirmedEmail" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrFailedUnconfirmedEmail(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -988,9 +965,7 @@ func AuthErrFailedUnconfirmedEmail(mm ...*authActionProps) *errors.Error {
// AuthErrInternalLoginDisabledByConfig returns "system:auth.internalLoginDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInternalLoginDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1022,9 +997,7 @@ func AuthErrInternalLoginDisabledByConfig(mm ...*authActionProps) *errors.Error
// AuthErrInternalSignupDisabledByConfig returns "system:auth.internalSignupDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInternalSignupDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1056,9 +1029,7 @@ func AuthErrInternalSignupDisabledByConfig(mm ...*authActionProps) *errors.Error
// AuthErrPasswordChangeFailedForUnknownUser returns "system:auth.passwordChangeFailedForUnknownUser" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordChangeFailedForUnknownUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1090,9 +1061,7 @@ func AuthErrPasswordChangeFailedForUnknownUser(mm ...*authActionProps) *errors.E
// AuthErrPasswordResetFailedOldPasswordCheckFailed returns "system:auth.passwordResetFailedOldPasswordCheckFailed" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordResetFailedOldPasswordCheckFailed(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1124,9 +1093,7 @@ func AuthErrPasswordResetFailedOldPasswordCheckFailed(mm ...*authActionProps) *e
// AuthErrPasswordSetFailedReusedPasswordCheckFailed returns "system:auth.passwordSetFailedReusedPasswordCheckFailed" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordSetFailedReusedPasswordCheckFailed(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1158,9 +1125,7 @@ func AuthErrPasswordSetFailedReusedPasswordCheckFailed(mm ...*authActionProps) *
// AuthErrPasswordCreateFailedForUnknownUser returns "system:auth.passwordCreateFailedForUnknownUser" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordCreateFailedForUnknownUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1192,9 +1157,7 @@ func AuthErrPasswordCreateFailedForUnknownUser(mm ...*authActionProps) *errors.E
// AuthErrPasswordResetDisabledByConfig returns "system:auth.passwordResetDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordResetDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1226,9 +1189,7 @@ func AuthErrPasswordResetDisabledByConfig(mm ...*authActionProps) *errors.Error
// AuthErrPasswordCreateDisabledByConfig returns "system:auth.passwordCreateDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordCreateDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1260,9 +1221,7 @@ func AuthErrPasswordCreateDisabledByConfig(mm ...*authActionProps) *errors.Error
// AuthErrPasswordNotSecure returns "system:auth.passwordNotSecure" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordNotSecure(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1294,9 +1253,7 @@ func AuthErrPasswordNotSecure(mm ...*authActionProps) *errors.Error {
// AuthErrExternalDisabledByConfig returns "system:auth.externalDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrExternalDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1330,9 +1287,7 @@ func AuthErrExternalDisabledByConfig(mm ...*authActionProps) *errors.Error {
// AuthErrProfileWithoutValidEmail returns "system:auth.profileWithoutValidEmail" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrProfileWithoutValidEmail(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1366,9 +1321,7 @@ func AuthErrProfileWithoutValidEmail(mm ...*authActionProps) *errors.Error {
// AuthErrCredentialsLinkedToInvalidUser returns "system:auth.credentialsLinkedToInvalidUser" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrCredentialsLinkedToInvalidUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1400,9 +1353,7 @@ func AuthErrCredentialsLinkedToInvalidUser(mm ...*authActionProps) *errors.Error
// AuthErrInvalidToken returns "system:auth.invalidToken" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidToken(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1434,9 +1385,7 @@ func AuthErrInvalidToken(mm ...*authActionProps) *errors.Error {
// AuthErrNotAllowedToImpersonate returns "system:auth.notAllowedToImpersonate" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToImpersonate(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1468,9 +1417,7 @@ func AuthErrNotAllowedToImpersonate(mm ...*authActionProps) *errors.Error {
// AuthErrNotAllowedToRemoveTOTP returns "system:auth.notAllowedToRemoveTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToRemoveTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1502,9 +1449,7 @@ func AuthErrNotAllowedToRemoveTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrUnconfiguredTOTP returns "system:auth.unconfiguredTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrUnconfiguredTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1536,9 +1481,7 @@ func AuthErrUnconfiguredTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrNotAllowedToConfigureTOTP returns "system:auth.notAllowedToConfigureTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrNotAllowedToConfigureTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1570,9 +1513,7 @@ func AuthErrNotAllowedToConfigureTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrEnforcedMFAWithTOTP returns "system:auth.enforcedMFAWithTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrEnforcedMFAWithTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1604,9 +1545,7 @@ func AuthErrEnforcedMFAWithTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrInvalidTOTP returns "system:auth.invalidTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1638,9 +1577,7 @@ func AuthErrInvalidTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrDisabledMFAWithTOTP returns "system:auth.disabledMFAWithTOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrDisabledMFAWithTOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1672,9 +1609,7 @@ func AuthErrDisabledMFAWithTOTP(mm ...*authActionProps) *errors.Error {
// AuthErrDisabledMFAWithEmailOTP returns "system:auth.disabledMFAWithEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrDisabledMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1706,9 +1641,7 @@ func AuthErrDisabledMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
// AuthErrEnforcedMFAWithEmailOTP returns "system:auth.enforcedMFAWithEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrEnforcedMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1740,9 +1673,7 @@ func AuthErrEnforcedMFAWithEmailOTP(mm ...*authActionProps) *errors.Error {
// AuthErrInvalidEmailOTP returns "system:auth.invalidEmailOTP" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrInvalidEmailOTP(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1774,9 +1705,7 @@ func AuthErrInvalidEmailOTP(mm ...*authActionProps) *errors.Error {
// AuthErrRateLimitExceeded returns "system:auth.rateLimitExceeded" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrRateLimitExceeded(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1810,9 +1739,7 @@ func AuthErrRateLimitExceeded(mm ...*authActionProps) *errors.Error {
// AuthErrMaxUserLimitReached returns "system:auth.maxUserLimitReached" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrMaxUserLimitReached(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
@@ -1842,6 +1769,38 @@ func AuthErrMaxUserLimitReached(mm ...*authActionProps) *errors.Error {
return e
}
// AuthErrDisabledSendUserInviteEmail returns "system:auth.disabledSendUserInviteEmail" as *errors.Error
//
// This function is auto-generated.
func AuthErrDisabledSendUserInviteEmail(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("sending user invite email is disabled", nil),
errors.Meta("type", "disabledSendUserInviteEmail"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.disabledSendUserInviteEmail"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
@@ -1850,7 +1809,6 @@ func AuthErrMaxUserLimitReached(mm ...*authActionProps) *errors.Error {
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
//
func (svc auth) recordAction(ctx context.Context, props *authActionProps, actionFn func(...*authActionProps) *authAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is

View File

@@ -85,6 +85,9 @@ actions:
- action: accessTokensRemoved
log: "access tokens for {{user}} removed"
- action: sendInviteEMail
log: "invite email sent to {{email}}"
errors:
- error: invalidCredentials
message: "invalid username and password combination"
@@ -213,3 +216,7 @@ errors:
- error: maxUserLimitReached
message: "you have reached your user limit, contact your Corteza administrator"
severity: warning
- error: disabledSendUserInviteEmail
message: "sending user invite email is disabled"
severity: warning

View File

@@ -32,6 +32,7 @@ const (
credentialsTypeCreatePasswordToken = "password-create-token"
credentialsTypeMfaTotpSecret = "mfa-totp-secret"
credentialsTypeMFAEmailOTP = "mfa-email-otp"
credentialsTypeInviteEmailToken = "invite-email-token"
credentialsTokenLength = 32
@@ -40,6 +41,10 @@ const (
passwordMinLength = 8
passwordMaxLength = 256
passwordTokenValidity = 24
emailOTPValidity = 60
inviteTokenValidity = 72
)
var (
@@ -211,7 +216,7 @@ func (svc *auth) createUserToken(ctx context.Context, u *types.User, kind string
case credentialsTypeMFAEmailOTP:
expSec := svc.settings.Auth.MultiFactor.EmailOTP.Expires
if expSec == 0 {
expSec = 60
expSec = emailOTPValidity
}
expiresAt = now().Add(time.Second * time.Duration(expSec))
@@ -221,13 +226,22 @@ func (svc *auth) createUserToken(ctx context.Context, u *types.User, kind string
case credentialsTypeCreatePasswordToken:
expSec := svc.settings.Auth.Internal.PasswordCreate.Expires
if expSec == 0 {
expSec = 24
expSec = passwordTokenValidity
}
expiresAt = now().Add(time.Hour * time.Duration(expSec))
// random password string, "3i[g0|)z"
token = fmt.Sprintf("%s", rand.Password(credentialsTokenLength))
case credentialsTypeInviteEmailToken:
expSec := svc.settings.Auth.Internal.SendUserInviteEmail.Expires
if expSec == 0 {
expSec = inviteTokenValidity
}
// set the expiration time for all invited user tokens sent via email to 72 hours.
expiresAt = now().Add(time.Hour * time.Duration(expSec))
token = string(rand.Bytes(credentialsTokenLength))
default:
// 1h expiration for all tokens send via email
expiresAt = now().Add(time.Minute * 60)
@@ -423,6 +437,10 @@ func (svc *auth) ValidatePasswordCreateToken(ctx context.Context, token string)
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeCreatePasswordToken)
}
func (svc *auth) ValidateInviteEmailToken(ctx context.Context, token string) (user *types.User, err error) {
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeInviteEmailToken)
}
// ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info
func (svc *auth) ExchangePasswordResetToken(ctx context.Context, token string) (u *types.User, t string, err error) {
var (
@@ -1184,3 +1202,40 @@ func checkPasswordStrength(password string, pc types.PasswordConstraints) bool {
return true
}
func (svc *auth) SendInviteEmail(ctx context.Context, email string) (err error) {
var (
u *types.User
aam = &authActionProps{
user: u,
email: email,
}
)
err = func() error {
if !svc.settings.Auth.Internal.SendUserInviteEmail.Enabled {
return AuthErrDisabledSendUserInviteEmail(aam)
}
if u, err = store.LookupUserByEmail(ctx, svc.store, email); err != nil {
return err
}
ctx = internalAuth.SetIdentityToContext(ctx, u)
token, err := svc.createUserToken(ctx, u, credentialsTypeInviteEmailToken)
if err != nil {
return err
}
err = svc.notifications.InviteEmail(ctx, u.Email, token)
if err != nil {
return err
}
return nil
}()
return svc.recordAction(ctx, aam, AuthActionSendInviteEMail, err)
}

View File

@@ -30,6 +30,7 @@ type (
EmailConfirmation(ctx context.Context, emailAddress string, url string) error
PasswordReset(ctx context.Context, emailAddress string, url string) error
PasswordCreate(url string) (string, error)
InviteEmail(ctx context.Context, emailAddress string, token string) error
}
)
@@ -68,6 +69,12 @@ func (svc authNotification) PasswordCreate(token string) (string, error) {
return fmt.Sprintf("%s/create-password?token=%s", svc.opt.BaseURL, url.QueryEscape(token)), nil
}
func (svc authNotification) InviteEmail(ctx context.Context, emailAddress string, token string) error {
return svc.send(ctx, "auth_email_user_invite", emailAddress, map[string]interface{}{
"URL": fmt.Sprintf("%s/accept-invite?token=%s", svc.opt.BaseURL, url.QueryEscape(token)),
})
}
func (svc authNotification) newMail() *gomail.Message {
var (
m = mail.New()

View File

@@ -58,6 +58,12 @@ type (
// Can users reset their passwords
PasswordReset struct{ Enabled bool } `json:"-" kv:"password-reset"`
// When enabled, users added via CLI will receive an email with a link to reset their password.
SendUserInviteEmail struct {
Enabled bool
Expires uint
} `kv:"send-user-invite-email" json:"sendUserInviteEmail"`
// PasswordCreate setting for create password for user via generated link with token
// If user has no password then link redirects to create password page
// Otherwise it redirects to profile page of that user