CouchDB authorization in Nutshell

Eric Avdey
Jul 18, 2018 · 5 min read

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade