Pārlūkot izejas kodu

feat(krb): added test cases for kerberos authentication

firest 1 gadu atpakaļ
vecāks
revīzija
69874b61f9

+ 0 - 19
.ci/docker-compose-file/docker-compose-kafka.yaml

@@ -16,24 +16,6 @@ services:
     user: "${DOCKER_USER:-root}"
     volumes:
       - /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret
-  kdc:
-    hostname: kdc.emqx.net
-    image:  ghcr.io/emqx/emqx-builder/5.3-9:1.15.7-26.2.5-3-ubuntu22.04
-    container_name: kdc.emqx.net
-    expose:
-      - 88 # kdc
-      - 749 # admin server
-    # ports:
-    #   - 88:88
-    #   - 749:749
-    networks:
-      emqx_bridge:
-    volumes:
-      - /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret
-      - ./kerberos/krb5.conf:/etc/kdc/krb5.conf
-      - ./kerberos/krb5.conf:/etc/krb5.conf
-      - ./kerberos/run.sh:/usr/bin/run.sh
-    command: run.sh
   kafka_1:
     image: wurstmeister/kafka:2.13-2.8.1
     # ports:
@@ -76,4 +58,3 @@ services:
       - ./kerberos/krb5.conf:/etc/kdc/krb5.conf
       - ./kerberos/krb5.conf:/etc/krb5.conf
     command: kafka-entrypoint.sh
-

+ 21 - 0
.ci/docker-compose-file/docker-compose-kdc.yaml

@@ -0,0 +1,21 @@
+version: '3.9'
+
+services:
+  kdc:
+    hostname: kdc.emqx.net
+    image:  ghcr.io/emqx/emqx-builder/5.3-9:1.15.7-26.2.5-3-ubuntu22.04
+    container_name: kdc.emqx.net
+    expose:
+      - 88 # kdc
+      - 749 # admin server
+    # ports:
+    #   - "88:88"
+    #   - "749:749"
+    networks:
+      emqx_bridge:
+    volumes:
+      - /tmp/emqx-ci/emqx-shared-secret:/var/lib/secret
+      - ./kerberos/krb5.conf:/etc/kdc/krb5.conf
+      - ./kerberos/krb5.conf:/etc/krb5.conf
+      - ./kerberos/run.sh:/usr/bin/run.sh
+    command: run.sh

+ 1 - 0
.ci/docker-compose-file/kerberos/krb5.conf

@@ -6,6 +6,7 @@
   rdns = false
   dns_lookup_kdc   = no
   dns_lookup_realm = no
+  default_keytab_name = /var/lib/secret/erlang.keytab
 
 [realms]
   KDC.EMQX.NET = {

+ 12 - 1
.ci/docker-compose-file/kerberos/run.sh

@@ -6,20 +6,31 @@ echo "Remove old keytabs"
 rm -f /var/lib/secret/kafka.keytab > /dev/null 2>&1
 rm -f /var/lib/secret/rig.keytab > /dev/null 2>&1
 
+rm -f /var/lib/secret/erlang.keytab > /dev/null 2>&1
+rm -f /var/lib/secret/krb_authn_cli.keytab > /dev/null 2>&1
+
 echo "Create realm"
 
 kdb5_util -P emqx -r KDC.EMQX.NET create -s
 
 echo "Add principals"
 
-kadmin.local -w password -q "add_principal -randkey kafka/kafka-1.emqx.net@KDC.EMQX.NET"
+kadmin.local -w password -q "add_principal -randkey kafka/kafka-1.emqx.net@KDC.EMQX.NET" > /dev/null
 kadmin.local -w password -q "add_principal -randkey rig@KDC.EMQX.NET"  > /dev/null
 
+# For Kerberos Authn
+kadmin.local -w password -q "add_principal -randkey mqtt/erlang.emqx.net@KDC.EMQX.NET" > /dev/null
+kadmin.local -w password -q "add_principal -randkey krb_authn_cli@KDC.EMQX.NET"  > /dev/null
+
 
 echo "Create keytabs"
 
 kadmin.local -w password -q "ktadd  -k /var/lib/secret/kafka.keytab -norandkey kafka/kafka-1.emqx.net@KDC.EMQX.NET " > /dev/null
 kadmin.local -w password -q "ktadd  -k /var/lib/secret/rig.keytab -norandkey rig@KDC.EMQX.NET " > /dev/null
 
+# For Kerberos Authn
+kadmin.local -w password -q "ktadd  -k /var/lib/secret/erlang.keytab -norandkey mqtt/erlang.emqx.net@KDC.EMQX.NET " > /dev/null
+kadmin.local -w password -q "ktadd  -k /var/lib/secret/krb_authn_cli.keytab -norandkey krb_authn_cli@KDC.EMQX.NET " > /dev/null
+
 echo STARTING KDC
 /usr/sbin/krb5kdc -n

+ 2 - 2
apps/emqx/rebar.config

@@ -46,7 +46,7 @@
             {meck, "0.9.2"},
             {proper, "1.4.0"},
             {bbmustache, "1.10.0"},
-            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.10.0"}}}
+            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.12.0"}}}
         ]},
         {extra_src_dirs, [
             {"test", [recursive]},
@@ -58,7 +58,7 @@
             {meck, "0.9.2"},
             {proper, "1.4.0"},
             {bbmustache, "1.10.0"},
-            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.9.7"}}}
+            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.12.0"}}}
         ]},
         {extra_src_dirs, [{"test", [recursive]}]}
     ]}

