Skip to content

Commit 2ac1ac0

Browse files
authored
WFE: Don't remove contacts on empty update-account request (#8049)
When we receive an update-account request which is not empty, but doesn't contain the "contact" field, don't assume that they want to remove their contacts. Only remove contacts if the "contact" field is present, but empty. Add a unit test and an integration test which will catch regressions in this behavior.
1 parent dd566a9 commit 2ac1ac0

File tree

3 files changed

+106
-16
lines changed

3 files changed

+106
-16
lines changed

test/integration/account_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,32 @@ func TestAccountDeactivate(t *testing.T) {
4848
// account object as it used to make the request, and a wholly missing
4949
// contacts field doesn't overwrite whatever eggsampler was holding in memory.
5050
}
51+
52+
func TestAccountUpdate_UnspecifiedContacts(t *testing.T) {
53+
t.Parallel()
54+
55+
c, err := acme.NewClient("http://boulder.service.consul:4001/directory")
56+
if err != nil {
57+
t.Fatalf("failed to connect to acme directory: %s", err)
58+
}
59+
60+
acctKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
61+
if err != nil {
62+
t.Fatalf("failed to generate account key: %s", err)
63+
}
64+
65+
acct, err := c.NewAccount(acctKey, false, true, "mailto:example@"+random_domain())
66+
if err != nil {
67+
t.Fatalf("failed to create initial account: %s", err)
68+
}
69+
70+
// This request does not include the Contact field, meaning that the contacts
71+
// should remain unchanged (i.e. not be removed).
72+
acct, err = c.UpdateAccount(acct)
73+
if err != nil {
74+
t.Errorf("failed to no-op update account: %s", err)
75+
}
76+
if len(acct.Contact) != 1 {
77+
t.Errorf("unexpected number of contacts: want 1, got %d", len(acct.Contact))
78+
}
79+
}

wfe2/wfe.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,12 +1441,16 @@ func (wfe *WebFrontEndImpl) updateAccount(
14411441
return nil, probs.Malformed("Invalid value provided for status field")
14421442
}
14431443

