CouchDB authorization in Nutshell

Preparations

Let’s start with a fresh instance of CouchDB.


$ docker run --rm -name wismut -it -d -p 5984:5984 couchdb:2.1.1
d51418b1b9de0903ac6ab0b526ee76e51da6d7c183a5dee227c91f843390019d
$ 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.1.1”,
“features”: [
“scheduler”
],
“vendor”: {
“name”: “The Apache Software Foundation”
}
}

All right, we good to go.

Party mode

Initially we have neither admin or regular users.

$ curl $db/_node/nonode@nohost/_config/admins
{}
$ curl $db/_users
{“error”:”not_found”,”reason”:”Database does not exist.”}

CouchDB in so-called “party mode” — anyone can access any end-point and execute any action. For example:

# check health
$ curl $db/_up
{“status”:”ok”}
# get list of existing databases
$ curl $db/_dbs/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}
# create a new database
$ curl -X PUT $db/cabal
{“ok”:true}
# add a document to it
$ curl -X POST $db/cabal -d'{“name”:”alice”}'
{“ok”:true,”id”:”fda2c2b75ab368c5c27e332314000c0e”,”rev”:”1–2350796167caec1f6ba70e9145a9e102"}
# read any document
$ curl $db/cabal/fda2c2b75ab368c5c27e332314000c0e | jq .
{
“_id”: “fda2c2b75ab368c5c27e332314000c0e”,
“_rev”: “1–2350796167caec1f6ba70e9145a9e102”,
“name”: “alice”
}
# and delete any database
$ curl -X DELETE $db/cabal
{“ok”:true}

Everyone is an admin! 💃

Adding a real admin

Now it’s time to crash the party, let’s add a proper CouchDB admin

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

We immediately loosing an access to config end-point and now need to provide credentials to check if our admin creation worked. Which is nicely recursive, since if we need the credentials then yes, sure it did.

$ curl $db/_node/nonode@nohost/_config/admins
{“error”:”unauthorized”,”reason”:”You are not a server admin.”}
$ curl -u root:god $db/_node/nonode@nohost/_config/admins
{“root”:”-pbkdf2-c139cb7ce5e9777a577c412cf2ac4acb2097170a,642853a8e4d032cfe25d343adc5aaa67,10"}

So we now have an admin, but we still can access some end-points.

# we still can check health
$ curl $db/_up
{“status”:”ok”}
# but can’t list all the existing databases
$ curl $db/_dbs/_all_docs
{“error”:”unauthorized”,”reason”:”You are not a server admin.”}
# or create a new database
$ curl -X PUT $db/cabal
{“error”:”unauthorized”,”reason”:”You are not a server admin.”}
```

More importantly, while we now need admin credentials to create or delete a database, default database access is still all permissive.

Let’s demonstrate this.

# create a new database
curl -u root:god -X PUT $db/cabal
{“ok”:true}

This new database has an anonymous access, anyone can list and read docs or create and delete new docs. However db deletion is admin operation and still require admin’s credentials.

# list docs
$ curl $db/cabal/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}
# create a new doc
$ curl -X POST $db/cabal -d'{“name”:”alice”}'
{“ok”:true,”id”:”fda2c2b75ab368c5c27e332314000e88",”rev”:”1–2350796167caec1f6ba70e9145a9e102"}
# and delete that doc
$ curl -X DELETE $db/cabal/fda2c2b75ab368c5c27e332314000e88?rev=1–2350796167caec1f6ba70e9145a9e102
{“ok”:true,”id”:”fda2c2b75ab368c5c27e332314000e88",”rev”:”2-ca76e6de865e85190aa9bc4176f2bf70"}
# but we still can't delete the whole database
$ curl -X DELETE $db/cabal
{“error”:”unauthorized”,”reason”:”You are not a server admin.”}

All of this is not particularly amusing and is happening because by default our database security object is empty.

$ curl $db/cabal/_security
{}

So let’s fix that.

Adding a database user

First we need some proper CouchDB users and for that we need to create a special database _users and add a user document into it.

$ curl -u root:god -X PUT $db/_users
{“ok”:true}
# we are adding new database user “eiri” with password “kami”
$ curl -X PUT $db/_users/org.couchdb.user:eiri -d '{“name”: “eiri”, “password”: “kami”, “roles”: [], “type”: “user”}'
{“ok”:true,”id”:”org.couchdb.user:eiri”,”rev”:”1-d48efd6008b411efab07819149e100b5"}

This might be slightly confusing, so to reiterate: we have server admin, which is a record in configure file and we have database user, which is a document in _users database. Those are different types of authorization: server admin controls server operations and database users controls database operations on per database base. How exactly the latter can be configured is a topic for another post, so I’m not going to touch it now.

Fine, now let’s update our database security model and see where it’ll get us.

$ curl -u root:god -X PUT $db/cabal/_security -d '{“admins”:{“names”:[“eiri”],”roles”:[“_admin”]}, “members”:{“names”:[“eiri”],”roles”:[“_admin”]}}'
{“ok”:true}

We now need to provide credentials, either admin’s or our user’s to work with this database

$ curl $db/cabal/_all_docs
{“error”:”unauthorized”,”reason”:”You are not authorized to access this db.”}
$ curl -X POST $db/cabal -d'{“name”:”alice”}'
{“error”:”unauthorized”,”reason”:”You are not authorized to access this db.”}
# but with user creds
$ curl -u eiri:kami $db/cabal/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}
$ curl -u eiri:kami -X POST $db/cabal -d'{“name”:”alice”}'
{“ok”:true,”id”:”fda2c2b75ab368c5c27e332314001b9e”,”rev”:”1–2350796167caec1f6ba70e9145a9e102"}
$ curl $db/cabal/fda2c2b75ab368c5c27e332314001b9e
{“error”:”unauthorized”,”reason”:”You are not authorized to access this db.”}
# yet
$ curl -u eiri:kami $db/cabal/fda2c2b75ab368c5c27e332314001b9e | jq .
{
“_id”: “fda2c2b75ab368c5c27e332314001b9e”,
“_rev”: “1–2350796167caec1f6ba70e9145a9e102”,
“name”: “alice”
}

Remainder, we still need server admin credentials to delete the database even though out user is this database admin.

$ curl -u eiri:kami -X DELETE $db/cabal
{“error”:”unauthorized”,”reason”:”You are not a server admin.”}

Sessions

It might be a hustle (and security hazard) to always pass credentials in command line, so we can acquire a session token and use that instead.

$ curl -X POST $db/_session -d'{“name”:”eiri”,”password”:”kami”}' -v

< HTTP/1.1 200 OK
< Set-Cookie: AuthSession=ZWlyaTo1QjRGNTU2NzqtlDF8N5XvJLIx4evcb2V9IiEetg; Version=1; Path=/; HttpOnly
< Server: CouchDB/2.1.1 (Erlang OTP/17)
< Date: Wed, 18 Jul 2018 14:57:43 GMT
< Content-Type: application/json
< Content-Length: 37
< Cache-Control: must-revalidate
<
{“ok”:true,”name”:”eiri”,”roles”:[]}
$ echo "-H Cookie:AuthSession=ZWlyaTo1QjRGNTU2NzqtlDF8N5XvJLIx4evcb2V9IiEetg; Version=1; Path=/; HttpOnly" >> ~/.curlrc
$ curl $db/cabal/_all_docs
{“total_rows”:1,”offset”:0,”rows”:[
{“id”:”fda2c2b75ab368c5c27e332314001b9e”,”key”:”fda2c2b75ab368c5c27e332314001b9e”,”value”:{“rev”:”1–2350796167caec1f6ba70e9145a9e102"}}
]}

Don’t forget to remove the cookie header from ~/.curlrc afterwards or further examples will not to work.

Tightening an access with “require_valid_user”

It might be no good to have even a small period of time between creation of a new database and updating its security object to keep the database in “whoever do whatever” mode. To remediate that we can set config parameter require_valid_user of [chttpd] section to true.

$ curl -u root:god -X PUT $db/_node/nonode@nohost/_config/chttpd/require_valid_user -d’”true”’
“false”

What’s that "false" is about? It’s a previous value of the config parameter.

From now on CouchDB will require a proper credential to access any end-point. Even to simply check health.

$ curl $db/_up
{“error”:”unauthorized”,”reason”:”Authentication required.”}
$ curl -u root:god $db/_up
{“status”:”ok”}

Bummer. Yet any freshly created database is immediately protected.

$ curl -u root:god -X PUT $db/camarilla
{“ok”:true}
$ curl $db/camarilla/_all_docs
{“error”:”unauthorized”,”reason”:”Authentication required.”}
# even though our security object is still empty
$ curl -u root:god $db/camarilla/_security
{}
$ curl -u root:god $db/camarilla/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}

Once we add a proper user to this database we can access it normally.

$ curl -u root:god -X PUT $db/camarilla/_security -d '{“admins”:{“names”:[“eiri”],”roles”:[“_admin”]}, “members”:{“names”:[“eiri”],”roles”:[“_admin”]}}'
$ curl -u eiri:kami $db/camarilla/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}

A session token is going to work too, though we now need credentials to access /_sessions end-point.

$ curl -u eiri:kami -X POST $db/_session -d’{“name”:”eiri”,”password”:”kami”}’ -v

< HTTP/1.1 200 OK
< Set-Cookie: AuthSession=ZWlyaTo1QjRGNTg1Rjq4En2ZdEbcUJvFvjVzJt9PgMwl1A; Version=1; Path=/; HttpOnly
< Server: CouchDB/2.1.1 (Erlang OTP/17)
< Date: Wed, 18 Jul 2018 15:10:23 GMT
< Content-Type: application/json
< Content-Length: 37
< Cache-Control: must-revalidate
<
{“ok”:true,”name”:”eiri”,”roles”:[]}
$ curl -H'Cookie:AuthSession=ZWlyaTo1QjRGNTg1Rjq4En2ZdEbcUJvFvjVzJt9PgMwl1A; Version=1; Path=/; HttpOnly' $db/camarilla/_all_docs
{“total_rows”:0,”offset”:0,”rows”:[
]}

Fin

Well that’s about it. It’s time to shut down CouchDB and go home

$ docker stop wismut
wismut
$ curl $db -v
* 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.