blob: 75d77e81cfd3a67aaf2c273794f8c4bd9fee08a1 [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(chttpd_auth_cache).
-behaviour(gen_server).
-export([start_link/0, get_user_creds/2, update_user_creds/3]).
-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]).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_js_functions.hrl").
-define(CACHE, chttpd_auth_cache_lru).
-record(state, {
changes_pid,
last_seq="0"
}).
%% public functions
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
get_user_creds(Req, UserName) when is_list(UserName) ->
get_user_creds(Req, ?l2b(UserName));
get_user_creds(_Req, UserName) when is_binary(UserName) ->
Resp = case couch_auth_cache:get_admin(UserName) of
nil ->
get_from_cache(UserName);
Props ->
case get_from_cache(UserName) of
nil ->
Props;
UserProps when is_list(UserProps) ->
couch_auth_cache:add_roles(Props,
couch_util:get_value(<<"roles">>, UserProps))
end
end,
maybe_validate_user_creds(Resp).
update_user_creds(_Req, UserDoc, _Ctx) ->
{_, Ref} = spawn_monitor(fun() ->
case fabric:update_doc(dbname(), UserDoc, []) of
{ok, _} ->
exit(ok);
Else ->
exit(Else)
end
end),
receive
{'DOWN', Ref, _, _, ok} ->
ok;
{'DOWN', Ref, _, _, Else} ->
Else
end.
get_from_cache(UserName) ->
try ets_lru:lookup_d(?CACHE, UserName) of
{ok, Props} ->
couch_stats:increment_counter([couchdb, auth_cache_hits]),
couch_log:debug("cache hit for ~s", [UserName]),
Props;
_ ->
couch_stats:increment_counter([couchdb, auth_cache_misses]),
couch_log:debug("cache miss for ~s", [UserName]),
case load_user_from_db(UserName) of
nil ->
nil;
Props ->
ets_lru:insert(?CACHE, UserName, Props),
Props
end
catch
error:badarg ->
couch_stats:increment_counter([couchdb, auth_cache_misses]),
couch_log:debug("cache miss for ~s", [UserName]),
load_user_from_db(UserName)
end.
%% gen_server callbacks
init([]) ->
self() ! {start_listener, 0},
{ok, #state{}}.
handle_call(_Call, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'DOWN', _, _, Pid, Reason}, #state{changes_pid=Pid} = State) ->
Seq = case Reason of
{seq, EndSeq} ->
EndSeq;
_ ->
couch_log:notice("~p changes listener died ~p", [?MODULE, Reason]),
0
end,
erlang:send_after(5000, self(), {start_listener, Seq}),
{noreply, State#state{last_seq=Seq}};
handle_info({start_listener, Seq}, State) ->
ensure_auth_ddoc_exists(dbname(), <<"_design/_auth">>),
{noreply, State#state{changes_pid = spawn_changes(Seq)}};
handle_info(_Msg, State) ->
{noreply, State}.
terminate(_Reason, #state{changes_pid = Pid}) when is_pid(Pid) ->
exit(Pid, kill);
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, #state{}=State, _Extra) ->
{ok, State}.
%% private functions
spawn_changes(Since) ->
{Pid, _} = spawn_monitor(?MODULE, listen_for_changes, [Since]),
Pid.
listen_for_changes(Since) ->
CBFun = fun ?MODULE:changes_callback/2,
Args = #changes_args{
feed = "continuous",
since = Since,
heartbeat = true,
filter = {default, main_only}
},
fabric:changes(dbname(), CBFun, Since, Args).
changes_callback(waiting_for_updates, Acc) ->
{ok, Acc};
changes_callback(start, Since) ->
{ok, Since};
changes_callback({stop, EndSeq, _Pending}, _) ->
exit({seq, EndSeq});
changes_callback({change, {Change}}, _) ->
case couch_util:get_value(id, Change) of
<<"_design/", _/binary>> ->
ok;
DocId ->
UserName = username(DocId),
couch_log:debug("Invalidating cached credentials for ~s", [UserName]),
ets_lru:remove(?CACHE, UserName)
end,
{ok, couch_util:get_value(seq, Change)};
changes_callback(timeout, Acc) ->
{ok, Acc};
changes_callback({error, _}, EndSeq) ->
exit({seq, EndSeq}).
load_user_from_db(UserName) ->
try fabric:open_doc(dbname(), docid(UserName), [?ADMIN_CTX, ejson_body, conflicts]) of
{ok, Doc} ->
{Props} = couch_doc:to_json_obj(Doc, []),
Props;
_Else ->
couch_log:warning("no record of user ~s", [UserName]),
nil
catch error:database_does_not_exist ->
nil
end.
dbname() ->
config:get("chttpd_auth", "authentication_db", "_users").
docid(UserName) ->
<<"org.couchdb.user:", UserName/binary>>.
username(<<"org.couchdb.user:", UserName/binary>>) ->
UserName.
ensure_auth_ddoc_exists(DbName, DDocId) ->
case fabric:open_doc(DbName, DDocId, [?ADMIN_CTX, ejson_body]) of
{not_found, _Reason} ->
{ok, AuthDesign} = couch_auth_cache:auth_design_doc(DDocId),
update_doc_ignoring_conflict(DbName, AuthDesign, [?ADMIN_CTX]);
{ok, Doc} ->
{Props} = couch_doc:to_json_obj(Doc, []),
case couch_util:get_value(<<"validate_doc_update">>, Props, []) of
?AUTH_DB_DOC_VALIDATE_FUNCTION ->
ok;
_ ->
Props1 = lists:keyreplace(<<"validate_doc_update">>, 1, Props,
{<<"validate_doc_update">>,
?AUTH_DB_DOC_VALIDATE_FUNCTION}),
update_doc_ignoring_conflict(DbName, couch_doc:from_json_obj({Props1}), [?ADMIN_CTX])
end;
{error, Reason} ->
couch_log:notice("Failed to ensure auth ddoc ~s/~s exists for reason: ~p", [DbName, DDocId, Reason]),
ok
end,
ok.
update_doc_ignoring_conflict(DbName, Doc, Options) ->
try
fabric:update_doc(DbName, Doc, Options)
catch
throw:conflict ->
ok
end.
maybe_validate_user_creds(nil) ->
nil;
% throws if UserCreds includes a _conflicts member
% returns UserCreds otherwise
maybe_validate_user_creds(UserCreds) ->
AllowConflictedUserDocs = config:get_boolean("chttpd_auth", "allow_conflicted_user_docs", false),
case {couch_util:get_value(<<"_conflicts">>, UserCreds), AllowConflictedUserDocs} of
{undefined, _} ->
{ok, UserCreds, nil};
{_, true} ->
{ok, UserCreds, nil};
{_ConflictList, false} ->
throw({unauthorized,
<<"User document conflicts must be resolved before the document",
" is used for authentication purposes.">>
})
end.