%% ----------------------------------------------------------------------------
%% RabbitMQ OAuth2 Plugin
%%
%% See https://github.com/rabbitmq/rabbitmq-server/blob/master/deps/rabbitmq_auth_backend_oauth2/ for details.
%%
%% ----------------------------------------------------------------------------

%% OAuth Resource identity. Usage:
%% - This is the identity of a RabbitMQ server/cluster used as the
%% recipient of JWT Tokens (see audience claim, https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3).
%% - This is also the resource identifier used by RabbitMQ server/cluster in the authorization and access token
%% requests (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-indicators-05#page-3)
%%
%% Up to version 3.12, RabbitMQ's scopes followed this pattern : <resource_server_id>.<scope>.
%% Nowadays, there is a new setting called scope_prefix and RabbitMQ's scopes follow this pattern instead:
%% <scope_prefix><scope>. Note that there is no dot in between.
%% The default value of this setting is `<resource_server_id>.`.
%% {resource_server_id, <<"my_rabbit_server">>},

{mapping,
 "auth_oauth2.resource_server_id",
 "rabbitmq_auth_backend_oauth2.resource_server_id",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.resource_server_id",
 fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.resource_server_id", Conf))
 end}.

%% A prefix used for scopes to avoid scope collisions (or unintended overlap). If not configured,
%% it is defaulted to `<resource_server_id>.` to maintain backward compatibility. Empty string is a permitted value.
%%
%% {scope_prefix, <<"api:/rabbitmq:">>},

{mapping,
 "auth_oauth2.scope_prefix",
 "rabbitmq_auth_backend_oauth2.scope_prefix",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.scope_prefix",
 fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.scope_prefix", Conf))
 end}.

%% An identifier used for JWT Tokens compliant with Rich Authorization Request spec
%% RabbitMq uses this field as discriminator to filter out permissions meant for RabbitMQ
%% as a Resource server
%%
%% {resource_server_type, <<"rabbitmq">>},

{mapping,
 "auth_oauth2.resource_server_type",
 "rabbitmq_auth_backend_oauth2.resource_server_type",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.resource_server_type",
 fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.resource_server_type", Conf))
 end}.

%% Configure the plugin to also look in other fields using additional_scopes_key (maps to extra_scopes_source in the old format)
%%
%% {additional_scopes_key, <<"my_custom_scope_key">>},

{mapping,
 "auth_oauth2.additional_scopes_key",
 "rabbitmq_auth_backend_oauth2.extra_scopes_source",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.extra_scopes_source",
 fun(Conf) ->
    list_to_binary(cuttlefish:conf_get("auth_oauth2.additional_scopes_key", Conf))
 end}.


%% Configure the plugin to skip validation of the aud field
%%
%% {verify_aud, true},

{mapping,
 "auth_oauth2.verify_aud",
 "rabbitmq_auth_backend_oauth2.verify_aud",
 [{datatype, {enum, [true, false]}}]}.

%% Configure the preferred username's JWT claim(s). These are the JWT claims or attributes
%% used to determine the user's identity, a.k.a username.
%% RabbitMQ appends `sub` and `client_id` claims.
%% e.g. RabbitMQ first looks for username claim. If it is not present, it looks for user_name
%% and so forth. If it cannot find any, it defauls to unknown.
%% {preferred_username_claims, [<<"username">>, <<"user_name">>, <<"email">> ]},

{mapping,
 "auth_oauth2.preferred_username_claims.$preferred_username_claims",
 "rabbitmq_auth_backend_oauth2.preferred_username_claims",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.preferred_username_claims",
 fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.preferred_username_claims", Conf),
    [list_to_binary(V) || {_, V} <- lists:reverse(Settings)]
 end}.



%% ID of the default signing key
%%
%% {default_key, <<"key-1">>},

{mapping,
 "auth_oauth2.default_key",
 "rabbitmq_auth_backend_oauth2.key_config.default_key",
 [{datatype, string}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.key_config.default_key",
 fun(Conf) -> list_to_binary(cuttlefish:conf_get("auth_oauth2.default_key", Conf)) end}.

%% A map of signing keys
%%
%% {signing_keys, #{<<"id1">> => {pem, <<"value1">>}, <<"id2">> => {pem, <<"value2">>}}}
%% validator doesn't work

{mapping,
 "auth_oauth2.signing_keys.$name",
 "rabbitmq_auth_backend_oauth2.key_config.signing_keys",
 [{datatype, file}, {validators, ["file_accessible"]}]}.

{translation,
 "rabbitmq_auth_backend_oauth2.key_config.signing_keys",
 fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.signing_keys", Conf),
    TryReadingFileFun =
        fun(Path) ->
            case file:read_file(Path) of
                {ok, Bin} ->
                    string:trim(Bin, trailing, "\n");
                _ ->
                    %% this throws and makes Cuttlefish treak the key as invalid
                    cuttlefish:invalid("file does not exist or cannot be read by the node")
            end
        end,
    SigningKeys =
        lists:map(fun({Id, Path}) ->
                    {list_to_binary(lists:last(Id)), {pem, TryReadingFileFun(Path)}}
                  end, Settings),
    maps:from_list(SigningKeys)
 end}.

{mapping,
 "auth_oauth2.issuer",
 "rabbitmq_auth_backend_oauth2.issuer",
 [{datatype, string}, {validators, ["uri", "https_uri"]}]}.

{mapping,
 "auth_oauth2.token_endpoint",
 "rabbitmq_auth_backend_oauth2.token_endpoint",
 [{datatype, string}, {validators, ["uri", "https_uri"]}]}.

{mapping,
 "auth_oauth2.jwks_url",
 "rabbitmq_auth_backend_oauth2.key_config.jwks_url",
 [{datatype, string}, {validators, ["uri", "https_uri"]}]}.

{mapping,
 "auth_oauth2.https.peer_verification",
 "rabbitmq_auth_backend_oauth2.key_config.peer_verification",
 [{datatype, {enum, [verify_peer, verify_none]}}]}.

% Alias configuration variable. `auth_oauth2.https.peer_verification` will be soon deprecated
{mapping,
 "auth_oauth2.https.verify",
 "rabbitmq_auth_backend_oauth2.key_config.verify",
 [{datatype, {enum, [verify_peer, verify_none]}}]}.

{mapping,
 "auth_oauth2.https.cacertfile",
 "rabbitmq_auth_backend_oauth2.key_config.cacertfile",
 [{datatype, file}, {validators, ["file_accessible"]}]}.

{mapping,
 "auth_oauth2.https.depth",
 "rabbitmq_auth_backend_oauth2.key_config.depth",
 [{datatype, integer}]}.

{mapping,
 "auth_oauth2.https.hostname_verification",
 "rabbitmq_auth_backend_oauth2.key_config.hostname_verification",
 [{datatype, {enum, [wildcard, none]}}]}.

{mapping,
 "auth_oauth2.https.crl_check",
 "rabbitmq_auth_backend_oauth2.key_config.crl_check",
 [{datatype, {enum, [true, false, peer, best_effort]}}]}.

{mapping,
 "auth_oauth2.https.fail_if_no_peer_cert",
 "rabbitmq_auth_backend_oauth2.key_config.fail_if_no_peer_cert",
 [{datatype, {enum, [true, false]}}]}.

{validator, "https_uri", "According to the JWT Specification, Key Server URL must be https.",
 fun(Uri) -> string:nth_lexeme(Uri, 1, "://") == "https" end}.

{mapping,
 "auth_oauth2.algorithms.$algorithm",
 "rabbitmq_auth_backend_oauth2.key_config.algorithms",
 [{datatype, string}]}.

{translation, "rabbitmq_auth_backend_oauth2.key_config.algorithms",
 fun(Conf) ->
     Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.algorithms", Conf),
     [list_to_binary(V) || {_, V} <- Settings]
 end}.


%% This setting is only required when there are +1 auth_oauth2.oauth_providers
%% If this setting is omitted, its default to the first oauth_provider

{mapping,
  "auth_oauth2.default_oauth_provider",
  "rabbitmq_auth_backend_oauth2.default_oauth_provider",
  [{datatype, string}]}.

{mapping,
  "auth_oauth2.oauth_providers.$name.issuer",
  "rabbitmq_auth_backend_oauth2.oauth_providers",
  [{datatype, string}, {validators, ["uri", "https_uri"]}]
}.

{mapping,
  "auth_oauth2.oauth_providers.$name.token_endpoint",
  "rabbitmq_auth_backend_oauth2.oauth_providers",
  [{datatype, string}, {validators, ["uri", "https_uri"]}]
}.

{mapping,
 "auth_oauth2.oauth_providers.$name.jwks_uri",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, string}, {validators, ["uri", "https_uri"]}]
}.

{mapping,
 "auth_oauth2.oauth_providers.$name.https.verify",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, {enum, [verify_peer, verify_none]}}]}.

