CouchDB authorization in a database

Eric Avdey
9 min readAug 18, 2018

--

Photo by Carl Raw on Unsplash

An attentive reader of my previous article might have noticed that when I was talking about securing a database I’ve updated its security object with a document that contained a role “_admin” and probably thought that this is some kind of a special “magical” attribute. Well, this is not the case! A role in a security object can be anything you want. No, really.

Preparations

Let’s start with a fresh docker instance of CouchDB. (There are a new version went out since my last article.)

$ docker run --rm --name wismut -it -d -p 5984:5984 couchdb:2.2.0
Unable to find image 'couchdb:2.2.0' locally
2.2.0: Pulling from library/couchdb
...
Status: Downloaded newer image for couchdb:2.2.0
28a80f4f481d48d1822ec19b10773e8c353ef80d9e3b4bb396ca64dd8b500b34
$ export db=http://127.0.0.1:5984$ cat ~/.curlrc
-s
-H Accept:application/json
-H Content-Type:application/json
$ curl $db | jq .
{
"couchdb": "Welcome",
"version": "2.2.0",
"git_sha": "2a16ec4",
"features": [
"pluggable-storage-engines",
"scheduler"
],
"vendor": {
"name": "The Apache Software Foundation"
}
}

Now let’s create a database with some docs so we have something to work with.

$ curl -X PUT $db/asgard
{"ok":true}
$ curl -X POST $db/asgard/_bulk_docs -d @- << EOF
{"docs": [
{"name": "fehu", "meaning": "wealth"},
{"name": "uruz", "meaning": "ox"},
{"name": "gebo", "meaning": "gift"},
{"name": "wunjo", "meaning": "joy"},
{"name": "isaz", "meaning": "ice"}
]}
EOF
[{"ok":true,"id":"3aa32cca4cccaab53f77339fcc000e94","rev":"1-5bb5c186c00ca4aa7e4f18db641f82a1"},{"ok":true,"id":"3aa32cca4cccaab53f77339fcc001be9","rev":"1-72a75baf3bca933a9ceacf0b9dbfd22c"},{"ok":true,"id":"3aa32cca4cccaab53f77339fcc001f7f","rev":"1-747c43f39bfd5c6e4fd2134763c4dcee"},{"ok":true,"id":"3aa32cca4cccaab53f77339fcc002e11","rev":"1-1727fd4ca2619b0e56a7e5e27641fae9"},{"ok":true,"id":"3aa32cca4cccaab53f77339fcc002faf","rev":"1-510cc6291d7b88d13d0287e2683cc4c0"}]

We’ll also need _users database and some users with it.

$ curl -X PUT $db/_users{"ok":true}$ curl -X POST $db/_users/_bulk_docs -d @- << EOF
{"docs": [
{"_id": "org.couchdb.user:odin", "name": "odin", "password": "odin", "roles": [], "type": "user"},
{"_id": "org.couchdb.user:frigg", "name": "frigg", "password": "frigg", "roles": [], "type": "user"},
{"_id": "org.couchdb.user:thor", "name": "thor", "password": "thor", "roles": [], "type": "user"}
]}
EOF
[{"ok":true,"id":"org.couchdb.user:odin","rev":"1-1d8025abb839d16f4b11e57bfcf6524a"},{"ok":true,"id":"org.couchdb.user:frigg","rev":"1-9ea892a2f9ca3493db2f9a445f4be6ac"},{"ok":true,"id":"org.couchdb.user:thor","rev":"1-34e10864f2f910527e51a153c5ef5b44"}]$ curl -X POST $db/_users/_bulk_docs -d @- << EOF
{"docs": [
{"_id": "org.couchdb.user:njord", "name": "njord", "password": "njord", "roles": [], "type": "user"},
{"_id": "org.couchdb.user:freyr", "name": "freyr", "password": "freyr", "roles": [], "type": "user"},
{"_id": "org.couchdb.user:freyja", "name": "freyja", "password": "freyja", "roles": [], "type": "user"}
]}
EOF
[{"ok":true,"id":"org.couchdb.user:njord","rev":"1-63528aa07964bd1b82577936e978d8d5"},{"ok":true,"id":"org.couchdb.user:freyr","rev":"1-886b6a0191a51a7ad1feb2bc69472f31"},{"ok":true,"id":"org.couchdb.user:freyja","rev":"1-95216428778d2d25a5f4bc75166f0e54"}]

