From 23d255abafc626b00e58ed82ac2be902459624cb Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 12 Feb 2025 16:22:53 +0200 Subject: [PATCH 1/4] Add delete request endpoint that creates a channel event for FBA channels --- channel_event.go | 1 + handlers/meta/facebook_test.go | 32 ++++++++++++++++++++++ handlers/meta/handlers.go | 50 +++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/channel_event.go b/channel_event.go index 087ce1ab6..9a4c747f0 100644 --- a/channel_event.go +++ b/channel_event.go @@ -21,6 +21,7 @@ const ( EventTypeWelcomeMessage ChannelEventType = "welcome_message" EventTypeOptIn ChannelEventType = "optin" EventTypeOptOut ChannelEventType = "optout" + EventDeletionRequest ChannelEventType = "delete_request" ) //----------------------------------------------------------------------------- diff --git a/handlers/meta/facebook_test.go b/handlers/meta/facebook_test.go index 2ef3ca0d8..ea0404b34 100644 --- a/handlers/meta/facebook_test.go +++ b/handlers/meta/facebook_test.go @@ -322,6 +322,38 @@ func TestFacebookDescribeURN(t *testing.T) { AssertChannelLogRedaction(t, clog, []string{"a123", "wac_admin_system_user_token"}) } +func TestDeleteRequest(t *testing.T) { + RunIncomingTestCases(t, facebookTestChannels, newHandler("FBA", "Facebook"), []IncomingTestCase{ + { + Label: "Receive Delete request FBA", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"218471"}`, + PrepRequest: addValidSignature, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "Deletion Request Received", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + ExpectedEvents: []ExpectedEvent{ + {Type: courier.EventDeletionRequest, URN: "facebook:218471", Extra: map[string]string{"userID": "218471"}}, + }, + }, + { + Label: "Receive Delete request FBA", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"abc1234"}`, + PrepRequest: addValidSignature, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "invalid facebook id", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + }, + }) +} + func TestFacebookVerify(t *testing.T) { RunIncomingTestCases(t, facebookTestChannels, newHandler("FBA", "Facebook"), []IncomingTestCase{ { diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 53a4f7e7a..72a600fd1 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -17,6 +17,7 @@ import ( "time" "github.com/buger/jsonparser" + "github.com/getsentry/sentry-go" "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers" "github.com/nyaruka/courier/handlers/meta/messenger" @@ -85,6 +86,7 @@ func (h *handler) Initialize(s courier.Server) error { h.SetServer(s) s.AddHandlerRoute(h, http.MethodGet, "receive", courier.ChannelLogTypeWebhookVerify, h.receiveVerify) s.AddHandlerRoute(h, http.MethodPost, "receive", courier.ChannelLogTypeMultiReceive, handlers.JSONPayload(h, h.receiveEvents)) + s.AddHandlerRoute(h, http.MethodPost, "delete", courier.ChannelLogTypeEventReceive, handlers.JSONPayload(h, h.deleteEvents)) return nil } @@ -130,7 +132,7 @@ func (h *handler) WriteRequestError(ctx context.Context, w http.ResponseWriter, // GetChannel returns the channel func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Channel, error) { - if r.Method == http.MethodGet { + if r.Method == http.MethodGet || r.URL.Path == "/c/fba/delete" { return nil, nil } @@ -214,6 +216,52 @@ func (h *handler) resolveMediaURL(mediaID string, token string, clog *courier.Ch return mediaURL, err } +type DeletionRequestData struct { + Algorithm string `json:"algorithm"` + Expires int64 `json:"expires"` + IssuedAt int64 `json:"issued_at"` + UserID string `json:"user_id"` +} + +type DeleteConfirmationData struct { + URL string `json:"url"` + ConfirmationCode string `json:"confirmation_code"` +} + +// deleteEvents is our HTTP handler function for deleting data requests +func (h *handler) deleteEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *DeletionRequestData, clog *courier.ChannelLog) ([]courier.Event, error) { + err := h.validateSignature(r) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + urn, err := urns.New(urns.Facebook, payload.UserID) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, errors.New("invalid facebook id")) + } + date := parseTimestamp(payload.IssuedAt) + + events := make([]courier.Event, 0, 2) + data := make([]any, 0, 2) + + payloadJson, _ := json.Marshal(payload) + sentry.CaptureMessage(fmt.Sprintf("Data Deletion Request: %s", payloadJson)) + + event := h.Backend().NewChannelEvent(channel, courier.EventDeletionRequest, urn, clog).WithOccurredOn(date).WithExtra(map[string]string{"userID": payload.UserID}) + + err = h.Backend().WriteChannelEvent(ctx, event, clog) + if err != nil { + return nil, err + } + + confirmationURL := fmt.Sprintf("https://%s/channels/events/read/%s/", h.Server().Config().Domain, event.UUID()) + + events = append(events, event) + data = append(data, DeleteConfirmationData{URL: confirmationURL, ConfirmationCode: string(event.UUID())}) + + return events, courier.WriteDataResponse(w, http.StatusOK, "Deletion Request Received", data) +} + // receiveEvents is our HTTP handler function for incoming messages and status updates func (h *handler) receiveEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *Notifications, clog *courier.ChannelLog) ([]courier.Event, error) { err := h.validateSignature(r) From 082e35707a12a3d8d609d37da95f0914444ec1ac Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 12 Feb 2025 17:11:34 +0200 Subject: [PATCH 2/4] Rename to use delete_contact --- channel_event.go | 2 +- handlers/meta/facebook_test.go | 2 +- handlers/meta/handlers.go | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/channel_event.go b/channel_event.go index 9a4c747f0..59cdffb0a 100644 --- a/channel_event.go +++ b/channel_event.go @@ -21,7 +21,7 @@ const ( EventTypeWelcomeMessage ChannelEventType = "welcome_message" EventTypeOptIn ChannelEventType = "optin" EventTypeOptOut ChannelEventType = "optout" - EventDeletionRequest ChannelEventType = "delete_request" + EventDeleteContact ChannelEventType = "delete_contact" ) //----------------------------------------------------------------------------- diff --git a/handlers/meta/facebook_test.go b/handlers/meta/facebook_test.go index ea0404b34..0e647bce1 100644 --- a/handlers/meta/facebook_test.go +++ b/handlers/meta/facebook_test.go @@ -336,7 +336,7 @@ func TestDeleteRequest(t *testing.T) { NoInvalidChannelCheck: true, NoLogsExpected: true, ExpectedEvents: []ExpectedEvent{ - {Type: courier.EventDeletionRequest, URN: "facebook:218471", Extra: map[string]string{"userID": "218471"}}, + {Type: courier.EventDeleteContact, URN: "facebook:218471", Extra: map[string]string{"userID": "218471"}}, }, }, { diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 72a600fd1..62ec82a7e 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -86,7 +86,7 @@ func (h *handler) Initialize(s courier.Server) error { h.SetServer(s) s.AddHandlerRoute(h, http.MethodGet, "receive", courier.ChannelLogTypeWebhookVerify, h.receiveVerify) s.AddHandlerRoute(h, http.MethodPost, "receive", courier.ChannelLogTypeMultiReceive, handlers.JSONPayload(h, h.receiveEvents)) - s.AddHandlerRoute(h, http.MethodPost, "delete", courier.ChannelLogTypeEventReceive, handlers.JSONPayload(h, h.deleteEvents)) + s.AddHandlerRoute(h, http.MethodPost, "delete", courier.ChannelLogTypeEventReceive, handlers.JSONPayload(h, h.deleteContactEvents)) return nil } @@ -228,8 +228,8 @@ type DeleteConfirmationData struct { ConfirmationCode string `json:"confirmation_code"` } -// deleteEvents is our HTTP handler function for deleting data requests -func (h *handler) deleteEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *DeletionRequestData, clog *courier.ChannelLog) ([]courier.Event, error) { +// deleteContactEvents is our HTTP handler function for deleting data requests +func (h *handler) deleteContactEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *DeletionRequestData, clog *courier.ChannelLog) ([]courier.Event, error) { err := h.validateSignature(r) if err != nil { return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) @@ -247,7 +247,7 @@ func (h *handler) deleteEvents(ctx context.Context, channel courier.Channel, w h payloadJson, _ := json.Marshal(payload) sentry.CaptureMessage(fmt.Sprintf("Data Deletion Request: %s", payloadJson)) - event := h.Backend().NewChannelEvent(channel, courier.EventDeletionRequest, urn, clog).WithOccurredOn(date).WithExtra(map[string]string{"userID": payload.UserID}) + event := h.Backend().NewChannelEvent(channel, courier.EventDeleteContact, urn, clog).WithOccurredOn(date).WithExtra(map[string]string{"userID": payload.UserID}) err = h.Backend().WriteChannelEvent(ctx, event, clog) if err != nil { From de2770e4ab07a24b8bc9daead6d2b436df021fbd Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 4 Mar 2025 16:34:52 +0200 Subject: [PATCH 3/4] Only accept delete requests for contacts we have in the DB --- handlers/meta/facebook_test.go | 32 ++++++++++++++++++++++++-------- handlers/meta/handlers.go | 6 ++++++ handlers/test.go | 7 +++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/handlers/meta/facebook_test.go b/handlers/meta/facebook_test.go index 0e647bce1..435ada648 100644 --- a/handlers/meta/facebook_test.go +++ b/handlers/meta/facebook_test.go @@ -323,12 +323,14 @@ func TestFacebookDescribeURN(t *testing.T) { } func TestDeleteRequest(t *testing.T) { + urn, _ := urns.New(urns.Facebook, "218471") RunIncomingTestCases(t, facebookTestChannels, newHandler("FBA", "Facebook"), []IncomingTestCase{ { - Label: "Receive Delete request FBA", - URL: "/c/fba/delete", - Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"218471"}`, - PrepRequest: addValidSignature, + Label: "Receive Delete request FBA", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"218471"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, ExpectedRespStatus: 200, ExpectedBodyContains: "Deletion Request Received", @@ -340,10 +342,24 @@ func TestDeleteRequest(t *testing.T) { }, }, { - Label: "Receive Delete request FBA", - URL: "/c/fba/delete", - Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"abc1234"}`, - PrepRequest: addValidSignature, + Label: "Receive Delete request FBA, contact not existing", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"123456"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "ignoring request, no existing contact matched", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + }, + { + Label: "Receive Delete request FBA, invalid facebook ID", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"abc1234"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, ExpectedRespStatus: 200, ExpectedBodyContains: "invalid facebook id", diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 62ec82a7e..23f716a24 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -239,6 +239,12 @@ func (h *handler) deleteContactEvents(ctx context.Context, channel courier.Chann if err != nil { return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, errors.New("invalid facebook id")) } + + contact, err := h.Server().Backend().GetContact(ctx, channel, urn, nil, "", false, clog) + if contact == nil { + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request, no existing contact matched") + } + date := parseTimestamp(payload.IssuedAt) events := make([]courier.Event, 0, 2) diff --git a/handlers/test.go b/handlers/test.go index a0f1480d9..ee6920e59 100644 --- a/handlers/test.go +++ b/handlers/test.go @@ -52,6 +52,8 @@ type IncomingTestCase struct { NoInvalidChannelCheck bool PrepRequest RequestPrepFunc + ExistingDBURNs []urns.URN + URL string Data string Headers map[string]string @@ -159,6 +161,11 @@ func RunIncomingTestCases(t *testing.T, channels []courier.Channel, handler cour handler.Initialize(s) for _, tc := range testCases { + for _, urn := range tc.ExistingDBURNs { + ctx, _ := context.WithTimeout(context.Background(), time.Second*10) + s.Backend().GetContact(ctx, channels[0], urn, nil, "", true, nil) + } + t.Run(tc.Label, func(t *testing.T) { require := require.New(t) From 1c888d3ee1f3097a3172819baf5246d600465d21 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Thu, 6 Mar 2025 16:24:13 +0200 Subject: [PATCH 4/4] Update confirmation URL --- handlers/meta/handlers.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 23f716a24..7f0cc341f 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -17,7 +17,6 @@ import ( "time" "github.com/buger/jsonparser" - "github.com/getsentry/sentry-go" "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers" "github.com/nyaruka/courier/handlers/meta/messenger" @@ -250,9 +249,6 @@ func (h *handler) deleteContactEvents(ctx context.Context, channel courier.Chann events := make([]courier.Event, 0, 2) data := make([]any, 0, 2) - payloadJson, _ := json.Marshal(payload) - sentry.CaptureMessage(fmt.Sprintf("Data Deletion Request: %s", payloadJson)) - event := h.Backend().NewChannelEvent(channel, courier.EventDeleteContact, urn, clog).WithOccurredOn(date).WithExtra(map[string]string{"userID": payload.UserID}) err = h.Backend().WriteChannelEvent(ctx, event, clog) @@ -260,7 +256,7 @@ func (h *handler) deleteContactEvents(ctx context.Context, channel courier.Chann return nil, err } - confirmationURL := fmt.Sprintf("https://%s/channels/events/read/%s/", h.Server().Config().Domain, event.UUID()) + confirmationURL := fmt.Sprintf("https://%s/public/forgetme/", h.Server().Config().Domain) events = append(events, event) data = append(data, DeleteConfirmationData{URL: confirmationURL, ConfirmationCode: string(event.UUID())})