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 = ×tamp } // 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 }