All right, everything’s ready — game’s on.

Theory

In CouchDB a database security defined by so-called “security object” — a JSON document made of object with two elements "admins" and "members" each of which is an object with another two elements: "names" and "roles" with values of those been arrays.

That "roles" element here can be somehow misleading for people familiar with security model in RDBMS databases, for example MySQL, because there are a “role” is a named collection of privileges. This is different in CouchDB, it has only two roles in that sense and those are mentioned "admins" and "members".

A named collection of privileges "members" grants permissions to create, update, read and delete documents in the database. A named collection of privileges "admins" adds to those permissions an ability to create the indexes (also known as “design documents”) and modify a database security object. That’s it — no create or drop tables, no triggering of stored procedures, no GRANT OPTION. (Ok, there are also permission on setting a revisions limit of the database, but I’m not going to touch it here.)

CouchDB’s "roles" is something different, but to get there, first we need to take a look how “admins” and “members” work in real life.

Rules of Asgard

Protect your runes

To get a clean baseline we need to stop “admin party” and add a server admin, after all if everyone is admin it could be hard to see if anything is restricted.

$ curl -X PUT $db/_node/nonode@nohost/_config/admins/root -d '"god"'
""

Now let’s make Odin a member of Asgard, I think it is only appropriate.

$ curl -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": [],
"roles": []
},
"members": {
"names": ["odin"],
"roles": []
}
}
EOF
{"error":"error","reason":"no_majority"}

All right here is a first gotcha. That “no_majority” error? Is a big lie. What it actually meant to say was “not_authorized”, but since a node didn’t get a consensus, the error message got overwritten. Let’s try again, now as the server admin.

$ curl -u root:god -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": [],
"roles": []
},
"members": {
"names": ["odin"],
"roles": []
}
}
EOF
{"ok":true}

That’s better. To check it quickly:

$ curl $db/asgard/_all_docs{"error":"unauthorized","reason":"You are not authorized to access this db."}$ curl -u odin:odin $db/asgard/_all_docs{"total_rows":5,"offset":0,"rows":[
{"id":"d06204ec41af365f73970bb67600025e","key":"d06204ec41af365f73970bb67600025e","value":{"rev":"1-5bb5c186c00ca4aa7e4f18db641f82a1"}},
{"id":"d06204ec41af365f73970bb676000aa7","key":"d06204ec41af365f73970bb676000aa7","value":{"rev":"1-72a75baf3bca933a9ceacf0b9dbfd22c"}},
{"id":"d06204ec41af365f73970bb6760011bd","key":"d06204ec41af365f73970bb6760011bd","value":{"rev":"1-747c43f39bfd5c6e4fd2134763c4dcee"}},
{"id":"d06204ec41af365f73970bb676001a7f","key":"d06204ec41af365f73970bb676001a7f","value":{"rev":"1-1727fd4ca2619b0e56a7e5e27641fae9"}},
{"id":"d06204ec41af365f73970bb676001d4a","key":"d06204ec41af365f73970bb676001d4a","value":{"rev":"1-510cc6291d7b88d13d0287e2683cc4c0"}}
]}
$ curl -u odin:odin -X PUT $db/asgard/d06204ec41af365f73970bb67600025e?rev=1-5bb5c186c00ca4aa7e4f18db641f82a1 -d '{"name": "fehu", "meaning":"wealth"}'{"ok":true,"id":"d06204ec41af365f73970bb67600025e","rev":"2-c94375a00bf762e73d08a170617ba08d"}

Good, now Odin can read and modify the runes of Asgard. He not supposed to be able to change Asgard rules though.

$ curl -u odin:odin -X PUT $db/asgard/_security -d @- << EOF
> {
> "admins": {
> "names": ["odin"],
> "roles": []
> },
> "members": {
> "names": [],
> "roles": []
> }
> }
> EOF
{"error":"error","reason":"no_majority"}

