Current section

Files

Jump to
brod_gssapi src brod_gssapi_v1.erl
Raw

src/brod_gssapi_v1.erl

%% @private
-module(brod_gssapi_v1).
-export([auth/1, auth/6]).
-define(HANDSHAKE_V1, 1).
%%% Flow :
%%%
%%% Note any error is immediately returned back up the stack to kafka_protocol.
%%%
%%% -----------------------------------------------------------------
%%% 1) Initialize a TGT cache for the specified principal via
%%% sasl_auth:kinit/2 and continue to step 2 if successfull.
%%%
%%% 2) Initial a new sasl context for the auth session and continue
%%% to step 3 if successful.
%%%
%%% 3) Select a mechanism (GSSAPI) and start a sasl auth session.
%%% A successful start returns our first sasl token which is used
%%% in step 5. Move forward to step 3 if successful.
%%%
%%% 4) We now perform a handshake with Kafka using the SaslHandshake call
%%% with GSSAPI mechanism (see references for detail).
%%% If Kafka supports GSSAPI mechanism, we are OK to move ahead to step 5.
%%%
%%% 5) At this point we we send the token received via sasl_auth in step 3
%%% to kafka. Kafka will respond with either a new token or an
%%% error. Continue to step 6 if a token was received.
%%%
%%% 6) Now we send the token received from kafka in step 5 to our first
%%% sasl_auth:client_step/2 call. If successfull, we get back
%%% `{sasl_continue, Token}` if successful and we may continue to
%%% step 7.
%%%
%%% 7. Now we simply repeat steps 4 and 6 until we sasl_auth:client_step/2
%%% returns `{sasl_ok, Token}' or we receive an error from either
%%% sasl_auth:client_step/2 or kafka.
%%%
%%% 8) Once we have received `{sasl_ok, Token}' from sasl_auth:client_step/2,
%%% we are done but need to since this last returned sasl token to kafka
%%% to ensure we are authenticated.
%%%
%%% -------------------------------------------------------------------
%%% References :
%%% The Kerberos V5 ("GSSAPI") SASL Mechnism - https://datatracker.ietf.org/doc/html/rfc4752
%%% KIP-43 - https://cwiki.apache.org/confluence/display/KAFKA/KIP-43%3A+Kafka+SASL+enhancements
%%% Introduction to SASL - https://docs.oracle.com/cd/E23824_01/html/819-2145/sasl.intro.20.html
%% For backwards compat with version <= 0.2
-spec auth(
Host :: string(),
Sock :: gen_tcp:socket() | ssl:sslsocket(),
Mod :: gen_tcp | ssl,
ClientId :: binary(),
Timeout :: pos_integer(),
SaslOpts :: term()
) -> ok | {error, Reason :: term()}.
auth(
Host,
Sock,
Mod,
ClientId,
Timeout,
Opts
) ->
State = brod_gssapi:new(Host, Sock, Mod, ClientId, ?HANDSHAKE_V1, Timeout, Opts),
auth(State).
%%%-------------------------------------------------------------------
%% @doc
%% Returns 'ok' if authentication successfully completed. See spec in behavior
%% @end
% Observed Sequence of handshake is as follows:
-spec auth(brod_gssapi:state()) -> ok | {error, Reason :: term()}.
auth(State) ->
case auth_init(State) of
{ok, State1} ->
case auth_begin(State1) of
{ok, SaslRes} ->
auth_continue(State1, SaslRes);
Error ->
Error
end;
Error ->
Error
end.
-spec auth_init(State :: brod_gssapi:state()) -> {ok, brod_gssapi:state()} | {error, term()}.
auth_init(#{keytab := Keytab, principal := Principal, host := Host} = State) ->
case sasl_auth:kinit(Keytab, Principal) of
ok ->
case sasl_auth:client_new(<<"kafka">>, Host, Principal) of
{ok, SaslConn} ->
{ok, State#{sasl_conn => SaslConn}};
Error ->
Error
end;
Error ->
Error
end.
-spec auth_begin(State :: brod_gssapi:state()) -> {ok, term()} | {error, term()}.
auth_begin(#{sasl_conn := Conn} = State) ->
case sasl_auth:client_start(Conn) of
{ok, SaslRes} ->
case handshake(State) of
ok ->
{ok, SaslRes};
Error ->
Error
end;
Other ->
Other
end.
-spec auth_continue(State :: brod_gssapi:state(), {atom(), Challenge :: binary()}) ->
ok | {error, term()}.
auth_continue(State, {sasl_ok, Challenge}) ->
case send_sasl_token(State, Challenge) of
{ok, _} ->
set_sock_opts(State, [{active, once}]);
Error ->
Error
end;
auth_continue(#{handshake_vsn := 1, sasl_conn := Conn} = State, {sasl_continue, Challenge}) ->
case send_sasl_token(State, Challenge) of
{ok, Token} ->
case sasl_auth:client_step(Conn, Token) of
{ok, SaslRes} ->
auth_continue(State, SaslRes);
Other ->
Other
end;
Error ->
Error
end.
%%====================================================================
%% Internal functions
%%====================================================================
-dialyzer({nowarn_function, set_sock_opts/2}).
-spec set_sock_opts(State :: brod_gssapi:state(), [gen_tcp:option()]) -> ok | {error, inet:posix()}.
set_sock_opts(#{sock := Sock, transport_mod := gen_tcp}, Opts) ->
inet:setopts(Sock, Opts);
set_sock_opts(#{sock := Sock, transport_mod := ssl}, Opts) ->
ssl:setopts(Sock, Opts).
-spec send_sasl_token(State :: brod_gssapi:state(), Challenge :: binary()) ->
{ok, binary()} | {error, term()}.
send_sasl_token(State, Challenge) ->
#{handshake_vsn := HandshakeVsn, timeout := Timeout} = State,
#{sock := Sock, transport_mod := Mod, client_id := ClientId} = State,
Req = kpro_req_lib:make(sasl_authenticate, HandshakeVsn, [{auth_bytes, Challenge}]),
Rsp = kpro_lib:send_and_recv(Req, Sock, Mod, ClientId, Timeout),
case kpro:find(error_code, Rsp) of
no_error ->
{ok, kpro:find(auth_bytes, Rsp)};
_ ->
{error, kpro:find(error_message, Rsp)}
end.
-spec handshake(State :: brod_gssapi:state()) -> ok | {error, term()}.
handshake(State) ->
#{handshake_vsn := HandshakeVsn, mechanism := Mech, timeout := Timeout} = State,
#{sock := Sock, transport_mod := Mod, client_id := ClientId} = State,
Req = kpro_req_lib:make(sasl_handshake, HandshakeVsn, [{mechanism, Mech}]),
Rsp = kpro_lib:send_and_recv(Req, Sock, Mod, ClientId, Timeout),
case kpro:find(error_code, Rsp) of
no_error ->
ok;
unsupported_sasl_mechanism ->
EnabledMechanisms = kpro:find(enabled_mechanisms, Rsp),
Msg = io_lib:format(
"sasl mechanism ~s is not enabled in "
"kafka, enabled mechanism(s): ~s",
[Mech, string:join(EnabledMechanisms, ",")]
),
{error, iolist_to_binary(Msg)};
Other ->
{error, Other}
end.