Removing a channel from a doc


#1

Hi All,

I’m writing an app for my company that allows users to create books (of documents) and share those books to others. Each user has a GUID for a unique ID, and each book also has a GUID.

I’ve created a “channel” property in each book that contains the owner’s GUID, which therefore gives that user access. A book owner can then share that document to others, and the app adds the recipients GUID to the book’s channel property.

So far, so good. This works (sync gateway is really awesome). Both users can see the books and documents and make changes.

Now I want to allow users to ‘unlink’ to a document - that is I want to allow the recipient above to remove their own access to that document leaving only the owner. I was hoping that would be as simple as removing their own GUID from the channels property, right?

Unfortunately… it doesn’t seem like that is the case…

Below is a dump of a books object from Couchbase after:
a) book create by owner: D48F785B-18D4-45B1-8E92-A7A00E1C7EF1
b) owner shares with user: 05110569-52D4-41CE-9305-21D4023540A1
c) user 05110569-52D4-41CE-9305-21D4023540A1 then unlinks (i.e. removes their ID from the channels property).

Might be a little hard to see, but it seems like there are 3 channel properties:

  • my one at the top level of the book. That now correctly only has D48…E71
  • another in the history property (which shows the history of the channels)
  • another in the _sync property.

This last one doesn’t seem to update properly?
In any case - after removing the 0511…0A1 user from the channels property, they still have access to the book. I’ve tried deleting all local data then pulling down a fresh copy, and the book is still included.

NOTE: I have read that the right approach to remove the local property is to purge the doc - the problem is that the purge seems to happen before the change to channels syncs up to the server. So no channels changes go up.

So here is my questions:
a) Am I using the right approach for setting the channel for a document?
(or is it better for me to use a sync function (I’ve tried both. The docs imply either should work)
b) What is the right way for a user to remove their own access to a document - is it to simply remove their GUID from the channels array? If so… when do I purge the doc locally?

I appreciate any advice you can give.
Cheers.
Paul

{
  "_sync": {
    "rev": "5-3e6363d7aa53e2f2a48ce4a776aedae6",
    "sequence": 64,
    "recent_sequences": [
      41,
      46,
      53,
      60,
      64
    ],
    "history": {
      "revs": [
        "5-3e6363d7aa53e2f2a48ce4a776aedae6",
        "3-0f3fab2e99abcc95ff3d52164db49c80",
        "4-3b263198ad7386f7d65398b2f2d52910",
        "1-85731b3571c156ea402421ff6854478c",
        "2-2dda576fde32e70ea086967931d7c982"
      ],
      "parents": [
        2,
        4,
        1,
        -1,
        3
      ],
      "channels": [
        [
          "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
        ],
        [
          "05110569-52D4-41CE-9305-21D4023540A1",
          "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
        ],
        [
          "05110569-52D4-41CE-9305-21D4023540A1",
          "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
        ],
        [
          "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
        ],
        [
          "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
        ]
      ]
    },
    "channels": {
      "05110569-52D4-41CE-9305-21D4023540A1": {
        "seq": 64,
        "rev": "5-3e6363d7aa53e2f2a48ce4a776aedae6"
      },
      "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1": null
    },
    "time_saved": "2016-05-23T15:33:54.273112245+12:00"
  },
  "bid": "EABF380C-8FBE-482B-892A-E3DAEA7797EB",
  "channels": [
    "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1"
  ],
  "owner_id": "D48F785B-18D4-45B1-8E92-A7A00E1C7EF1",
  "owner_name": "John Doe",
  "title": "Shared Book",
  "type": "book",
  "version": 2
}

#2

For clarification… here is my sync config:
{ "log": ["*"], "interface": ":4984", "adminInterface": ":4985", "databases": { "private": { "users": { "GUEST": {"disabled": false, "admin_channels": ["*"] } }, "server": "http://localhost:8091", "username": "mytestdb", "password": "password", "bucket": "mytestdb", "sync": function(doc, oldDoc) { channel(doc.channels); } } } }

So to be clear - I am using the sync function, even though the docs say you can either use a sync function or a channels property.


#3

@paulr

It’s possible that your clients are not authenticating with Sync Gateway and defaulting to the enabled GUEST account.

Try disabling the GUEST account in your Sync Gateway config.

"GUEST": {"disabled": true, "admin_channels": [] }

Also make sure that your clients are syncing against the Public REST API on port 4984 and not the Admin port on 4985.

You can use curl to debug the document access for a user using basic auth, first call _all_docs on the Admin REST API to get a list of all documents in your DB.

curl -X GET http://localhost:4985/db/_all_docs

Now run the same command for a named user account to see if they have access to the expected subset of documents.

