From cc61afc8ecf7d697e0cea64051bee10d434284a1 Mon Sep 17 00:00:00 2001 From: Tor Colvin Date: Fri, 10 Jan 2025 05:08:58 -0500 Subject: [PATCH] [3.3 forward port] CBG-4376 create unsupported option for sending change in a channel filter on channel filter removal (#7269) --- db/blip_handler.go | 3 +- db/database.go | 29 ++++---- rest/blip_channel_filter_test.go | 117 +++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 rest/blip_channel_filter_test.go diff --git a/db/blip_handler.go b/db/blip_handler.go index a192e2d762..d2db9ea508 100644 --- a/db/blip_handler.go +++ b/db/blip_handler.go @@ -483,7 +483,7 @@ func (bh *blipHandler) sendChanges(sender *blip.Sender, opts *sendChangesOptions // If change is a removal and we're running with protocol V3 and change change is not a tombstone // fall into 3.0 removal handling. // Changes with change.Revoked=true have already evaluated UserHasDocAccess in changes.go, don't check again. - if change.allRemoved && bh.activeCBMobileSubprotocol >= CBMobileReplicationV3 && !change.Deleted && !change.Revoked { + if change.allRemoved && bh.activeCBMobileSubprotocol >= CBMobileReplicationV3 && !change.Deleted && !change.Revoked && !bh.db.Options.UnsupportedOptions.BlipSendDocsWithChannelRemoval { // If client doesn't want removals / revocations, don't send change if !opts.revocations { continue @@ -494,7 +494,6 @@ func (bh *blipHandler) sendChanges(sender *blip.Sender, opts *sendChangesOptions if err == nil && userHasAccessToDoc { continue } - // If we can't determine user access due to an error, log error and fall through to send change anyway. // In the event of an error we should be cautious and send a revocation anyway, even if the user // may actually have an alternate access method. This is the safer approach security-wise and diff --git a/db/database.go b/db/database.go index 25dff0ed8b..5784819d98 100644 --- a/db/database.go +++ b/db/database.go @@ -231,20 +231,21 @@ type APIEndpoints struct { // UnsupportedOptions are not supported for external use type UnsupportedOptions struct { - UserViews *UserViewsOptions `json:"user_views,omitempty"` // Config settings for user views - OidcTestProvider *OidcTestProviderOptions `json:"oidc_test_provider,omitempty"` // Config settings for OIDC Provider - APIEndpoints *APIEndpoints `json:"api_endpoints,omitempty"` // Config settings for API endpoints - WarningThresholds *WarningThresholds `json:"warning_thresholds,omitempty"` // Warning thresholds related to _sync size - DisableCleanSkippedQuery bool `json:"disable_clean_skipped_query,omitempty"` // Clean skipped sequence processing bypasses final check (deprecated: CBG-2672) - OidcTlsSkipVerify bool `json:"oidc_tls_skip_verify,omitempty"` // Config option to enable self-signed certs for OIDC testing. - SgrTlsSkipVerify bool `json:"sgr_tls_skip_verify,omitempty"` // Config option to enable self-signed certs for SG-Replicate testing. - RemoteConfigTlsSkipVerify bool `json:"remote_config_tls_skip_verify,omitempty"` // Config option to enable self signed certificates for external JavaScript load. - GuestReadOnly bool `json:"guest_read_only,omitempty"` // Config option to restrict GUEST document access to read-only - ForceAPIForbiddenErrors bool `json:"force_api_forbidden_errors,omitempty"` // Config option to force the REST API to return forbidden errors - ConnectedClient bool `json:"connected_client,omitempty"` // Enables BLIP connected-client APIs - UseQueryBasedResyncManager bool `json:"use_query_resync_manager,omitempty"` // Config option to use Query based resync manager to perform Resync op - DCPReadBuffer int `json:"dcp_read_buffer,omitempty"` // Enables user to set their own DCP read buffer - KVBufferSize int `json:"kv_buffer,omitempty"` // Enables user to set their own KV pool buffer + UserViews *UserViewsOptions `json:"user_views,omitempty"` // Config settings for user views + OidcTestProvider *OidcTestProviderOptions `json:"oidc_test_provider,omitempty"` // Config settings for OIDC Provider + APIEndpoints *APIEndpoints `json:"api_endpoints,omitempty"` // Config settings for API endpoints + WarningThresholds *WarningThresholds `json:"warning_thresholds,omitempty"` // Warning thresholds related to _sync size + DisableCleanSkippedQuery bool `json:"disable_clean_skipped_query,omitempty"` // Clean skipped sequence processing bypasses final check (deprecated: CBG-2672) + OidcTlsSkipVerify bool `json:"oidc_tls_skip_verify,omitempty"` // Config option to enable self-signed certs for OIDC testing. + SgrTlsSkipVerify bool `json:"sgr_tls_skip_verify,omitempty"` // Config option to enable self-signed certs for SG-Replicate testing. + RemoteConfigTlsSkipVerify bool `json:"remote_config_tls_skip_verify,omitempty"` // Config option to enable self signed certificates for external JavaScript load. + GuestReadOnly bool `json:"guest_read_only,omitempty"` // Config option to restrict GUEST document access to read-only + ForceAPIForbiddenErrors bool `json:"force_api_forbidden_errors,omitempty"` // Config option to force the REST API to return forbidden errors + ConnectedClient bool `json:"connected_client,omitempty"` // Enables BLIP connected-client APIs + UseQueryBasedResyncManager bool `json:"use_query_resync_manager,omitempty"` // Config option to use Query based resync manager to perform Resync op + DCPReadBuffer int `json:"dcp_read_buffer,omitempty"` // Enables user to set their own DCP read buffer + KVBufferSize int `json:"kv_buffer,omitempty"` // Enables user to set their own KV pool buffer + BlipSendDocsWithChannelRemoval bool `json:"blip_send_docs_with_channel_removal,omitempty"` // Enables sending docs with channel removals using channel filters } type WarningThresholds struct { diff --git a/rest/blip_channel_filter_test.go b/rest/blip_channel_filter_test.go new file mode 100644 index 0000000000..cb72561b7e --- /dev/null +++ b/rest/blip_channel_filter_test.go @@ -0,0 +1,117 @@ +// Copyright 2024-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. + +package rest + +import ( + "fmt" + "net/http" + "testing" + + "github.com/couchbase/sync_gateway/channels" + "github.com/couchbase/sync_gateway/db" + "github.com/stretchr/testify/require" +) + +func TestChannelFilterRemovalFromChannel(t *testing.T) { + btcRunner := NewBlipTesterClientRunner(t) + btcRunner.Run(func(t *testing.T, _ []string) { + for _, sendDocWithChannelRemoval := range []bool{true, false} { + t.Run(fmt.Sprintf("sendDocWithChannelRemoval=%v", sendDocWithChannelRemoval), func(t *testing.T) { + rt := NewRestTester(t, &RestTesterConfig{ + SyncFn: channels.DocChannelsSyncFunction, + PersistentConfig: true, + }) + defer rt.Close() + + dbConfig := rt.NewDbConfig() + dbConfig.Unsupported = &db.UnsupportedOptions{ + BlipSendDocsWithChannelRemoval: sendDocWithChannelRemoval, + } + rt.CreateDatabase("db", dbConfig) + rt.CreateUser("alice", []string{"*"}) + rt.CreateUser("bob", []string{"A"}) + + btc := btcRunner.NewBlipTesterClientOptsWithRT(rt, &BlipTesterClientOpts{ + Username: "alice", + Channels: []string{"A"}, + SendRevocations: false, + }) + defer btc.Close() + + client := btcRunner.SingleCollection(btc.id) + const docID = "doc1" + version1 := rt.PutDoc("doc1", `{"channels":["A"]}`) + rt.WaitForPendingChanges() + + response := rt.SendUserRequest("GET", "/{{.keyspace}}/_changes?since=0&channels=A&include_docs=true", "", "alice") + RequireStatus(t, response, http.StatusOK) + + expectedChanges1 := fmt.Sprintf(` +{ + "results": [ + {"seq":1, "id": "_user/alice", "changes":[]}, + {"seq":3, "id": "doc1", "doc": {"_id": "doc1", "_rev":"%s", "channels": ["A"]}, "changes": [{"rev":"%s"}]} + ], + "last_seq": "3" +}`, version1.RevID, version1.RevID) + require.JSONEq(t, expectedChanges1, string(response.BodyBytes())) + + client.StartPullSince(BlipTesterPullOptions{Continuous: false, Since: "0", Channels: "A"}) + + btcRunner.WaitForVersion(btc.id, docID, version1) + + // remove channel A from doc1 + version2 := rt.UpdateDoc(docID, version1, `{"channels":["B"]}`) + markerDocID := "marker" + markerDocVersion := rt.PutDoc(markerDocID, `{"channels":["A"]}`) + rt.WaitForPendingChanges() + + // alice will see doc1 rev2 with body + response = rt.SendUserRequest("GET", "/{{.keyspace}}/_changes?since=2&channels=A&include_docs=true", "", "alice") + RequireStatus(t, response, http.StatusOK) + + aliceExpectedChanges2 := fmt.Sprintf(` +{ + "results": [ + {"seq":4, "id": "%s", "doc": {"_id": "%s", "_rev":"%s", "channels": ["B"]}, "changes": [{"rev":"%s"}]}, + {"seq":5, "id": "%s", "doc": {"_id": "%s", "_rev":"%s", "channels": ["A"]}, "changes": [{"rev":"%s"}]} + ], + "last_seq": "5" +}`, docID, docID, version2.RevID, version2.RevID, markerDocID, markerDocID, markerDocVersion.RevID, markerDocVersion.RevID) + require.JSONEq(t, aliceExpectedChanges2, string(response.BodyBytes())) + + client.StartPullSince(BlipTesterPullOptions{Continuous: false, Since: "0", Channels: "A"}) + + if sendDocWithChannelRemoval { + data := btcRunner.WaitForVersion(btc.id, docID, version2) + require.Equal(t, `{"channels":["B"]}`, string(data)) + } else { + client.WaitForVersion(markerDocID, markerDocVersion) + doc, ok := client.GetDoc(docID) + require.True(t, ok) + require.Equal(t, `{"channels":["A"]}`, string(doc)) + } + + // bob will not see doc1 + response = rt.SendUserRequest("GET", "/{{.keyspace}}/_changes?since=2&channels=A&include_docs=true", "", "bob") + RequireStatus(t, response, http.StatusOK) + + bobExpectedChanges2 := fmt.Sprintf(` +{ + "results": [ + {"seq":4, "id": "doc1", "removed":["A"], "doc": {"_id": "doc1", "_rev":"%s", "_removed": true}, "changes": [{"rev":"%s"}]}, + {"seq":5, "id": "%s", "doc": {"_id": "%s", "_rev":"%s", "channels": ["A"]}, "changes": [{"rev":"%s"}]} + ], + "last_seq": "5" +}`, version2.RevID, version2.RevID, markerDocID, markerDocID, markerDocVersion.RevID, markerDocVersion.RevID) + require.JSONEq(t, bobExpectedChanges2, string(response.BodyBytes())) + }) + } + }) +}