%%--------------------------------------------------------------------
  Copyright ( c ) 2022 - 2023 EMQ Technologies Co. , Ltd. All Rights Reserved .
%%
  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(emqx_authz_jwt_SUITE).

-compile(nowarn_export_all).
-compile(export_all).

-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authn/include/emqx_authn.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").

-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").

-define(SECRET, <<"some_secret">>).
-define(AUTHN_PATH, [authentication]).

all() ->
    emqx_common_test_helpers:all(?MODULE).

groups() ->
    [].

init_per_suite(Config) ->
    ok = emqx_common_test_helpers:start_apps(
        [emqx_conf, emqx_authn, emqx_authz],
        fun set_special_configs/1
    ),
    ok = emqx_authentication:initialize_authentication(?GLOBAL, []),
    Config.

end_per_suite(_Config) ->
    ok = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_authz, emqx_conf]).

init_per_testcase(_TestCase, Config) ->
    emqx_authn_test_lib:delete_authenticators(
        ?AUTHN_PATH,
        ?GLOBAL
    ),
    AuthConfig = authn_config(),
    {ok, _} = emqx:update_config(
        ?AUTHN_PATH,
        {create_authenticator, ?GLOBAL, AuthConfig}
    ),

    ok = emqx_authz_test_lib:reset_authorizers(),
    Config.

end_per_testcase(_TestCase, _Config) ->
    emqx_authn_test_lib:delete_authenticators(
        ?AUTHN_PATH,
        ?GLOBAL
    ),
    ok = emqx_authz_test_lib:restore_authorizers().

set_special_configs(emqx_authz) ->
    ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
    ok.

%%------------------------------------------------------------------------------
%% Tests
%%------------------------------------------------------------------------------

t_topic_rules(_Config) ->
    Payload = #{
        <<"exp">> => erlang:system_time(second) + 60,
        <<"acl">> => #{
            <<"pub">> => [
                <<"eq testpub1/${username}">>,
                <<"testpub2/${clientid}">>,
                <<"testpub3/#">>
            ],
            <<"sub">> => [
                <<"eq testsub1/${username}">>,
                <<"testsub2/${clientid}">>,
                <<"testsub3/#">>
            ],
            <<"all">> => [
                <<"eq testall1/${username}">>,
                <<"testall2/${clientid}">>,
                <<"testall3/#">>
            ]
        },
        <<"username">> => <<"username">>
    },
    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    Cases = [
        {deny, <<"testpub1/username">>},
        {deny, <<"testpub2/clientid">>},
        {deny, <<"testpub3/foobar">>},

        {deny, <<"testsub1/username">>},
        {allow, <<"testsub1/${username}">>},
        {allow, <<"testsub2/clientid">>},
        {allow, <<"testsub3/foobar">>},
        {allow, <<"testsub3/+/foobar">>},
        {allow, <<"testsub3/#">>},

        {deny, <<"testsub2/username">>},
        {deny, <<"testsub1/clientid">>},
        {deny, <<"testsub4/foobar">>},

        {deny, <<"testall1/username">>},
        {allow, <<"testall1/${username}">>},
        {allow, <<"testall2/clientid">>},
        {allow, <<"testall3/foobar">>},
        {allow, <<"testall3/+/foobar">>},
        {allow, <<"testall3/#">>},

        {deny, <<"testall2/username">>},
        {deny, <<"testall1/clientid">>},
        {deny, <<"testall4/foobar">>}
    ],

    lists:foreach(
        fun
            ({allow, Topic}) ->
                ?assertMatch(
                    {ok, #{}, [0]},
                    emqtt:subscribe(C, Topic, 0)
                );
            ({deny, Topic}) ->
                ?assertMatch(
                    {ok, #{}, [?RC_NOT_AUTHORIZED]},
                    emqtt:subscribe(C, Topic, 0)
                )
        end,
        Cases
    ),

    ok = emqtt:disconnect(C).

t_check_pub(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]},
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ok = emqtt:publish(C, <<"a/b">>, <<"hi">>, 0),

    receive
        {publish, #{topic := <<"a/b">>}} ->
            ?assert(false, "Publish to `a/b` should not be allowed")
    after 100 -> ok
    end,

    ok = emqtt:disconnect(C).

t_check_no_recs(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{},
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_no_acl_claim(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_str_exp(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"exp">> => integer_to_binary(erlang:system_time(second) + 10),
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]}
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_expire(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]},
        <<"exp">> => erlang:system_time(second) + 5
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),
    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:unsubscribe(C, <<"a/b">>)
    ),

    timer:sleep(6000),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_no_expire(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]}
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),
    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:unsubscribe(C, <<"a/b">>)
    ),

    ok = emqtt:disconnect(C).

