Skip to content

Commit eff36e1

Browse files
committed
WIP Support AMQP 1.0 token renewal
Closes #9259. ## What? Allow an AMQP 1.0 client to renew an OAuth 2.0 token before it expires. ## Why? This allows clients to keep the AMQP connection open instead of having to create a new connection whenever the token expires. ## How? As explained in #9259 (comment) the client can `PUT` a new token on HTTP API v2 path `/auth/tokens`. RabbitMQ will then: 1. Store the new token on the given connection. 2. Recheck access to the connection's vhost. 3. Clear all permission caches in the AMQP sessions. 4. Recheck write permissions to exchanges for links publishing to RabbitMQ, and recheck read permissions from queues for links consuming from RabbitMQ. The latter complies with the user expectation in #11364. TODOs: * Check new log process metadata * Update docs
1 parent 3df9675 commit eff36e1

File tree

8 files changed

+411
-55
lines changed

8 files changed

+411
-55
lines changed

deps/rabbit/src/rabbit_access_control.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ check_user_id0(ClaimedUserName, #user{username = ActualUserName,
249249
end.
250250

251251
-spec update_state(User :: rabbit_types:user(), NewState :: term()) ->
252-
{'ok', rabbit_types:auth_user()} |
252+
{'ok', rabbit_types:user()} |
253253
{'refused', string()} |
254254
{'error', any()}.
255255

deps/rabbit/src/rabbit_amqp_management.erl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,19 @@ handle_http_req(<<"GET">>,
381381
Bindings0 = rabbit_binding:list_for_source_and_destination(SrcXName, DstName),
382382
Bindings = [B || B = #binding{key = K} <- Bindings0, K =:= Key],
383383
RespPayload = encode_bindings(Bindings),
384-
{<<"200">>, RespPayload, PermCaches}.
384+
{<<"200">>, RespPayload, PermCaches};
385+
386+
handle_http_req(<<"PUT">>,
387+
[<<"auth">>, <<"tokens">>],
388+
_Query,
389+
ReqPayload,
390+
_Vhost,
391+
_User,
392+
ConnPid,
393+
PermCaches) ->
394+
{binary, Token} = ReqPayload,
395+
ok = rabbit_amqp_reader:set_credential(ConnPid, Token),
396+
{<<"204">>, null, PermCaches}.
385397

386398
decode_queue({map, KVList}) ->
387399
M = lists:foldl(

deps/rabbit/src/rabbit_amqp_reader.erl

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
-export([init/1,
1515
info/2,
16-
mainloop/2]).
16+
mainloop/2,
17+
set_credential/2]).
1718

1819
-export([system_continue/3,
1920
system_terminate/4,
@@ -53,6 +54,7 @@
5354
channel_max :: non_neg_integer(),
5455
auth_mechanism :: sasl_init_unprocessed | {binary(), module()},
5556
auth_state :: term(),
57+
credential_timer :: undefined | reference(),
5658
properties :: undefined | {map, list(tuple())}
5759
}).
5860

@@ -139,6 +141,11 @@ server_properties() ->
139141
Props = [{{symbol, <<"node">>}, {utf8, atom_to_binary(node())}} | Props1],
140142
{map, Props}.
141143

144+
-spec set_credential(pid(), binary()) -> ok.
145+
set_credential(Pid, Credential) ->
146+
Pid ! {set_credential, Credential},
147+
ok.
148+
142149
%%--------------------------------------------------------------------------
143150

144151
inet_op(F) -> rabbit_misc:throw_on_error(inet_error, F).
@@ -243,6 +250,8 @@ handle_other({'$gen_cast', {force_event_refresh, _Ref}}, State) ->
243250
State;
244251
handle_other(terminate_connection, _State) ->
245252
stop;
253+
handle_other({set_credential, Cred}, State) ->
254+
set_credential0(Cred, State);
246255
handle_other(credential_expired, State) ->
247256
Error = error_frame(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS, "credential expired", []),
248257
handle_exception(State, 0, Error);
@@ -416,15 +425,17 @@ handle_connection_frame(
416425
},
417426
helper_sup = HelperSupPid,
418427
sock = Sock} = State0) ->
419-
logger:update_process_metadata(#{amqp_container => ContainerId}),
420428
Vhost = vhost(Hostname),
429+
logger:update_process_metadata(#{amqp_container => ContainerId,
430+
vhost => Vhost,
431+
user => Username}),
421432
ok = check_user_loopback(State0),
422433
ok = check_vhost_exists(Vhost, State0),
423434
ok = check_vhost_alive(Vhost),
424435
ok = rabbit_access_control:check_vhost_access(User, Vhost, {socket, Sock}, #{}),
425436
ok = check_vhost_connection_limit(Vhost, Username),
426437
ok = check_user_connection_limit(Username),
427-
ok = ensure_credential_expiry_timer(User),
438+
Timer = maybe_start_credential_expiry_timer(User),
428439
rabbit_core_metrics:auth_attempt_succeeded(<<>>, Username, amqp10),
429440
notify_auth(user_authentication_success, Username, State0),
430441
rabbit_log_connection:info(
@@ -499,7 +510,8 @@ handle_connection_frame(
499510
outgoing_max_frame_size = OutgoingMaxFrameSize,
500511
channel_max = EffectiveChannelMax,
501512
properties = Properties,
502-
timeout = ReceiveTimeoutMillis},
513+
timeout = ReceiveTimeoutMillis,
514+
credential_timer = Timer},
503515
heartbeater = Heartbeater},
504516
State = start_writer(State1),
505517
HostnameVal = case Hostname of
@@ -871,39 +883,57 @@ check_user_connection_limit(Username) ->
871883
end.
872884