curl -X GET http://someuser:somepassword@localhost:4984/db/_all_docs


#4

Hi Andy,
First - thanks for the response

I am authenticating, but it is just with a common password. Here’s the swift code that sets up the sync:

let gServerAddress = "10.0.2.175"
let gSyncURL = "http://\(gServerAddress):4984/mytestdb/"
let gServerURL = NSURL(string: gSyncURL)!

var _push: CBLReplication!
var _pull: CBLReplication!

// Set up the local DB
gDBMgr = try CBLManager(directory: folder!, options:nil) 
gDB = try! gDBMgr.databaseNamed(kDatabaseName)

 // ... stuff to set up views omitted...

// Set up replication
_push = setupReplication(gDB.createPushReplication(gServerURL))
_pull = setupReplication(gDB.createPullReplication(gServerURL))
_push.continuous = true
_pull.continuous = true
var auth: CBLAuthenticatorProtocol?
auth = CBLAuthenticator.basicAuthenticatorWithName("mytestdb", password: "password")
_push.authenticator = auth
_pull.authenticator = auth
_pull.channels = [UserManager.user.id]
_push.start()
_pull.start()

I haven’t really dealt with authentication properly yet. As you can see - I am logging in to the sync gateway with the same credentials the sync gateway uses to connect to couchbase (“bucket name” and “password”).

So I tried disabling the GUEST user, and syncing stopped completely. Seems that syncing is using GUEST even though I specify auth credentials.

So I tried your two commands:
$ curl -X GET http://localhost:4985/mytestdb/_all_docs
returned all the docs.
$ curl -X GET http://mytestdb:password@localhost:4985/mytestdb/_all_docs
again returned all docs.

I obviously haven’t set up users… I’ve been using the “bucket” name and password (set when creating the bucked in Couchbase), and using the “pull.channels” to try and limit what the user sees.

I’ll have to do some reading about setting up users and managing them with syncing.

Here’s my use case:
a) a few thousand users
b) each user “owns” a set of books containing some docs. Only they have access to those books (limited by channels)
c) each user can share a book to one or more other users. The owner adds other users id to the channels for the books/docs
d) Either the owner can stop sharing or the other users can “unlink”. At which point the books and docs should disappear. To make the docs disappear, I need to modify their channels, and purge them from the local DBs (unless syncing will naturally remove them when the channels change).

Can you point me to any examples of this kinds of use?
Again - many thanks for your help.
Paul.


#5

@paulr

The second curl command should be against the Public REST API on port :4984, then I expect you would get an authentication error.

For client validation you can create a user in the Sync Gateway config (only use this method for dev) e.g.

"foo": {"disabled": false, "password":"foo", "admin_channels": ["foo"]}

Then create a subset of documents that map to the foo channel:
"channels":["foo"];

Then rerun the curl command against the Public REST API:

curl -X GET http://foo:foo@serverhost:4984/mytestdb/_all_docs

Once you have validated that you only see the subset of documents, your Sync Gateway is correctly set up and you can update your test client code to.

auth = CBLAuthenticator.basicAuthenticatorWithName("foo", password: "foo")


#6

Got it.

So I create users in sync gateway when they first run the app and enter their details. According to the online docs - it is done using the Admin REST API, and “you need to have some other server side mechanism that calls through to this API”.

So I’ll need to create a REST service that exposes a “user create” API. (I already have one that presents some public keys stored in Couchbase).

When it creates a user named “ABC”, it will also set the users’ admin_channels property to [“ABC”], which will give the user access to docs with a channel property of [“ABC”].

I’ll get that going tomorrow.

One other question… from a “best practice” point of view - does what I’m doing make sense?
Is it a good idea to add channels to docs to grant users access. Is that scalable? Secure?
So each user will have a single “admin channel”.
Each book/doc will have one or more “channels” (i.e. user IDs) based on who that doc has been shared with?

I’ll basically have one channel per user, and each doc will have 1 channel per user that has access.

Thanks.
Paul.


#7

Ok. Have authentication running sweetly - I have a REST service that apps can contact to get user accounts created via the Sync REST API. When users are created, I set their “admin_channels” to their ID to give them access to their docs. But I still have the original issue with syncing not cleaning out docs when channels change.

Here’s the situation:
0) Users are created with access to a channel with the same id as them. That is…
user: id=“abc”, admin_channels=[“abc”]
a) User 1 (id=“123”, admin_channels=[“123”]) creates a document. Channels for the document are [“123”].
b) User 1 shares the document with user 2 (id=“456”, admin_channels=[“456”]). He sets the channels for the document to [“123”, “456”]. Now both of them have access to the document.
c) Later, user 2 decides he doesn’t want access to the document anymore. He removes his channel from the book. Now the book has channels [“123”]. That change properly propagates to the server, and to user1. That’s pretty slick.