+ 1 - 9
apps/emqx/src/emqx_channel.erl

@@ -397,15 +397,7 @@ handle_in(
                     connecting ->
                         process_connect(NProperties, NChannel);
                     reauthenticating ->
-                        {ok, Auth, NChannel1} =
-                            handle_out(
-                                auth,
-                                {?RC_SUCCESS, NProperties},
-                                NChannel#channel{conn_state = connected}
-                            ),
-                        {ok, Replies, NChannel2} =
-                            process_connect(NProperties, NChannel1),
-                        {ok, [?REPLY_OUTGOING(Auth) | Replies], NChannel2};
+                        process_connect(NProperties, NChannel);
                     _ ->
                         handle_out(
                             auth,

+ 1 - 0
apps/emqx_auth_kerberos/docker-ct

@@ -0,0 +1 @@
+kdc

+ 4 - 0
apps/emqx_auth_kerberos/include/emqx_auth_kerberos.hrl

@@ -13,4 +13,8 @@
 
 -define(AUTHN_TYPE_KERBEROS, {?AUTHN_MECHANISM_GSSAPI, ?AUTHN_BACKEND}).
 
+-define(AUTHN_METHOD, <<"GSSAPI-KERBEROS">>).
+
+-define(SERVICE, <<"mqtt">>).
+
 -endif.

+ 1 - 1
apps/emqx_auth_kerberos/src/emqx_auth_kerberos.app.src

@@ -13,6 +13,6 @@
     {env, []},
     {modules, []},
 
-    {licenses, ["Apache 2.0"]},
+    {licenses, ["BSL"]},
     {links, []}
 ]}.

+ 6 - 4
apps/emqx_auth_kerberos/src/emqx_authn_kerberos.erl

@@ -44,7 +44,7 @@ destroy(_) ->
 
 authenticate(
     #{
-        auth_method := <<"GSSAPI-KERBEROS">>,
+        auth_method := ?AUTHN_METHOD,
         auth_data := AuthData,
         auth_cache := AuthCache
     },
@@ -55,8 +55,10 @@ authenticate(
             auth_continue(SaslConn, AuthData);
         _ ->
             case auth_new(Principal) of
-                {ok, SaslConn} -> auth_begin(SaslConn, AuthData);
-                Error -> Error
+                {ok, SaslConn} ->
+                    auth_begin(SaslConn, AuthData);
+                Error ->
+                    Error
             end
     end;
 authenticate(_Credential, _State) ->
@@ -75,7 +77,7 @@ get_server_fqdn(Principal) ->
 
 auth_new(Principal) ->
     ServerFQDN = get_server_fqdn(Principal),
-    case sasl_auth:server_new(<<"emqx">>, Principal, ServerFQDN) of
+    case sasl_auth:server_new(?SERVICE, Principal, ServerFQDN) of
         {ok, SaslConn} ->
             {ok, SaslConn};
         Error ->

+ 1 - 3
apps/emqx_auth_kerberos/src/emqx_authn_kerberos_schema.erl

@@ -47,9 +47,7 @@ fields(kerberos) ->
                 })},
             {keytab_file,
                 ?HOCON(binary(), #{
-                    required => false,
-                    %% This is hidden for now because it has to be /etc/krb5.keytab
-                    importance => ?IMPORTANCE_HIDDEN,
+                    default => <<"/etc/krb5.keytab">>,
                     desc => ?DESC(keytab_file)
                 })}
         ].

+ 280 - 0
apps/emqx_auth_kerberos/test/emqx_authn_kerberos_SUITE.erl