Again: no_majority = not_authorized. The server admin to rescue.

curl -u root:god -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": ["odin"],
"roles": []
},
"members": {
"names": [],
"roles": []
}
}
EOF
{"ok":true}

And here is a second gotcha:

$ curl $db/asgard/_all_docs{"total_rows":5,"offset":0,"rows":[
{"id":"d06204ec41af365f73970bb67600025e","key":"d06204ec41af365f73970bb67600025e","value":{"rev":"2-c94375a00bf762e73d08a170617ba08d"}},
{"id":"d06204ec41af365f73970bb676000aa7","key":"d06204ec41af365f73970bb676000aa7","value":{"rev":"1-72a75baf3bca933a9ceacf0b9dbfd22c"}},
{"id":"d06204ec41af365f73970bb6760011bd","key":"d06204ec41af365f73970bb6760011bd","value":{"rev":"1-747c43f39bfd5c6e4fd2134763c4dcee"}},
{"id":"d06204ec41af365f73970bb676001a7f","key":"d06204ec41af365f73970bb676001a7f","value":{"rev":"1-1727fd4ca2619b0e56a7e5e27641fae9"}},
{"id":"d06204ec41af365f73970bb676001d4a","key":"d06204ec41af365f73970bb676001d4a","value":{"rev":"1-510cc6291d7b88d13d0287e2683cc4c0"}}
]}

Anyone can read docs in asgard again — what’s the deal with that? Well, an empty array for "names" in "members" doesn’t mean nobody, it means anybody. Weird, I know. Fortunately we have Asgard’s Admin Odin now, so he can fix that.

$ curl -u odin:odin -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": ["odin"],
"roles": []
},
"members": {
"names": ["frigg", "thor", "njord", "freyr", "freyja"],
"roles": []
}
}
EOF
{"ok":true}$ curl $db/asgard{"error":"unauthorized","reason":"You are not authorized to access this db."}$ curl -u thor:thor $db/asgard | jq .db_name
"asgard"

Index your runes, control your updates

While we on this let’s check another permission granted to “admins” — an ability to create indexes.

$ curl -u thor:thor -X PUT $db/asgard/_design/runes -d @- << EOF
{
"views": {
"name": {
"map": "function(doc) { emit(doc.name); }"
}
}
}
EOF
{"error":"forbidden","reason":"You are not a db or server admin."}# but Odin can do that...
curl -u odin:odin -X PUT $db/asgard/_design/runes -d @- << EOF
{
"views": {
"name": {
"map": "function(doc) { emit(doc.name); }"
}
}
}
EOF
{"ok":true,"id":"_design/runes","rev":"1-30aab27827e54bda77a5ac9ae6b72f6c"}# and then all the members can read from this new index
$ curl -u frigg:frigg $db/asgard/_design/runes/_view/name
{"total_rows":5,"offset":0,"rows":[
{"id":"d06204ec41af365f73970bb67600025e","key":"fehu","value":null},
{"id":"d06204ec41af365f73970bb6760011bd","key":"gebo","value":null},
{"id":"d06204ec41af365f73970bb676001d4a","key":"isaz","value":null},
{"id":"d06204ec41af365f73970bb676000aa7","key":"uruz","value":null},
{"id":"d06204ec41af365f73970bb676001a7f","key":"wunjo","value":null}
]}

This is cool, but what’s more interesting is that index aka design document can have a “document update validation” function for controlling, naturally, who and how can update documents in a database. Since there are no granular permission mechanism for docs access, it can act as a somehow substitute for that.

We’ll make sure that only Asir can edit runes in Asgard, which, again, I think is only appropriate.

$ curl -u odin:odin -X PUT $db/asgard/_design/runes?rev=1-30aab27827e54bda77a5ac9ae6b72f6c -d @- << EOF
{
"validate_doc_update": "function(newDoc, oldDoc, userCtx, secObj) { if ([\"odin\", \"frigg\",\"thor\"].indexOf(userCtx.name) !== -1) { return; } else { throw({forbidden: \"Only asir can edit runes!\"})}}",
"views": {
"name": {
"map": "function(doc) { emit(doc.name); }"
}
}
}
EOF
{"ok":true,"id":"_design/runes","rev":"2-7d8e13ad99c2d39d7307a52493b2ca66"}

