From 9c867e88779f8976631882680db9abd865d04b75 Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Mon, 16 Feb 2015 12:05:57 -0800 Subject: [PATCH] Introduce an `allowed_owner` hook COUCHDB-2585 --- src/global_changes_httpd.erl | 13 ++- test/global_changes_hooks_tests.erl | 142 ++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 test/global_changes_hooks_tests.erl diff --git a/src/global_changes_httpd.erl b/src/global_changes_httpd.erl index af25b7c..be6cfdc 100644 --- a/src/global_changes_httpd.erl +++ b/src/global_changes_httpd.erl @@ -41,9 +41,9 @@ handle_global_changes_req(#httpd{method='GET'}=Req) -> Limit = couch_util:get_value(limit, Options), %Options1 = lists:keydelete(limit, 1, Options), Options1 = Options, - chttpd:verify_is_server_admin(Req), + Owner = allowed_owner(Req), Acc = #acc{ - username=admin, + username=Owner, feed=Feed, resp=Req, heartbeat_interval=Heartbeat, @@ -248,3 +248,12 @@ to_non_neg_int(Value) -> catch error:badarg -> throw({bad_request, invalid_integer}) end. + +allowed_owner(Req) -> + case application:get_env(global_changes, allowed_owner) of + undefined -> + chttpd:verify_is_server_admin(Req), + admin; + {ok, {M, F, A}} -> + M:F(Req, A) + end. diff --git a/test/global_changes_hooks_tests.erl b/test/global_changes_hooks_tests.erl new file mode 100644 index 0000000..3b6ccbc --- /dev/null +++ b/test/global_changes_hooks_tests.erl @@ -0,0 +1,142 @@ +-module(global_changes_hooks_tests). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +-export([allowed_owner/2]). + +start() -> + Ctx = test_util:start_couch([chttpd]), + DbName = ?tempdb(), + ok = fabric:create_db(DbName, [?ADMIN_CTX]), + application:set_env(global_changes, dbname, DbName), + {Ctx, DbName}. + +stop({Ctx, DbName}) -> + ok = fabric:delete_db(DbName, [?ADMIN_CTX]), + test_util:stop_couch(Ctx), + ok. + +setup(default) -> + add_admin("admin", <<"pass">>), + config:delete("couch_httpd_auth", "authentication_redirect", false), + config:set("couch_httpd_auth", "require_valid_user", "false", false), + get_host(); +setup(A) -> + Host = setup(default), + ok = application:set_env(global_changes, allowed_owner, + {?MODULE, allowed_owner, A}), + Host. + +teardown(_) -> + delete_admin("admin"), + application:unset_env(global_changes, allowed_owner), + ok. + +allowed_owner(Req, "throw") -> + throw({unauthorized, <<"Exception thrown.">>}); +allowed_owner(Req, "pass") -> + "super". + +allowed_owner_hook_test_() -> + { + "Check allowed_owner hook", + { + setup, + fun start/0, fun stop/1, + [ + disabled_allowed_owner_integration_point(), + enabled_allowed_owner_integration_point() + ] + } + }. + +disabled_allowed_owner_integration_point() -> + { + "disabled allowed_owner integration point", + { + foreach, + fun() -> setup(default) end, fun teardown/1, + [ + fun should_not_fail_for_admin/1, + fun should_fail_for_non_admin/1 + ] + } + }. + +enabled_allowed_owner_integration_point() -> + { + "enabled allowed_owner integration point", + [ + { + foreach, + fun() -> setup("throw") end, fun teardown/1, + [fun should_throw/1] + }, + { + foreach, + fun() -> setup("pass") end, fun teardown/1, + [fun should_pass/1] + } + ] + }. + +should_not_fail_for_admin(Host) -> + ?_test(begin + Headers = [{basic_auth, {"admin", "pass"}}], + {Status, [Error, Reason]} = + request(Host, Headers, [<<"error">>, <<"reason">>]), + ?assertEqual(200, Status), + ?assertEqual(undefined, Error), + ?assertEqual(undefined, Reason) + end). + +should_fail_for_non_admin(Host) -> + ?_test(begin + Headers = [], + {Status, [Error, Reason]} = + request(Host, Headers, [<<"error">>, <<"reason">>]), + ?assertEqual(401, Status), + ?assertEqual(<<"unauthorized">>, Error), + ?assertEqual(<<"You are not a server admin.">>, Reason) + end). + +should_pass(Host) -> + ?_test(begin + Headers = [{basic_auth, {"admin", "pass"}}], + {Status, [Error, Reason]} = + request(Host, Headers, [<<"error">>, <<"reason">>]), + ?assertEqual(200, Status), + ?assertEqual(undefined, Error), + ?assertEqual(undefined, Reason) + end). + +should_throw(Host) -> + ?_test(begin + Headers = [{basic_auth, {"admin", "pass"}}], + {Status, [Error, Reason]} = + request(Host, Headers, [<<"error">>, <<"reason">>]), + ?assertEqual(401, Status), + ?assertEqual(<<"unauthorized">>, Error), + ?assertEqual(<<"Exception thrown.">>, Reason) + end). + +request(Host, Headers, ToDecode) -> + Url = Host ++ "/_db_updates", + {ok, Status, _Headers, BinBody} = test_request:get(Url, Headers), + {Body} = jiffy:decode(BinBody), + Values = [couch_util:get_value(Key, Body) || Key <- ToDecode], + {Status, Values}. + +add_admin(User, Pass) -> + Hashed = couch_passwords:hash_admin_password(Pass), + config:set("admins", User, ?b2l(Hashed), false). + +delete_admin(User) -> + config:delete("admins", User, false). + +get_host() -> + Addr = config:get("httpd", "bind_address", "127.0.0.1"), + Port = config:get("chttpd", "port", "5984"), + Host = "http://" ++ Addr ++ ":" ++ Port, + Host.