t_check_undefined_expire(_Config) ->
    Acl = #{expire => undefined, rules => #{<<"sub">> => [<<"a/b">>]}},
    Client = #{acl => Acl},

    ?assertMatch(
        {matched, allow},
        emqx_authz_client_info:authorize(Client, subscribe, <<"a/b">>, undefined)
    ),

    ?assertMatch(
        {matched, deny},
        emqx_authz_client_info:authorize(Client, subscribe, <<"a/bar">>, undefined)
    ).

%%------------------------------------------------------------------------------
%% Helpers
%%------------------------------------------------------------------------------

authn_config() ->
    #{
        <<"mechanism">> => <<"jwt">>,
        <<"use_jwks">> => <<"false">>,
        <<"algorithm">> => <<"hmac-based">>,
        <<"secret">> => ?SECRET,
        <<"secret_base64_encoded">> => <<"false">>,
        <<"acl_claim_name">> => <<"acl">>,
        <<"verify_claims">> => #{
            <<"username">> => ?PH_USERNAME
        }
    }.

generate_jws(Payload) ->
    JWK = jose_jwk:from_oct(?SECRET),
    Header = #{
        <<"alg">> => <<"HS256">>,
        <<"typ">> => <<"JWT">>
    },
    Signed = jose_jwt:sign(JWK, Header, Payload),
    {_, JWS} = jose_jws:compact(Signed),
    JWS.

--------------------------------------------------------------------

 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
 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.
--------------------------------------------------------------------
------------------------------------------------------------------------------
 Tests
------------------------------------------------------------------------------
------------------------------------------------------------------------------
 Helpers
------------------------------------------------------------------------------
  Copyright ( c ) 2022 - 2023 EMQ Technologies Co. , Ltd. All Rights Reserved .
  Licensed under the Apache License , Version 2.0 ( the " License " ) ;
  distributed under the License is distributed on an " AS IS " BASIS ,

-module(emqx_authz_jwt_SUITE).

-compile(nowarn_export_all).
-compile(export_all).

-include_lib("emqx/include/emqx_placeholder.hrl").
-include_lib("emqx_authn/include/emqx_authn.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").

-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").

-define(SECRET, <<"some_secret">>).
-define(AUTHN_PATH, [authentication]).

all() ->
    emqx_common_test_helpers:all(?MODULE).

groups() ->
    [].

init_per_suite(Config) ->
    ok = emqx_common_test_helpers:start_apps(
        [emqx_conf, emqx_authn, emqx_authz],
        fun set_special_configs/1
    ),
    ok = emqx_authentication:initialize_authentication(?GLOBAL, []),
    Config.

end_per_suite(_Config) ->
    ok = emqx_common_test_helpers:stop_apps([emqx_authn, emqx_authz, emqx_conf]).

init_per_testcase(_TestCase, Config) ->
    emqx_authn_test_lib:delete_authenticators(
        ?AUTHN_PATH,
        ?GLOBAL
    ),
    AuthConfig = authn_config(),
    {ok, _} = emqx:update_config(
        ?AUTHN_PATH,
        {create_authenticator, ?GLOBAL, AuthConfig}
    ),

    ok = emqx_authz_test_lib:reset_authorizers(),
    Config.

end_per_testcase(_TestCase, _Config) ->
    emqx_authn_test_lib:delete_authenticators(
        ?AUTHN_PATH,
        ?GLOBAL
    ),
    ok = emqx_authz_test_lib:restore_authorizers().

set_special_configs(emqx_authz) ->
    ok = emqx_authz_test_lib:reset_authorizers();
set_special_configs(_) ->
    ok.