@@ -0,0 +1,280 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%--------------------------------------------------------------------
+
+-module(emqx_authn_kerberos_SUITE).
+
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+-include("emqx_auth_kerberos.hrl").
+
+-include_lib("emqx/include/emqx_mqtt.hrl").
+-include_lib("emqx_auth/include/emqx_authn.hrl").
+
+-define(PATH, [authentication]).
+
+-define(INVALID_SVR_PRINCIPAL, <<"not-exists/erlang.emqx.nett@KDC.EMQX.NET">>).
+
+-define(SVR_HOST, "erlang.emqx.net").
+-define(SVR_PRINCIPAL, <<"mqtt/erlang.emqx.net@KDC.EMQX.NET">>).
+-define(SVR_KEYTAB_FILE, <<"/var/lib/secret/erlang.keytab">>).
+
+-define(CLI_NAME, "krb_authn_cli").
+-define(CLI_PRINCIPAL, <<"krb_authn_cli@KDC.EMQX.NET">>).
+-define(CLI_KEYTAB_FILE, <<"/var/lib/secret/krb_authn_cli.keytab">>).
+
+-define(HOST, "127.0.0.1").
+-define(PORT, 1883).
+
+all() ->
+    emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+    Apps = emqx_cth_suite:start([emqx, emqx_conf, emqx_auth, emqx_auth_kerberos], #{
+        work_dir => ?config(priv_dir, Config)
+    }),
+    IdleTimeout = emqx_config:get([mqtt, idle_timeout]),
+    [{apps, Apps}, {idle_timeout, IdleTimeout} | Config].
+
+end_per_suite(Config) ->
+    ok = emqx_config:put([mqtt, idle_timeout], ?config(idle_timeout, Config)),
+    ok = emqx_cth_suite:stop(?config(apps, Config)),
+    ok.
+
+init_per_testcase(_Case, Config) ->
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ),
+    Config.
+
+end_per_testcase(_Case, Config) ->
+    Config.
+
+%%------------------------------------------------------------------------------
+%% Tests
+%%------------------------------------------------------------------------------
+
+t_create(_Config) ->
+    ValidConfig = raw_config(),
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, ValidConfig}
+    ),
+
+    {ok, [#{provider := emqx_authn_kerberos}]} =
+        emqx_authn_chains:list_authenticators(?GLOBAL).
+
+t_create_invalid(_Config) ->
+    InvalidConfig0 = raw_config(),
+    InvalidConfig = InvalidConfig0#{<<"principal">> := ?INVALID_SVR_PRINCIPAL},
+
+    {error, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, InvalidConfig}
+    ),
+
+    ?assertEqual(
+        {error, {not_found, {chain, ?GLOBAL}}},
+        emqx_authn_chains:list_authenticators(?GLOBAL)
+    ).
+
+t_authenticate(_Config) ->
+    init_auth(),
+
+    {ok, Handler, CT1} = setup_cli(),
+    {ok, C} = emqtt:start_link(
+        #{
+            host => ?HOST,
+            port => ?PORT,
+            proto_ver => v5,
+            properties =>
+                #{
+                    'Authentication-Method' => ?AUTHN_METHOD,
+                    'Authentication-Data' => CT1
+                },
+            custom_auth_callbacks =>
+                #{
+                    init => auth_init(Handler),
+                    handle_auth => fun auth_handle/3
+                }
+        }
+    ),
+    ?assertMatch({ok, _}, emqtt:connect(C)),
+    stop_cli(Handler),
+    ok.
+
+t_authenticate_bad_props(_Config) ->
+    erlang:process_flag(trap_exit, true),
+    init_auth(),
+
+    {ok, Handler, CT1} = setup_cli(),
+    {ok, C} = emqtt:start_link(
+        #{
+            host => ?HOST,
+            port => ?PORT,
+            proto_ver => v5,
+            properties =>
+                #{
+                    'Authentication-Method' => <<"SCRAM-SHA-512">>,
+                    'Authentication-Data' => CT1
+                },
+            custom_auth_callbacks =>
+                #{
+                    init => auth_init(Handler),
+                    handle_auth => fun auth_handle/3
+                }
+        }
+    ),
+
+    ?assertMatch({error, {not_authorized, _}}, emqtt:connect(C)),
+    stop_cli(Handler),
+    ok.
+
+t_authenticate_bad_token(_Config) ->
+    erlang:process_flag(trap_exit, true),
+    init_auth(),
+
+    {ok, Handler, CT1} = setup_cli(),
+    {ok, C} = emqtt:start_link(
+        #{
+            host => ?HOST,
+            port => ?PORT,
+            proto_ver => v5,
+            properties =>
+                #{
+                    'Authentication-Method' => <<"SCRAM-SHA-512">>,
+                    'Authentication-Data' => <<CT1/binary, "invalid">>
+                },
+            custom_auth_callbacks =>
+                #{
+                    init => auth_init(Handler),
+                    handle_auth => fun auth_handle/3
+                }
+        }
+    ),
+
+    ?assertMatch({error, {not_authorized, _}}, emqtt:connect(C)),
+    stop_cli(Handler),
+    ok.
+
+t_destroy(_) ->
+    State = init_auth(),
+
+    emqx_authn_test_lib:delete_authenticators(
+        [authentication],
+        ?GLOBAL
+    ),
+
+    {ok, Handler, CT1} = setup_cli(),
+
+    ?assertMatch(
+        ignore,
+        emqx_authn_mongodb:authenticate(
+            #{
+                auth_method => ?AUTHN_METHOD,
+                auth_data => CT1,
+                auth_cache => undefined
+            },
+            State
+        )
+    ),
+
+    stop_cli(Handler),
+    ok.
+
+%%------------------------------------------------------------------------------
+%% Helpers
+%%------------------------------------------------------------------------------
+
+raw_config() ->
+    #{
+        <<"mechanism">> => <<"gssapi">>,
+        <<"backend">> => <<"kerberos">>,
+        <<"principal">> => ?SVR_PRINCIPAL,
+        <<"keytab_file">> => ?SVR_KEYTAB_FILE
+    }.
+
+init_auth() ->
+    Config = raw_config(),
+
+    {ok, _} = emqx:update_config(
+        ?PATH,
+        {create_authenticator, ?GLOBAL, Config}
+    ),
+
+    {ok, [#{state := State}]} = emqx_authn_chains:list_authenticators(?GLOBAL),
+
+    State.
+
+%%------------------------------------------------------------------------------
+%% Custom auth
+
+auth_init(Handler) ->
+    fun() -> #{handler => Handler, step => 1} end.
+
+auth_handle(AuthState, Reason, Props) ->
+    ct:pal(">>> auth packet received:\n  rc: ~p\n  props:\n  ~p", [Reason, Props]),
+    do_auth_handle(AuthState, Reason, Props).
+
+do_auth_handle(
+    #{handler := Handler, step := Step} = AuthState0,
+    continue_authentication,
+    #{
+        'Authentication-Method' := ?AUTHN_METHOD,
+        'Authentication-Data' := ST
+    }
+) when Step =< 3 ->
+    {ok, CT} = call_cli_agent(Handler, {step, ST}),
+    AuthState = AuthState0#{step := Step + 1},
+    OutProps = #{
+        'Authentication-Method' => ?AUTHN_METHOD,
+        'Authentication-Data' => CT
+    },
+    {continue, {?RC_CONTINUE_AUTHENTICATION, OutProps}, AuthState};
+do_auth_handle(_AuthState, _Reason, _Props) ->
+    {stop, protocol_error}.
+
+%%------------------------------------------------------------------------------
+%% Client Agent
+
+setup_cli() ->
+    Pid = erlang:spawn(fun() -> cli_agent_loop(#{}) end),
+    {ok, CT1} = call_cli_agent(Pid, setup),
+    {ok, Pid, CT1}.
+
+call_cli_agent(Pid, Msg) ->
+    Ref = erlang:make_ref(),
+    erlang:send(Pid, {call, self(), Ref, Msg}),
+    receive
+        {Ref, Data} ->
+            {ok, Data}
+    after 3000 ->
+        error("client agent timeout")
+    end.
+
+stop_cli(Pid) ->
+    erlang:send(Pid, stop).
+
+cli_agent_loop(State) ->
+    receive
+        stop ->
+            ok;
+        {call, From, Ref, Msg} ->
+            {ok, Reply, State2} = cli_agent_handler(Msg, State),
+            erlang:send(From, {Ref, Reply}),
+            cli_agent_loop(State2)
+    end.
+
+cli_agent_handler(setup, State) ->
+    ok = sasl_auth:kinit(?CLI_KEYTAB_FILE, ?CLI_PRINCIPAL),
+    {ok, Client} = sasl_auth:client_new(?SERVICE, ?SVR_HOST, ?CLI_PRINCIPAL, ?CLI_NAME),
+    {ok, {sasl_continue, CT1}} = sasl_auth:client_start(Client),
+    {ok, CT1, State#{client => Client}};
+cli_agent_handler({step, ST}, #{client := Client} = State) ->
+    {ok, {_, CT}} = sasl_auth:client_step(Client, ST),
+    {ok, CT, State}.

+ 1 - 0
apps/emqx_bridge_azure_event_hub/docker-ct

@@ -1,2 +1,3 @@
 toxiproxy
+kdc
 kafka

+ 1 - 0
apps/emqx_bridge_confluent/docker-ct

@@ -1,2 +1,3 @@
 toxiproxy
+kdc
 kafka

+ 1 - 0
apps/emqx_bridge_kafka/docker-ct

@@ -1,2 +1,3 @@
 toxiproxy
+kdc
 kafka

+ 1 - 1
apps/emqx_machine/priv/reboot_lists.eterm

@@ -137,7 +137,7 @@
             emqx_bridge_syskeeper,
             emqx_bridge_confluent,
             emqx_ds_shared_sub,
-            emqx_auth_gssapi,
+            emqx_auth_kerberos,
             emqx_auth_ext,
             emqx_cluster_link,
             emqx_ds_builtin_raft

+ 1 - 1
apps/emqx_retainer/rebar.config

@@ -30,7 +30,7 @@
 {profiles, [
     {test, [
         {deps, [
-            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.10.0"}}}
+            {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.2.3.4"}}}
         ]}
     ]}
 ]}.

+ 8 - 4
mix.exs

@@ -159,7 +159,8 @@ defmodule EMQXUmbrella.MixProject do
       common_dep(:uuid),
       {:quickrand, github: "okeuday/quickrand", tag: "v2.0.6", override: true},
       common_dep(:ra),
-      {:mimerl, "1.2.0", override: true}
+      {:mimerl, "1.2.0", override: true},
+      common_dep(:sasl_auth)
     ]
   end
 
@@ -246,7 +247,7 @@ defmodule EMQXUmbrella.MixProject do
   def common_dep(:emqtt),
     do:
       {:emqtt,
-       github: "emqx/emqtt", tag: "1.10.1", override: true, system_env: maybe_no_quic_env()}
+       github: "emqx/emqtt", tag: "1.12.0", override: true, system_env: maybe_no_quic_env()}
 
   def common_dep(:typerefl),
     do: {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true}
@@ -273,6 +274,9 @@ defmodule EMQXUmbrella.MixProject do
       system_env: emqx_app_system_env()
     }
 
+  def common_dep(:sasl_auth),
+    do: {:sasl_auth, github: "kafka4beam/sasl_auth", tag: "v2.1.1", override: true}
+
   ###############################################################################################
   # BEGIN DEPRECATED FOR MIX BLOCK
   # These should be removed once we fully migrate to mix
@@ -376,10 +380,10 @@ defmodule EMQXUmbrella.MixProject do
       :emqx_gateway_jt808,
       :emqx_bridge_syskeeper,
       :emqx_ds_shared_sub,
-      :emqx_auth_gssapi,
       :emqx_auth_ext,
       :emqx_cluster_link,
-      :emqx_ds_builtin_raft
+      :emqx_ds_builtin_raft,
+      :emqx_auth_kerberos
     ])
   end
 