{mapping,
 "auth_oauth2.oauth_providers.$name.https.cacertfile",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, file}, {validators, ["file_accessible"]}]}.

{mapping,
 "auth_oauth2.oauth_providers.$name.https.depth",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, integer}]}.

{mapping,
 "auth_oauth2.oauth_providers.$name.https.hostname_verification",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, {enum, [wildcard, none]}}]}.

{mapping,
 "auth_oauth2.oauth_providers.$name.https.crl_check",
 "rabbitmq_auth_backend_oauth2.oauth_providers",
 [{datatype, {enum, [true, false, peer, best_effort]}}]}.

{translation, "rabbitmq_auth_backend_oauth2.oauth_providers",
 fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.oauth_providers", Conf),
    AuthBackends  = [{Name, {list_to_atom(Key), list_to_binary(V)}} || {["auth_oauth2","oauth_providers", Name, Key], V} <- Settings ],
    Https  = [{Name, {https, {list_to_atom(Key), V}}} || {["auth_oauth2","oauth_providers", Name, "https", Key], V} <- Settings ],

    %% Aggregate all options for one provider
    KeyFun = fun({Name, _}) -> list_to_binary(Name) end,
    ValueFun = fun({_, V}) -> V end,
    ProviderNameToListOfSettings = maps:groups_from_list(KeyFun, ValueFun, AuthBackends),
    ProviderNameToListOfHttpsSettings = maps:groups_from_list(KeyFun, fun({_, {https, V}}) -> V end, Https),
    ProviderNameToListWithHttps = maps:map(fun(K1,L1) -> [{https, L1}] end, ProviderNameToListOfHttpsSettings),
    NewGroup = maps:merge_with(fun(K, V1, V2) -> V1 ++ V2 end, ProviderNameToListOfSettings, ProviderNameToListWithHttps),

    ListOrSingleFun = fun(K, List) ->
      case K of
        ssl_options -> proplists:get_all_values(K, List);
        _ ->
          case proplists:lookup_all(K, List) of
            [One] -> proplists:get_value(K, List);
            [One|_] = V -> V
          end
      end
    end,
    GroupKeyConfigFun = fun(K, List) ->
        ListKeys = proplists:get_keys(List),
        [{K, ListOrSingleFun(K, List)} || K <- ListKeys]
     end,
    maps:map(GroupKeyConfigFun, NewGroup)

 end}.

