  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
%
%   -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 ~r", [?MODULE, Reason]),
            0
    end,
    erlang:send_after(5000, self(), {start_listener, Seq}),
    {noreply, State#state{last_seq=Seq}};
handle_info({start_listener, Seq}, State) ->
    {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) ->
    ensure_auth_ddoc_exists(dbname(), <<"_design/_auth">>),
    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:debug("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.

 the License at

   -2.0

 Unless required by applicable law or agreed to in writing, software
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 License for the specific language governing permissions and limitations under
 the License.
 public functions
 gen_server callbacks
 private functions
  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
  distributed under the License is distributed on an " AS IS " BASIS , WITHOUT

-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"
}).


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.


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 ~r", [?MODULE, Reason]),
            0
    end,
    erlang:send_after(5000, self(), {start_listener, Seq}),
    {noreply, State#state{last_seq=Seq}};
handle_info({start_listener, Seq}, State) ->
    {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}.


spawn_changes(Since) ->
    {Pid, _} = spawn_monitor(?MODULE, listen_for_changes, [Since]),
    Pid.

listen_for_changes(Since) ->
    ensure_auth_ddoc_exists(dbname(), <<"_design/_auth">>),
    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:debug("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.
