% 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(couch_auth_cache).

-export([
    get_user_creds/1,
    get_user_creds/2,
    update_user_creds/3,
    get_admin/1,
    add_roles/2,
    auth_design_doc/1,
    ensure_users_db_exists/0
]).

-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_js_functions.hrl").

-spec get_user_creds(UserName :: string() | binary()) ->
    {ok, Credentials :: list(), term()} | nil.

get_user_creds(UserName) ->
    get_user_creds(nil, UserName).

-spec get_user_creds(Req :: #httpd{} | nil, UserName :: string() | binary()) ->
    {ok, Credentials :: list(), term()} | nil.

get_user_creds(Req, UserName) when is_list(UserName) ->
    get_user_creds(Req, ?l2b(UserName));
get_user_creds(_Req, UserName) ->
    UserCreds =
        case get_admin(UserName) of
            nil ->
                get_from_db(UserName);
            Props ->
                case get_from_db(UserName) of
                    nil ->
                        Props;
                    UserProps when is_list(UserProps) ->
                        add_roles(Props, couch_util:get_value(<<"roles">>, UserProps))
                end
        end,
    validate_user_creds(UserCreds).

update_user_creds(_Req, UserDoc, _AuthCtx) ->
    ok = ensure_users_db_exists(),
    couch_util:with_db(users_db(), fun(UserDb) ->
        {ok, _NewRev} = couch_db:update_doc(UserDb, UserDoc, [?ADMIN_CTX]),
        ok
    end).

add_roles(Props, ExtraRoles) ->
    CurrentRoles = couch_util:get_value(<<"roles">>, Props),
    lists:keyreplace(<<"roles">>, 1, Props, {<<"roles">>, CurrentRoles ++ ExtraRoles}).

get_admin(UserName) when is_binary(UserName) ->
    get_admin(?b2l(UserName));
get_admin(UserName) when is_list(UserName) ->
    case config:get("admins", UserName) of
        "-hashed-" ++ HashedPwdAndSalt ->
            % the name is an admin, now check to see if there is a user doc
            % which has a matching name, salt, and password_sha
            [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
            make_admin_doc_simple(HashedPwd, Salt);
        "-pbkdf2-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha">>, HashedPwd, Salt, Iterations);
        "-pbkdf2:sha-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha">>, HashedPwd, Salt, Iterations);
        "-pbkdf2:sha224-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha224">>, HashedPwd, Salt, Iterations);
        "-pbkdf2:sha256-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha256">>, HashedPwd, Salt, Iterations);
        "-pbkdf2:sha384-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha384">>, HashedPwd, Salt, Iterations);
        "-pbkdf2:sha512-" ++ HashedPwdSaltAndIterations ->
            [HashedPwd, Salt, Iterations] = string:tokens(HashedPwdSaltAndIterations, ","),
            make_admin_doc_pbkdf2(<<"sha512">>, HashedPwd, Salt, Iterations);
        _Else ->
            nil
    end.

make_admin_doc_simple(HashedPwd, Salt) ->
    [
        {<<"roles">>, [<<"_admin">>]},
        {<<"salt">>, ?l2b(Salt)},
        {<<"password_scheme">>, <<"simple">>},
        {<<"password_sha">>, ?l2b(HashedPwd)}
    ].

make_admin_doc_pbkdf2(PRF, DerivedKey, Salt, Iterations) ->
    [
        {<<"roles">>, [<<"_admin">>]},
        {<<"salt">>, ?l2b(Salt)},
        {<<"iterations">>, list_to_integer(Iterations)},
        {<<"password_scheme">>, <<"pbkdf2">>},
        {<<"pbkdf2_prf">>, PRF},
        {<<"derived_key">>, ?l2b(DerivedKey)}
    ].

get_from_db(UserName) ->
    ok = ensure_users_db_exists(),
    couch_util:with_db(users_db(), fun(Db) ->
        DocId = <<"org.couchdb.user:", UserName/binary>>,
        try
            {ok, Doc} = couch_db:open_doc(Db, DocId, [conflicts]),
            {DocProps} = couch_doc:to_json_obj(Doc, []),
            DocProps
        catch
            _:_Error ->
                nil
        end
    end).

validate_user_creds(nil) ->
    nil;
validate_user_creds(UserCreds) ->
    case couch_util:get_value(<<"_conflicts">>, UserCreds) of
        undefined ->
            ok;
        _ConflictList ->
            throw(
                {unauthorized,
                    <<"User document conflicts must be resolved before the document",
                        " is used for authentication purposes.">>}
            )
    end,
    {ok, UserCreds, nil}.

users_db() ->
    DbNameList = config:get("couch_httpd_auth", "authentication_db", "_users"),
    ?l2b(DbNameList).

ensure_users_db_exists() ->
    Options = [?ADMIN_CTX, nologifmissing],
    case couch_db:open(users_db(), Options) of
        {ok, Db} ->
            ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
            couch_db:close(Db);
        _Error ->
            {ok, Db} = couch_db:create(users_db(), Options),
            ok = ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
            couch_db:close(Db)
    end,
    ok.

ensure_auth_ddoc_exists(Db, DDocId) ->
    case couch_db:open_doc(Db, DDocId) of
        {not_found, _Reason} ->
            {ok, AuthDesign} = auth_design_doc(DDocId),
            {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []);
        {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}
                    ),
                    couch_db:update_doc(Db, couch_doc:from_json_obj({Props1}), [])
            end
    end,
    ok.

auth_design_doc(DocId) ->
    DocProps = [
        {<<"_id">>, DocId},
        {<<"language">>, <<"javascript">>},
        {<<"validate_doc_update">>, ?AUTH_DB_DOC_VALIDATE_FUNCTION}
    ],
    {ok, couch_doc:from_json_obj({DocProps})}.
