Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.2.2 backport] CBG-4377 create unsupported option for sending change in a channel filter on channel filter removal #7291

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions db/blip_handler.go
Original file line number Diff line number Diff line change
@@ -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
29 changes: 15 additions & 14 deletions db/database.go
Original file line number Diff line number Diff line change
@@ -232,20 +232,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 {
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"]}`)
require.NoError(t, 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()))

require.NoError(t, 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"]}`)
require.NoError(t, 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()))

require.NoError(t, 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()))
})
}
})
}