Skip to content

Commit 04bcf79

Browse files
gregns1bbrkstorcolvin
authored andcommitted
CBG-3212: add api to fetch a document by its CV value (#6579)
* CBG-3212: add api to fetch a document by its CV value * test fix * rebased SourceAndVersion -> Version rename * Update currentRevChannels on CV revcache load and doc.updateChannels * fix spelling * Remove currentRevChannels * Move common GetRev/GetCV work into documentRevisionForRequest function * Pass revision.RevID into authorizeUserForChannels * Update db/crud.go Co-authored-by: Tor Colvin <tor.colvin@couchbase.com> --------- Co-authored-by: Ben Brooks <ben.brooks@couchbase.com> Co-authored-by: Tor Colvin <tor.colvin@couchbase.com>
1 parent c54cbda commit 04bcf79

10 files changed

Lines changed: 256 additions & 47 deletions

db/changes_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func TestDocDeletionFromChannelCoalescedRemoved(t *testing.T) {
289289
func TestCVPopulationOnChangeEntry(t *testing.T) {
290290
db, ctx := setupTestDB(t)
291291
defer db.Close(ctx)
292-
collection := GetSingleDatabaseCollectionWithUser(t, db)
292+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
293293
collectionID := collection.GetCollectionID()
294294
bucketUUID := db.BucketUUID
295295

@@ -561,7 +561,7 @@ func TestCurrentVersionPopulationOnChannelCache(t *testing.T) {
561561
base.SetUpTestLogging(t, base.LevelDebug, base.KeyCRUD, base.KeyImport, base.KeyDCP, base.KeyCache, base.KeyHTTP)
562562
db, ctx := setupTestDB(t)
563563
defer db.Close(ctx)
564-
collection := GetSingleDatabaseCollectionWithUser(t, db)
564+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
565565
collectionID := collection.GetCollectionID()
566566
bucketUUID := db.BucketUUID
567567
collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout)

db/crud.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -314,14 +314,29 @@ func (db *DatabaseCollectionWithUser) getRev(ctx context.Context, docid, revid s
314314
// No rev ID given, so load active revision
315315
revision, err = db.revisionCache.GetActive(ctx, docid)
316316
}
317-
318317
if err != nil {
319318
return DocumentRevision{}, err
320319
}
321320

321+
return db.documentRevisionForRequest(ctx, docid, revision, &revid, nil, maxHistory, historyFrom)
322+
}
323+
324+
// documentRevisionForRequest processes the given DocumentRevision and returns a version of it for a given client request, depending on access, deleted, etc.
325+
func (db *DatabaseCollectionWithUser) documentRevisionForRequest(ctx context.Context, docID string, revision DocumentRevision, revID *string, cv *Version, maxHistory int, historyFrom []string) (DocumentRevision, error) {
326+
// ensure only one of cv or revID is specified
327+
if cv != nil && revID != nil {
328+
return DocumentRevision{}, fmt.Errorf("must have one of cv or revID in documentRevisionForRequest (had cv=%v revID=%v)", cv, revID)
329+
}
330+
var requestedVersion string
331+
if revID != nil {
332+
requestedVersion = *revID
333+
} else if cv != nil {
334+
requestedVersion = cv.String()
335+
}
336+
322337
if revision.BodyBytes == nil {
323338
if db.ForceAPIForbiddenErrors() {
324-
base.InfofCtx(ctx, base.KeyCRUD, "Doc: %s %s is missing", base.UD(docid), base.MD(revid))
339+
base.InfofCtx(ctx, base.KeyCRUD, "Doc: %s %s is missing", base.UD(docID), base.MD(requestedVersion))
325340
return DocumentRevision{}, ErrForbidden
326341
}
327342
return DocumentRevision{}, ErrMissing
@@ -340,16 +355,17 @@ func (db *DatabaseCollectionWithUser) getRev(ctx context.Context, docid, revid s
340355
_, requestedHistory = trimEncodedRevisionsToAncestor(ctx, requestedHistory, historyFrom, maxHistory)
341356
}
342357

343-
isAuthorized, redactedRev := db.authorizeUserForChannels(docid, revision.RevID, revision.Channels, revision.Deleted, requestedHistory)
358+
isAuthorized, redactedRevision := db.authorizeUserForChannels(docID, revision.RevID, cv, revision.Channels, revision.Deleted, requestedHistory)
344359
if !isAuthorized {
345-
if revid == "" {
360+
// client just wanted active revision, not a specific one
361+
if requestedVersion == "" {
346362
return DocumentRevision{}, ErrForbidden
347363
}
348364
if db.ForceAPIForbiddenErrors() {
349-
base.InfofCtx(ctx, base.KeyCRUD, "Not authorized to view doc: %s %s", base.UD(docid), base.MD(revid))
365+
base.InfofCtx(ctx, base.KeyCRUD, "Not authorized to view doc: %s %s", base.UD(docID), base.MD(requestedVersion))
350366
return DocumentRevision{}, ErrForbidden
351367
}
352-
return redactedRev, nil
368+
return redactedRevision, nil
353369
}
354370

355371
// If the revision is a removal cache entry (no body), but the user has access to that removal, then just
@@ -358,13 +374,26 @@ func (db *DatabaseCollectionWithUser) getRev(ctx context.Context, docid, revid s
358374
return DocumentRevision{}, ErrMissing
359375
}
360376

361-
if revision.Deleted && revid == "" {
377+
if revision.Deleted && requestedVersion == "" {
362378
return DocumentRevision{}, ErrDeleted
363379
}
364380

365381
return revision, nil
366382
}
367383

384+
func (db *DatabaseCollectionWithUser) GetCV(ctx context.Context, docid string, cv *Version, includeBody bool) (revision DocumentRevision, err error) {
385+
if cv != nil {
386+
revision, err = db.revisionCache.GetWithCV(ctx, docid, cv, RevCacheOmitDelta)
387+
} else {
388+
revision, err = db.revisionCache.GetActive(ctx, docid)
389+
}
390+
if err != nil {
391+
return DocumentRevision{}, err
392+
}
393+
394+
return db.documentRevisionForRequest(ctx, docid, revision, nil, cv, 0, nil)
395+
}
396+
368397
// GetDelta attempts to return the delta between fromRevId and toRevId. If the delta can't be generated,
369398
// returns nil.
370399
func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromRevID, toRevID string) (delta *RevisionDelta, redactedRev *DocumentRevision, err error) {
@@ -396,7 +425,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
396425
if fromRevision.Delta != nil {
397426
if fromRevision.Delta.ToRevID == toRevID {
398427

399-
isAuthorized, redactedBody := db.authorizeUserForChannels(docID, toRevID, fromRevision.Delta.ToChannels, fromRevision.Delta.ToDeleted, encodeRevisions(ctx, docID, fromRevision.Delta.RevisionHistory))
428+
isAuthorized, redactedBody := db.authorizeUserForChannels(docID, toRevID, nil, fromRevision.Delta.ToChannels, fromRevision.Delta.ToDeleted, encodeRevisions(ctx, docID, fromRevision.Delta.RevisionHistory))
400429
if !isAuthorized {
401430
return nil, &redactedBody, nil
402431
}
@@ -419,7 +448,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
419448
}
420449

421450
deleted := toRevision.Deleted
422-
isAuthorized, redactedBody := db.authorizeUserForChannels(docID, toRevID, toRevision.Channels, deleted, toRevision.History)
451+
isAuthorized, redactedBody := db.authorizeUserForChannels(docID, toRevID, nil, toRevision.Channels, deleted, toRevision.History)
423452
if !isAuthorized {
424453
return nil, &redactedBody, nil
425454
}
@@ -478,7 +507,7 @@ func (db *DatabaseCollectionWithUser) GetDelta(ctx context.Context, docID, fromR
478507
return nil, nil, nil
479508
}
480509

481-
func (col *DatabaseCollectionWithUser) authorizeUserForChannels(docID, revID string, channels base.Set, isDeleted bool, history Revisions) (isAuthorized bool, redactedRev DocumentRevision) {
510+
func (col *DatabaseCollectionWithUser) authorizeUserForChannels(docID, revID string, cv *Version, channels base.Set, isDeleted bool, history Revisions) (isAuthorized bool, redactedRev DocumentRevision) {
482511

483512
if col.user != nil {
484513
if err := col.user.AuthorizeAnyCollectionChannel(col.ScopeName, col.Name, channels); err != nil {
@@ -490,6 +519,7 @@ func (col *DatabaseCollectionWithUser) authorizeUserForChannels(docID, revID str
490519
RevID: revID,
491520
History: history,
492521
Deleted: isDeleted,
522+
CV: cv,
493523
}
494524
if isDeleted {
495525
// Deletions are denoted by the deleted message property during 2.x replication
@@ -1045,7 +1075,7 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
10451075
if existingDoc != nil {
10461076
doc, unmarshalErr := db.unmarshalDocumentWithXattrs(ctx, newDoc.ID, existingDoc.Body, existingDoc.Xattrs, existingDoc.Cas, DocUnmarshalRev)
10471077
if unmarshalErr != nil {
1048-
return nil, nil, "", base.HTTPErrorf(http.StatusBadRequest, "Error unmarshaling exsiting doc")
1078+
return nil, nil, "", base.HTTPErrorf(http.StatusBadRequest, "Error unmarshaling existing doc")
10491079
}
10501080
matchRev = doc.CurrentRev
10511081
}

db/crud_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
sgbucket "github.com/couchbase/sg-bucket"
2222
"github.com/couchbase/sync_gateway/base"
23+
"github.com/couchbase/sync_gateway/channels"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
)
@@ -1957,3 +1958,181 @@ func TestPutExistingCurrentVersionWithNoExistingDoc(t *testing.T) {
19571958
assert.True(t, reflect.DeepEqual(syncData.HLV.PreviousVersions, pv))
19581959
assert.Equal(t, "1-3a208ea66e84121b528f05b5457d1134", syncData.CurrentRev)
19591960
}
1961+
1962+
// TestGetCVWithDocResidentInCache:
1963+
// - Two test cases, one with doc a user will have access to, one without
1964+
// - Purpose is to have a doc that is resident in rev cache and use the GetCV function to retrieve these docs
1965+
// - Assert that the doc the user has access to is corrected fetched
1966+
// - Assert the doc the user doesn't have access to is fetched but correctly redacted
1967+
func TestGetCVWithDocResidentInCache(t *testing.T) {
1968+
const docID = "doc1"
1969+
1970+
testCases := []struct {
1971+
name string
1972+
docChannels []string
1973+
access bool
1974+
}{
1975+
{
1976+
name: "getCVWithUserAccess",
1977+
docChannels: []string{"A"},
1978+
access: true,
1979+
},
1980+
{
1981+
name: "getCVWithoutUserAccess",
1982+
docChannels: []string{"B"},
1983+
access: false,
1984+
},
1985+
}
1986+
for _, testCase := range testCases {
1987+
t.Run(testCase.name, func(t *testing.T) {
1988+
db, ctx := setupTestDB(t)
1989+
defer db.Close(ctx)
1990+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
1991+
collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout)
1992+
1993+
// Create a user with access to channel A
1994+
authenticator := db.Authenticator(base.TestCtx(t))
1995+
user, err := authenticator.NewUser("alice", "letmein", channels.BaseSetOf(t, "A"))
1996+
require.NoError(t, err)
1997+
require.NoError(t, authenticator.Save(user))
1998+
collection.user, err = authenticator.GetUser("alice")
1999+
require.NoError(t, err)
2000+
2001+
// create doc with the channels for the test case
2002+
docBody := Body{"channels": testCase.docChannels}
2003+
rev, doc, err := collection.Put(ctx, docID, docBody)
2004+
require.NoError(t, err)
2005+
2006+
vrs := doc.HLV.Version
2007+
src := doc.HLV.SourceID
2008+
sv := &Version{Value: vrs, SourceID: src}
2009+
revision, err := collection.GetCV(ctx, docID, sv, true)
2010+
require.NoError(t, err)
2011+
if testCase.access {
2012+
assert.Equal(t, rev, revision.RevID)
2013+
assert.Equal(t, sv, revision.CV)
2014+
assert.Equal(t, docID, revision.DocID)
2015+
assert.Equal(t, []byte(`{"channels":["A"]}`), revision.BodyBytes)
2016+
} else {
2017+
assert.Equal(t, rev, revision.RevID)
2018+
assert.Equal(t, sv, revision.CV)
2019+
assert.Equal(t, docID, revision.DocID)
2020+
assert.Equal(t, []byte(RemovedRedactedDocument), revision.BodyBytes)
2021+
}
2022+
})
2023+
}
2024+
}
2025+
2026+
// TestGetByCVForDocNotResidentInCache:
2027+
// - Setup db with rev cache size of 1
2028+
// - Put two docs forcing eviction of the first doc
2029+
// - Use GetCV function to fetch the first doc, forcing the rev cache to load the doc from bucket
2030+
// - Assert the doc revision fetched is correct to the first doc we created
2031+
func TestGetByCVForDocNotResidentInCache(t *testing.T) {
2032+
db, ctx := SetupTestDBWithOptions(t, DatabaseContextOptions{
2033+
RevisionCacheOptions: &RevisionCacheOptions{
2034+
Size: 1,
2035+
},
2036+
})
2037+
defer db.Close(ctx)
2038+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
2039+
collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout)
2040+
2041+
// Create a user with access to channel A
2042+
authenticator := db.Authenticator(base.TestCtx(t))
2043+
user, err := authenticator.NewUser("alice", "letmein", channels.BaseSetOf(t, "A"))
2044+
require.NoError(t, err)
2045+
require.NoError(t, authenticator.Save(user))
2046+
collection.user, err = authenticator.GetUser("alice")
2047+
require.NoError(t, err)
2048+
2049+
const (
2050+
doc1ID = "doc1"
2051+
doc2ID = "doc2"
2052+
)
2053+
2054+
revBody := Body{"channels": []string{"A"}}
2055+
rev, doc, err := collection.Put(ctx, doc1ID, revBody)
2056+
require.NoError(t, err)
2057+
2058+
// put another doc that should evict first doc from cache
2059+
_, _, err = collection.Put(ctx, doc2ID, revBody)
2060+
require.NoError(t, err)
2061+
2062+
// get by CV should force a load from bucket and have a cache miss
2063+
vrs := doc.HLV.Version
2064+
src := doc.HLV.SourceID
2065+
sv := &Version{Value: vrs, SourceID: src}
2066+
revision, err := collection.GetCV(ctx, doc1ID, sv, true)
2067+
require.NoError(t, err)
2068+
2069+
// assert the fetched doc is the first doc we added and assert that we did in fact get cache miss
2070+
assert.Equal(t, int64(1), db.DbStats.Cache().RevisionCacheMisses.Value())
2071+
assert.Equal(t, rev, revision.RevID)
2072+
assert.Equal(t, sv, revision.CV)
2073+
assert.Equal(t, doc1ID, revision.DocID)
2074+
assert.Equal(t, []byte(`{"channels":["A"]}`), revision.BodyBytes)
2075+
}
2076+
2077+
// TestGetCVActivePathway:
2078+
// - Two test cases, one with doc a user will have access to, one without
2079+
// - Purpose is top specify nil CV to the GetCV function to force the GetActive code pathway
2080+
// - Assert doc that is created is fetched correctly when user has access to doc
2081+
// - Assert that correct error is returned when user has no access to the doc
2082+
func TestGetCVActivePathway(t *testing.T) {
2083+
const docID = "doc1"
2084+
2085+
testCases := []struct {
2086+
name string
2087+
docChannels []string
2088+
access bool
2089+
}{
2090+
{
2091+
name: "activeFetchWithUserAccess",
2092+
docChannels: []string{"A"},
2093+
access: true,
2094+
},
2095+
{
2096+
name: "activeFetchWithoutUserAccess",
2097+
docChannels: []string{"B"},
2098+
access: false,
2099+
},
2100+
}
2101+
for _, testCase := range testCases {
2102+
t.Run(testCase.name, func(t *testing.T) {
2103+
db, ctx := setupTestDB(t)
2104+
defer db.Close(ctx)
2105+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
2106+
collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout)
2107+
2108+
// Create a user with access to channel A
2109+
authenticator := db.Authenticator(base.TestCtx(t))
2110+
user, err := authenticator.NewUser("alice", "letmein", channels.BaseSetOf(t, "A"))
2111+
require.NoError(t, err)
2112+
require.NoError(t, authenticator.Save(user))
2113+
collection.user, err = authenticator.GetUser("alice")
2114+
require.NoError(t, err)
2115+
2116+
// test get active path by specifying nil cv
2117+
revBody := Body{"channels": testCase.docChannels}
2118+
rev, doc, err := collection.Put(ctx, docID, revBody)
2119+
require.NoError(t, err)
2120+
revision, err := collection.GetCV(ctx, docID, nil, true)
2121+
2122+
if testCase.access == true {
2123+
require.NoError(t, err)
2124+
vrs := doc.HLV.Version
2125+
src := doc.HLV.SourceID
2126+
sv := &Version{Value: vrs, SourceID: src}
2127+
assert.Equal(t, rev, revision.RevID)
2128+
assert.Equal(t, sv, revision.CV)
2129+
assert.Equal(t, docID, revision.DocID)
2130+
assert.Equal(t, []byte(`{"channels":["A"]}`), revision.BodyBytes)
2131+
} else {
2132+
require.Error(t, err)
2133+
assert.ErrorContains(t, err, ErrForbidden.Error())
2134+
assert.Equal(t, DocumentRevision{}, revision)
2135+
}
2136+
})
2137+
}
2138+
}

db/database_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1839,7 +1839,7 @@ func TestChannelQuery(t *testing.T) {
18391839

18401840
db, ctx := setupTestDB(t)
18411841
defer db.Close(ctx)
1842-
collection := GetSingleDatabaseCollectionWithUser(t, db)
1842+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
18431843
_, err := collection.UpdateSyncFun(ctx, `function(doc, oldDoc) {
18441844
channel(doc.channels);
18451845
}`)

0 commit comments

Comments
 (0)