nickva commented on code in PR #6069: URL: https://github.com/apache/couchdb/pull/6069#discussion_r3825625593
########## src/couch_replicator/src/couch_replicator_auth_ibm.erl: ########## @@ -0,0 +1,750 @@ +% 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 +% +% http://www.apache.org/licenses/LICENSE-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. + +% This module allows a replication source or target to use an IAM api key for authentication. +% +% Features; +% +% Automatic refreshing of time-limited token before expiration +% Deduplication - only one token will be acquired for each distinct IAM api key +% +% Implementation details +% +% As api keys are sensitive, the only copy of api keys is held in a private ETS table +% owned by this module's gen_server. An opaque reference is returned to clients (this is +% a message authentication code where the key is a non-persisted value generated by the +% gen_server) + +-module(couch_replicator_auth_ibm). + +-behaviour(couch_replicator_auth). +-behaviour(gen_server). +-behaviour(config_listener). + +-export([ + sup_initialize/0, + sup_cleanup/1, + initialize/1, + update_headers/2, + handle_response/3, + cleanup/1 +]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +% config_listener callbacks +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). +-compile({no_auto_import, [now/0]}). + +-define(EARLY_REFRESH_MS, 300_000). +-define(JITTER_MS, 60_000). +-define(MIN_REFRESH_MS, 10_000). +-define(MINUTE, 60). +-define(PUBLIC, couch_replicator_auth_ibm_public). +-define(PRIVATE, couch_replicator_auth_ibm_private). + +-record(worker_state, { + api_key_mac, + last_used +}). + +-record(state, { + gun_mref, + gun_pid, + mac_key, + token_uri_map +}). + +-record(public_entry, { + api_key_mac, + token +}). + +-record(private_entry, { + api_key_mac, + api_key_uuid, + api_key, + expires_ref, + gun_body = [], + gun_status_code, + gun_stream_ref, + last_used, + refresh_ref, + token_updated_at, + waiters = [] +}). + +%% callbacks + +sup_initialize() -> + application:ensure_all_started(gun), + {ok, _} = gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +sup_cleanup(_) -> + ok = gen_server:stop(?MODULE). + +initialize(#httpdb{} = HttpDb) -> + case extract_api_key(HttpDb) of + {ok, APIKey} -> + case gen_server:call(?MODULE, {register, APIKey}) of + {ok, APIKeyMAC} -> + WorkerState = #worker_state{api_key_mac = APIKeyMAC, last_used = now()}, + {ok, HttpDb, WorkerState}; + {error, Reason} -> + {error, Reason} + end; + {error, _} -> + ignore + end. + +update_headers(WorkerState, Headers) when is_list(Headers) -> + case get_token(WorkerState) of + {ok, Token} -> + { + [{~"Authorization", <<"Bearer ", Token/binary>>} | Headers], + update_last_used(WorkerState) + }; + {error, Reason} -> + couch_log:warning("Error when retrieving token: ~p", [Reason]), + {Headers, WorkerState} + end. + +handle_response(WorkerState, _StatusCode, _Headers) -> + {continue, WorkerState}. + +cleanup(_WorkerState) -> + ok. + +get_token(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC} = WorkerState, + case ets:lookup(?PUBLIC, APIKeyMAC) of + [#public_entry{token = Token}] when is_binary(Token) -> + {ok, Token}; + [#public_entry{}] -> + gen_server:call(?MODULE, {get_token, APIKeyMAC}, token_timeout()); + [] -> + {error, no_token} + end. + +update_last_used(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC, last_used = LastUsed} = WorkerState, + Now = now(), + %% only send a message once per minute at most. + case Now - LastUsed > ?MINUTE of + true -> + ?MODULE ! {update_last_used, APIKeyMAC}, + WorkerState#worker_state{last_used = Now}; + false -> + WorkerState + end. + +%% gen_server callbacks. + +init(_) -> + case token_uri_map() of + {ok, TokenURIMap} -> + ?PUBLIC = ets:new(?PUBLIC, [protected, {keypos, #public_entry.api_key_mac}, named_table]), + ?PRIVATE = ets:new(?PRIVATE, [ + private, {keypos, #private_entry.api_key_mac}, named_table + ]), + ok = config:listen_for_changes(?MODULE, nil), + start_gun(#state{ + mac_key = crypto:strong_rand_bytes(32), + token_uri_map = TokenURIMap + }); + {error, Reason} -> + {error, Reason} + end. + +handle_call({register, APIKey}, _From, State) -> + case ets:match_object(?PRIVATE, #private_entry{api_key = APIKey, _ = '_'}) of + [#private_entry{} = Entry] -> + {reply, {ok, Entry#private_entry.api_key_mac}, State}; + [] -> + GunStreamRef = acquire_token(APIKey, State), + APIKeyMAC = mac(State#state.mac_key, APIKey), + true = ets:insert_new(?PUBLIC, #public_entry{ + api_key_mac = APIKeyMAC + }), + true = ets:insert_new(?PRIVATE, #private_entry{ + api_key = APIKey, + api_key_mac = APIKeyMAC, + gun_stream_ref = GunStreamRef, + last_used = now() + }), + {reply, {ok, APIKeyMAC}, State} + end; +handle_call({get_token, APIKeyMAC}, From, State) -> + case ets:lookup(?PUBLIC, APIKeyMAC) of + [] -> + {reply, {error, no_such_api_key}, State}; + [#public_entry{token = Token}] when Token /= undefined -> + {reply, {ok, Token}, State}; + [#public_entry{}] -> + [#private_entry{} = Entry] = ets:lookup(?PRIVATE, APIKeyMAC), + ets:insert(?PRIVATE, Entry#private_entry{waiters = [From | Entry#private_entry.waiters]}), + case Entry of + #private_entry{gun_stream_ref = GunStreamRef} when + GunStreamRef /= undefined + -> + ok; + #private_entry{} -> + self() ! {refresh_token, APIKeyMAC} + end, + {noreply, State} + end; +handle_call(_Msg, _From, State) -> + {reply, {error, unexpected_msg}, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({update_last_used, APIKeyMAC}, State) -> + ets:update_element(?PRIVATE, APIKeyMAC, {#private_entry.last_used, now()}), + {noreply, State}; +handle_info({refresh_token, APIKeyMAC}, State) -> + case ets:lookup(?PRIVATE, APIKeyMAC) of + [] -> + ok; + [#private_entry{gun_stream_ref = undefined} = Entry] -> + case Entry#private_entry.last_used > Entry#private_entry.token_updated_at of Review Comment: Will this always work? If either last_used or token_update_at might be undefined or a non-integer we might get unexpected states here. Monotonic times also always start with negative values. It should be fine to compare them as both of them came from the same monotonic clock and are initialized from it. ########## src/docs/src/replication/replicator.rst: ########## @@ -809,6 +809,28 @@ they are used. If they are not, then URL userinfo is checked. If credentials are found there, then those credentials are used, otherwise basic auth header is used. +Using an IBM IAM apikey +======================= + +If you've enabled the optional `couch_replicator_auth_ibm_auth` Review Comment: Extra `_auth` at the end ########## src/couch_replicator/src/couch_replicator_auth_ibm.erl: ########## @@ -0,0 +1,750 @@ +% 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 +% +% http://www.apache.org/licenses/LICENSE-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. + +% This module allows a replication source or target to use an IAM api key for authentication. +% +% Features; +% +% Automatic refreshing of time-limited token before expiration +% Deduplication - only one token will be acquired for each distinct IAM api key +% +% Implementation details +% +% As api keys are sensitive, the only copy of api keys is held in a private ETS table +% owned by this module's gen_server. An opaque reference is returned to clients (this is +% a message authentication code where the key is a non-persisted value generated by the +% gen_server) + +-module(couch_replicator_auth_ibm). + +-behaviour(couch_replicator_auth). +-behaviour(gen_server). +-behaviour(config_listener). + +-export([ + sup_initialize/0, + sup_cleanup/1, + initialize/1, + update_headers/2, + handle_response/3, + cleanup/1 +]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +% config_listener callbacks +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). +-compile({no_auto_import, [now/0]}). + +-define(EARLY_REFRESH_MS, 300_000). +-define(JITTER_MS, 60_000). +-define(MIN_REFRESH_MS, 10_000). +-define(MINUTE, 60). +-define(PUBLIC, couch_replicator_auth_ibm_public). +-define(PRIVATE, couch_replicator_auth_ibm_private). + +-record(worker_state, { + api_key_mac, + last_used +}). + +-record(state, { + gun_mref, + gun_pid, + mac_key, + token_uri_map +}). + +-record(public_entry, { + api_key_mac, + token +}). + +-record(private_entry, { + api_key_mac, + api_key_uuid, + api_key, + expires_ref, + gun_body = [], + gun_status_code, + gun_stream_ref, + last_used, + refresh_ref, + token_updated_at, + waiters = [] +}). + +%% callbacks + +sup_initialize() -> + application:ensure_all_started(gun), + {ok, _} = gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +sup_cleanup(_) -> + ok = gen_server:stop(?MODULE). + +initialize(#httpdb{} = HttpDb) -> + case extract_api_key(HttpDb) of + {ok, APIKey} -> + case gen_server:call(?MODULE, {register, APIKey}) of + {ok, APIKeyMAC} -> + WorkerState = #worker_state{api_key_mac = APIKeyMAC, last_used = now()}, + {ok, HttpDb, WorkerState}; + {error, Reason} -> + {error, Reason} + end; + {error, _} -> + ignore + end. + +update_headers(WorkerState, Headers) when is_list(Headers) -> + case get_token(WorkerState) of + {ok, Token} -> + { + [{~"Authorization", <<"Bearer ", Token/binary>>} | Headers], + update_last_used(WorkerState) + }; + {error, Reason} -> + couch_log:warning("Error when retrieving token: ~p", [Reason]), + {Headers, WorkerState} + end. + +handle_response(WorkerState, _StatusCode, _Headers) -> + {continue, WorkerState}. + +cleanup(_WorkerState) -> + ok. + +get_token(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC} = WorkerState, + case ets:lookup(?PUBLIC, APIKeyMAC) of + [#public_entry{token = Token}] when is_binary(Token) -> + {ok, Token}; + [#public_entry{}] -> + gen_server:call(?MODULE, {get_token, APIKeyMAC}, token_timeout()); + [] -> + {error, no_token} + end. + +update_last_used(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC, last_used = LastUsed} = WorkerState, + Now = now(), + %% only send a message once per minute at most. + case Now - LastUsed > ?MINUTE of + true -> + ?MODULE ! {update_last_used, APIKeyMAC}, + WorkerState#worker_state{last_used = Now}; + false -> + WorkerState + end. + +%% gen_server callbacks. + +init(_) -> + case token_uri_map() of + {ok, TokenURIMap} -> + ?PUBLIC = ets:new(?PUBLIC, [protected, {keypos, #public_entry.api_key_mac}, named_table]), + ?PRIVATE = ets:new(?PRIVATE, [ + private, {keypos, #private_entry.api_key_mac}, named_table + ]), + ok = config:listen_for_changes(?MODULE, nil), + start_gun(#state{ + mac_key = crypto:strong_rand_bytes(32), + token_uri_map = TokenURIMap + }); + {error, Reason} -> + {error, Reason} + end. + +handle_call({register, APIKey}, _From, State) -> + case ets:match_object(?PRIVATE, #private_entry{api_key = APIKey, _ = '_'}) of + [#private_entry{} = Entry] -> + {reply, {ok, Entry#private_entry.api_key_mac}, State}; Review Comment: Should we bump use here since we're using it the key? ########## src/couch_replicator/src/couch_replicator_auth_ibm.erl: ########## @@ -0,0 +1,750 @@ +% 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 +% +% http://www.apache.org/licenses/LICENSE-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. + +% This module allows a replication source or target to use an IAM api key for authentication. +% +% Features; +% +% Automatic refreshing of time-limited token before expiration +% Deduplication - only one token will be acquired for each distinct IAM api key +% +% Implementation details +% +% As api keys are sensitive, the only copy of api keys is held in a private ETS table +% owned by this module's gen_server. An opaque reference is returned to clients (this is +% a message authentication code where the key is a non-persisted value generated by the +% gen_server) + +-module(couch_replicator_auth_ibm). + +-behaviour(couch_replicator_auth). +-behaviour(gen_server). +-behaviour(config_listener). + +-export([ + sup_initialize/0, + sup_cleanup/1, + initialize/1, + update_headers/2, + handle_response/3, + cleanup/1 +]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +% config_listener callbacks +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). +-compile({no_auto_import, [now/0]}). Review Comment: Minor nit: avoid overriding bult-ins. `now_sec()` is not terribly longer. And besides now traditionally returns a `{mega, sec, usec}` tuple so the code looks like we'd be handling that value. ########## src/couch_replicator/src/couch_replicator_auth_ibm.erl: ########## @@ -0,0 +1,750 @@ +% 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 +% +% http://www.apache.org/licenses/LICENSE-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. + +% This module allows a replication source or target to use an IAM api key for authentication. +% +% Features; +% +% Automatic refreshing of time-limited token before expiration +% Deduplication - only one token will be acquired for each distinct IAM api key +% +% Implementation details +% +% As api keys are sensitive, the only copy of api keys is held in a private ETS table +% owned by this module's gen_server. An opaque reference is returned to clients (this is +% a message authentication code where the key is a non-persisted value generated by the +% gen_server) + +-module(couch_replicator_auth_ibm). + +-behaviour(couch_replicator_auth). +-behaviour(gen_server). +-behaviour(config_listener). + +-export([ + sup_initialize/0, + sup_cleanup/1, + initialize/1, + update_headers/2, + handle_response/3, + cleanup/1 +]). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2 +]). + +% config_listener callbacks +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl"). +-compile({no_auto_import, [now/0]}). + +-define(EARLY_REFRESH_MS, 300_000). +-define(JITTER_MS, 60_000). +-define(MIN_REFRESH_MS, 10_000). +-define(MINUTE, 60). +-define(PUBLIC, couch_replicator_auth_ibm_public). +-define(PRIVATE, couch_replicator_auth_ibm_private). + +-record(worker_state, { + api_key_mac, + last_used +}). + +-record(state, { + gun_mref, + gun_pid, + mac_key, + token_uri_map +}). + +-record(public_entry, { + api_key_mac, + token +}). + +-record(private_entry, { + api_key_mac, + api_key_uuid, + api_key, + expires_ref, + gun_body = [], + gun_status_code, + gun_stream_ref, + last_used, + refresh_ref, + token_updated_at, + waiters = [] +}). + +%% callbacks + +sup_initialize() -> + application:ensure_all_started(gun), + {ok, _} = gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +sup_cleanup(_) -> + ok = gen_server:stop(?MODULE). + +initialize(#httpdb{} = HttpDb) -> + case extract_api_key(HttpDb) of + {ok, APIKey} -> + case gen_server:call(?MODULE, {register, APIKey}) of + {ok, APIKeyMAC} -> + WorkerState = #worker_state{api_key_mac = APIKeyMAC, last_used = now()}, + {ok, HttpDb, WorkerState}; + {error, Reason} -> + {error, Reason} + end; + {error, _} -> + ignore + end. + +update_headers(WorkerState, Headers) when is_list(Headers) -> + case get_token(WorkerState) of + {ok, Token} -> + { + [{~"Authorization", <<"Bearer ", Token/binary>>} | Headers], + update_last_used(WorkerState) + }; + {error, Reason} -> + couch_log:warning("Error when retrieving token: ~p", [Reason]), + {Headers, WorkerState} + end. + +handle_response(WorkerState, _StatusCode, _Headers) -> + {continue, WorkerState}. + +cleanup(_WorkerState) -> + ok. + +get_token(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC} = WorkerState, + case ets:lookup(?PUBLIC, APIKeyMAC) of + [#public_entry{token = Token}] when is_binary(Token) -> + {ok, Token}; + [#public_entry{}] -> + gen_server:call(?MODULE, {get_token, APIKeyMAC}, token_timeout()); + [] -> + {error, no_token} + end. + +update_last_used(#worker_state{} = WorkerState) -> + #worker_state{api_key_mac = APIKeyMAC, last_used = LastUsed} = WorkerState, + Now = now(), + %% only send a message once per minute at most. + case Now - LastUsed > ?MINUTE of + true -> + ?MODULE ! {update_last_used, APIKeyMAC}, + WorkerState#worker_state{last_used = Now}; + false -> + WorkerState + end. + +%% gen_server callbacks. + +init(_) -> + case token_uri_map() of + {ok, TokenURIMap} -> + ?PUBLIC = ets:new(?PUBLIC, [protected, {keypos, #public_entry.api_key_mac}, named_table]), + ?PRIVATE = ets:new(?PRIVATE, [ + private, {keypos, #private_entry.api_key_mac}, named_table + ]), + ok = config:listen_for_changes(?MODULE, nil), + start_gun(#state{ + mac_key = crypto:strong_rand_bytes(32), + token_uri_map = TokenURIMap + }); + {error, Reason} -> + {error, Reason} + end. + +handle_call({register, APIKey}, _From, State) -> + case ets:match_object(?PRIVATE, #private_entry{api_key = APIKey, _ = '_'}) of + [#private_entry{} = Entry] -> + {reply, {ok, Entry#private_entry.api_key_mac}, State}; + [] -> + GunStreamRef = acquire_token(APIKey, State), + APIKeyMAC = mac(State#state.mac_key, APIKey), + true = ets:insert_new(?PUBLIC, #public_entry{ + api_key_mac = APIKeyMAC + }), + true = ets:insert_new(?PRIVATE, #private_entry{ + api_key = APIKey, + api_key_mac = APIKeyMAC, + gun_stream_ref = GunStreamRef, + last_used = now() + }), + {reply, {ok, APIKeyMAC}, State} + end; +handle_call({get_token, APIKeyMAC}, From, State) -> + case ets:lookup(?PUBLIC, APIKeyMAC) of + [] -> + {reply, {error, no_such_api_key}, State}; + [#public_entry{token = Token}] when Token /= undefined -> + {reply, {ok, Token}, State}; + [#public_entry{}] -> + [#private_entry{} = Entry] = ets:lookup(?PRIVATE, APIKeyMAC), + ets:insert(?PRIVATE, Entry#private_entry{waiters = [From | Entry#private_entry.waiters]}), + case Entry of + #private_entry{gun_stream_ref = GunStreamRef} when + GunStreamRef /= undefined + -> + ok; + #private_entry{} -> + self() ! {refresh_token, APIKeyMAC} + end, + {noreply, State} + end; +handle_call(_Msg, _From, State) -> + {reply, {error, unexpected_msg}, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({update_last_used, APIKeyMAC}, State) -> + ets:update_element(?PRIVATE, APIKeyMAC, {#private_entry.last_used, now()}), + {noreply, State}; +handle_info({refresh_token, APIKeyMAC}, State) -> + case ets:lookup(?PRIVATE, APIKeyMAC) of + [] -> + ok; + [#private_entry{gun_stream_ref = undefined} = Entry] -> + case Entry#private_entry.last_used > Entry#private_entry.token_updated_at of + true -> + couch_log:notice("~p: refreshing api key ~s", [ + ?MODULE, Entry#private_entry.api_key_uuid + ]), + GunStreamRef = acquire_token(Entry#private_entry.api_key, State), + ets:insert(?PRIVATE, Entry#private_entry{gun_stream_ref = GunStreamRef}); + false -> + %% let it expire + ok + end; + [#private_entry{}] -> + ok + end, + {noreply, State}; +handle_info({expire_api_key_entry, APIKeyMAC}, State) -> + case ets:lookup(?PRIVATE, APIKeyMAC) of + [] -> + ok; + [#private_entry{} = Entry] -> + couch_log:warning("~p: api key entry ~s passed expiration time", [ + ?MODULE, Entry#private_entry.api_key_uuid + ]), + ets:delete(?PUBLIC, APIKeyMAC), + ets:delete(?PRIVATE, APIKeyMAC), + cancel_timer(Entry#private_entry.refresh_ref), + cancel_timer(Entry#private_entry.expires_ref), + reply_all(Entry, {error, expired_api_key_entry}) + end, + {noreply, State}; +handle_info( + {gun_response, GunPid, GunStreamRef, fin, StatusCode, _Headers}, + #state{gun_pid = GunPid} = State +) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + reply_and_reset(Entry, {error, {unexpected_status_code, StatusCode}}); + _ -> + ok + end, + {noreply, State}; +handle_info( + {gun_response, GunPid, GunStreamRef, nofin, StatusCode, _Headers}, + #state{gun_pid = GunPid} = State +) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + ets:insert(?PRIVATE, Entry#private_entry{gun_status_code = StatusCode}); + _ -> + ok + end, + {noreply, State}; +handle_info({gun_data, GunPid, GunStreamRef, nofin, Data}, #state{gun_pid = GunPid} = State) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + ets:insert(?PRIVATE, Entry#private_entry{ + gun_body = [Data | Entry#private_entry.gun_body] + }); + _ -> + ok + end, + {noreply, State}; +handle_info({gun_data, GunPid, GunStreamRef, fin, Data}, #state{gun_pid = GunPid} = State) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + ResponseBody = lists:reverse([Data | Entry#private_entry.gun_body]), + case Entry#private_entry.gun_status_code of + 200 -> + case decode_iam_response(ResponseBody) of + {ok, Token, ExpiresInMs} -> + UUID = api_key_uuid(Token), + couch_log:notice("~p: refreshed api key ~s", [ + ?MODULE, UUID + ]), + cancel_timer(Entry#private_entry.refresh_ref), + cancel_timer(Entry#private_entry.expires_ref), + + RefreshRef = erlang:send_after( + max( + ?MIN_REFRESH_MS, + ExpiresInMs - ?EARLY_REFRESH_MS - rand:uniform(?JITTER_MS) + ), + self(), + {refresh_token, Entry#private_entry.api_key_mac} + ), + ExpiresRef = erlang:send_after( + ExpiresInMs, + self(), + {expire_api_key_entry, Entry#private_entry.api_key_mac} + ), + true = ets:insert(?PUBLIC, #public_entry{ + api_key_mac = Entry#private_entry.api_key_mac, + token = Token + }), + reply_and_reset( + Entry#private_entry{ + api_key_uuid = UUID, + expires_ref = ExpiresRef, + refresh_ref = RefreshRef, + token_updated_at = now() + }, + {ok, Token} + ); + {error, Reason} -> + couch_log:notice("~p: failed to refresh api key ~s: ~p", [ + ?MODULE, Entry#private_entry.api_key_uuid, Reason + ]), + reply_and_reset(Entry, {error, Reason}) + end; + StatusCode -> + ErrorMessage = extract_error_message(ResponseBody), + couch_log:notice("~p: failed to refresh api key ~s: ~p", [ + ?MODULE, Entry#private_entry.api_key_uuid, ErrorMessage + ]), + case StatusCode of + 500 -> + erlang:send_after( + ?MIN_REFRESH_MS, + self(), + {refresh_token, Entry#private_entry.api_key_mac} + ); + _ -> + ok + end, + reply_and_reset(Entry, {error, ErrorMessage}) + end; + [] -> + ok + end, + {noreply, State}; +handle_info({gun_error, GunPid, GunStreamRef, Reason}, #state{gun_pid = GunPid} = State) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + reply_and_reset(Entry, {error, Reason}); + _ -> + ok + end, + {noreply, State}; +handle_info({gun_error, GunPid, Reason}, #state{gun_pid = GunPid} = State) -> + couch_log:warning("~p: gun error ~p", [?MODULE, Reason]), + {noreply, State}; +handle_info( + {'DOWN', GunMRef, process, GunPid, Reason}, #state{gun_pid = GunPid, gun_mref = GunMRef} = State +) -> + couch_log:warning("~p: gun process crashed for reason: ~p", [?MODULE, Reason]), + handle_info(restart_gun, State#state{gun_pid = undefined, gun_mref = undefined}); +handle_info({gun_up, GunPid, _Protocol}, #state{gun_pid = GunPid} = State) -> + {noreply, State}; +handle_info({gun_down, GunPid, _Protocol, closed, []}, #state{gun_pid = GunPid} = State) -> + {noreply, State}; +handle_info({gun_down, GunPid, _Protocol, Reason, KilledStreams}, #state{gun_pid = GunPid} = State) -> + couch_log:warning("~p: gun connection down for reason: ~p", [?MODULE, Reason]), + lists:foreach( + fun(GunStreamRef) -> + case match_on_gun_stream_ref(GunStreamRef) of + [#private_entry{} = Entry] -> + reply_and_reset(Entry, {error, Reason}); + [] -> + ok + end + end, + KilledStreams + ), + {noreply, State}; +handle_info(restart_gun, State) -> + case start_gun(State) of + {ok, NewState} -> + {noreply, NewState}; + {error, Reason} -> + couch_log:warning("~p: gun restart failed for reason: ~p", [?MODULE, Reason]), + erlang:send_after(5000, self(), restart_gun), + {noreply, State} + end; +handle_info(restart_config_listener, State) -> + ok = config:listen_for_changes(?MODULE, nil), + {noreply, State}; +handle_info(token_url_change, State0) -> + case token_uri_map() of + {ok, TokenURIMap} -> + stop_gun(State0), Review Comment: Should we also demonitor and flush here? Stop will call gun:flush() but our monitor is separate from that machinery. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
