blob: 98d3e6fb6d8f423bff47e017075ea9bac0a4468e [file] [log] [blame]
-module(couchperuser).
-behaviour(gen_server).
-include_lib("couch/include/couch_db.hrl").
-export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export([init_changes/2, change_filter/3]).
%% Note that this doesn't actually depend on having a registered name
-define(NAME, ?MODULE).
%% db_name and changes_pid are useful information to have, but unused
-record(state, {db_name, changes_pid, changes_ref}).
%% the entire filter state is currently unused, but may be useful later
-record(filter, {server}).
start_link() ->
gen_server:start_link({local, ?NAME}, ?MODULE, [], []).
init([]) ->
?LOG_DEBUG("couchperuser daemon: starting link.", []),
Db_Name = ?l2b(couch_config:get(
"couch_httpd_auth", "authentication_db", "_users")),
ok = watch_config_changes(),
{Pid, Ref} = spawn_opt(?MODULE, init_changes, [self(), Db_Name],
[link, monitor]),
{ok, #state{db_name=Db_Name,
changes_pid=Pid,
changes_ref=Ref}}.
watch_config_changes() ->
Server = self(),
couch_config:register(
fun ("couch_httpd_auth", "authentication_db", _Value, _Persist) ->
gen_server:cast(Server, stop);
(_Section, _Key, _Value, _Persist) ->
ok
end).
admin_ctx() ->
{user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
init_changes(Parent, Db_Name) ->
{ok, Db} = couch_db:open_int(Db_Name, [admin_ctx(), sys_db]),
FunAcc = {fun ?MODULE:change_filter/3, #filter{server=Parent}},
(couch_changes:handle_changes(
#changes_args{feed="continuous", timeout=infinity},
{json_req, null},
Db))(FunAcc).
change_filter({change, {Doc}, _Prepend}, _ResType, Acc=#filter{}) ->
Deleted = couch_util:get_value(<<"deleted">>, Doc, false),
case lists:keyfind(<<"id">>, 1, Doc) of
{_Key, <<"org.couchdb.user:", User/binary>>} ->
case Deleted of
true ->
%% TODO: Let's not complicate this with GC for now!
Acc;
false ->
ensure_security(User, ensure_user_db(User), Acc)
end;
_ ->
Acc
end;
change_filter(_Event, _ResType, Acc) ->
Acc.
terminate(_Reason, _State) ->
%% Everything should be linked or monitored, let nature
%% take its course.
ok.
ensure_user_db(User) ->
User_Db = user_db_name(User),
case couch_db:open_int(User_Db, [admin_ctx(), nologifmissing]) of
Ok={ok, _Db} ->
Ok;
_Err ->
couch_db:create(User_Db, [admin_ctx()])
end.
ensure_security(User, {ok, Db}, Acc) ->
{SecProps} = couch_db:get_security(Db),
{Admins} = couch_util:get_value(<<"admins">>, SecProps, {[]}),
Names = couch_util:get_value(<<"names">>, Admins, []),
case lists:member(User, Names) of
true ->
ok;
false ->
update_security(Db, SecProps, Admins, [User | Names])
end,
couch_db:close(Db),
Acc.
update_security(Db, SecProps, Admins, Names) ->
couch_db:set_security(
Db,
{lists:keystore(
<<"admins">>, 1, SecProps,
{<<"admins">>,
{lists:keystore(
<<"names">>, 1, Admins, {<<"names">>, Names})}})}).
user_db_name(User) ->
<<"userdb-", (iolist_to_binary(mochihex:to_hex(User)))/binary>>.
handle_call(_Msg, _From, State) ->
{reply, error, State}.
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'DOWN', Ref, _, _, _Reason}, State=#state{changes_ref=Ref}) ->
{stop, normal, State};
handle_info(_Msg, State) ->
{noreply, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.