Here before of a document update we are checking if user’s name is in a list of allowed users and if not returning an unauthorized error.

$ curl -u njord:njord $db/asgard/d06204ec41af365f73970bb67600025e{"_id":"d06204ec41af365f73970bb67600025e","_rev":"2-c94375a00bf762e73d08a170617ba08d","name":"fehu","meaning":"wealth"}$ curl -u njord:njord -X PUT $db/asgard/d06204ec41af365f73970bb67600025e?rev=2-c94375a00bf762e73d08a170617ba08d -d '{"name": "fehu", "meaning":"wealth"}'{"error":"forbidden","reason":"Only asir can edit runes!"}# but Frigg can do that
$ curl -u frigg:frigg -X PUT $db/asgard/d06204ec41af365f73970bb67600025e?rev=2-c94375a00bf762e73d08a170617ba08d -d '{"name": "fehu", "meaning":"wealth"}'
{"ok":true,"id":"d06204ec41af365f73970bb67600025e","rev":"3-7cd5a52be6add9f66d4f7a929ef61d19"}

This finally brings us to why we need "roles".

Roles of Asgard

Updating a security object every time you need to add or remove a user is a tedious and error prune process and it can get so much worse if there are tons of databases to manage. This is where "roles" come handy.

First let’s separate Asir from Vanir and update our users accordingly.

$ curl -u root:god -X POST $db/_users/_bulk_docs -d @- << EOF
{"docs": [
{"_id": "org.couchdb.user:odin", "_rev": "1-77c6bded1b2530bddd987952bdc6435d", "name": "odin", "password": "odin", "roles": ["asir"], "type": "user"},
{"_id": "org.couchdb.user:frigg", "_rev": "1-115e94ee69b277cf68d8b8a453b22fbf", "name": "frigg", "password": "frigg", "roles": ["asir"], "type": "user"},
{"_id": "org.couchdb.user:thor", "_rev": "1-83d7879a783ce32e005bbd558cf77654", "name": "thor", "password": "thor", "roles": ["asir"], "type": "user"}
]}
EOF
[{"ok":true,"id":"org.couchdb.user:odin","rev":"2-dde657d64d90446300ffc8dc7a7a4df9"},{"ok":true,"id":"org.couchdb.user:frigg","rev":"2-c1465e8ce9f26d72e0b3915387bf3f7a"},{"ok":true,"id":"org.couchdb.user:thor","rev":"2-8acbcf9b93ddd5b3ffcd8f0b48799e14"}]$ curl -u root:god -X POST $db/_users/_bulk_docs -d @- << EOF
{"docs": [
{"_id": "org.couchdb.user:njord", "_rev": "1-874452d507bb5e4c475159fcfc84c9b5", "name": "njord", "password":
"njord", "roles": ["vanir"], "type": "user"},
{"_id": "org.couchdb.user:freyr", "_rev": "1-84eb1944ec7185f2b77357333ed03c50", "name": "freyr", "password": "freyr", "roles": ["vanir"], "type": "user"},
{"_id": "org.couchdb.user:freyja", "_rev": "1-eee3eb4381835b86adfe21274c1f5fa1", "name": "freyja", "password": "freyja", "roles": ["vanir"], "type": "user"}
]}
EOF
[{"ok":true,"id":"org.couchdb.user:njord","rev":"2-c402551c890b2451d14019c1cb7dc4c9"},{"ok":true,"id":"org.couchdb.user:freyr","rev":"2-a71880fe8f9bae3f4e8513e32266aca5"},{"ok":true,"id":"org.couchdb.user:freyja","rev":"2-33a5343e87b8d2d6cf4d67bf2cfc48e2"}]

Now we can update our security object and just said that in Asgard Asir are admins and Vanir are just members (I know it’s not technically true, but they are all the gods after all)

