blob: 623884b0e34118074d9fbeda0fbc26c9c260c4dc [file] [log] [blame]
#!/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,
before_doc_update = nil, % nil | fun(Doc, Db) -> NewDoc
after_doc_read = nil % nil | fun(Doc, Db) -> NewDoc
}).
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),
delete_db(auth_db_name()),
delete_db(auth_db_2_name()),
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) ->
couch_server:delete(
Name, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]).