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 /validate end point. The request MUST have an Authorization header 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 axum crate 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 axum use. As part of this, I'm going to make obnam-server be async using tokio.

  • Added a dependency on tokio with features macros and rt-mult-thread. I may need to adjust that later, but this will get me started. Made main be an async function decorated with #[tokio::main]. That's all I needed to turn obnam-server into an async program. Of course, it doesn't actually make any use of that yet.

  • Ran into problem. main is async, but subcommands are not. That means I can't call axum stuff 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 .await in some places, but that goes with the territory in an async program.

  • Added a stub for obnam serve.

  • Added axum as a dependency, using default features. I may want to adjust that, too, later, once I know better what I need. The http2 feature is interesting, if nothing else.

  • Adding a dummy end point was easy. Suspiciously easy, actually, even if I've used axum and other Rust HTTP web application frameworks before. I have flashbacks to Python and gunicorn which 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 ApiState type 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 /validate end 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.