873885

874-
%% TODO Provide a means for the client to refresh the credential.
875-
%% This could be either via:
876-
%% 1. SASL (if multiple authentications are allowed on the same AMQP 1.0 connection), see
877-
%% https://datatracker.ietf.org/doc/html/rfc4422#section-3.8 , or
878-
%% 2. Claims Based Security (CBS) extension, see https://docs.oasis-open.org/amqp/amqp-cbs/v1.0/csd01/amqp-cbs-v1.0-csd01.html
879-
%% and https://github.com/rabbitmq/rabbitmq-server/issues/9259
880-
%% 3. Simpler variation of 2. where a token is put to a special /token node.
881-
%%
882-
%% If the user does not refresh their credential on time (the only implementation currently),
883-
%% close the entire connection as we must assume that vhost access could have been revoked.
884-
%%
885-
%% If the user refreshes their credential on time (to be implemented), the AMQP reader should
886-
%% 1. rabbit_access_control:check_vhost_access/4
887-
%% 2. send a message to all its sessions which should then erase the permission caches and
888-
%% re-check all link permissions (i.e. whether reading / writing to exchanges / queues is still allowed).
889-
%% 3. cancel the current timer, and set a new timer
890-
%% similary as done for Stream connections, see https://github.com/rabbitmq/rabbitmq-server/issues/10292
891-
ensure_credential_expiry_timer(User) ->
886+
set_credential0(Cred,
887+
State = #v1{connection = #v1_connection{
888+
user = User0,
889+
vhost = Vhost,
890+
credential_timer = OldTimer} = Conn,
891+
tracked_channels = Chans,
892+
sock = Sock}) ->
893+
rabbit_log:info("updating credential", []),
894+
case rabbit_access_control:update_state(User0, Cred) of
895+
{ok, User} ->
896+
try rabbit_access_control:check_vhost_access(User, Vhost, {socket, Sock}, #{}) of
897+
ok ->
898+
maps:foreach(fun(_ChanNum, Pid) ->
899+
rabbit_amqp_session:reset_authz(Pid, User)
900+
end, Chans),
901+
case OldTimer of
902+
undefined -> ok;
903+
Ref -> erlang:cancel_timer(Ref, [{info, false}])
904+
end,
905+
NewTimer = maybe_start_credential_expiry_timer(User),
906+
State#v1{connection = Conn#v1_connection{
907+
user = User,
908+
credential_timer = NewTimer}}
909+
catch _:Reason ->
910+
Error = error_frame(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
911+
"access to vhost ~s failed for new credential: ~p",
912+
[Vhost, Reason]),
913+
handle_exception(State, 0, Error)
914+
end;
915+
Err ->
916+
Error = error_frame(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
917+
"credential update failed: ~p",
918+
[Err]),
919+
handle_exception(State, 0, Error)
920+
end.
921+
922+
maybe_start_credential_expiry_timer(User) ->
892923
case rabbit_access_control:expiry_timestamp(User) of
893924
never ->
894-
ok;
925+
undefined;
895926
Ts when is_integer(Ts) ->
896927
Time = (Ts - os:system_time(second)) * 1000,
897928
rabbit_log:debug(
898-
"Credential expires in ~b ms frow now (absolute timestamp = ~b seconds since epoch)",
929+
"credential expires in ~b ms frow now (absolute timestamp = ~b seconds since epoch)",
899930
[Time, Ts]),
900931
case Time > 0 of
901932
true ->
902-
_TimerRef = erlang:send_after(Time, self(), credential_expired),
903-
ok;
933+
erlang:send_after(Time, self(), credential_expired);
904934
false ->
905935
protocol_error(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
906-
"Credential expired ~b ms ago", [abs(Time)])
936+
"credential expired ~b ms ago", [abs(Time)])
907937
end
908938
end.
909939

deps/rabbit/src/rabbit_amqp_session.erl

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@
9090
list_local/0,
9191
conserve_resources/3,
9292
check_resource_access/4,
93-
check_read_permitted_on_topic/4
93+
check_read_permitted_on_topic/4,
94+
reset_authz/2
9495
]).
9596