1444-
var contacts []string
1445-
if accountUpdateRequest.Contact != nil {
1446-
contacts = *accountUpdateRequest.Contact
1444+
if accountUpdateRequest.Contact == nil {
1445+
// We use a pointer-to-slice for the contacts field so that we can tell the
1446+
// difference between the request not including the contact field, and the
1447+
// request including an empty contact list. If the field was omitted
1448+
// entirely, they don't want us to update it, so there's no work to do here.
1449+
return currAcct, nil
14471450
}
14481451

1449-
updatedAcct, err := wfe.ra.UpdateRegistrationContact(ctx, &rapb.UpdateRegistrationContactRequest{RegistrationID: currAcct.ID, Contacts: contacts})
1452+
updatedAcct, err := wfe.ra.UpdateRegistrationContact(ctx, &rapb.UpdateRegistrationContactRequest{
1453+
RegistrationID: currAcct.ID, Contacts: *accountUpdateRequest.Contact})
14501454
if err != nil {
14511455
return nil, web.ProblemDetailsForError(err, "Unable to update account")
14521456
}

wfe2/wfe_test.go

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"net/http/httptest"
2222
"net/url"
2323
"os"
24+
"reflect"
25+
"slices"
2426
"sort"
2527
"strconv"
2628
"strings"
@@ -197,13 +199,17 @@ func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, in *co
197199

198200
func (ra *MockRegistrationAuthority) UpdateRegistrationContact(ctx context.Context, in *rapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) {
199201
return &corepb.Registration{
202+
Status: string(core.StatusValid),
200203
Contact: in.Contacts,
201204
Key: []byte(test1KeyPublicJSON),
202205
}, nil
203206
}
204207

205208
func (ra *MockRegistrationAuthority) UpdateRegistrationKey(ctx context.Context, in *rapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) {
206-
return &corepb.Registration{Key: in.Jwk}, nil
209+
return &corepb.Registration{
210+
Status: string(core.StatusValid),
211+
Key: in.Jwk,
212+
}, nil
207213
}
208214

209215
func (ra *MockRegistrationAuthority) PerformValidation(context.Context, *rapb.PerformValidationRequest, ...grpc.CallOption) (*corepb.Authorization, error) {
@@ -1773,15 +1779,6 @@ func TestAuthorizationChallengeHandlerNamespace(t *testing.T) {
17731779
responseWriter.Body.Reset()
17741780
}
17751781

1776-
func contains(s []string, e string) bool {
1777-
for _, a := range s {
1778-
if a == e {
1779-
return true
1780-
}
1781-
}
1782-
return false
1783-
}
1784-
17851782
func TestAccount(t *testing.T) {
17861783
wfe, _, signer := setupWFE(t)
17871784
mux := wfe.Handler(metrics.NoopRegisterer)
@@ -1836,7 +1833,7 @@ func TestAccount(t *testing.T) {
18361833
wfe.Account(ctx, newRequestEvent(), responseWriter, request)
18371834
test.AssertNotContains(t, responseWriter.Body.String(), probs.ErrorNS)
18381835
links := responseWriter.Header()["Link"]
1839-
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
1836+
test.AssertEquals(t, slices.Contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
18401837
responseWriter.Body.Reset()
18411838

18421839
// Test POST valid JSON with garbage in URL but valid account ID
@@ -1877,6 +1874,66 @@ func TestAccount(t *testing.T) {
18771874
}`)
18781875
}
18791876

1877+
func TestUpdateAccount(t *testing.T) {
1878+
t.Parallel()
1879+
wfe, _, _ := setupWFE(t)
1880+
1881+
for _, tc := range []struct {
1882+
name string
1883+
req string
1884+
wantAcct *core.Registration
1885+
}{
1886+
{
1887+
name: "deactivate clears contact",
1888+
req: `{"status": "deactivated"}`,
1889+
wantAcct: &core.Registration{Status: core.StatusDeactivated},
1890+
},
1891+
{
1892+
name: "deactivate takes priority over contact change",
1893+
req: `{"status": "deactivated", "contact": ["mailto:admin@example.com"]}`,
1894+
wantAcct: &core.Registration{Status: core.StatusDeactivated},
1895+
},
1896+
{
1897+
name: "change contact",
1898+
req: `{"contact": ["mailto:admin@example.com"]}`,
1899+
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:admin@example.com"}},
1900+
},
1901+
{
1902+
name: "change contact with unchanged status",
1903+
req: `{"status": "valid", "contact": ["mailto:admin@example.com"]}`,
1904+
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:admin@example.com"}},
1905+
},
1906+
{
1907+
name: "unchanged status leaves contact untouched",
1908+
req: `{"status": "valid"}`,
1909+
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:webmaster@example.com"}},
1910+
},
1911+
} {
1912+
t.Run(tc.name, func(t *testing.T) {
1913+
t.Parallel()
1914+
1915+
acct := core.Registration{
1916+
Status: core.StatusValid,
1917+
Contact: &[]string{"mailto:webmaster@example.com"},
1918+
}
1919+
1920+
gotAcct, gotProb := wfe.updateAccount(context.Background(), []byte(tc.req), &acct)
1921+
if gotProb != nil {
1922+
t.Fatalf("want success, got problem %s", gotProb)
1923+
}
1924+
1925+
if tc.wantAcct != nil {
1926+
if gotAcct.Status != tc.wantAcct.Status {
1927+
t.Errorf("want status %s, got %s", tc.wantAcct.Status, gotAcct.Status)
1928+
}
1929+
if !reflect.DeepEqual(gotAcct.Contact, tc.wantAcct.Contact) {
1930+
t.Errorf("want contact %v, got %v", tc.wantAcct.Contact, gotAcct.Contact)
1931+
}
1932+
}
1933+
})
1934+
}
1935+
}
1936+
18801937
type mockSAWithCert struct {
18811938
sapb.StorageAuthorityReadOnlyClient
18821939
cert *x509.Certificate
@@ -2930,7 +2987,7 @@ func TestKeyRollover(t *testing.T) {
29302987
Payload: `{"oldKey":` + test1KeyPublicJSON + `,"account":"http://localhost/acme/acct/1"}`,
29312988
ExpectedResponse: `{
29322989
"key": ` + string(newJWKJSON) + `,
2933-
"status": ""
2990+
"status": "valid"
29342991
}`,
29352992
NewKey: newKeyPriv,
29362993
},

0 commit comments

Comments
 (0)