From 748f5350462395b023366997d77c45a48cced8b3 Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Mon, 10 Nov 2014 23:16:10 +0000 Subject: [PATCH 1/7] Only admin can read _changes on clustered _users COUCHDB-2452 1/5 --- src/chttpd_db.erl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index 24eb78c..d51143c 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -56,7 +56,16 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, do_db_req(Req, Handler) end. -handle_changes_req(#httpd{method='GET'}=Req, Db) -> +handle_changes_req(#httpd{method='GET'}=Req, #db{name=DbName}=Db) -> + AuthDbName = ?l2b(config:get("chttpd_auth", "authentication_db")), + case AuthDbName of + DbName -> + % in the authentication database, _changes is admin-only. + ok = couch_db:check_is_admin(Db); + _Else -> + % on other databases, _changes is free for all. + ok + end, #changes_args{filter=Raw, style=Style} = Args0 = parse_changes_query(Req), ChangesArgs = Args0#changes_args{ filter_fun = couch_changes:configure_filter(Raw, Style, Req, Db) From ce83780490b4fefaa0df5df2ed7447cc6331befd Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Mon, 10 Nov 2014 23:19:37 +0000 Subject: [PATCH 2/7] Export function for updating auth docs The process of writing documents to the authentication DB varies depending whether the authentication DB is on the admin or the clustered interface. Authentication handlers should therefore abstract the details away and provide a generic update_doc function. This commit adds an update_auth_doc function to the chttpd auth cache which proxies to fabric:update_doc/3. COUCHDB-2452 2/5 --- src/chttpd_auth_cache.erl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/chttpd_auth_cache.erl b/src/chttpd_auth_cache.erl index a78ab9e..afe522a 100644 --- a/src/chttpd_auth_cache.erl +++ b/src/chttpd_auth_cache.erl @@ -17,6 +17,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([listen_for_changes/1, changes_callback/2]). +-export([update_auth_doc/1]). -include_lib("couch/include/couch_db.hrl"). @@ -33,6 +34,10 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +update_auth_doc(Doc) -> + DbName = ?l2b(config:get("chttpd_auth", "authentication_db", "_users")), + fabric:update_doc(DbName, Doc, []). + get_user_creds(UserName) when is_list(UserName) -> get_user_creds(?l2b(UserName)); get_user_creds(UserName) when is_binary(UserName) -> From 1cea10dc98cb1d04ed0b0a081687324f92177dea Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Mon, 10 Nov 2014 23:37:02 +0000 Subject: [PATCH 3/7] Strip non-public fields from _users all_docs on 5984 A _users DB on the admin interface will strip non-public fields from documents in the _all_docs view when include_docs=true. This commit creates the same behaviour when the _users DB is on the clustered interface by using the appropriate callback in couch_mrview_http. COUCHDB-2452 3/5 --- src/chttpd_db.erl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index d51143c..afe1bf6 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -548,9 +548,20 @@ all_docs_view(Req, Db, Keys) -> end, Args = Args0#mrargs{preflight_fun=ETagFun}, Options = [{user_ctx, Req#httpd.user_ctx}], + DbName = ?b2l(Db#db.name), + UsersDbName = config:get("chttpd_auth", + "authentication_db", + "_users"), + IsAdmin = case catch couch_db:check_is_admin(Db) of + {unauthorized, _} -> + false; + ok -> + true + end, + Callback = couch_mrview_http:get_view_callback(DbName, UsersDbName, IsAdmin), {ok, Resp} = couch_httpd:etag_maybe(Req, fun() -> VAcc0 = #vacc{db=Db, req=Req}, - fabric:all_docs(Db, Options, fun couch_mrview_http:view_cb/2, VAcc0, Args) + fabric:all_docs(Db, Options, Callback, VAcc0, Args) end), case is_record(Resp, vacc) of true -> {ok, Resp#vacc.resp}; From 9b5406b4b6d39ec5df6d1061c270f7a90e797a67 Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Mon, 10 Nov 2014 23:41:35 +0000 Subject: [PATCH 4/7] Only admins access _users _all_docs on 5984 When couch_httpd_auth/users_db_public is set to false and the _users DB is on the admin interface (5986) only admins can read the _all_docs view. This commit creates the same behaviour on the clustered interface (5984) when chttpd_auth/users_db_public is set to false. Note: This duplicates code in couch_db:maybe_add_sys_db_callbacks/2 and couch_mrview_http:all_docs/3. COUCHDB-2452 4/5 --- src/chttpd_db.erl | 52 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index afe1bf6..8f624c2 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -257,9 +257,21 @@ delete_db_req(#httpd{}=Req, DbName) -> throw(Error) end. +get_db_options(DbName) -> + IsReplicatorDb = DbName == config:get("replicator", "db", "_replicator"), + IsUsersDb = DbName ==config:get("chttpd_auth", "authentication_db", "_users") orelse + binary_to_list(mem3:dbname(DbName)) == config:get("chttpd_auth", "authentication_db", "_users"), + case {IsReplicatorDb, IsUsersDb} of + {false, false} -> + []; + _Else -> + [sys_db] + end. + do_db_req(#httpd{path_parts=[DbName|_], user_ctx=Ctx}=Req, Fun) -> cassim:get_security(DbName, [{user_ctx,Ctx}]), % calls check_is_reader - Fun(Req, #db{name=DbName, user_ctx=Ctx}). + Options = get_db_options(DbName), + Fun(Req, #db{name=DbName, user_ctx=Ctx, options=Options}). db_req(#httpd{method='GET',path_parts=[DbName]}=Req, _Db) -> % measure the time required to generate the etag, see if it's worth it @@ -425,9 +437,9 @@ db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) -> db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs">>]}=Req, Db) -> case chttpd:qs_json_value(Req, "keys", nil) of Keys when is_list(Keys) -> - all_docs_view(Req, Db, Keys); + all_docs_req(Req, Db, Keys); nil -> - all_docs_view(Req, Db, undefined); + all_docs_req(Req, Db, undefined); _ -> throw({bad_request, "`keys` parameter must be an array."}) end; @@ -436,9 +448,9 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_all_docs">>]}=Req, Db) -> {Fields} = chttpd:json_body_obj(Req), case couch_util:get_value(<<"keys">>, Fields, nil) of Keys when is_list(Keys) -> - all_docs_view(Req, Db, Keys); + all_docs_req(Req, Db, Keys); nil -> - all_docs_view(Req, Db, undefined); + all_docs_req(Req, Db, undefined); _ -> throw({bad_request, "`keys` body member must be an array."}) end; @@ -541,6 +553,36 @@ db_req(#httpd{path_parts=[_, DocId]}=Req, Db) -> db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) -> db_attachment_req(Req, Db, DocId, FileNameParts). +all_docs_req(Req, Db, Keys) -> + case couch_db:is_system_db(Db) of + true -> + case (catch couch_db:check_is_admin(Db)) of + ok -> + all_docs_view(Req, Db, Keys); + _ -> + DbName = ?b2l(Db#db.name), + case config:get("chttpd_auth", + "authentication_db", + "_users") of + DbName -> + UsersDbPublic = config:get("chttpd_auth", "users_db_public", "false"), + PublicFields = config:get("chttpd_auth", "public_fields"), + case {UsersDbPublic, PublicFields} of + {"true", PublicFields} when PublicFields =/= undefined -> + all_docs_view(Req, Db, Keys); + {_, _} -> + throw({forbidden, <<"Only admins can access _all_docs", + " of system databases.">>}) + end; + _ -> + throw({forbidden, <<"Only admins can access _all_docs", + " of system databases.">>}) + end + end; + false -> + all_docs_view(Req, Db, Keys) + end. + all_docs_view(Req, Db, Keys) -> Args0 = couch_mrview_http:parse_params(Req, Keys), ETagFun = fun(Sig, Acc0) -> From 25ec565c18379ce5090b35c3186f6d4a27fbb6c8 Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Thu, 13 Nov 2014 20:32:34 +0000 Subject: [PATCH 5/7] Only admins access _users design documents The check for admin when opening a design document in the authentication DB was previously being carried out in a callback function called when the document was read from the shard. In order to allow admins to access the design document via the clustered interface it is necessary to either modify the chttpd/fabric plumbing so that the user context can be passed through for all design document calls, or alternatively move the check to the http layer where we already have the user context. Due to the number of places we would need to modify fabric to allow the option to be passed through the latter approach is taken. This commit checks for admin in the http layer for requests which access design documents in the authentication DB. The couch internals part of that work can be found in related commit: couchdb-couch/6266b95415f8c8d8cde49a8ce221e9d31ebf18b8 COUCHDB-2452 5/5 --- src/chttpd_db.erl | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index 8f624c2..b78592a 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -202,6 +202,13 @@ handle_design_req(#httpd{ path_parts=[_DbName, _Design, Name, <<"_",_/binary>> = Action | _Rest], design_url_handlers = DesignUrlHandlers }=Req, Db) -> + case catch(check_admin_if_auth_db(Db)) of + ok -> + ok; + _ -> + throw({forbidden, + <<"Only administrators can view design docs in the users database.">>}) + end, DbName = mem3:dbname(Db#db.name), case ddoc_cache:open(DbName, <<"_design/", Name/binary>>) of {ok, DDoc} -> @@ -622,6 +629,18 @@ db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) -> send_updated_doc(Req, Db, DocId, couch_doc_from_req(Req, DocId, Body)); db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> + case DocId of + <<"_design/", _/binary>> -> + case catch(check_admin_if_auth_db(Db)) of + ok -> + ok; + _ -> + throw({forbidden, + <<"Only administrators can view design docs in the users database.">>}) + end; + _Else -> + ok + end, #doc_query_args{ rev = Rev, open_revs = Revs, @@ -1508,6 +1527,22 @@ put_security(#httpd{user_ctx=Ctx}=Req, Db, FetchRev) -> end end. +check_admin_if_auth_db(Db) -> + DbName = mem3:dbname(Db#db.name), + AuthDbName = ?l2b(config:get("chttpd_auth", "authentication_db")), + case AuthDbName of + DbName -> + {SecProps} = fabric:get_security(DbName), + case (catch couch_db:check_is_admin(Db#db{security=SecProps})) of + ok -> + ok; + _ -> + throw(forbidden) + end; + _Else -> + ok + end. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). From f2fcc285a4dfee352a8348ec6ad0f251543c5f3b Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Wed, 26 Nov 2014 23:05:38 +0000 Subject: [PATCH 6/7] Restart changes listener on config changes Previously if chttpd_auth/authentication_db was changed in the config then a changes listener would not be started for the new authentication DB until the current changes request timed out. During that time any changes to the users DB (e.g. password changes) would not take effect. This is primarily a problem when running share/www/script/test/users_db_security.js however it could conceivably become a problem under normal running conditions. This commit adds a config listener which causes the current changes listener to be killed when the chttpd_auth/authentication_db config value is changed. It will then be restarted via the existing handle_info/2 clause. --- src/chttpd_auth_cache.erl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/chttpd_auth_cache.erl b/src/chttpd_auth_cache.erl index afe522a..550858a 100644 --- a/src/chttpd_auth_cache.erl +++ b/src/chttpd_auth_cache.erl @@ -12,12 +12,14 @@ -module(chttpd_auth_cache). -behaviour(gen_server). +-behaviour(config_listener). -export([start_link/0, get_user_creds/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([listen_for_changes/1, changes_callback/2]). -export([update_auth_doc/1]). +-export([handle_config_change/5]). -include_lib("couch/include/couch_db.hrl"). @@ -76,8 +78,12 @@ get_from_cache(UserName) -> %% gen_server callbacks init([]) -> + ok = config:listen_for_changes(?MODULE, nil), {ok, #state{changes_pid = spawn_changes(0)}}. +handle_call(restart_listener, _From, #state{changes_pid=Pid} = State) -> + exit(Pid, kill), + {reply, ok, State}; handle_call(_Call, _From, State) -> {noreply, State}. @@ -96,6 +102,12 @@ handle_info({'DOWN', _, _, Pid, Reason}, #state{changes_pid=Pid} = State) -> {noreply, State#state{last_seq=Seq}}; handle_info({start_listener, Seq}, State) -> {noreply, State#state{changes_pid = spawn_changes(Seq)}}; +handle_info({gen_event_EXIT, {config_listener, ?MODULE}, _Reason}, State) -> + erlang:send_after(5000, self(), restart_config_listener), + {noreply, State}; +handle_info(restart_config_listener, State) -> + ok = config:listen_for_changes(?MODULE, nil), + {noreply, State}; handle_info(_Msg, State) -> {noreply, State}. @@ -105,6 +117,11 @@ terminate(_Reason, #state{changes_pid = Pid}) -> code_change(_OldVsn, #state{}=State, _Extra) -> {ok, State}. +handle_config_change("chttpd_auth", "authentication_db", _, _, _) -> + {ok, gen_server:call(?MODULE, restart_listener, infinity)}; +handle_config_change(_, _, _, _, _) -> + {ok, nil}. + %% private functions spawn_changes(Since) -> From e5301cab58f2044f33a905fa95de52e8c4c0f48b Mon Sep 17 00:00:00 2001 From: Mike Wallace Date: Fri, 16 Jan 2015 14:38:00 +0000 Subject: [PATCH 7/7] [squash] simplify IsUsersDb check I realised the second part of the orelse is not needed here. Because this code is in chttpd we will only be dealing with the cluster-level DB name, not internal shard names, so there is never a need to call mem3:dbname. We still need to convert from binary to list though. The replication DB check is also removed as that information is not used here. To squash into 9b5406b4b6d39ec5df6d1061c270f7a90e797a67 --- src/chttpd_db.erl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index b78592a..97886ea 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -265,11 +265,9 @@ delete_db_req(#httpd{}=Req, DbName) -> end. get_db_options(DbName) -> - IsReplicatorDb = DbName == config:get("replicator", "db", "_replicator"), - IsUsersDb = DbName ==config:get("chttpd_auth", "authentication_db", "_users") orelse - binary_to_list(mem3:dbname(DbName)) == config:get("chttpd_auth", "authentication_db", "_users"), - case {IsReplicatorDb, IsUsersDb} of - {false, false} -> + IsUsersDb = binary_to_list(DbName) == config:get("chttpd_auth", "authentication_db", "_users"), + case IsUsersDb of + false -> []; _Else -> [sys_db]