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>>.