couch replication auth plugin for IBM IAM with refresh - #6069
Conversation
49d39f0 to
de37af9
Compare
nickva
left a comment
There was a problem hiding this comment.
This is a good start. But I think we can simplify it by using the same pattern as in https://github.com/apache/couchdb/blob/main/src/couch_replicator/src/couch_replicator_auth_session.erl with a gen_server, also use a first type synchronous token fetch in init.
For ibrowse requests don't use the default lb pool, replicator has it's own connection pool so we'd be using that instead (see https://github.com/apache/couchdb/blob/main/src/couch_replicator/src/couch_replicator_auth_session.erl).
Also wondering how easy it would be to have this not just IBM-specific. We have JWT token auth for our server side. Maybe this can be adapted somehow to work in a more generic way so it could work with Azure, AWS, other IAM-like auth systems. But, perhaps configuring it then would be tricky and keeping the plugins separate just to be able to configure the token url, then maybe separate plugins is just easier.
For config and naming maybe we should use some indication this for a replicator auth plugin not some generic IBM integration (thinking of "ibm_iam" section).
| ignore -> | ||
| ignore; | ||
| {ok, APIKey} -> | ||
| Acquirer = spawn_link(?MODULE, token_acquirer, [APIKey]), |
There was a problem hiding this comment.
It might be better to do a first time synchronous token fetch on init. So when the job starts we'd know right away it has an invalid creds
There was a problem hiding this comment.
I had that earlier but it entailed duplication of the 'get token' ibrowse code (one synchronous, the other not). so I simplified to one version.
There was a problem hiding this comment.
That's why I think we can just use a similar structure as the session plugin and always use the synchronous version.
|
I do want to think of a more generic thing, and we should maybe make it possible for an auth plugin to have application like semantics (so they can start a supervision tree etc). in that version things become simpler. the auth plugin would launch a gen_server that makes a public ETS table. each replication job would insert the apikey when it starts and then fetch the token in update_headers as needed. the gen_server could refresh all the tokens slightly in advance of their expiration, handle retries on timeout or 500's, etc. |
|
I tried adding a supervisor-level API for replicator auth plugins #6070 |
As discussed in the comments of the new IAM plugin in [1], it would be nice to
have a per-plugin supervisor-level context for each plugin. So, for example,
they can create some kind of a ETS cache table for their tokens. This is what
we implement here. The API is simple:
* `sup_initialize() -> Ctx`
* `sup_cleanup(Ctx) -> ok`
The APIs are optional to implement. The name pattern mirror the regular plugin
API names: `initialize(...) -> {ok, ..., Ctx}` and `cleanup(Ctx) -> ok`
[1] #6069
As discussed in the comments of the new IAM plugin in [1], it would be nice to
have a per-plugin supervisor-level context for each plugin. So, for example,
they can create some kind of a ETS cache table for their tokens. This is what
we implement here. The API is simple:
* `sup_initialize() -> Ctx`
* `sup_cleanup(Ctx) -> ok`
The APIs are optional to implement. The name pattern mirror the regular plugin
API names: `initialize(...) -> {ok, ..., Ctx}` and `cleanup(Ctx) -> ok`
[1] #6069
5952a7a to
ed0a99e
Compare
As discussed in the comments of the new IAM plugin in [1], it would be nice to
have a per-plugin supervisor-level context for each plugin. So, for example,
they can create some kind of a ETS cache table for their tokens. This is what
we implement here. The API is simple:
* `sup_initialize() -> Ctx`
* `sup_cleanup(Ctx) -> ok`
The APIs are optional to implement. The name pattern mirror the regular plugin
API names: `initialize(...) -> {ok, ..., Ctx}` and `cleanup(Ctx) -> ok`
[1] #6069
d904aae to
bbda84a
Compare
e81e46a to
19183b7
Compare
4dab79e to
9ce7723
Compare
a34d85b to
c188e34
Compare
There was a problem hiding this comment.
Looks very nice. Using gun is great and like the shared ets and using sigils.
One thing that might be possible to simplify not handling all the gun async messages in the gen_server, it's easy to miss some and get into a blocked or stuck state. I think we could still avoid blocking the gen_server with a gun_await/await_body inline if we spawn a separate process to handle the connection and just return the result/error in a single DOWN message (kind of like how we have opener processes for couch_server / indexes).
IBM refresh time is an hour so even for 100 different keys it's only ~30 second intervals and most likely users will just use a few keys only.
So maybe something like
{_, Ref} = spawn_monitor(fun() ->
exit(try fetch(URIMap, APIKey, Timeout) catch T:E -> {error, {T,E}} end)
end)
fetch(...)->
gun:post(...)
case gun:await(...) ->
{response, nofin, 200} ->
{ok, Body} = gun:await_body(...)
decode(Body);
...
end.| {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) -> |
There was a problem hiding this comment.
Gun docs indicated there are also gun_error messages send and we don't have clauses for them
| handle_info(token_url_change, State) -> | ||
| case token_uri_map() of | ||
| {ok, TokenURIMap} -> | ||
| {noreply, State#state{token_uri_map = TokenURIMap}}; |
There was a problem hiding this comment.
Wonder if we should clear connections or reset anything with the old url here
| end, | ||
| {noreply, State}; | ||
| handle_info( | ||
| {gun_response, GunPid, GunStreamRef, nofin, StatusCode, _Headers}, |
There was a problem hiding this comment.
Is there a gun_response with fin to handle? Got headers but no body, maybe some redirect or 500 case?
There was a problem hiding this comment.
hm, yes, perhaps. will add a clause in case (an http 204, say. IAM don't say they send it, but we should handle every possibility)
| {continue, APIKeyMAC}. | ||
|
|
||
| cleanup(_APIKeyMAC) -> | ||
| ok. |
There was a problem hiding this comment.
If we don't do cleanup does it mean we'd end up refreshing a once used API key forever even if there are no more jobs using it?
There was a problem hiding this comment.
... yes. the problem is I've no way to know when no replication job refers to a particular apikey. 'register' gets called multiple times per replication, which is why I ripped out my refcounting (which did ref-decrementing in cleanup and deleted entries when that hit 0).
perhaps I can add a 'last used' timestamp in the private table and just not renew an entry that hasn't been used for a while
| 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), |
There was a problem hiding this comment.
In some places dealing with periodic execution (replication jobs starts) we usually add a bit of jitter to avoid a thundering herd problem wonder if we should add a bit of that here as well.
There was a problem hiding this comment.
I figure they'd be staggered already but can certainly add some jitter here
Overview
Teach the CouchDB replicator how to acquire an access token from an apikey from IBM's IAM authentication service.
Note that an access token has an expiration (currently one hour but subject to change).
The plugin fetches a new token 5 minutes before the currently held one expires.
Testing recommendations
will be covered by tests
Related Issues or Pull Requests
N/A
Checklist
rel/overlay/etc/default.inisrc/docsfolder