But here is the problem. the document stays in user 2’s local database, even though user 2 no longer should have access to it. The local record correctly does not have user 2’s ID in its channel list, so should not be available. In fact, it never goes away.

Possible solutions:
a) purge. Problem is that we need to wait for the sync to happen (and it may be much later if the client is offline). Otherwise the change to channels never propagates to the server. The purge must happen at some indeterminate point of time in the future after the sync.
b) find a way to include the channel as a filter on the views I used to fetch data locally (I’m currently filtering on a “type” field).

Anyone have any ideas as to how an app can remove itself from access to a document, and to have that document be removed from the local db?

Thanks and Cheers.
Paul.


#8

This has been a limitation of Sync Gateway: when a client loses access to a channel, it doesn’t get notified of all the docs it loses access to. It’s been a hard design problem and I don’t know if the team has figured it out yet; @adamf, can you add anything?

If the client has initiated the removal, then it can purge the docs, although as you point out it has to do so after the push completes. That’s something the client can schedule internally. You could keep a set of doc IDs to purge, and when a push updates, use the -isDocumentPending: method to check whether a doc can be purged yet.


#9

@jens is correct that Sync Gateway doesn’t currently have a way to provide notification about removed documents when a user loses access to a channel.

However, your scenario is a bit different - in this case it’s just the document being removed from a channel, and not the user being removed from the channel. Sync Gateway does provide notification in that scenario (sending a _removed version of the document, to indicate to the client that the doc has been removed from the channel).

The problem you’re hitting, though, is that the user already has a copy of the removed document, because they were the one that initiated the removal. We’ve discussed this in the past (https://github.com/couchbase/couchbase-lite-ios/issues/671#issuecomment-112664530), but don’t have an obvious solution at this point (as clients will not always want to remove locally created documents).

The best approach currently would be the one Jens suggests - have the client manage it’s own purge of the documents.


#10

Ok. Understood. This is a difficult problem.

My proposed solution is as follows:
a) I have a single document containing details of the user in the local db (so its easy to find and get the user’s ID)
b) I propose that I use the view to hide any documents that I no longer have access to.
c) So all my views will become something like this:

get the user info document (type=="user"). // This gives me the user id
if user.id in doc.channels then
    emit(doc.type, nil)

d) I will then force the views to be recalculated when I remove my access from a document
e) Lastly, I will periodically purge any docs I don’t have access to, after they have been sync’d using the -isDocumentPending call.

I’m not sure how to write the view so that it retrieves another doc yet or how to force views to be rebuilt… hints would be most welcome.

Do you think that will work?

Many thanks for the rapid and high quality responses I have received @adamf, @jens and @andy.
Cheers.
Paul.

p.s. one other point: thank you for the hint of -isDocumentPending. When I look in the documentation for Replication I don’t see it. But when I search for “isDocumentPending”, I found this which mentions it. If you hadn’t given me the name, I wouldn’t have known about it. There’s a few places where the docs seem a little behind where the product is at. For example: CBLDatabase.addChangeListener vs CBLDatabase.addObserver.

Having said that… couchbase and sync gateway have been awesome so far.


#11

The cross-platform API docs only cover the ‘official’ API. There are features (like that one) that aren’t implemented on all platforms yet, so they don’t show up in that documentation, only in the per-platform APIs.

To see the iOS/Mac APIs, you can look at the header files in the framework (or just Command-click the name of a class in Xcode), or view the HTML documentation that’s included in the download.


#12

Any methods that deal with notifications are going to vary greatly by platform, because every platform has its own built-in architecture for using them. For example Cocoa has NSNotification and Key-Value Observing, C# has Delegates and Events, Java has JavaBeans-based Listeners…


#13

Ok. Makes sense that there are differences between architectures.

BTW I didn’t realise there were local HTML files (in fact - just checked the couchbaselite and sync gateway downloads for OSX - neither have html files). I’ll hunt around.

Lastly I figured out a better (obvious) way to do the view. If I delay view creation until after the user info document is created, then I will know the unique user id. So now I can hard-code that local user id into all the views. Voila! All docs should disappear from view immediately my ID is removed from the channel property, even though the documents still live locally.

The new view pseudocode:

if "<longuniqueuserid>" in doc.channels then
    emit(doc.type, nil)

Cheers.
Paul.


#14

Works like a charm now. Both user1 can remove user2 from a doc, and user2 can remove themselves from a doc.

Sometime later I’ll have to write the code that removes dead data left lying in user2’s local db.

Cheers.
Paul.