+ 1 - 1
rebar.config

@@ -91,7 +91,7 @@
     {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.7"}}},
     {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.8"}}},
     {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
-    {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.10.1"}}},
+    {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.12.0"}}},
     {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.2.1"}}},
     % NOTE: depends on recon 2.5.x
     {observer_cli, "1.7.1"},

+ 1 - 0
rebar.config.erl

@@ -126,6 +126,7 @@ is_community_umbrella_app("apps/emqx_ds_shared_sub") -> false;
 is_community_umbrella_app("apps/emqx_auth_ext") -> false;
 is_community_umbrella_app("apps/emqx_cluster_link") -> false;
 is_community_umbrella_app("apps/emqx_ds_builtin_raft") -> false;
+is_community_umbrella_app("apps/emqx_auth_kerberos") -> false;
 is_community_umbrella_app(_) -> true.
 
 %% BUILD_WITHOUT_JQ

+ 3 - 0
scripts/ct/run.sh

@@ -256,6 +256,9 @@ for dep in ${CT_DEPS}; do
 	couchbase)
 	    FILES+=( '.ci/docker-compose-file/docker-compose-couchbase.yaml' )
 	    ;;
+	kdc)
+	    FILES+=( '.ci/docker-compose-file/docker-compose-kdc.yaml' )
+	    ;;
         *)
             echo "unknown_ct_dependency $dep"
             exit 1