Updated Branches:
  refs/heads/master 573a7bb90 -> 84510eb0d

Clear credentials cache if _users db crashes

COUCHDB-1357


Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/84510eb0
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/84510eb0
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/84510eb0

Branch: refs/heads/master
Commit: 84510eb0d182b99891cce80579ba16cc67e71889
Parents: 573a7bb
Author: Filipe David Borba Manana <fdman...@apache.org>
Authored: Tue Dec 6 22:21:42 2011 +0000
Committer: Filipe David Borba Manana <fdman...@apache.org>
Committed: Fri Dec 16 15:13:39 2011 +0000

----------------------------------------------------------------------
 share/www/script/test/cookie_auth.js   |    1 -
 share/www/script/test/oauth.js         |    1 -
 share/www/script/test/proxyauth.js     |    1 -
 share/www/script/test/reader_acl.js    |    1 -
 share/www/script/test/replicator_db.js |    8 +-
 src/couchdb/couch_auth_cache.erl       |   47 ++---
 test/etap/075-auth-cache.t             |  272 +++++++++++++++++++++++++++
 test/etap/Makefile.am                  |    1 +
 8 files changed, 300 insertions(+), 32 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/share/www/script/test/cookie_auth.js
----------------------------------------------------------------------
diff --git a/share/www/script/test/cookie_auth.js 
b/share/www/script/test/cookie_auth.js
index 9d98fff..a3fbb7a 100644
--- a/share/www/script/test/cookie_auth.js
+++ b/share/www/script/test/cookie_auth.js
@@ -34,7 +34,6 @@ couchTests.cookie_auth = function(debug) {
       // try using an invalid cookie
       var usersDb = new CouchDB("test_suite_users", 
{"X-Couch-Full-Commit":"false"});
       usersDb.deleteDb();
-      usersDb.createDb();
       
       // test that the users db is born with the auth ddoc
       var ddoc = usersDb.open("_design/_auth");

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/share/www/script/test/oauth.js
----------------------------------------------------------------------
diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js
index 9915bff..6398f94 100644
--- a/share/www/script/test/oauth.js
+++ b/share/www/script/test/oauth.js
@@ -125,7 +125,6 @@ couchTests.oauth = function(debug) {
         "Authorization": adminBasicAuthHeaderValue()
       });
       usersDb.deleteDb();
