Skip to content

Commit 8754171

Browse files
committed
fix: keep smtp_pass consistent in auth config.
1 parent eb42f5e commit 8754171

File tree

2 files changed

+141
-41
lines changed

2 files changed

+141
-41
lines changed

internal/provider/settings_resource.go

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -312,15 +312,19 @@ func readAuthConfig(ctx context.Context, state *SettingsResourceModel, client *a
312312
msg := fmt.Sprintf("Unable to read auth settings, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
313313
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
314314
}
315-
// API treats sensitive fields as write-only
316-
var body LocalAuthConfig
315+
// API treats sensitive fields as write-only, preserve them from state
316+
var stateBody LocalAuthConfig
317317
if !state.Auth.IsNull() {
318-
if diags := state.Auth.Unmarshal(&body); diags.HasError() {
318+
if diags := state.Auth.Unmarshal(&stateBody); diags.HasError() {
319319
return diags
320320
}
321321
}
322-
body.overrideSensitiveFields(httpResp.JSON200)
323-
if state.Auth, err = parseConfig(state.Auth, *httpResp.JSON200); err != nil {
322+
// Convert response to UpdateAuthConfigBody type for consistent marshaling
323+
resultBody := convertAuthResponse(ctx, httpResp.JSON200)
324+
// Override sensitive fields with values from state
325+
copySensitiveFields(stateBody.UpdateAuthConfigBody, &resultBody)
326+
327+
if state.Auth, err = parseConfig(state.Auth, resultBody); err != nil {
324328
msg := fmt.Sprintf("Unable to read auth settings, got error: %s", err)
325329
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
326330
}
@@ -342,11 +346,12 @@ func updateAuthConfig(ctx context.Context, plan *SettingsResourceModel, client *
342346
msg := fmt.Sprintf("Unable to update auth settings, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
343347
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
344348
}
349+
// Convert response to UpdateAuthConfigBody type for consistent marshaling
350+
resultBody := convertAuthResponse(ctx, httpResp.JSON200)
345351
// Copy over sensitive fields from TF plan
346-
local := LocalAuthConfig{UpdateAuthConfigBody: body}
347-
local.overrideSensitiveFields(httpResp.JSON200)
352+
copySensitiveFields(body, &resultBody)
348353

349-
if plan.Auth, err = parseConfig(plan.Auth, *httpResp.JSON200); err != nil {
354+
if plan.Auth, err = parseConfig(plan.Auth, resultBody); err != nil {
350355
msg := fmt.Sprintf("Unable to update auth settings, got error: %s", err)
351356
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
352357
}
@@ -460,44 +465,69 @@ type LocalAuthConfig struct {
460465
api.UpdateAuthConfigBody
461466
}
462467

463-
func (c LocalAuthConfig) overrideSensitiveFields(resp *api.AuthConfigResponse) {
468+
// convertAuthResponse converts AuthConfigResponse to UpdateAuthConfigBody using JSON marshaling.
469+
// This ensures we use consistent JSON tags (with omitempty) for marshaling.
470+
func convertAuthResponse(ctx context.Context, resp *api.AuthConfigResponse) api.UpdateAuthConfigBody {
471+
// Marshal the response to JSON and unmarshal into UpdateAuthConfigBody
472+
// This handles field mapping and type conversions automatically
473+
data, err := json.Marshal(resp)
474+
if err != nil {
475+
tflog.Error(ctx, "Failed to marshal auth config response", map[string]interface{}{
476+
"error": err.Error(),
477+
})
478+
return api.UpdateAuthConfigBody{}
479+
}
480+
481+
var body api.UpdateAuthConfigBody
482+
if err := json.Unmarshal(data, &body); err != nil {
483+
tflog.Error(ctx, "Failed to unmarshal auth config to UpdateAuthConfigBody", map[string]interface{}{
484+
"error": err.Error(),
485+
})
486+
return api.UpdateAuthConfigBody{}
487+
}
488+
489+
return body
490+
}
491+
492+
// copySensitiveFields copies sensitive field values from source to target.
493+
func copySensitiveFields(source api.UpdateAuthConfigBody, target *api.UpdateAuthConfigBody) {
464494
// Email provider secrets
465-
resp.SmtpPass = c.SmtpPass
495+
target.SmtpPass = source.SmtpPass
466496
// SMS provider secrets
467-
resp.SmsTwilioAuthToken = c.SmsTwilioAuthToken
468-
resp.SmsTwilioVerifyAuthToken = c.SmsTwilioVerifyAuthToken
469-
resp.SmsMessagebirdAccessKey = c.SmsMessagebirdAccessKey
470-
resp.SmsTextlocalApiKey = c.SmsTextlocalApiKey
471-
resp.SmsVonageApiSecret = c.SmsVonageApiSecret
497+
target.SmsTwilioAuthToken = source.SmsTwilioAuthToken
498+
target.SmsTwilioVerifyAuthToken = source.SmsTwilioVerifyAuthToken
499+
target.SmsMessagebirdAccessKey = source.SmsMessagebirdAccessKey
500+
target.SmsTextlocalApiKey = source.SmsTextlocalApiKey
501+
target.SmsVonageApiSecret = source.SmsVonageApiSecret
472502
// Captcha provider secrets
473-
resp.SecurityCaptchaSecret = c.SecurityCaptchaSecret
503+
target.SecurityCaptchaSecret = source.SecurityCaptchaSecret
474504
// External provider secrets
475-
resp.ExternalAppleSecret = c.ExternalAppleSecret
476-
resp.ExternalAzureSecret = c.ExternalAzureSecret
477-
resp.ExternalBitbucketSecret = c.ExternalBitbucketSecret
478-
resp.ExternalDiscordSecret = c.ExternalDiscordSecret
479-
resp.ExternalFacebookSecret = c.ExternalFacebookSecret
480-
resp.ExternalFigmaSecret = c.ExternalFigmaSecret
481-
resp.ExternalGithubSecret = c.ExternalGithubSecret
482-
resp.ExternalGitlabSecret = c.ExternalGitlabSecret
483-
resp.ExternalGoogleSecret = c.ExternalGoogleSecret
484-
resp.ExternalKakaoSecret = c.ExternalKakaoSecret
485-
resp.ExternalKeycloakSecret = c.ExternalKeycloakSecret
486-
resp.ExternalLinkedinOidcSecret = c.ExternalLinkedinOidcSecret
487-
resp.ExternalNotionSecret = c.ExternalNotionSecret
488-
resp.ExternalSlackOidcSecret = c.ExternalSlackOidcSecret
489-
resp.ExternalSlackSecret = c.ExternalSlackSecret
490-
resp.ExternalSpotifySecret = c.ExternalSpotifySecret
491-
resp.ExternalTwitchSecret = c.ExternalTwitchSecret
492-
resp.ExternalTwitterSecret = c.ExternalTwitterSecret
493-
resp.ExternalWorkosSecret = c.ExternalWorkosSecret
494-
resp.ExternalZoomSecret = c.ExternalZoomSecret
505+
target.ExternalAppleSecret = source.ExternalAppleSecret
506+
target.ExternalAzureSecret = source.ExternalAzureSecret
507+
target.ExternalBitbucketSecret = source.ExternalBitbucketSecret
508+
target.ExternalDiscordSecret = source.ExternalDiscordSecret
509+
target.ExternalFacebookSecret = source.ExternalFacebookSecret
510+
target.ExternalFigmaSecret = source.ExternalFigmaSecret
511+
target.ExternalGithubSecret = source.ExternalGithubSecret
512+
target.ExternalGitlabSecret = source.ExternalGitlabSecret
513+
target.ExternalGoogleSecret = source.ExternalGoogleSecret
514+
target.ExternalKakaoSecret = source.ExternalKakaoSecret
515+
target.ExternalKeycloakSecret = source.ExternalKeycloakSecret
516+
target.ExternalLinkedinOidcSecret = source.ExternalLinkedinOidcSecret
517+
target.ExternalNotionSecret = source.ExternalNotionSecret
518+
target.ExternalSlackOidcSecret = source.ExternalSlackOidcSecret
519+
target.ExternalSlackSecret = source.ExternalSlackSecret
520+
target.ExternalSpotifySecret = source.ExternalSpotifySecret
521+
target.ExternalTwitchSecret = source.ExternalTwitchSecret
522+
target.ExternalTwitterSecret = source.ExternalTwitterSecret
523+
target.ExternalWorkosSecret = source.ExternalWorkosSecret
524+
target.ExternalZoomSecret = source.ExternalZoomSecret
495525
// Hook provider secrets
496-
resp.HookCustomAccessTokenSecrets = c.HookCustomAccessTokenSecrets
497-
resp.HookMfaVerificationAttemptSecrets = c.HookMfaVerificationAttemptSecrets
498-
resp.HookPasswordVerificationAttemptSecrets = c.HookPasswordVerificationAttemptSecrets
499-
resp.HookSendEmailSecrets = c.HookSendEmailSecrets
500-
resp.HookSendSmsSecrets = c.HookSendSmsSecrets
526+
target.HookCustomAccessTokenSecrets = source.HookCustomAccessTokenSecrets
527+
target.HookMfaVerificationAttemptSecrets = source.HookMfaVerificationAttemptSecrets
528+
target.HookPasswordVerificationAttemptSecrets = source.HookPasswordVerificationAttemptSecrets
529+
target.HookSendEmailSecrets = source.HookSendEmailSecrets
530+
target.HookSendSmsSecrets = source.HookSendSmsSecrets
501531
}
502532

503533
type NetworkConfig struct {

internal/provider/settings_resource_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
package provider
55

66
import (
7+
"bytes"
78
"encoding/json"
89
"errors"
910
"fmt"
11+
"io"
1012
"net/http"
13+
"strings"
1114
"testing"
1215

1316
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -338,6 +341,73 @@ func unmarshalStateAttr(state *terraform.InstanceState, attr string) (map[string
338341
return out, nil
339342
}
340343

344+
func TestAccSettingsResource_SmtpPass(t *testing.T) {
345+
// Setup mock api
346+
defer gock.OffAll()
347+
gock.New("https://api.supabase.com").
348+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
349+
Reply(http.StatusOK).
350+
JSON(api.AuthConfigResponse{
351+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
352+
MailerOtpExp: 3600,
353+
MfaPhoneOtpLength: 6,
354+
SmsOtpLength: 6,
355+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
356+
})
357+
gock.New("https://api.supabase.com").
358+
Patch("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
359+
AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) {
360+
body, err := io.ReadAll(req.Body)
361+
if err != nil {
362+
return false, err
363+
}
364+
req.Body = io.NopCloser(bytes.NewBuffer(body))
365+
bodyStr := string(body)
366+
return strings.Contains(bodyStr, `"smtp_pass"`) &&
367+
strings.Contains(bodyStr, `"secret_password_123"`), nil
368+
}).
369+
Reply(http.StatusOK).
370+
JSON(api.AuthConfigResponse{
371+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
372+
MailerOtpExp: 3600,
373+
MfaPhoneOtpLength: 6,
374+
SmsOtpLength: 6,
375+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
376+
})
377+
gock.New("https://api.supabase.com").
378+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
379+
Reply(http.StatusOK).
380+
JSON(api.AuthConfigResponse{
381+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
382+
MailerOtpExp: 3600,
383+
MfaPhoneOtpLength: 6,
384+
SmsOtpLength: 6,
385+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
386+
})
387+
388+
resource.Test(t, resource.TestCase{
389+
PreCheck: func() { testAccPreCheck(t) },
390+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
391+
Steps: []resource.TestStep{
392+
{
393+
Config: `
394+
resource "supabase_settings" "test" {
395+
project_ref = "mayuaycdtijbctgqbycg"
396+
397+
auth = jsonencode({
398+
site_url = "http://localhost:3000"
399+
smtp_pass = "secret_password_123"
400+
})
401+
}
402+
`,
403+
Check: resource.ComposeAggregateTestCheckFunc(
404+
resource.TestCheckResourceAttr("supabase_settings.test", "id", "mayuaycdtijbctgqbycg"),
405+
),
406+
},
407+
},
408+
})
409+
}
410+
341411
const testAccSettingsResourceConfig = `
342412
resource "supabase_settings" "production" {
343413
project_ref = "mayuaycdtijbctgqbycg"

0 commit comments

Comments
 (0)