$ curl -u odin:odin -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": [],
"roles": ["asir"]
},
"members": {
"names": [],
"roles": ["vanir"]
}
}
EOF
{"ok":true}# a quick check
$ curl $db/asgard/_all_docs
{"error":"unauthorized","reason":"You are not authorized to access this db."}$ curl -u odin:odin $db/asgard | jq .db_name
"asgard"
$ curl -u njord:njord $db/asgard | jq .db_name
"asgard"
# no coup d'état in Asgard!
$ curl -u njord:njord -X PUT $db/asgard/_security -d @- << EOF
{
"admins": {
"names": [],
"roles": ["vanir"]
},
"members": {
"names": [],
"roles": ["asir"]
}
}
EOF
{"error":"error","reason":"no_majority"}

Also let’s make Asgard runes read-only for Vanir

$ curl -u odin:odin -X PUT $db/asgard/_design/runes?rev=3-30e80882ae459af2974608c2d01053de -d @- << EOF
{
"validate_doc_update": "function(newDoc, oldDoc, userCtx, secObj) { if (userCtx.roles.indexOf('asir') !== -1) { return; } else { throw({forbidden: \"Only true Asir can edit runes!\"})}}",
"views": {
"name": {
"map": "function(doc) { emit(doc.name); }"
}
}
}
EOF
{"ok":true,"id":"_design/runes","rev":"4-85bc97af513366613e50e476d9252f8c"}$ curl -u njord:njord -X PUT $db/asgard/d06204ec41af365f73970bb67600025e?rev=3-7cd5a52be6add9f66d4f7a929ef61d19 -d '{"name": "fehu", "meaning":"wealth"}'{"error":"forbidden","reason":"Only true Asir can edit runes!"}$ curl -u frigg:frigg -X PUT $db/asgard/d06204ec41af365f73970bb67600025e?rev=3-7cd5a52be6add9f66d4f7a929ef61d19 -d '{"name": "fehu", "meaning":"wealth"}'{"ok":true,"id":"d06204ec41af365f73970bb67600025e","rev":"4-2b68af5062d97456910fd2fec6534c4f"}

In sense we reversed an assertion in our “validate_doc_update” function and instead of keeping inside of the design document a list of names we’d have to update with every new user we are checking for an attribute that should be changed outside of it. And this is what roles are about — adding another level of indirection, so changing user’s permissions will be a question of updating a user, not a question of updating all the databases this user need to access.

It is quite similar how groups work in UNIX — if you want to give a user sysadmin privileges you are not sharing root password, you are adding her to a group wheel at least if you in BSD world. So why not to call CouchDB “roles” “groups” instead?

Well, because they are kind of the roles too. If we define our roles as “_admin” and “_editor” and map them to “admins” and “members” we’ll get approximately the same model of how permission handled in RDBMS with RBAC model (in saintly managed RDBMS anyway). We can create a role “_reader” and update a validate update function to make a database read-only for the members of this role. We can even get creative, make use of schema-less nature of CouchDB and create editor role based on properties of the documents, for example a role “_post_editor” and role “_comment_editor” for a database that holds online forum documents with types “post” and “comment”. It’s not a perfect security model, but not the worst either.

Lies, damned lies and _admin role

This is a later update.

Ok, I should admit that I lied about “_admin” role not been special. Since all security in CouchDB rotates around an idea of a user contexts represented by a user document, a server admin also needs one. It gets composed on authentication and server admin assigned a special role “_admin” to distinguish her status. You can’t assign this role to a regular user making her a server admin thought.

$ curl -u root:god -X PUT $db/_users/org.couchdb.user:odin -d '{"name": "odin", "password": "odin", "roles": ["_admin"], "type": "user"}'{"error":"forbidden","reason":"No system roles (starting with underscore) in users db."}

However it can come handy in an update validation function, to tell server admin’s document update from an update from assigned database user.

Fin

All right, that’s it. Time to stop CouchDB and go home.

$ docker stop wismut
wismut
$ curl -v $db
* Rebuilt URL to: http://127.0.0.1:5984/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connection failed
* connect to 127.0.0.1 port 5984 failed: Connection refused
* Failed to connect to 127.0.0.1 port 5984: Connection refused
* Closing connection 0
$ unset db

Bye now.

--

--