Couchbase lite sync - and Access Removed

Hi

I often get notices like this on the console when debugging my app:

 Received ID: User:1D21AAE3DCBFEEB5C1258221004D2AFC (access removed)

Basically, a message from the replication that I have received an “access removed” notification…

History is that some time back I did replicate documents of type User, but I have changed that a while ago in the sync. gateway. I understand that for old installations that used to have these documents it makes sense they will get this notification.

However, when I create a new database by replication from the server (to be used as a built-in database packed with the app) and I have that installed in the app - then it seems to still do this type of replication (which takes time - the final number of User documents is still correct - only one).

Is there something I need to do to “compact” or otherwise stop this behaviour? The real problem is that it takes some time on first launch of the app and that is a little annoying to the user…

Edit:
Should have mentioned I’m on Coucbhase Lite 2.8.6, sync.gateway 2.8.2, and Coucbase CE 7.0.2 build 6703

@jda
You can take a look at our Replicator.AddDocumentReplicationListener (Class Replicator) that may solve your problem in CBL 2.8.x.
And in CBL 3.0, we added new property ReplicatorConfiguration.EnableAutoPurge (Class ReplicatorConfiguration) that may also be helpful to you.

Hi Sandy

I’ve been off work over Easter. Thanks for your reply. I actually do have a document listener:

docListener = replicator.AddDocumentReplicationListener((sender, args) =>
{
    if (args.IsPush)
    {
        Logger.Debug($"Push replication finished sending {args.Documents.Count} documents");
        foreach (var document in args.Documents)
        {
            if (document.Error == null)
            {
                pushCount++;
                Logger.Debug($"  Sent ID: {document.Id}" + (document.Flags.HasFlag(DocumentFlags.Deleted) ? " (deletion)" : "") + (document.Flags.HasFlag(DocumentFlags.AccessRemoved) ? " (access removed)" : ""));

… this is actually where I write the information about the access having been removed.

Should I manually purge those documents?

This is what confuses me as when I create a new database locally by replicating everything from the server I don’t understand why I would receive those documents in the first place and then loose access to them subsequently… These documents were replicated in a much earlier version of the database but have since been taken out via the sync.formula on the sync.gateway.

I appreciate this could be rather complicated - but I still have some “dark spots” where I don’t fully understand what (or perhaps more “why”) is going on in the full picture of replication.

@jda
You may find the info in this wiki (couchbase-lite-core/replication-protocol.adoc at master · couchbase/couchbase-lite-core · GitHub) helpful.
And you can find the Purge API here (Class Database).

Hmmm… I think I may need a more overview/conceptual description instead of just the listing of the different low level message types. E.g. it says nothing about having to purge in the situation I originally asked for… - or I just couldn’t find it :slight_smile:

Hi @jda you might want to review the entire import process and shared bucket access in our documentation:

I did notice you posted you are using the CE edition, and I would note that the default configuration is different between the community edition and enterprise edition:

I wouldn’t want to recommend a solution without knowing more about the situation like your full configuration because sync works differently based on how you have things configured. At worse case, turning on verbose logging will dump a ton of information out that usually can give a very detailed view into exactly why this is happening, although I would admit the logs can be VERY detailed and without a good understanding of how sync works, they can look EXTREMELY confusing.

Thanks,
Aaron

Hi Aaron

I missed your reply in the rush…

This is my sync_gateway.json:

{
	"maxFileDescriptors": 250000,
	"logging": {
		"console": {
			"color_enabled": true,
			"log_keys": ["HTTP+", "Sync"]
			}
		},
	"adminInterface": "0.0.0.0:4985",
	"interface": "0.0.0.0:4984",
	"databases": {
		"data": {
			"use_views":false,
			"num_index_replicas":0,
			"bucket": "data",
			"server": "http://db1,db2:8091",
			"username": "remoteUser",
			"password": "........",
			"enable_shared_bucket_access": true,
			"import_docs": true,
            "users": { "GUEST": { "disabled": true, "admin_channels": ["!"] } },
            "import_filter": `
            function(doc) {
                // Some document types not allowed on mobile
                if (doc.type == 'EnvLake' || doc.type == 'EnvMeasurement' || doc.type == 'ActivityLog' || doc.type == 'Feedback' || doc.type == 'Image') {
                    return false;
                }
                return true;
            }
            `,
			"sync": `
			function (doc, oldDoc) {
                function _log(t) {
                    // Write to sg_info.log
                    console.log('' + t);
                }
                function _getUserKey(d) {
                    var key = null;
                    if (d) {
                        if (d.type == 'User') {
                            key = d.key;
                        } else {
                            key = d.userkey;
                        }
                    }
                    return key;
                }
            
				if (doc && doc._deleted) {
                    // Doc. deleted -> if public then require
					if(oldDoc){
	                    var userkey = _getUserKey(oldDoc);
	                    _log('delete doc id: ' + doc._id + ', userkey=' + userkey);
    	                if (userkey != null) {
		                    requireUser(userkey);
            	        } else {
                    	    requireAdmin();
						}
                    }
                    _log('doc deleted, id: ' + (doc._id || 'no id!') + ', ' + (oldDoc ? ('old key=' + oldDoc.key + ', userkey=' + userkey) : 'no oldDoc'));
                    return;
                }
                _log('doc id: ' + (doc._id || 'no id!') + ', ispublic: ' + doc.ispublic + ', userkey=' + doc.userkey + ', ' + (oldDoc ? (oldDoc._deleted ? 'oldDoc is deleted' : ('old key=' + oldDoc.key + ', oldDoc.userkey=' + oldDoc.userkey + ' update')) : ' creation'));
                // Document type is mandatory
                if (typeof doc.type === 'undefined') {
                    _log('Document type missing: ' + JSON.stringify(doc));
                    throw ({ forbidden: "Document type is required. id=" + doc._id });
                }
				if (doc.type == 'EnvLake' || doc.type == 'EnvMeasurement') {
                    throw ({ forbidden: "Document type '" + doc.type + "' not allowed on mobile. id=" + doc._id });
                }
                // Avoid sync'ing new fishery types
				if (typeof doc.fisherytype !== 'undefined' && doc.fisherytype !== '1') {
                    throw ({ forbidden: "Fishery type: '" + doc.fisherytype + "' not allowed on mobile. Document type '" + doc.type + "', id=" + doc._id });
                }
				// Document key is mandatory
                if (typeof doc.key === 'undefined') {
                    _log('Document key missing: ' + JSON.stringify(doc));
                    throw ({ forbidden: "Document key is required. id=" + doc._id });
                }
                // Update: Cannot allow change of type or key
                if (oldDoc != null && !oldDoc._deleted) {
                    // Update
                    if (oldDoc.type != doc.type) {
                        throw ({ forbidden: "Can't change doc type" });
                    }
                    if (oldDoc.key != doc.key) {
                        throw ({ forbidden: "Can't change doc key" });
                    }
                }
                // Document sync is disabled (used for type Image - but generic implementation)
                if (doc.issyncdisabled) {
                    throw ({ forbidden: "Sync. disabled for id=" + doc._id });
                }
				var includeForStats = false;
                var userkey = _getUserKey(doc);
                // All public docs (not deleted) are available in the app
                if (doc.ispublic && doc.deleted != true) {
                    _log('public, id: ' + (doc._id || 'no id!'));
                    channel('!');
                } else if (doc.type == 'FishingTrip' || doc.type == 'Catch') {
                    // All fishing trips and catches are available (for stats and quotas)
					includeForStats = (userkey == null);
                    _log('Trip/catch for user: ' + (doc.userkey || "<external>") + ', id: ' + (doc._id || 'no id!'));
                    channel('!');
                 }
                // All users are available (for stats)
// 2021.04.07/Jda - replaced by Summary doc.
//                if (doc.type == 'User' && doc.deleted != true) {
//                    _log('User doc, id: ' + (doc._id || 'no id!'));
//                    channel('!');
//                 }

                // Allow anyone to create a Feedback or Observation on the server
                if (oldDoc == null && userkey == null && (doc.type == 'Feedback' || doc.type == 'Observation')) {
                    _log('Created ' + doc.type + ': ' + (doc._id || 'no id!') + ', key: ' + doc.key + ' as anonymous user ');
                    return;
                }
                // Allow app user to create an ActivityLog on the server
                if (oldDoc == null && doc.type == 'ActivityLog') {
                    _log('Created ' + doc.type + ': ' + (doc._id || 'no id!') + ', key: ' + doc.key + ' for user: ' + doc.userkey);
                    return;
                }

                // Only non-public docs "owned" by user can be created/updated (and replicated)
                if (userkey != null) {
                    if (oldDoc != null && ! oldDoc._deleted) {
                        // Update
                        if (oldDoc.userkey && oldDoc.userkey != doc.userkey) {
                            throw ({ forbidden: "Can't change user key" });
                        }
                    }
                    _log('User owned, id: ' + (doc._id || 'no id!') + ', type: ' + doc.type + ', user: ' + userkey);
					if(doc.type != 'Image'){	// Do not send images TO mobile
	                    channel('channel.' + userkey);
					}
                    access(userkey, 'channel.' + userkey);
					requireUser(userkey);
                } else if (doc.ispublic || includeForStats) {
	                requireAdmin();
                } else {
                    // Creation/update without user
                    _log('Document type cannot be created without user key: ' + (doc.type === 'Image' ? doc._id : JSON.stringify(doc)));
                    throw ({ forbidden: "This document type cannot be created without user key. id=" + doc._id });
                }
             }
			`,
      		"allow_conflicts": false,
      		"revs_limit": 20
        }
    }
}

Server is now on 7.1.1 (CE) and sync.gateway 2.8.3. I see that there is a new sync gateway - but I also see quite some changes so I have not dared (!) to migrate over yet. There will need to be some advantages for the effort :wink:

One thing I observed is that it seem to be more complicated to edit the sync. formulas as that will have to be done via API calls - is this really correct or did I misunderstand something??

1 Like

Jda,

So my highly educated guess based on your posting and your config file is this is a revision thing where you are keeping the last twenty revisions of the document, and sync needs to make sure you have the latest. Without delta sync, it’s a full pull of the document to compare since the documents, revision information, and tombstone information is all stored in Couchbase Server (I can tell this from your config file: enabled_shared_bucket_access=true). If I ask for a document of type “User” where the Id = 1, it still has to go to the server, pull the document, and then the revision information (metadata) to make sure you have the latest version. Since this is all stored in CB Server, Sync Gateway will take a small amount of type to calculate what is the latest revision and then send only that down to the mobile client, which is what you are seeing.

Delta sync does help quite a bit with this, and it’s one of my favorite features:

I would note that Delta Sync is an Enterprise-only feature but is well worth it given how much time and bandwidth it can save. As for truncating versions, you are doing that in the config file when you set the number of revisions to save.

The new version of Sync Gateway is available, and one thing it does offer is a backward compatibility mode where you can use your older 2.8.3 config file with it, and it should just work, so moving to the newer version is less painful since you can use that older config file but get some of the newer features and fixes as part of upgrading. I would always recommend POCing the upgrade before just throwing it in somewhere like your Dev, Staging, or Production environment, but I’ve already done this in several of my projects, and the new version with my older config files works great.

Editing the sync function manually in your config file is time-consuming when you have several servers in your cluster, and you have to watch out for your escape characters, etc. Most customers do use the API for updating it as they can put it into automation scripts. It’s my preferred method just from an automation standpoint.

-Aaron

Hi Aaron

Thanks for a detailed explanation! It helps.

However, I’m not sure why I still get the the “access removed” for documents that shouldn’t be on the mobile any more. As you can see from the code commented out:

// All users are available (for stats)
// 2021.04.07/Jda - replaced by Summary doc.
//                if (doc.type == 'User' && doc.deleted != true) {
//                    _log('User doc, id: ' + (doc._id || 'no id!'));
//                    channel('!');
//                 }

Now I only sync. documents related directly to the user if the user is logged in. And that consists of ONE ‘User’ doc (in the key field) and a number of other documents owned by that user (in the userkey field). I have a small function to get that value and put in the userkey variable used:

if (userkey != null) {
	if (oldDoc != null && ! oldDoc._deleted) {
		// Update
		if (oldDoc.userkey && oldDoc.userkey != doc.userkey) {
			throw ({ forbidden: "Can't change user key" });
		}
	}
	_log('User owned, id: ' + (doc._id || 'no id!') + ', type: ' + doc.type + ', user: ' + userkey);
	if(doc.type != 'Image'){	// Do not send images TO mobile
		channel('channel.' + userkey);
	}
	access(userkey, 'channel.' + userkey);
	requireUser(userkey);
} else if (doc.ispublic || includeForStats) {
	requireAdmin();
} else {
	// Creation/update without user
	_log('Document type cannot be created without user key: ' + (doc.type === 'Image' ? doc._id : JSON.stringify(doc)));
	throw ({ forbidden: "This document type cannot be created without user key. id=" + doc._id });
}

So when I create a new builtin-database for the app I start by replicating all of the documents into an empty database - and therefore I don’t understand why it considers the User-docs that are now not part of the that the user’s sync. Is that perhaps due to it having been sync’ed before? Or being sync’ed for other users?

I appreciate the considerations for automation - but with one sync. gateway that is kind of an “overkill” :slight_smile: But I’ll certainly have a go at upgrading to the new version using the previous config file.