blob: 370be3104df558eee62a0f377ab9751017598f90 [file] [log] [blame]
% 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.
-module(couchdb_peruser).
-behaviour(gen_server).
-behaviour(config_listener).
-include_lib("couch/include/couch_db.hrl").
-define(USERDB_PREFIX, "userdb-").
% gen_server callbacks
-export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
% config_listener callbacks
-export([handle_config_change/5, handle_config_terminate/3]).
-export([init_changes/2, change_filter/3]).
%% 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(?MODULE, [], []).
init([]) ->
couch_log:debug("couchdb_peruser daemon: starting link.", []),
Db_Name = ?l2b(config:get(
"couch_httpd_auth", "authentication_db", "_users")),
Server = self(),
ok = config:listen_for_changes(?MODULE, Server),
{Pid, Ref} = spawn_opt(?MODULE, init_changes, [Server, Db_Name],
[link, monitor]),
{ok, #state{db_name=Db_Name,
changes_pid=Pid,
changes_ref=Ref}}.
handle_config_change("couch_httpd_auth", "authentication_db", _Value, _Persist, State) ->
gen_server:cast(State, stop),
remove_handler;
handle_config_change("couchdb_peruser", _Key, _Value, _Persist, State) ->
gen_server:cast(State, stop),
remove_handler;
handle_config_change(_Section, _Key, _Value, _Persist, State) ->
{ok, State}.
handle_config_terminate(_, stop, _) -> ok;
handle_config_terminate(Self, _, _) ->
spawn(fun() ->
timer:sleep(5000),
config:listen_for_changes(?MODULE, Self)
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_db_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 ->
UserDb = ensure_user_db(User),
ensure_security(User, UserDb),
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) ->
UserDb = user_db_name(User),
try
fabric_db_info:go(UserDb)
catch error:database_does_not_exist ->
fabric_db_create:go(UserDb, [admin_ctx()])
end,
UserDb.
add_user(User, Prop, {Modified, SecProps}) ->
{PropValue} = couch_util:get_value(Prop, SecProps, {[]}),
Names = couch_util:get_value(<<"names">>, PropValue, []),
case lists:member(User, Names) of
true ->
{Modified, SecProps};
false ->
{true,
lists:keystore(
Prop, 1, SecProps,
{Prop,
{lists:keystore(
<<"names">>, 1, PropValue,
{<<"names">>, [User | Names]})}})}
end.
ensure_security(User, UserDb) ->
{ok, Shards} = fabric_db_meta:get_all_security(UserDb, [admin_ctx()]),
{_ShardInfo, {SecProps}} = hd(Shards),
% assert that shards have the same security object
true = lists:all(fun(Shard) -> {_, {SecProps}} =:= Shard end, Shards),
case lists:foldl(
fun(Prop, SAcc) -> add_user(User, Prop, SAcc) end,
{false, SecProps},
[<<"admins">>, <<"members">>]) of
{false, _} ->
ok;
{true, SecProps1} ->
fabric_db_meta:set_security(UserDb, {SecProps1}, [admin_ctx()])
end.
user_db_name(User) ->
HexUser = list_to_binary(
[string:to_lower(integer_to_list(X, 16)) || <<X>> <= User]),
<<?USERDB_PREFIX, HexUser/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}.