Skip to content
This repository was archived by the owner on May 25, 2021. It is now read-only.

Add users db security rules on clustered interface #12

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/chttpd_auth_cache.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +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").

Expand All @@ -33,6 +36,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) ->
Expand Down Expand Up @@ -71,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}.

Expand All @@ -91,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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 5000?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the same value as for restarting the config listener over in https://github.com/apache/couchdb-couch/blob/master/src/couch_auth_cache.erl#L235 - I don't know exactly why 5000 was used there, I'd guess possibly to limit the frequency in the event of repeated gen_event_EXIT messages?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be. I'm ok with, just wondering about magic numbers (:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 5000 is to limit restart frequency which can cause badness in its own right.

{noreply, State};
handle_info(restart_config_listener, State) ->
ok = config:listen_for_changes(?MODULE, nil),
{noreply, State};
handle_info(_Msg, State) ->
{noreply, State}.

Expand All @@ -100,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) ->
Expand Down
109 changes: 102 additions & 7 deletions src/chttpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -193,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} ->
Expand Down Expand Up @@ -248,9 +264,19 @@ delete_db_req(#httpd{}=Req, DbName) ->
throw(Error)
end.

get_db_options(DbName) ->
IsUsersDb = binary_to_list(DbName) == config:get("chttpd_auth", "authentication_db", "_users"),
case IsUsersDb of
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
Expand Down Expand Up @@ -416,9 +442,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;
Expand All @@ -427,9 +453,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;
Expand Down Expand Up @@ -532,16 +558,57 @@ 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) ->
couch_mrview_http:check_view_etag(Sig, Acc0, Req)
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};
Expand All @@ -560,6 +627,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,
Expand Down Expand Up @@ -1446,6 +1525,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").

Expand Down