fix #8 security issue, add end-to-end testing script
diff --git a/Makefile b/Makefile
index f3b90f1..8dcaf66 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,9 @@
compile:
rebar compile
+test:
+ ./end-to-end-test.sh
+
plugin: compile
mkdir -p $(PLUGIN_DIST)
cp -r $(PLUGIN_DIRS) $(PLUGIN_DIST)
diff --git a/end-to-end-test.sh b/end-to-end-test.sh
new file mode 100755
index 0000000..e752b00
--- /dev/null
+++ b/end-to-end-test.sh
@@ -0,0 +1,143 @@
+#!/bin/bash
+set -e
+# Send SIGTERM to process group on exit, ensures background tasks die
+# if script exits
+cleanup () {
+ kill -TERM 0
+ wait
+}
+HERE=$(pwd)
+PLUGIN="couchperuser"
+COUCHDB_SRC="${HERE}/.eunit/couchdb"
+COUCHDB_VER="1.6.1"
+COUCHDB_REL="apache-couchdb-${COUCHDB_VER}"
+COUCHDB_URL="http://apache.osuosl.org/couchdb/source/${COUCHDB_VER}/${COUCHDB_REL}.tar.gz"
+COUCHDB_PREFIX="${COUCHDB_SRC}/${COUCHDB_REL}-build"
+COUCHDB_PLUGINS="${COUCHDB_PREFIX}/lib/couchdb/plugins"
+COUCHDB_DATA="${COUCHDB_PREFIX}/var/lib/couchdb"
+couch () {
+ echo "http://$1:$2@127.0.0.1:5985$3"
+}
+if [ ! -d "${COUCHDB_SRC}" ]; then
+ echo "Downloading and unpacking ${COUCHDB_PREFIX}"
+ mkdir -p "${COUCHDB_SRC}"
+ pushd "${COUCHDB_SRC}"
+ curl -f "${COUCHDB_URL}" | tar zxf -
+ popd
+fi
+if [ ! -e "${COUCHDB_PREFIX}/bin/couchdb" ]; then
+ echo "Compiling ${COUCHDB_PREFIX}"
+ pushd "${COUCHDB_SRC}/${COUCHDB_REL}"
+ ./configure --prefix="${COUCHDB_PREFIX}"
+ make
+ make install
+ popd
+fi
+if [ ! -d "${COUCHDB_PLUGINS}/${PLUGIN}" ]; then
+ echo "Symlinking plugin dir"
+ if [ ! -d "${COUCHDB_PLUGINS}" ]; then
+ mkdir -p "${COUCHDB_PLUGINS}"
+ fi
+ ln -sf "${HERE}" "${COUCHDB_PLUGINS}/${PLUGIN}"
+fi
+PATH="${COUCHDB_PREFIX}/bin:${PATH}"
+rebar compile
+if [ -d "${COUCHDB_DATA}" ]; then
+ echo "Removing existing data files"
+ rm -rf "${COUCHDB_DATA}"
+fi
+echo "Writing local.ini"
+cat <<EOF > "${COUCHDB_PREFIX}/etc/couchdb/local.ini"
+[couchdb]
+uuid = 92a5cfcd7c8ad3a05b225e2fa8aba48f
+[httpd]
+port = 5985
+bind_address = 127.0.0.1
+[couch_httpd_auth]
+secret = d8c211410a5fb33d2458aba4b7bb593c
+[admins]
+; admin : password
+admin = -pbkdf2-341d6b96564af2f7a1a88fada73dbcd6ab7e061e,8aa4052c63662a4bf8cf575891513595,10
+EOF
+couchdb &
+trap 'cleanup' SIGINT SIGTERM EXIT
+while ! (curl -f -s "$(couch)" 2>&1 >/dev/null); do
+ sleep 0.1
+done
+secret_json () {
+ echo "{\"secret\":\"$1\"}"
+}
+to_hex () {
+ printf "%s" "$1" | xxd -ps
+}
+userdb () {
+ echo "/userdb-$(to_hex "$1")$2"
+}
+put_json () {
+ curl -f -s \
+ -HContent-Type:application/json \
+ -XPUT "$1" \
+ --data-binary "$2"
+}
+get_json () {
+ curl -f -s "$1"
+}
+create_user () {
+ put_json \
+ "$(couch "" "" "/_users/org.couchdb.user:$1")" \
+ "{\"_id\": \"org.couchdb.user:$1\",\"name\": \"$1\",\"roles\": [],\"type\": \"user\",\"password\": \"password\"}" \
+ >/dev/null
+}
+fail () {
+ printf "FAIL: %s\n" "$(printf "$@")" 1>&2
+ exit 1
+}
+create_user eve
+for user in alice bob; do
+ # Create users with no authentication
+ create_user "${user}"
+ # Expect database to exist, but give it some time to trigger the change
+ for x in $(seq 10); do
+ if ! (get_json "$(couch "${user}" "password" "$(userdb "${user}")")" > /dev/null); then
+ if [ "$x" -ge "10" ]; then
+ fail "Expected create of user %s to create db %s" "${user}" "$(userdb "{user}")"
+ else
+ sleep 0.2
+ fi
+ else
+ break
+ fi
+ done
+ # Write doc with correct authentication
+ if ! (put_json \
+ "$(couch "${user}" "password" $(userdb "${user}" "/secret"))" \
+ "$(secret_json "${user}")" \
+ >/dev/null); then
+ fail "User %s could not PUT %s" "${user}" "$(userdb "${user}" "/secret")"
+ fi
+ # Read doc with correct authentication
+ if ! (get_json "$(couch "${user}" "password" $(userdb "${user}" "/secret"))" >/dev/null); then
+ fail "User %s could not GET %s" "${user}" "$(userdb "${user}" "/secret")"
+ fi
+ # Try to read doc without authentication
+ if (get_json "$(couch "" "" $(userdb "${user}" "/secret"))" >/dev/null); then
+ fail "Expected unauthenticated read for %s database %s to fail" "${user}" "$(userdb "${user}" "/secret")"
+ fi
+ # Try to read doc with incorrect authentication
+ if (get_json "$(couch "eve" "password" $(userdb "${user}" "/secret"))" >/dev/null); then
+ fail "Expected %s read for %s database %s to fail" "eve" "${user}" "$(userdb "${user}" "/secret")"
+ fi
+ # Try to write doc without authentication
+ if (put_json "$(couch "" "" $(userdb "${user}" "/notsecret"))" "{\"secret\":\"oops\"}" >/dev/null); then
+ fail "Expected unauthenticated write for %s database %s to fail" "${user}" $(userdb "${user}" "/notsecret")
+ fi
+ # Try to write doc with incorrect authentication
+ if (put_json "$(couch "eve" "password" $(userdb "${user}" "/notsecret"))" "{\"secret\":\"oops\"}" >/dev/null); then
+ fail "Expected %s write for %s database %s to fail" "eve" "${user}" $(userdb "${user}" "/notsecret")
+ fi
+done
+trap - SIGINT SIGTERM EXIT
+for job in $(jobs -p); do
+ kill -SIGTERM $job
+done
+wait
diff --git a/src/couchperuser.app.src b/src/couchperuser.app.src
index 0702b68..14a3aa1 100644
--- a/src/couchperuser.app.src
+++ b/src/couchperuser.app.src
@@ -1,7 +1,7 @@
%% -*- mode: erlang -*-
{application, couchperuser, [
{description, "couchperuser - maintains per-user databases in CouchDB"},
- {vsn, "1.0.1"},
+ {vsn, "1.1.0"},
{modules, []},
{registered, [couchperuser]},
{applications, [kernel, stdlib]},
diff --git a/src/couchperuser.erl b/src/couchperuser.erl
index 98d3e6f..a6ffa2c 100644
--- a/src/couchperuser.erl
+++ b/src/couchperuser.erl
@@ -58,7 +58,13 @@
%% TODO: Let's not complicate this with GC for now!
Acc;
false ->
- ensure_security(User, ensure_user_db(User), Acc)
+ {ok, Db} = ensure_user_db(User),
+ try
+ ensure_security(User, Db)
+ after
+ couch_db:close(Db)
+ end,
+ Acc
end;
_ ->
Acc
@@ -80,27 +86,33 @@
couch_db:create(User_Db, [admin_ctx()])
end.
-ensure_security(User, {ok, Db}, Acc) ->
- {SecProps} = couch_db:get_security(Db),
- {Admins} = couch_util:get_value(<<"admins">>, SecProps, {[]}),
- Names = couch_util:get_value(<<"names">>, Admins, []),
+add_user(User, Prop, {Modified, SecProps}) ->
+ {PropValue} = couch_util:get_value(Prop, SecProps, {[]}),
+ Names = couch_util:get_value(<<"names">>, PropValue, []),
case lists:member(User, Names) of
true ->
- ok;
+ {Modified, SecProps};
false ->
- update_security(Db, SecProps, Admins, [User | Names])
- end,
- couch_db:close(Db),
- Acc.
+ {true,
+ lists:keystore(
+ Prop, 1, SecProps,
+ {Prop,
+ {lists:keystore(
+ <<"names">>, 1, PropValue,
+ {<<"names">>, [User | Names]})}})}
+ end.
-update_security(Db, SecProps, Admins, Names) ->
- couch_db:set_security(
- Db,
- {lists:keystore(
- <<"admins">>, 1, SecProps,
- {<<"admins">>,
- {lists:keystore(
- <<"names">>, 1, Admins, {<<"names">>, Names})}})}).
+ensure_security(User, Db) ->
+ {SecProps} = couch_db:get_security(Db),
+ case lists:foldl(
+ fun (Prop, SAcc) -> add_user(User, Prop, SAcc) end,
+ {false, SecProps},
+ [<<"admins">>, <<"members">>]) of
+ {false, _} ->
+ ok;
+ {true, SecProps1} ->
+ couch_db:set_security(Db, {SecProps1})
+ end.
user_db_name(User) ->
<<"userdb-", (iolist_to_binary(mochihex:to_hex(User)))/binary>>.