diff --git a/db/crud.go b/db/crud.go index c168d5b79c..0fde5caf2f 100644 --- a/db/crud.go +++ b/db/crud.go @@ -546,8 +546,12 @@ func (col *DatabaseCollectionWithUser) authorizeDoc(ctx context.Context, doc *Do channelsForRev, ok := doc.channelsForRev(revid) if !ok { - // No such revision; let the caller proceed and return a 404 + // No such revision + // let the caller proceed and return a 404 return nil + } else if channelsForRev == nil { + // non-leaf (no channel info) - force 404 (caller would find the rev if it tried to look) + return ErrMissing } return col.user.AuthorizeAnyCollectionChannel(col.ScopeName, col.Name, channelsForRev) @@ -695,7 +699,7 @@ func (db *DatabaseCollectionWithUser) get1xRevFromDoc(ctx context.Context, doc * // Update: this applies to non-deletions too, since the client may have lost access to // the channel and gotten a "removed" entry in the _changes feed. It then needs to // incorporate that tombstone and for that it needs to see the _revisions property. - if revid == "" || doc.History[revid] == nil { + if revid == "" || doc.History[revid] == nil || err == ErrMissing { return nil, false, err } if doc.History[revid].Deleted { diff --git a/db/database_test.go b/db/database_test.go index 6baf24f73e..cfa7935efc 100644 --- a/db/database_test.go +++ b/db/database_test.go @@ -457,6 +457,70 @@ func TestIsServerless(t *testing.T) { } } +func TestUncachedOldRevisionChannel(t *testing.T) { + db, ctx := setupTestDB(t) + defer db.Close(ctx) + collection := GetSingleDatabaseCollectionWithUser(t, db) + collection.ChannelMapper = channels.NewChannelMapper(ctx, channels.DocChannelsSyncFunction, db.Options.JavascriptTimeout) + + auth := db.Authenticator(base.TestCtx(t)) + + userAlice, err := auth.NewUser("alice", "pass", base.SetOf("ABC")) + require.NoError(t, err, "Error creating user") + + collection.user = userAlice + + // Create the first revision of doc1. + rev1Body := Body{ + "k1": "v1", + "channels": []string{"ABC"}, + } + rev1ID, _, err := collection.Put(ctx, "doc1", rev1Body) + require.NoError(t, err, "Error creating doc") + + rev2Body := Body{ + "k2": "v2", + "channels": []string{"ABC"}, + BodyRev: rev1ID, + } + rev2ID, _, err := collection.Put(ctx, "doc1", rev2Body) + require.NoError(t, err, "Error creating doc") + + rev3Body := Body{ + "k3": "v3", + "channels": []string{"ABC"}, + BodyRev: rev2ID, + } + rev3ID, _, err := collection.Put(ctx, "doc1", rev3Body) + require.NoError(t, err, "Error creating doc") + require.NotEmpty(t, rev3ID, "Error creating doc") + + body, err := collection.Get1xRevBody(ctx, "doc1", rev2ID, true, nil) + require.NoError(t, err, "Error getting 1x rev body") + + // old rev was cached so still retains channel information + _, rev1Digest := ParseRevID(ctx, rev1ID) + _, rev2Digest := ParseRevID(ctx, rev2ID) + bodyExpected := Body{ + "k2": "v2", + "channels": []string{"ABC"}, + BodyRevisions: Revisions{ + RevisionsStart: 2, + RevisionsIds: []string{rev2Digest, rev1Digest}, + }, + BodyId: "doc1", + BodyRev: rev2ID, + } + require.Equal(t, bodyExpected, body) + + // Flush the revision cache to force load from backup revision + collection.FlushRevisionCacheForTest() + + // 404 because we lost the non-leaf channel information after cache flush + _, _, _, _, _, _, _, _, err = collection.Get1xRevAndChannels(ctx, "doc1", rev2ID, false) + assertHTTPError(t, err, 404) +} + // Test removal handling for unavailable multi-channel revisions. func TestGetRemovalMultiChannel(t *testing.T) { db, ctx := setupTestDB(t) diff --git a/db/document.go b/db/document.go index 54b3de226c..27688176a1 100644 --- a/db/document.go +++ b/db/document.go @@ -119,16 +119,13 @@ func (sd *SyncData) UnmarshalJSON(b []byte) error { // determine set of current channels based on removal entries. func (sd *SyncData) getCurrentChannels() base.Set { - if len(sd.Channels) > 0 { - ch := base.SetOf() - for channelName, channelRemoval := range sd.Channels { - if channelRemoval == nil || channelRemoval.Seq == 0 { - ch.Add(channelName) - } + ch := base.SetOf() + for channelName, channelRemoval := range sd.Channels { + if channelRemoval == nil || channelRemoval.Seq == 0 { + ch.Add(channelName) } - return ch } - return nil + return ch } func (sd *SyncData) HashRedact(salt string) SyncData {