supabase-cli/pkg/config/auth.go

1098 lines
38 KiB
Go

package config
import (
"strconv"
"strings"
"time"
"github.com/go-errors/errors"
v1API "github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/diff"
)
type PasswordRequirements string
const (
NoRequirements PasswordRequirements = ""
LettersDigits PasswordRequirements = "letters_digits"
LowerUpperLettersDigits PasswordRequirements = "lower_upper_letters_digits"
LowerUpperLettersDigitsSymbols PasswordRequirements = "lower_upper_letters_digits_symbols"
)
func (r *PasswordRequirements) UnmarshalText(text []byte) error {
allowed := []PasswordRequirements{NoRequirements, LettersDigits, LowerUpperLettersDigits, LowerUpperLettersDigitsSymbols}
if *r = PasswordRequirements(text); !sliceContains(allowed, *r) {
return errors.Errorf("must be one of %v", allowed)
}
return nil
}
func (r PasswordRequirements) ToChar() v1API.UpdateAuthConfigBodyPasswordRequiredCharacters {
switch r {
case LettersDigits:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
case LowerUpperLettersDigits:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891
case LowerUpperLettersDigitsSymbols:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892
}
return v1API.Empty
}
func NewPasswordRequirement(c v1API.UpdateAuthConfigBodyPasswordRequiredCharacters) PasswordRequirements {
switch c {
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:
return LettersDigits
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891:
return LowerUpperLettersDigits
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892:
return LowerUpperLettersDigitsSymbols
}
return NoRequirements
}
type CaptchaProvider string
const (
HCaptchaProvider CaptchaProvider = "hcaptcha"
TurnstileProvider CaptchaProvider = "turnstile"
)
func (p *CaptchaProvider) UnmarshalText(text []byte) error {
allowed := []CaptchaProvider{HCaptchaProvider, TurnstileProvider}
if *p = CaptchaProvider(text); !sliceContains(allowed, *p) {
return errors.Errorf("must be one of %v", allowed)
}
return nil
}
type (
auth struct {
Enabled bool `toml:"enabled"`
Image string `toml:"-"`
SiteUrl string `toml:"site_url" mapstructure:"site_url"`
AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
JwtExpiry uint `toml:"jwt_expiry"`
EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval"`
EnableManualLinking bool `toml:"enable_manual_linking"`
EnableSignup bool `toml:"enable_signup"`
EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins"`
MinimumPasswordLength uint `toml:"minimum_password_length"`
PasswordRequirements PasswordRequirements `toml:"password_requirements"`
Captcha *captcha `toml:"captcha"`
Hook hook `toml:"hook"`
MFA mfa `toml:"mfa"`
Sessions sessions `toml:"sessions"`
Email email `toml:"email"`
Sms sms `toml:"sms"`
External external `toml:"external"`
// Custom secrets can be injected from .env file
JwtSecret string `toml:"-" mapstructure:"jwt_secret"`
AnonKey string `toml:"-" mapstructure:"anon_key"`
ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
ThirdParty thirdParty `toml:"third_party"`
}
external map[string]provider
thirdParty struct {
Firebase tpaFirebase `toml:"firebase"`
Auth0 tpaAuth0 `toml:"auth0"`
Cognito tpaCognito `toml:"aws_cognito"`
}
tpaFirebase struct {
Enabled bool `toml:"enabled"`
ProjectID string `toml:"project_id"`
}
tpaAuth0 struct {
Enabled bool `toml:"enabled"`
Tenant string `toml:"tenant"`
TenantRegion string `toml:"tenant_region"`
}
tpaCognito struct {
Enabled bool `toml:"enabled"`
UserPoolID string `toml:"user_pool_id"`
UserPoolRegion string `toml:"user_pool_region"`
}
email struct {
EnableSignup bool `toml:"enable_signup"`
DoubleConfirmChanges bool `toml:"double_confirm_changes"`
EnableConfirmations bool `toml:"enable_confirmations"`
SecurePasswordChange bool `toml:"secure_password_change"`
Template map[string]emailTemplate `toml:"template"`
Smtp *smtp `toml:"smtp"`
MaxFrequency time.Duration `toml:"max_frequency"`
OtpLength uint `toml:"otp_length"`
OtpExpiry uint `toml:"otp_expiry"`
}
smtp struct {
Enabled bool `toml:"enabled"`
Host string `toml:"host"`
Port uint16 `toml:"port"`
User string `toml:"user"`
Pass Secret `toml:"pass"`
AdminEmail string `toml:"admin_email"`
SenderName string `toml:"sender_name"`
}
emailTemplate struct {
Subject *string `toml:"subject"`
Content *string `toml:"content"`
// Only content path is accepted in config.toml
ContentPath string `toml:"content_path"`
}
sms struct {
EnableSignup bool `toml:"enable_signup"`
EnableConfirmations bool `toml:"enable_confirmations"`
Template string `toml:"template"`
Twilio twilioConfig `toml:"twilio" mapstructure:"twilio"`
TwilioVerify twilioConfig `toml:"twilio_verify" mapstructure:"twilio_verify"`
Messagebird messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"`
Textlocal textlocalConfig `toml:"textlocal" mapstructure:"textlocal"`
Vonage vonageConfig `toml:"vonage" mapstructure:"vonage"`
TestOTP map[string]string `toml:"test_otp"`
MaxFrequency time.Duration `toml:"max_frequency"`
}
captcha struct {
Enabled bool `toml:"enabled"`
Provider CaptchaProvider `toml:"provider"`
Secret Secret `toml:"secret"`
}
hook struct {
MFAVerificationAttempt *hookConfig `toml:"mfa_verification_attempt"`
PasswordVerificationAttempt *hookConfig `toml:"password_verification_attempt"`
CustomAccessToken *hookConfig `toml:"custom_access_token"`
SendSMS *hookConfig `toml:"send_sms"`
SendEmail *hookConfig `toml:"send_email"`
}
factorTypeConfiguration struct {
EnrollEnabled bool `toml:"enroll_enabled"`
VerifyEnabled bool `toml:"verify_enabled"`
}
phoneFactorTypeConfiguration struct {
factorTypeConfiguration
OtpLength uint `toml:"otp_length"`
Template string `toml:"template"`
MaxFrequency time.Duration `toml:"max_frequency"`
}
mfa struct {
TOTP factorTypeConfiguration `toml:"totp"`
Phone phoneFactorTypeConfiguration `toml:"phone"`
WebAuthn factorTypeConfiguration `toml:"web_authn"`
MaxEnrolledFactors uint `toml:"max_enrolled_factors"`
}
hookConfig struct {
Enabled bool `toml:"enabled"`
URI string `toml:"uri"`
Secrets Secret `toml:"secrets"`
}
sessions struct {
Timebox time.Duration `toml:"timebox"`
InactivityTimeout time.Duration `toml:"inactivity_timeout"`
}
twilioConfig struct {
Enabled bool `toml:"enabled"`
AccountSid string `toml:"account_sid"`
MessageServiceSid string `toml:"message_service_sid"`
AuthToken Secret `toml:"auth_token" mapstructure:"auth_token"`
}
messagebirdConfig struct {
Enabled bool `toml:"enabled"`
Originator string `toml:"originator"`
AccessKey Secret `toml:"access_key" mapstructure:"access_key"`
}
textlocalConfig struct {
Enabled bool `toml:"enabled"`
Sender string `toml:"sender"`
ApiKey Secret `toml:"api_key" mapstructure:"api_key"`
}
vonageConfig struct {
Enabled bool `toml:"enabled"`
From string `toml:"from"`
ApiKey string `toml:"api_key" mapstructure:"api_key"`
ApiSecret Secret `toml:"api_secret" mapstructure:"api_secret"`
}
provider struct {
Enabled bool `toml:"enabled"`
ClientId string `toml:"client_id"`
Secret Secret `toml:"secret"`
Url string `toml:"url"`
RedirectUri string `toml:"redirect_uri"`
SkipNonceCheck bool `toml:"skip_nonce_check"`
}
)
func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody {
body := v1API.UpdateAuthConfigBody{
SiteUrl: &a.SiteUrl,
UriAllowList: cast.Ptr(strings.Join(a.AdditionalRedirectUrls, ",")),
JwtExp: cast.UintToIntPtr(&a.JwtExpiry),
RefreshTokenRotationEnabled: &a.EnableRefreshTokenRotation,
SecurityRefreshTokenReuseInterval: cast.UintToIntPtr(&a.RefreshTokenReuseInterval),
SecurityManualLinkingEnabled: &a.EnableManualLinking,
DisableSignup: cast.Ptr(!a.EnableSignup),
ExternalAnonymousUsersEnabled: &a.EnableAnonymousSignIns,
PasswordMinLength: cast.UintToIntPtr(&a.MinimumPasswordLength),
PasswordRequiredCharacters: cast.Ptr(a.PasswordRequirements.ToChar()),
}
// When local config is not set, we assume platform defaults should not change
if a.Captcha != nil {
a.Captcha.toAuthConfigBody(&body)
}
a.Hook.toAuthConfigBody(&body)
a.MFA.toAuthConfigBody(&body)
a.Sessions.toAuthConfigBody(&body)
a.Email.toAuthConfigBody(&body)
a.Sms.toAuthConfigBody(&body)
a.External.toAuthConfigBody(&body)
return body
}
func (a *auth) FromRemoteAuthConfig(remoteConfig v1API.AuthConfigResponse) {
a.SiteUrl = cast.Val(remoteConfig.SiteUrl, "")
a.AdditionalRedirectUrls = strToArr(cast.Val(remoteConfig.UriAllowList, ""))
a.JwtExpiry = cast.IntToUint(cast.Val(remoteConfig.JwtExp, 0))
a.EnableRefreshTokenRotation = cast.Val(remoteConfig.RefreshTokenRotationEnabled, false)
a.RefreshTokenReuseInterval = cast.IntToUint(cast.Val(remoteConfig.SecurityRefreshTokenReuseInterval, 0))
a.EnableManualLinking = cast.Val(remoteConfig.SecurityManualLinkingEnabled, false)
a.EnableSignup = !cast.Val(remoteConfig.DisableSignup, false)
a.EnableAnonymousSignIns = cast.Val(remoteConfig.ExternalAnonymousUsersEnabled, false)
a.MinimumPasswordLength = cast.IntToUint(cast.Val(remoteConfig.PasswordMinLength, 0))
prc := cast.Val(remoteConfig.PasswordRequiredCharacters, "")
a.PasswordRequirements = NewPasswordRequirement(v1API.UpdateAuthConfigBodyPasswordRequiredCharacters(prc))
a.Captcha.fromAuthConfig(remoteConfig)
a.Hook.fromAuthConfig(remoteConfig)
a.MFA.fromAuthConfig(remoteConfig)
a.Sessions.fromAuthConfig(remoteConfig)
a.Email.fromAuthConfig(remoteConfig)
a.Sms.fromAuthConfig(remoteConfig)
a.External.fromAuthConfig(remoteConfig)
}
func (c captcha) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
if body.SecurityCaptchaEnabled = &c.Enabled; c.Enabled {
body.SecurityCaptchaProvider = cast.Ptr(string(c.Provider))
if len(c.Secret.SHA256) > 0 {
body.SecurityCaptchaSecret = &c.Secret.Value
}
}
}
func (c *captcha) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
// When local config is not set, we assume platform defaults should not change
if c == nil {
return
}
// Ignore disabled captcha fields to minimise config diff
if c.Enabled {
c.Provider = CaptchaProvider(cast.Val(remoteConfig.SecurityCaptchaProvider, ""))
if len(c.Secret.SHA256) > 0 {
c.Secret.SHA256 = cast.Val(remoteConfig.SecurityCaptchaSecret, "")
}
}
c.Enabled = cast.Val(remoteConfig.SecurityCaptchaEnabled, false)
}
func (h hook) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
// When local config is not set, we assume platform defaults should not change
if hook := h.CustomAccessToken; hook != nil {
if body.HookCustomAccessTokenEnabled = &hook.Enabled; hook.Enabled {
body.HookCustomAccessTokenUri = &hook.URI
if len(hook.Secrets.SHA256) > 0 {
body.HookCustomAccessTokenSecrets = &hook.Secrets.Value
}
}
}
if hook := h.SendEmail; hook != nil {
if body.HookSendEmailEnabled = &hook.Enabled; hook.Enabled {
body.HookSendEmailUri = &hook.URI
if len(hook.Secrets.SHA256) > 0 {
body.HookSendEmailSecrets = &hook.Secrets.Value
}
}
}
if hook := h.SendSMS; hook != nil {
if body.HookSendSmsEnabled = &hook.Enabled; hook.Enabled {
body.HookSendSmsUri = &hook.URI
if len(hook.Secrets.SHA256) > 0 {
body.HookSendSmsSecrets = &hook.Secrets.Value
}
}
}
// Enterprise and team only features
if hook := h.MFAVerificationAttempt; hook != nil {
if body.HookMfaVerificationAttemptEnabled = &hook.Enabled; hook.Enabled {
body.HookMfaVerificationAttemptUri = &hook.URI
if len(hook.Secrets.SHA256) > 0 {
body.HookMfaVerificationAttemptSecrets = &hook.Secrets.Value
}
}
}
if hook := h.PasswordVerificationAttempt; hook != nil {
if body.HookPasswordVerificationAttemptEnabled = &hook.Enabled; hook.Enabled {
body.HookPasswordVerificationAttemptUri = &hook.URI
if len(hook.Secrets.SHA256) > 0 {
body.HookPasswordVerificationAttemptSecrets = &hook.Secrets.Value
}
}
}
}
func (h *hook) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
// When local config is not set, we assume platform defaults should not change
if hook := h.CustomAccessToken; hook != nil {
// Ignore disabled hooks because their envs are not loaded
if hook.Enabled {
hook.URI = cast.Val(remoteConfig.HookCustomAccessTokenUri, "")
if len(hook.Secrets.SHA256) > 0 {
hook.Secrets.SHA256 = cast.Val(remoteConfig.HookCustomAccessTokenSecrets, "")
}
}
hook.Enabled = cast.Val(remoteConfig.HookCustomAccessTokenEnabled, false)
}
if hook := h.SendEmail; hook != nil {
if hook.Enabled {
hook.URI = cast.Val(remoteConfig.HookSendEmailUri, "")
if len(hook.Secrets.SHA256) > 0 {
hook.Secrets.SHA256 = cast.Val(remoteConfig.HookSendEmailSecrets, "")
}
}
hook.Enabled = cast.Val(remoteConfig.HookSendEmailEnabled, false)
}
if hook := h.SendSMS; hook != nil {
if hook.Enabled {
hook.URI = cast.Val(remoteConfig.HookSendSmsUri, "")
if len(hook.Secrets.SHA256) > 0 {
hook.Secrets.SHA256 = cast.Val(remoteConfig.HookSendSmsSecrets, "")
}
}
hook.Enabled = cast.Val(remoteConfig.HookSendSmsEnabled, false)
}
// Enterprise and team only features
if hook := h.MFAVerificationAttempt; hook != nil {
if hook.Enabled {
hook.URI = cast.Val(remoteConfig.HookMfaVerificationAttemptUri, "")
if len(hook.Secrets.SHA256) > 0 {
hook.Secrets.SHA256 = cast.Val(remoteConfig.HookMfaVerificationAttemptSecrets, "")
}
}
hook.Enabled = cast.Val(remoteConfig.HookMfaVerificationAttemptEnabled, false)
}
if hook := h.PasswordVerificationAttempt; hook != nil {
if hook.Enabled {
hook.URI = cast.Val(remoteConfig.HookPasswordVerificationAttemptUri, "")
if len(hook.Secrets.SHA256) > 0 {
hook.Secrets.SHA256 = cast.Val(remoteConfig.HookPasswordVerificationAttemptSecrets, "")
}
}
hook.Enabled = cast.Val(remoteConfig.HookPasswordVerificationAttemptEnabled, false)
}
}
func (m mfa) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
body.MfaMaxEnrolledFactors = cast.UintToIntPtr(&m.MaxEnrolledFactors)
body.MfaTotpEnrollEnabled = &m.TOTP.EnrollEnabled
body.MfaTotpVerifyEnabled = &m.TOTP.VerifyEnabled
body.MfaPhoneEnrollEnabled = &m.Phone.EnrollEnabled
body.MfaPhoneVerifyEnabled = &m.Phone.VerifyEnabled
body.MfaPhoneOtpLength = cast.UintToIntPtr(&m.Phone.OtpLength)
body.MfaPhoneTemplate = &m.Phone.Template
body.MfaPhoneMaxFrequency = cast.Ptr(int(m.Phone.MaxFrequency.Seconds()))
body.MfaWebAuthnEnrollEnabled = &m.WebAuthn.EnrollEnabled
body.MfaWebAuthnVerifyEnabled = &m.WebAuthn.VerifyEnabled
}
func (m *mfa) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
m.MaxEnrolledFactors = cast.IntToUint(cast.Val(remoteConfig.MfaMaxEnrolledFactors, 0))
m.TOTP.EnrollEnabled = cast.Val(remoteConfig.MfaTotpEnrollEnabled, false)
m.TOTP.VerifyEnabled = cast.Val(remoteConfig.MfaTotpVerifyEnabled, false)
m.Phone.EnrollEnabled = cast.Val(remoteConfig.MfaPhoneEnrollEnabled, false)
m.Phone.VerifyEnabled = cast.Val(remoteConfig.MfaPhoneVerifyEnabled, false)
m.Phone.OtpLength = cast.IntToUint(remoteConfig.MfaPhoneOtpLength)
m.Phone.Template = cast.Val(remoteConfig.MfaPhoneTemplate, "")
m.Phone.MaxFrequency = time.Duration(cast.Val(remoteConfig.MfaPhoneMaxFrequency, 0)) * time.Second
m.WebAuthn.EnrollEnabled = cast.Val(remoteConfig.MfaWebAuthnEnrollEnabled, false)
m.WebAuthn.VerifyEnabled = cast.Val(remoteConfig.MfaWebAuthnVerifyEnabled, false)
}
func (s sessions) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
body.SessionsTimebox = cast.Ptr(int(s.Timebox.Seconds()))
body.SessionsInactivityTimeout = cast.Ptr(int(s.InactivityTimeout.Seconds()))
}
func (s *sessions) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
s.Timebox = time.Duration(cast.Val(remoteConfig.SessionsTimebox, 0)) * time.Second
s.InactivityTimeout = time.Duration(cast.Val(remoteConfig.SessionsInactivityTimeout, 0)) * time.Second
}
func (e email) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
body.ExternalEmailEnabled = &e.EnableSignup
body.MailerSecureEmailChangeEnabled = &e.DoubleConfirmChanges
body.MailerAutoconfirm = cast.Ptr(!e.EnableConfirmations)
body.MailerOtpLength = cast.UintToIntPtr(&e.OtpLength)
body.MailerOtpExp = cast.UintToIntPtr(&e.OtpExpiry)
body.SecurityUpdatePasswordRequireReauthentication = &e.SecurePasswordChange
body.SmtpMaxFrequency = cast.Ptr(int(e.MaxFrequency.Seconds()))
// When local config is not set, we assume platform defaults should not change
if e.Smtp != nil {
e.Smtp.toAuthConfigBody(body)
}
if len(e.Template) == 0 {
return
}
var tmpl *emailTemplate
tmpl = cast.Ptr(e.Template["invite"])
body.MailerSubjectsInvite = tmpl.Subject
body.MailerTemplatesInviteContent = tmpl.Content
tmpl = cast.Ptr(e.Template["confirmation"])
body.MailerSubjectsConfirmation = tmpl.Subject
body.MailerTemplatesConfirmationContent = tmpl.Content
tmpl = cast.Ptr(e.Template["recovery"])
body.MailerSubjectsRecovery = tmpl.Subject
body.MailerTemplatesRecoveryContent = tmpl.Content
tmpl = cast.Ptr(e.Template["magic_link"])
body.MailerSubjectsMagicLink = tmpl.Subject
body.MailerTemplatesMagicLinkContent = tmpl.Content
tmpl = cast.Ptr(e.Template["email_change"])
body.MailerSubjectsEmailChange = tmpl.Subject
body.MailerTemplatesEmailChangeContent = tmpl.Content
tmpl = cast.Ptr(e.Template["reauthentication"])
body.MailerSubjectsReauthentication = tmpl.Subject
body.MailerTemplatesReauthenticationContent = tmpl.Content
}
func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
e.EnableSignup = cast.Val(remoteConfig.ExternalEmailEnabled, false)
e.DoubleConfirmChanges = cast.Val(remoteConfig.MailerSecureEmailChangeEnabled, false)
e.EnableConfirmations = !cast.Val(remoteConfig.MailerAutoconfirm, false)
e.OtpLength = cast.IntToUint(cast.Val(remoteConfig.MailerOtpLength, 0))
e.OtpExpiry = cast.IntToUint(remoteConfig.MailerOtpExp)
e.SecurePasswordChange = cast.Val(remoteConfig.SecurityUpdatePasswordRequireReauthentication, false)
e.MaxFrequency = time.Duration(cast.Val(remoteConfig.SmtpMaxFrequency, 0)) * time.Second
e.Smtp.fromAuthConfig(remoteConfig)
if len(e.Template) == 0 {
return
}
if t, ok := e.Template["invite"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsInvite
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesInviteContent
}
e.Template["invite"] = t
}
if t, ok := e.Template["confirmation"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsConfirmation
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesConfirmationContent
}
e.Template["confirmation"] = t
}
if t, ok := e.Template["recovery"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsRecovery
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesRecoveryContent
}
e.Template["recovery"] = t
}
if t, ok := e.Template["magic_link"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsMagicLink
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesMagicLinkContent
}
e.Template["magic_link"] = t
}
if t, ok := e.Template["email_change"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsEmailChange
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesEmailChangeContent
}
e.Template["email_change"] = t
}
if t, ok := e.Template["reauthentication"]; ok {
if t.Subject != nil {
t.Subject = remoteConfig.MailerSubjectsReauthentication
}
if t.Content != nil {
t.Content = remoteConfig.MailerTemplatesReauthenticationContent
}
e.Template["reauthentication"] = t
}
}
func (s smtp) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
if !s.Enabled {
// Setting a single empty string disables SMTP
body.SmtpHost = cast.Ptr("")
return
}
body.SmtpHost = &s.Host
body.SmtpPort = cast.Ptr(strconv.Itoa(int(s.Port)))
body.SmtpUser = &s.User
if len(s.Pass.SHA256) > 0 {
body.SmtpPass = &s.Pass.Value
}
body.SmtpAdminEmail = &s.AdminEmail
body.SmtpSenderName = &s.SenderName
}
func (s *smtp) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
// When local config is not set, we assume platform defaults should not change
if s == nil {
return
}
if s.Enabled {
s.Host = cast.Val(remoteConfig.SmtpHost, "")
s.User = cast.Val(remoteConfig.SmtpUser, "")
if len(s.Pass.SHA256) > 0 {
s.Pass.SHA256 = cast.Val(remoteConfig.SmtpPass, "")
}
s.AdminEmail = cast.Val(remoteConfig.SmtpAdminEmail, "")
s.SenderName = cast.Val(remoteConfig.SmtpSenderName, "")
portStr := cast.Val(remoteConfig.SmtpPort, "0")
if port, err := strconv.ParseUint(portStr, 10, 16); err == nil {
s.Port = uint16(port)
}
}
// Api resets all values when SMTP is disabled
s.Enabled = remoteConfig.SmtpHost != nil
}
func (s sms) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
body.ExternalPhoneEnabled = &s.EnableSignup
body.SmsMaxFrequency = cast.Ptr(int(s.MaxFrequency.Seconds()))
body.SmsAutoconfirm = &s.EnableConfirmations
body.SmsTemplate = &s.Template
if otpString := mapToEnv(s.TestOTP); len(otpString) > 0 {
body.SmsTestOtp = &otpString
// Set a 10 year validity for test OTP
timestamp := time.Now().UTC().AddDate(10, 0, 0).Format(time.RFC3339)
body.SmsTestOtpValidUntil = &timestamp
}
// Api only overrides configs of enabled providers
switch {
case s.Twilio.Enabled:
body.SmsProvider = cast.Ptr("twilio")
if len(s.Twilio.AuthToken.SHA256) > 0 {
body.SmsTwilioAuthToken = &s.Twilio.AuthToken.Value
}
body.SmsTwilioAccountSid = &s.Twilio.AccountSid
body.SmsTwilioMessageServiceSid = &s.Twilio.MessageServiceSid
case s.TwilioVerify.Enabled:
body.SmsProvider = cast.Ptr("twilio_verify")
if len(s.TwilioVerify.AuthToken.SHA256) > 0 {
body.SmsTwilioVerifyAuthToken = &s.TwilioVerify.AuthToken.Value
}
body.SmsTwilioVerifyAccountSid = &s.TwilioVerify.AccountSid
body.SmsTwilioVerifyMessageServiceSid = &s.TwilioVerify.MessageServiceSid
case s.Messagebird.Enabled:
body.SmsProvider = cast.Ptr("messagebird")
if len(s.Messagebird.AccessKey.SHA256) > 0 {
body.SmsMessagebirdAccessKey = &s.Messagebird.AccessKey.Value
}
body.SmsMessagebirdOriginator = &s.Messagebird.Originator
case s.Textlocal.Enabled:
body.SmsProvider = cast.Ptr("textlocal")
if len(s.Textlocal.ApiKey.SHA256) > 0 {
body.SmsTextlocalApiKey = &s.Textlocal.ApiKey.Value
}
body.SmsTextlocalSender = &s.Textlocal.Sender
case s.Vonage.Enabled:
body.SmsProvider = cast.Ptr("vonage")
if len(s.Vonage.ApiSecret.SHA256) > 0 {
body.SmsVonageApiSecret = &s.Vonage.ApiSecret.Value
}
body.SmsVonageApiKey = &s.Vonage.ApiKey
body.SmsVonageFrom = &s.Vonage.From
}
}
func (s *sms) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
s.EnableSignup = cast.Val(remoteConfig.ExternalPhoneEnabled, false)
s.MaxFrequency = time.Duration(cast.Val(remoteConfig.SmsMaxFrequency, 0)) * time.Second
s.EnableConfirmations = cast.Val(remoteConfig.SmsAutoconfirm, false)
s.Template = cast.Val(remoteConfig.SmsTemplate, "")
s.TestOTP = envToMap(cast.Val(remoteConfig.SmsTestOtp, ""))
// We are only interested in the provider that's enabled locally
switch {
case s.Twilio.Enabled:
if len(s.Twilio.AuthToken.SHA256) > 0 {
s.Twilio.AuthToken.SHA256 = cast.Val(remoteConfig.SmsTwilioAuthToken, "")
}
s.Twilio.AccountSid = cast.Val(remoteConfig.SmsTwilioAccountSid, "")
s.Twilio.MessageServiceSid = cast.Val(remoteConfig.SmsTwilioMessageServiceSid, "")
case s.TwilioVerify.Enabled:
if len(s.TwilioVerify.AuthToken.SHA256) > 0 {
s.TwilioVerify.AuthToken.SHA256 = cast.Val(remoteConfig.SmsTwilioVerifyAuthToken, "")
}
s.TwilioVerify.AccountSid = cast.Val(remoteConfig.SmsTwilioVerifyAccountSid, "")
s.TwilioVerify.MessageServiceSid = cast.Val(remoteConfig.SmsTwilioVerifyMessageServiceSid, "")
case s.Messagebird.Enabled:
if len(s.Messagebird.AccessKey.SHA256) > 0 {
s.Messagebird.AccessKey.SHA256 = cast.Val(remoteConfig.SmsMessagebirdAccessKey, "")
}
s.Messagebird.Originator = cast.Val(remoteConfig.SmsMessagebirdOriginator, "")
case s.Textlocal.Enabled:
if len(s.Textlocal.ApiKey.SHA256) > 0 {
s.Textlocal.ApiKey.SHA256 = cast.Val(remoteConfig.SmsTextlocalApiKey, "")
}
s.Textlocal.Sender = cast.Val(remoteConfig.SmsTextlocalSender, "")
case s.Vonage.Enabled:
if len(s.Vonage.ApiSecret.SHA256) > 0 {
s.Vonage.ApiSecret.SHA256 = cast.Val(remoteConfig.SmsVonageApiSecret, "")
}
s.Vonage.ApiKey = cast.Val(remoteConfig.SmsVonageApiKey, "")
s.Vonage.From = cast.Val(remoteConfig.SmsVonageFrom, "")
case !s.EnableSignup:
// Nothing to do if both local and remote providers are disabled.
return
}
if provider := cast.Val(remoteConfig.SmsProvider, ""); len(provider) > 0 {
s.Twilio.Enabled = provider == "twilio"
s.TwilioVerify.Enabled = provider == "twilio_verify"
s.Messagebird.Enabled = provider == "messagebird"
s.Textlocal.Enabled = provider == "textlocal"
s.Vonage.Enabled = provider == "vonage"
}
}
func (e external) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
if len(e) == 0 {
return
}
// Ignore configs of disabled providers because their envs are not loaded
if p, ok := e["apple"]; ok {
if body.ExternalAppleEnabled = &p.Enabled; *body.ExternalAppleEnabled {
body.ExternalAppleClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalAppleSecret = &p.Secret.Value
}
}
}
if p, ok := e["azure"]; ok {
if body.ExternalAzureEnabled = &p.Enabled; *body.ExternalAzureEnabled {
body.ExternalAzureClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalAzureSecret = &p.Secret.Value
}
body.ExternalAzureUrl = &p.Url
}
}
if p, ok := e["bitbucket"]; ok {
if body.ExternalBitbucketEnabled = &p.Enabled; *body.ExternalBitbucketEnabled {
body.ExternalBitbucketClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalBitbucketSecret = &p.Secret.Value
}
}
}
if p, ok := e["discord"]; ok {
if body.ExternalDiscordEnabled = &p.Enabled; *body.ExternalDiscordEnabled {
body.ExternalDiscordClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalDiscordSecret = &p.Secret.Value
}
}
}
if p, ok := e["facebook"]; ok {
if body.ExternalFacebookEnabled = &p.Enabled; *body.ExternalFacebookEnabled {
body.ExternalFacebookClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalFacebookSecret = &p.Secret.Value
}
}
}
if p, ok := e["figma"]; ok {
if body.ExternalFigmaEnabled = &p.Enabled; *body.ExternalFigmaEnabled {
body.ExternalFigmaClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalFigmaSecret = &p.Secret.Value
}
}
}
if p, ok := e["github"]; ok {
if body.ExternalGithubEnabled = &p.Enabled; *body.ExternalGithubEnabled {
body.ExternalGithubClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalGithubSecret = &p.Secret.Value
}
}
}
if p, ok := e["gitlab"]; ok {
if body.ExternalGitlabEnabled = &p.Enabled; *body.ExternalGitlabEnabled {
body.ExternalGitlabClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalGitlabSecret = &p.Secret.Value
}
body.ExternalGitlabUrl = &p.Url
}
}
if p, ok := e["google"]; ok {
if body.ExternalGoogleEnabled = &p.Enabled; *body.ExternalGoogleEnabled {
body.ExternalGoogleClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalGoogleSecret = &p.Secret.Value
}
body.ExternalGoogleSkipNonceCheck = &p.SkipNonceCheck
}
}
if p, ok := e["kakao"]; ok {
if body.ExternalKakaoEnabled = &p.Enabled; *body.ExternalKakaoEnabled {
body.ExternalKakaoClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalKakaoSecret = &p.Secret.Value
}
}
}
if p, ok := e["keycloak"]; ok {
if body.ExternalKeycloakEnabled = &p.Enabled; *body.ExternalKeycloakEnabled {
body.ExternalKeycloakClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalKeycloakSecret = &p.Secret.Value
}
body.ExternalKeycloakUrl = &p.Url
}
}
if p, ok := e["linkedin_oidc"]; ok {
if body.ExternalLinkedinOidcEnabled = &p.Enabled; *body.ExternalLinkedinOidcEnabled {
body.ExternalLinkedinOidcClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalLinkedinOidcSecret = &p.Secret.Value
}
}
}
if p, ok := e["notion"]; ok {
if body.ExternalNotionEnabled = &p.Enabled; *body.ExternalNotionEnabled {
body.ExternalNotionClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalNotionSecret = &p.Secret.Value
}
}
}
if p, ok := e["slack_oidc"]; ok {
if body.ExternalSlackOidcEnabled = &p.Enabled; *body.ExternalSlackOidcEnabled {
body.ExternalSlackOidcClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalSlackOidcSecret = &p.Secret.Value
}
}
}
if p, ok := e["spotify"]; ok {
if body.ExternalSpotifyEnabled = &p.Enabled; *body.ExternalSpotifyEnabled {
body.ExternalSpotifyClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalSpotifySecret = &p.Secret.Value
}
}
}
if p, ok := e["twitch"]; ok {
if body.ExternalTwitchEnabled = &p.Enabled; *body.ExternalTwitchEnabled {
body.ExternalTwitchClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalTwitchSecret = &p.Secret.Value
}
}
}
if p, ok := e["twitter"]; ok {
if body.ExternalTwitterEnabled = &p.Enabled; *body.ExternalTwitterEnabled {
body.ExternalTwitterClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalTwitterSecret = &p.Secret.Value
}
}
}
if p, ok := e["workos"]; ok {
if body.ExternalWorkosEnabled = &p.Enabled; *body.ExternalWorkosEnabled {
body.ExternalWorkosClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalWorkosSecret = &p.Secret.Value
}
body.ExternalWorkosUrl = &p.Url
}
}
if p, ok := e["zoom"]; ok {
if body.ExternalZoomEnabled = &p.Enabled; *body.ExternalZoomEnabled {
body.ExternalZoomClientId = &p.ClientId
if len(p.Secret.SHA256) > 0 {
body.ExternalZoomSecret = &p.Secret.Value
}
}
}
}
func (e external) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
if len(e) == 0 {
return
}
// Ignore configs of disabled providers because their envs are not loaded
if p, ok := e["apple"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalAppleClientId, "")
if ids := cast.Val(remoteConfig.ExternalAppleAdditionalClientIds, ""); len(ids) > 0 {
p.ClientId += "," + ids
}
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalAppleSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalAppleEnabled, false)
e["apple"] = p
}
if p, ok := e["azure"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalAzureClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalAzureSecret, "")
}
p.Url = cast.Val(remoteConfig.ExternalAzureUrl, "")
}
p.Enabled = cast.Val(remoteConfig.ExternalAzureEnabled, false)
e["azure"] = p
}
if p, ok := e["bitbucket"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalBitbucketClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalBitbucketSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalBitbucketEnabled, false)
e["bitbucket"] = p
}
if p, ok := e["discord"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalDiscordClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalDiscordSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalDiscordEnabled, false)
e["discord"] = p
}
if p, ok := e["facebook"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalFacebookClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalFacebookSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalFacebookEnabled, false)
e["facebook"] = p
}
if p, ok := e["figma"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalFigmaClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalFigmaSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalFigmaEnabled, false)
e["figma"] = p
}
if p, ok := e["github"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalGithubClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalGithubSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalGithubEnabled, false)
e["github"] = p
}
if p, ok := e["gitlab"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalGitlabClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalGitlabSecret, "")
}
p.Url = cast.Val(remoteConfig.ExternalGitlabUrl, "")
}
p.Enabled = cast.Val(remoteConfig.ExternalGitlabEnabled, false)
e["gitlab"] = p
}
if p, ok := e["google"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalGoogleClientId, "")
if ids := cast.Val(remoteConfig.ExternalGoogleAdditionalClientIds, ""); len(ids) > 0 {
p.ClientId += "," + ids
}
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalGoogleSecret, "")
}
p.SkipNonceCheck = cast.Val(remoteConfig.ExternalGoogleSkipNonceCheck, false)
}
p.Enabled = cast.Val(remoteConfig.ExternalGoogleEnabled, false)
e["google"] = p
}
if p, ok := e["kakao"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalKakaoClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalKakaoSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalKakaoEnabled, false)
e["kakao"] = p
}
if p, ok := e["keycloak"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalKeycloakClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalKeycloakSecret, "")
}
p.Url = cast.Val(remoteConfig.ExternalKeycloakUrl, "")
}
p.Enabled = cast.Val(remoteConfig.ExternalKeycloakEnabled, false)
e["keycloak"] = p
}
if p, ok := e["linkedin_oidc"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalLinkedinOidcClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalLinkedinOidcSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalLinkedinOidcEnabled, false)
e["linkedin_oidc"] = p
}
if p, ok := e["notion"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalNotionClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalNotionSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalNotionEnabled, false)
e["notion"] = p
}
if p, ok := e["slack_oidc"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalSlackOidcClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalSlackOidcSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalSlackOidcEnabled, false)
e["slack_oidc"] = p
}
if p, ok := e["spotify"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalSpotifyClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalSpotifySecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalSpotifyEnabled, false)
e["spotify"] = p
}
if p, ok := e["twitch"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalTwitchClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalTwitchSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalTwitchEnabled, false)
e["twitch"] = p
}
if p, ok := e["twitter"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalTwitterClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalTwitterSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalTwitterEnabled, false)
e["twitter"] = p
}
if p, ok := e["workos"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalWorkosClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalWorkosSecret, "")
}
p.Url = cast.Val(remoteConfig.ExternalWorkosUrl, "")
}
p.Enabled = cast.Val(remoteConfig.ExternalWorkosEnabled, false)
e["workos"] = p
}
if p, ok := e["zoom"]; ok {
if p.Enabled {
p.ClientId = cast.Val(remoteConfig.ExternalZoomClientId, "")
if len(p.Secret.SHA256) > 0 {
p.Secret.SHA256 = cast.Val(remoteConfig.ExternalZoomSecret, "")
}
}
p.Enabled = cast.Val(remoteConfig.ExternalZoomEnabled, false)
e["zoom"] = p
}
}
func (a *auth) DiffWithRemote(remoteConfig v1API.AuthConfigResponse) ([]byte, error) {
copy := a.Clone()
// Convert the config values into easily comparable remoteConfig values
currentValue, err := ToTomlBytes(copy)
if err != nil {
return nil, err
}
copy.FromRemoteAuthConfig(remoteConfig)
remoteCompare, err := ToTomlBytes(copy)
if err != nil {
return nil, err
}
return diff.Diff("remote[auth]", remoteCompare, "local[auth]", currentValue), nil
}