{mapping,
  "auth_oauth2.resource_servers.$name.id",
  "rabbitmq_auth_backend_oauth2.resource_servers",
  [{datatype, string}]
}.

{mapping,
  "auth_oauth2.resource_servers.$name.scope_prefix",
  "rabbitmq_auth_backend_oauth2.resource_servers",
  [{datatype, string}]
}.

{mapping,
  "auth_oauth2.resource_servers.$name.additional_scopes_key",
  "rabbitmq_auth_backend_oauth2.resource_servers",
  [{datatype, string}]
}.

{mapping,
  "auth_oauth2.resource_servers.$name.resource_server_type",
  "rabbitmq_auth_backend_oauth2.resource_servers",
  [{datatype, string}]
}.

{mapping,
  "auth_oauth2.resource_servers.$name.oauth_provider_id",
  "rabbitmq_auth_backend_oauth2.resource_servers",
  [{datatype, string}]
}.

{translation, "rabbitmq_auth_backend_oauth2.resource_servers",
 fun(Conf) ->
    Settings = cuttlefish_variable:filter_by_prefix("auth_oauth2.resource_servers", Conf),
    AuthBackends  = [{Name, {list_to_atom(Key), list_to_binary(V)}} || {["auth_oauth2","resource_servers", Name, Key], V} <- Settings],
    KeyFun = fun({Name,_}) -> list_to_binary(Name) end,
    ValueFun = fun({_,V}) -> V end,
    NewGroup = maps:groups_from_list(KeyFun, ValueFun, AuthBackends),
    ListOrSingleFun = fun(K, List) ->
      case K of
        key_config -> proplists:get_all_values(K, List);
        _ ->
          case proplists:lookup_all(K, List) of
            [One] -> proplists:get_value(K, List);
            [One|_] = V -> V
          end
      end
    end,
    GroupKeyConfigFun = fun(K, List) ->
        ListKeys = proplists:get_keys(List),
        [ {K,ListOrSingleFun(K,List)} || K <- ListKeys ]
     end,
    NewGroupTwo = maps:map(GroupKeyConfigFun, NewGroup),
    IndexByIdOrElseNameFun = fun(K, V, NewMap) ->
      case proplists:get_value(id, V) of
        undefined -> maps:put(K, V, NewMap);
        ID -> maps:put(ID, V, NewMap)
      end
    end,
    maps:fold(IndexByIdOrElseNameFun,#{}, NewGroupTwo)

 end}.
