Goal
The goal for today is to implement an API end point that validates an access token. This requires adding all the infrastructure to actually provide an HTTP API.
Plan
- Implement a
GET /validateend point. The request MUST have anAuthorizationheader with a valid API token. The successful response is a JSON representation of the token. For requests without a valid token, a 401 response is sent.
Notes
I going to use the
axumcrate for the HTTP API. It's currently popular among Rust programmers doing this kind of thing. I've also tried it out earlier for another project, and it seems nice enough.First step is to implement a dummy end point that does no validation, to get all the scaffolding up for
axumuse. As part of this, I'm going to makeobnam-serverbe async usingtokio.Added a dependency on
tokiowith featuresmacrosandrt-mult-thread. I may need to adjust that later, but this will get me started. Mademainbe an async function decorated with#[tokio::main]. That's all I needed to turnobnam-serverinto an async program. Of course, it doesn't actually make any use of that yet.Ran into problem.
mainis async, but subcommands are not. That means I can't callaxumstuff there, as that's async. While I'm sure I can work around that, it'd be easier to make subcommands async.Did that, not hard. Had to add an
.awaitin some places, but that goes with the territory in an async program.Added a stub for
obnam serve.Added
axumas a dependency, using default features. I may want to adjust that, too, later, once I know better what I need. Thehttp2feature is interesting, if nothing else.Adding a dummy end point was easy. Suspiciously easy, actually, even if I've used
axumand other Rust HTTP web application frameworks before. I have flashbacks to Python andgunicornwhich made my life unpleasant back in the day.Of course, it's only easy because I understand Rust somewhat.
My understanding of
axum, in brief:- a "server" is a thing that routes requests to handlers based on paths and HTTP methods (GET, PUT, etc)
- a "router" is a kind of server
- a "handler" is an async function that may have extractor arguments
- an "extractor" gets some data, usually from the incoming request, and returns that, and the router will pass the return value to the handler
- the router uses magic to match extractors to handler arguments
- might just be advanced use of the Rust type system, but I've not tried to understand how this works
- handlers have to be self-standing functions; they cannot be methods
- state between handlers has to be handled explicitly, but there's tools
for that
- there's a state extractor
As an experiment, added an
ApiStatetype that keeps a counter, and a handler that extracts the state and increments the counter for each request. Not too difficult.I should probably write a custom extractor for the token. Which turned out be not very hard. It validates the token, or fails. I like this design: it means the handlers don't have to remember to validate the token, as they won't even get called if there isn't a valid token. Checking that the token allows the requested operation still needs to be done in each handler. I might work around that by having extractors that also check the authorization.
Used that to implement the
/validateend point.
async fn validate(
_: State<ApiState>,
TokenExtractor(token): TokenExtractor,
) -> Json<ObnamTrustedToken> {
Json(token)
}
So simple once the supporting scaffolding is there. Building the scaffolding so far wasn't too bad, but required reading some documentation.
I'm running out of time today so I don't have time to implement acceptance tests for this. I'll continue with that next time.
Comments?
If you have feedback on this, please use the following fediverse thread: https://toot.liw.fi/@liw/116073881337123941.