t_topic_rules(_Config) ->
    Payload = #{
        <<"exp">> => erlang:system_time(second) + 60,
        <<"acl">> => #{
            <<"pub">> => [
                <<"eq testpub1/${username}">>,
                <<"testpub2/${clientid}">>,
                <<"testpub3/#">>
            ],
            <<"sub">> => [
                <<"eq testsub1/${username}">>,
                <<"testsub2/${clientid}">>,
                <<"testsub3/#">>
            ],
            <<"all">> => [
                <<"eq testall1/${username}">>,
                <<"testall2/${clientid}">>,
                <<"testall3/#">>
            ]
        },
        <<"username">> => <<"username">>
    },
    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    Cases = [
        {deny, <<"testpub1/username">>},
        {deny, <<"testpub2/clientid">>},
        {deny, <<"testpub3/foobar">>},

        {deny, <<"testsub1/username">>},
        {allow, <<"testsub1/${username}">>},
        {allow, <<"testsub2/clientid">>},
        {allow, <<"testsub3/foobar">>},
        {allow, <<"testsub3/+/foobar">>},
        {allow, <<"testsub3/#">>},

        {deny, <<"testsub2/username">>},
        {deny, <<"testsub1/clientid">>},
        {deny, <<"testsub4/foobar">>},

        {deny, <<"testall1/username">>},
        {allow, <<"testall1/${username}">>},
        {allow, <<"testall2/clientid">>},
        {allow, <<"testall3/foobar">>},
        {allow, <<"testall3/+/foobar">>},
        {allow, <<"testall3/#">>},

        {deny, <<"testall2/username">>},
        {deny, <<"testall1/clientid">>},
        {deny, <<"testall4/foobar">>}
    ],

    lists:foreach(
        fun
            ({allow, Topic}) ->
                ?assertMatch(
                    {ok, #{}, [0]},
                    emqtt:subscribe(C, Topic, 0)
                );
            ({deny, Topic}) ->
                ?assertMatch(
                    {ok, #{}, [?RC_NOT_AUTHORIZED]},
                    emqtt:subscribe(C, Topic, 0)
                )
        end,
        Cases
    ),

    ok = emqtt:disconnect(C).

t_check_pub(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]},
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ok = emqtt:publish(C, <<"a/b">>, <<"hi">>, 0),

    receive
        {publish, #{topic := <<"a/b">>}} ->
            ?assert(false, "Publish to `a/b` should not be allowed")
    after 100 -> ok
    end,

    ok = emqtt:disconnect(C).

t_check_no_recs(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{},
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_no_acl_claim(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"exp">> => erlang:system_time(second) + 10
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_str_exp(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"exp">> => integer_to_binary(erlang:system_time(second) + 10),
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]}
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_expire(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]},
        <<"exp">> => erlang:system_time(second) + 5
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),
    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:unsubscribe(C, <<"a/b">>)
    ),

    timer:sleep(6000),

    ?assertMatch(
        {ok, #{}, [?RC_NOT_AUTHORIZED]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ok = emqtt:disconnect(C).

t_check_no_expire(_Config) ->
    Payload = #{
        <<"username">> => <<"username">>,
        <<"acl">> => #{<<"sub">> => [<<"a/b">>]}
    },

    JWT = generate_jws(Payload),

    {ok, C} = emqtt:start_link(
        [
            {clean_start, true},
            {proto_ver, v5},
            {clientid, <<"clientid">>},
            {username, <<"username">>},
            {password, JWT}
        ]
    ),
    {ok, _} = emqtt:connect(C),
    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:subscribe(C, <<"a/b">>, 0)
    ),

    ?assertMatch(
        {ok, #{}, [0]},
        emqtt:unsubscribe(C, <<"a/b">>)
    ),

    ok = emqtt:disconnect(C).

t_check_undefined_expire(_Config) ->
    Acl = #{expire => undefined, rules => #{<<"sub">> => [<<"a/b">>]}},
    Client = #{acl => Acl},

    ?assertMatch(
        {matched, allow},
        emqx_authz_client_info:authorize(Client, subscribe, <<"a/b">>, undefined)
    ),

    ?assertMatch(
        {matched, deny},
        emqx_authz_client_info:authorize(Client, subscribe, <<"a/bar">>, undefined)
    ).


authn_config() ->
    #{
        <<"mechanism">> => <<"jwt">>,
        <<"use_jwks">> => <<"false">>,
        <<"algorithm">> => <<"hmac-based">>,
        <<"secret">> => ?SECRET,
        <<"secret_base64_encoded">> => <<"false">>,
        <<"acl_claim_name">> => <<"acl">>,
        <<"verify_claims">> => #{
            <<"username">> => ?PH_USERNAME
        }
    }.

generate_jws(Payload) ->
    JWK = jose_jwk:from_oct(?SECRET),
    Header = #{
        <<"alg">> => <<"HS256">>,
        <<"typ">> => <<"JWT">>
    },
    Signed = jose_jwt:sign(JWK, Header, Payload),
    {_, JWS} = jose_jws:compact(Signed),
    JWS.
