Skip to content

Commit

Permalink
[3.3 forward port] CBG-4376 create unsupported option for sending cha…
Browse files Browse the repository at this point in the history
…nge in a channel filter on channel filter removal (#7269)
  • Loading branch information
torcolvin authored Jan 10, 2025
1 parent d4037ef commit cc61afc
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 16 deletions.
3 changes: 1 addition & 2 deletions db/blip_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
29 changes: 15 additions & 14 deletions db/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
117 changes: 117 additions & 0 deletions rest/blip_channel_filter_test.go
Original file line number Diff line number Diff line change
@@ -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()))
})
}
})
}

0 comments on commit cc61afc

Please sign in to comment.