-      usersDb.createDb();
         
       // Create a user
       var jasonUserDoc = CouchDB.prepareUserDoc({

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/share/www/script/test/proxyauth.js
----------------------------------------------------------------------
diff --git a/share/www/script/test/proxyauth.js 
b/share/www/script/test/proxyauth.js
index 40af008..016cec5 100644
--- a/share/www/script/test/proxyauth.js
+++ b/share/www/script/test/proxyauth.js
@@ -34,7 +34,6 @@ couchTests.proxyauth = function(debug) {
   
   function TestFun() {
     usersDb.deleteDb();
-    usersDb.createDb();
     db.deleteDb();
     db.createDb();
     

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/share/www/script/test/reader_acl.js
----------------------------------------------------------------------
diff --git a/share/www/script/test/reader_acl.js 
b/share/www/script/test/reader_acl.js
index e662851..e0dde2c 100644
--- a/share/www/script/test/reader_acl.js
+++ b/share/www/script/test/reader_acl.js
@@ -18,7 +18,6 @@ couchTests.reader_acl = function(debug) {
   function testFun() {
     try {
       usersDb.deleteDb();
-      usersDb.createDb();
       secretDb.deleteDb();
       secretDb.createDb();
 

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/share/www/script/test/replicator_db.js
----------------------------------------------------------------------
diff --git a/share/www/script/test/replicator_db.js 
b/share/www/script/test/replicator_db.js
index e35635e..5eedc10 100644
--- a/share/www/script/test/replicator_db.js
+++ b/share/www/script/test/replicator_db.js
@@ -88,8 +88,10 @@ couchTests.replicator_db = function(debug) {
 
 
   function populate_db(db, docs) {
-    db.deleteDb();
-    db.createDb();
+    if (db.name !== usersDb.name) {
+      db.deleteDb();
+      db.createDb();
+    }
     for (var i = 0; i < docs.length; i++) {
       var d = docs[i];
       delete d._rev;
@@ -1486,10 +1488,12 @@ couchTests.replicator_db = function(debug) {
   ]);
 
   repDb.deleteDb();
+  usersDb.deleteDb();
   restartServer();
   run_on_modified_server(server_config_2, test_user_ctx_validation);
 
   repDb.deleteDb();
+  usersDb.deleteDb();
   restartServer();
   run_on_modified_server(server_config_2, 
test_replication_credentials_delegation);
 

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/src/couchdb/couch_auth_cache.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_auth_cache.erl b/src/couchdb/couch_auth_cache.erl
index 97dc7da..94adac8 100644
--- a/src/couchdb/couch_auth_cache.erl
+++ b/src/couchdb/couch_auth_cache.erl
@@ -30,7 +30,8 @@
 -record(state, {
     max_cache_size = 0,
     cache_size = 0,
-    db_notifier = nil
+    db_notifier = nil,
+    db_mon_ref = nil
 }).
 
 
@@ -103,16 +104,13 @@ init(_) ->
     ?STATE = ets:new(?STATE, [set, protected, named_table]),
     ?BY_USER = ets:new(?BY_USER, [set, protected, named_table]),
     ?BY_ATIME = ets:new(?BY_ATIME, [ordered_set, private, named_table]),
-    AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-    true = ets:insert(?STATE, {auth_db_name, ?l2b(AuthDbName)}),
-    true = ets:insert(?STATE, {auth_db, open_auth_db()}),
     process_flag(trap_exit, true),
     ok = couch_config:register(
         fun("couch_httpd_auth", "auth_cache_size", SizeList) ->
             Size = list_to_integer(SizeList),
             ok = gen_server:call(?MODULE, {new_max_cache_size, Size}, 
infinity);
-        ("couch_httpd_auth", "authentication_db", DbName) ->
-            ok = gen_server:call(?MODULE, {new_auth_db, ?l2b(DbName)}, 
infinity)
+        ("couch_httpd_auth", "authentication_db", _DbName) ->
+            ok = gen_server:call(?MODULE, reinit_cache, infinity)
         end
     ),
     {ok, Notifier} = couch_db_update_notifier:start_link(fun 
handle_db_event/1),
@@ -122,7 +120,7 @@ init(_) ->
             couch_config:get("couch_httpd_auth", "auth_cache_size", "50")
         )
     },
-    {ok, State}.
+    {ok, reinit_cache(State)}.
 
 
 handle_db_event({Event, DbName}) ->
@@ -130,8 +128,7 @@ handle_db_event({Event, DbName}) ->
     case DbName =:= AuthDbName of
     true ->
         case Event of
-        deleted -> gen_server:call(?MODULE, auth_db_deleted, infinity);
-        created -> gen_server:call(?MODULE, auth_db_created, infinity);
+        created -> gen_server:call(?MODULE, reinit_cache, infinity);
         compacted -> gen_server:call(?MODULE, auth_db_compacted, infinity);
         _Else   -> ok
         end;
@@ -140,21 +137,10 @@ handle_db_event({Event, DbName}) ->
     end.
 
 
-handle_call({new_auth_db, AuthDbName}, _From, State) ->
-    NewState = clear_cache(State),
-    true = ets:insert(?STATE, {auth_db_name, AuthDbName}),
-    true = ets:insert(?STATE, {auth_db, open_auth_db()}),
-    {reply, ok, NewState};
-
-handle_call(auth_db_deleted, _From, State) ->
-    NewState = clear_cache(State),
-    true = ets:insert(?STATE, {auth_db, nil}),
-    {reply, ok, NewState};
-
-handle_call(auth_db_created, _From, State) ->
-    NewState = clear_cache(State),
-    true = ets:insert(?STATE, {auth_db, open_auth_db()}),
-    {reply, ok, NewState};
+handle_call(reinit_cache, _From, State) ->
+    catch erlang:demonitor(State#state.db_mon_ref, [flush]),
+    exec_if_auth_db(fun(AuthDb) -> catch couch_db:close(AuthDb) end),
+    {reply, ok, reinit_cache(State)};
 
 handle_call(auth_db_compacted, _From, State) ->
     exec_if_auth_db(
@@ -201,8 +187,8 @@ handle_cast({cache_hit, UserName}, State) ->
     {noreply, State}.
 
 
-handle_info(_Msg, State) ->
-    {noreply, State}.
+handle_info({'DOWN', Ref, _, _, _Reason}, #state{db_mon_ref = Ref} = State) ->
+    {noreply, reinit_cache(State)}.
 
 
 terminate(_Reason, #state{db_notifier = Notifier}) ->
@@ -224,6 +210,15 @@ clear_cache(State) ->
     State#state{cache_size = 0}.
 
 
+reinit_cache(State) ->
+    NewState = clear_cache(State),
+    AuthDbName = ?l2b(couch_config:get("couch_httpd_auth", 
"authentication_db")),
+    true = ets:insert(?STATE, {auth_db_name, AuthDbName}),
+    AuthDb = open_auth_db(),
+    true = ets:insert(?STATE, {auth_db, AuthDb}),
+    NewState#state{db_mon_ref = couch_db:monitor(AuthDb)}.
+
+
 add_cache_entry(_, _, _, #state{max_cache_size = 0} = State) ->
     State;
 add_cache_entry(UserName, Credentials, ATime, State) ->

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/test/etap/075-auth-cache.t
----------------------------------------------------------------------
diff --git a/test/etap/075-auth-cache.t b/test/etap/075-auth-cache.t
new file mode 100755
index 0000000..5166c5a
--- /dev/null
+++ b/test/etap/075-auth-cache.t
@@ -0,0 +1,272 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-record(user_ctx, {
+    name = null,
+    roles = [],
+    handler
+}).
+
+-record(db, {
+    main_pid = nil,
+    update_pid = nil,
+    compactor_pid = nil,
+    instance_start_time, % number of microsecs since jan 1 1970 as a binary 
string
+    fd,
+    updater_fd,
+    fd_ref_counter,
+    header,
+    committed_update_seq,
+    fulldocinfo_by_id_btree,
+    docinfo_by_seq_btree,
+    local_docs_btree,
+    update_seq,
+    name,
+    filepath,
+    validate_doc_funs = [],
+    security = [],
+    security_ptr = nil,
+    user_ctx = #user_ctx{},
+    waiting_delayed_commit = nil,
+    revs_limit = 1000,
+    fsync_options = [],
+    options = [],
+    compression
+}).
+
+auth_db_name() -> <<"couch_test_auth_db">>.
+auth_db_2_name() -> <<"couch_test_auth_db_2">>.
+salt() -> <<"SALT">>.
+
+
+main(_) ->
+    test_util:init_code_path(),
+
+    etap:plan(19),
+    case (catch test()) of
+        ok ->
+            etap:end_tests();
+        Other ->
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+            etap:bail(Other)
+    end,
+    ok.
+
+
+test() ->
+    couch_server_sup:start_link(test_util:config_files()),
+    OrigName = couch_config:get("couch_httpd_auth", "authentication_db"),
+    couch_config:set(
+        "couch_httpd_auth", "authentication_db",
+        binary_to_list(auth_db_name()), false),
+
+    test_auth_db_crash(),
+
+    couch_config:set("couch_httpd_auth", "authentication_db", OrigName, false),
+    delete_db(auth_db_name()),
+    delete_db(auth_db_2_name()),
+    couch_server_sup:stop(),
+    ok.
+
+
+test_auth_db_crash() ->
+    Creds0 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(Creds0, nil, "Got nil when getting joe's credentials"),
+
+    etap:diag("Adding first version of Joe's user doc"),
+    PasswordHash1 = hash_password("pass1"),
+    {ok, Rev1} = update_user_doc(auth_db_name(), "joe", "pass1"),
+
+    Creds1 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds1), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds1), PasswordHash1,
+            "Cached credentials have the right password"),
+
+    etap:diag("Updating Joe's user doc password"),
+    PasswordHash2 = hash_password("pass2"),
+    {ok, _Rev2} = update_user_doc(auth_db_name(), "joe", "pass2", Rev1),
+
+    Creds2 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds2), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds2), PasswordHash2,
+            "Cached credentials have the new password"),
+
+    etap:diag("Shutting down the auth database process"),
+    shutdown_db(auth_db_name()),
+
+    {ok, UpdateRev} = get_doc_rev(auth_db_name(), "joe"),
+    PasswordHash3 = hash_password("pass3"),
+    {ok, _Rev3} = update_user_doc(auth_db_name(), "joe", "pass3", UpdateRev),
+
+    etap:is(get_user_doc_password_sha(auth_db_name(), "joe"),
+            PasswordHash3,
+            "Latest Joe's doc revision has the new password hash"),
+
+    Creds3 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds3), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds3), PasswordHash3,
+            "Cached credentials have the new password"),
+
+    etap:diag("Deleting Joe's user doc"),
+    delete_user_doc(auth_db_name(), "joe"),
+    Creds4 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(nil, Creds4,
+            "Joe's credentials not found in cache after user doc was deleted"),
+
+    etap:diag("Adding new user doc for Joe"),
+    PasswordHash5 = hash_password("pass5"),
+    {ok, _NewRev1} = update_user_doc(auth_db_name(), "joe", "pass5"),
+
+    Creds5 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds5), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds5), PasswordHash5,
+            "Cached credentials have the right password"),
+
+    full_commit(auth_db_name()),
+
+    etap:diag("Changing the auth database"),
+    couch_config:set(
+        "couch_httpd_auth", "authentication_db",
+        binary_to_list(auth_db_2_name()), false),
+    ok = timer:sleep(500),
+
+    Creds6 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(nil, Creds6,
+            "Joe's credentials not found in cache after auth database 
changed"),
+
+    etap:diag("Adding first version of Joe's user doc to new auth database"),
+    PasswordHash7 = hash_password("pass7"),
+    {ok, _} = update_user_doc(auth_db_2_name(), "joe", "pass7"),
+
+    Creds7 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds7), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds7), PasswordHash7,
+            "Cached credentials have the right password"),
+
+    etap:diag("Shutting down the auth database process"),
+    shutdown_db(auth_db_2_name()),
+
+    {ok, UpdateRev2} = get_doc_rev(auth_db_2_name(), "joe"),
+    PasswordHash8 = hash_password("pass8"),
+    {ok, _Rev8} = update_user_doc(auth_db_2_name(), "joe", "pass8", 
UpdateRev2),
+
+    etap:is(get_user_doc_password_sha(auth_db_2_name(), "joe"),
+            PasswordHash8,
+            "Latest Joe's doc revision has the new password hash"),
+
+    Creds8 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(is_list(Creds8), true, "Got joe's credentials from cache"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds8), PasswordHash8,
+            "Cached credentials have the new password"),
+
+    etap:diag("Changing the auth database again"),
+    couch_config:set(
+        "couch_httpd_auth", "authentication_db",
+        binary_to_list(auth_db_name()), false),
+    ok = timer:sleep(500),
+
+    Creds9 = couch_auth_cache:get_user_creds("joe"),
+    etap:is(Creds9, Creds5,
+            "Got same credentials as before the firt auth database change"),
+    etap:is(couch_util:get_value(<<"password_sha">>, Creds9), PasswordHash5,
+            "Cached credentials have the right password"),
+    ok.
+
+
+update_user_doc(DbName, UserName, Password) ->
+    update_user_doc(DbName, UserName, Password, nil).
+
+update_user_doc(DbName, UserName, Password, Rev) ->
+    User = iolist_to_binary(UserName),
+    Doc = couch_doc:from_json_obj({[
+        {<<"_id">>, <<"org.couchdb.user:", User/binary>>},
+        {<<"name">>, User},
+        {<<"type">>, <<"user">>},
+        {<<"salt">>, salt()},
+        {<<"password_sha">>, hash_password(Password)},
+        {<<"roles">>, []}
+    ] ++ case Rev of
+        nil -> [];
+        _ ->   [{<<"_rev">>, Rev}]
+    end}),
+    {ok, AuthDb} = open_auth_db(DbName),
+    {ok, NewRev} = couch_db:update_doc(AuthDb, Doc, []),
+    ok = couch_db:close(AuthDb),
+    {ok, couch_doc:rev_to_str(NewRev)}.
+
+
+hash_password(Password) ->
+    list_to_binary(
+        couch_util:to_hex(crypto:sha(iolist_to_binary([Password, salt()])))).
+
+
+shutdown_db(DbName) ->
+    {ok, AuthDb} = open_auth_db(DbName),
+    ok = couch_db:close(AuthDb),
+    couch_util:shutdown_sync(AuthDb#db.main_pid),
+    ok = timer:sleep(1000).
+
+
+get_doc_rev(DbName, UserName) ->
+    DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]),
+    {ok, AuthDb} = open_auth_db(DbName),
+    UpdateRev =
+    case couch_db:open_doc(AuthDb, DocId, []) of
+    {ok, Doc} ->
+        {Props} = couch_doc:to_json_obj(Doc, []),
+        couch_util:get_value(<<"_rev">>, Props);
+    {not_found, missing} ->
+        nil
+    end,
+    ok = couch_db:close(AuthDb),
+    {ok, UpdateRev}.
+
+
+get_user_doc_password_sha(DbName, UserName) ->
+    DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]),
+    {ok, AuthDb} = open_auth_db(DbName),
+    {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []),
+    ok = couch_db:close(AuthDb),
+    {Props} = couch_doc:to_json_obj(Doc, []),
+    couch_util:get_value(<<"password_sha">>, Props).
+
+
+delete_user_doc(DbName, UserName) ->
+    DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]),
+    {ok, AuthDb} = open_auth_db(DbName),
+    {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []),
+    {Props} = couch_doc:to_json_obj(Doc, []),
+    DeletedDoc = couch_doc:from_json_obj({[
+        {<<"_id">>, DocId},
+        {<<"_rev">>, couch_util:get_value(<<"_rev">>, Props)},
+        {<<"_deleted">>, true}
+    ]}),
+    {ok, _} = couch_db:update_doc(AuthDb, DeletedDoc, []),
+    ok = couch_db:close(AuthDb).
+
+
+full_commit(DbName) ->
+    {ok, AuthDb} = open_auth_db(DbName),
+    {ok, _} = couch_db:ensure_full_commit(AuthDb),
+    ok = couch_db:close(AuthDb).
+
+
+open_auth_db(DbName) ->
+    couch_db:open_int(
+        DbName, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]).
+
+
+delete_db(Name) ->
+    ok = couch_server:delete(
+        Name, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]).

http://git-wip-us.apache.org/repos/asf/couchdb/blob/84510eb0/test/etap/Makefile.am
----------------------------------------------------------------------
diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am
index fe5d838..2eb4e6a 100644
--- a/test/etap/Makefile.am
+++ b/test/etap/Makefile.am
@@ -57,6 +57,7 @@ EXTRA_DIST = \
     072-cleanup.t \
     073-changes.t \
     074-doc-update-conflicts.t \
+    075-auth-cache.t \
     080-config-get-set.t \
     081-config-override.1.ini \
     081-config-override.2.ini \

Reply via email to