9697
-export([init/1,
@@ -393,6 +394,10 @@ init({ReaderPid, WriterPid, ChannelNum, MaxFrameSize, User, Vhost, ConnName,
393394
handle_max = ClientHandleMax}}) ->
394395
process_flag(trap_exit, true),
395396
rabbit_process_flag:adjust_for_message_handling_proc(),
397+
logger:update_process_metadata(#{channel_number => ChannelNum,
398+
connection => ConnName,
399+
vhost => Vhost,
400+
user => User#user.username}),
396401

397402
ok = pg:join(pg_scope(), self(), self()),
398403
Alarms0 = rabbit_alarm:register(self(), {?MODULE, conserve_resources, []}),
@@ -480,6 +485,10 @@ list_local() ->
480485
conserve_resources(Pid, Source, {_, Conserve, _}) ->
481486
gen_server:cast(Pid, {conserve_resources, Source, Conserve}).
482487

488+
-spec reset_authz(pid(), rabbit_types:user()) -> ok.
489+
reset_authz(Pid, User) ->
490+
gen_server:cast(Pid, {reset_authz, User}).
491+
483492
handle_call(Msg, _From, State) ->
484493
Reply = {error, {not_understood, Msg}},
485494
reply(Reply, State).
@@ -574,7 +583,18 @@ handle_cast({conserve_resources, Alarm, Conserve},
574583
noreply(State);
575584
handle_cast(refresh_config, #state{cfg = #cfg{vhost = Vhost} = Cfg} = State0) ->
576585
State = State0#state{cfg = Cfg#cfg{trace_state = rabbit_trace:init(Vhost)}},
577-
noreply(State).
586+
noreply(State);
587+
handle_cast({reset_authz, User}, #state{cfg = Cfg} = State0) ->
588+
State1 = State0#state{
589+
permission_cache = [],
590+
topic_permission_cache = [],
591+
cfg = Cfg#cfg{user = User}},
592+
try recheck_authz(State1) of
593+
State ->
594+
noreply(State)
595+
catch exit:#'v1_0.error'{} = Error ->
596+
log_error_and_close_session(Error, State1)
597+
end.
578598

579599
log_error_and_close_session(
580600
Error, State = #state{cfg = #cfg{reader_pid = ReaderPid,
@@ -3522,6 +3542,29 @@ check_topic_authorisation(#exchange{type = topic,
35223542
check_topic_authorisation(_, _, _, _, Cache) ->
35233543
Cache.
35243544

3545+
recheck_authz(#state{incoming_links = IncomingLinks,
3546+
outgoing_links = OutgoingLinks,
3547+
permission_cache = Cache0,
3548+
cfg = #cfg{user = User}
3549+
} = State) ->
3550+
rabbit_log:debug("rechecking link authorizations", []),
3551+
Cache1 = maps:fold(
3552+
fun(_Handle, #incoming_link{exchange = X}, Cache) ->
3553+
case X of
3554+
#exchange{name = XName} ->
3555+
check_resource_access(XName, write, User, Cache);
3556+
#resource{} = XName ->
3557+
check_resource_access(XName, write, User, Cache);
3558+
to ->
3559+
Cache
3560+
end
3561+
end, Cache0, IncomingLinks),
3562+
Cache2 = maps:fold(
3563+
fun(_Handle, #outgoing_link{queue_name = QName}, Cache) ->
3564+
check_resource_access(QName, read, User, Cache)
3565+
end, Cache1, OutgoingLinks),
3566+
State#state{permission_cache = Cache2}.
3567+
35253568
check_user_id(Mc, User) ->
35263569
case rabbit_access_control:check_user_id(Mc, User) of
35273570
ok ->

deps/rabbit/src/rabbit_channel.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ force_event_refresh(Ref) ->
470470
list_queue_states(Pid) ->
471471
gen_server2:call(Pid, list_queue_states).
472472

473-
-spec update_user_state(pid(), rabbit_types:auth_user()) -> 'ok' | {error, channel_terminated}.
473+
-spec update_user_state(pid(), rabbit_types:user()) -> 'ok' | {error, channel_terminated}.
474474

475475
update_user_state(Pid, UserState) when is_pid(Pid) ->
476476
case erlang:is_process_alive(Pid) of

deps/rabbitmq_amqp_client/src/rabbitmq_amqp_client.erl

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
declare_exchange/3,
2929
bind_exchange/5,
3030
unbind_exchange/5,
31-
delete_exchange/2
31+
delete_exchange/2,
32+
33+
set_token/2
3234
].
3335

3436
-define(TIMEOUT, 20_000).
@@ -381,6 +383,23 @@ delete_exchange(LinkPair, ExchangeName) ->
381383
Err
382384
end.
383385

386+
%% Renew OAuth 2.0 token.
387+
-spec set_token(link_pair(), binary()) ->
388+
ok | {error, term()}.
389+
set_token(LinkPair, Token) ->
390+
Props = #{subject => <<"PUT">>,
391+
to => <<"/auth/tokens">>},
392+
Body = {binary, Token},
393+
case request(LinkPair, Props, Body) of
394+
{ok, Resp} ->
395+
case is_success(Resp) of
396+
true -> ok;
397+
false -> {error, Resp}
398+
end;
399+
Err ->
400+
Err
401+
end.
402+
384403
-spec request(link_pair(), amqp10_msg:amqp10_properties(), amqp10_prim()) ->
385404
{ok, Response :: amqp10_msg:amqp10_msg()} | {error, term()}.
386405
request(#link_pair{session = Session,

0 commit comments

Comments
 (0)