Files
NLnetLabs-krill/doc/development/multi_user/architecture.md
T

7.7 KiB

Multi-User: Architecture

Contents:

Key architectural decisions

  • Don't add to the state stored by the server as that will complicate later efforts to run Krill as a distributed service. Instead be stateless by storing (encrypted) session state on the client.

  • Use a "plugin" style model but only internally in the code, be able to support more identity providers later if wanted, but FFI to an actual separately loaded dynamic libraries was considered unnecessary overkill with downsides.

  • Don't roll our own OpenID Connect client code if there's an existing suitable Rust crate we can use instead.

Primary components & flow

Multi-user support centres on four key components in Krill: Authorizer, Actor, AuthPolicy and AuthProvider.

The Authorizer sits at the middle of the web. It owns the AuthProvider instance(s) and the AuthPolicy. It is also the sole issuer of Actor instances.

All command events are attributed to the .name() of an Actor, thus all internal actions performed by Krill require an Actor instance which must be obtained from the Authorizer.

Actor instances are obtained from the Authorizer in one of two ways:

  • By calling .actor_from_request() to authenticate an HTTP request.
  • By calling .actor_from_def() to obtain a representation of one of the built-in actors. This is only used by KrillServer to obtain the internal Krill actor to attribute internal Krill actions to, and by the test build API to upgrade an anonymous actor to a special testbed actor.

Authentication is delegated by the Authorizer to an AuthProvider which is a trait with three implementations:

  • AdminTokenAuthProvider
  • ConfigFileAuthProvider
  • OpenIDConnectAuthProvider

Authorization is done by calling through Actor::is_allowed(action, resource) to a shared reference to the AuthPolicy which in turn delegates to Oso.

Authentication

There is always an instance of AdminTokenAuthProvider because it is required to authenticate direct REST API calls and indirect REST API calls (via krillc) which use the admin token to authenticate. If the configured provider (via auth_type = "..." in krill.conf) is config-file or openid-connect there will also be an instance of ConfigFileAuthProvider or OpenIDConnectAuthProvider respectively.

The Authorizer asks the AdminTokenAuthProvider to attempt to authenticate a request first, if that fails and there is another AuthProvider that is then asked to authenticate. The AdminTokenAuthProvider is tried first because:

  • The check it performs is quick and cheap (simple string comparison) while the OpenIDConnectAuthProvider has to base64 decode, decrypt and deserialize the bearer token (although it has a v short lived cache to minimize the impact of bursts of parallel requests from the user agent), and
  • It can only fail in very simple ways, while it can be hard to know for a given error from the OpenIDConnectAuthProvider whether or not it should be a hard failure or if it would be okay to try the AdminTokenAuthProvider as a fallback.

ActorDef vs Actor vs LoggedInUser

A deliberate separation exists that is worth mentioning: ActorDef vs Actor vs LoggedInUser.

AuthProvider implementations return Result<Option<ActorDef>> from authenticate() to indicate either:

  • Ok(None) if no credentials were found, OR
  • Ok(Some<ActorDef>) if the credentials were good, OR
  • Err if credentials were present but incorrect.

An ActorDef defines what an actor could look like but is not an instance of Actor. It is returned instead of Actor because only the Authorizer is allowed to create Actor instances. The AuthProvider therefore indicates the kind of Actor that could be created if the Authorizer permits it.

AuthProvider implementations return Result<LoggedInUser> from login(). We could possibly return an ActorDef here but the LoggedInUser type is returned instead as its purposely limited to contain only information about the logged in user which is needed by Lagosta. Unlike ActorDef the type is also therefore serializable.

In both the authenticate() and login() cases the Authorizer does any additional checks required (e.g. checks with the Oso authorization policy engine if the user has the LOGIN permission) and only then accepts the recommendation of the AuthProvider. This is especially important in the OpenID Connect case because a corporate employee will be able to login to the central identity provider at their organization but that is not the same as saying that all 10,000 employees should have the right to login to Krill.

Built-in Actors

There are currently four built-in actors, which for convenience have entries in constants.rs: (ordered from most to least powerful)

Actor Represents Role Comments
ACTOR_DEF_KRILL Krill itself admin Used for initial startup and scheduled actions that are not directly attributable to a REST API client.
ACTOR_DEF_ADMIN_TOKEN A client using the admin API token admin Used by the users of Lagosta when auth_type = "admin-token" (the default), or by direct clients of the REST API, or indirect clients of the REST API via krillc.
ACTOR_DEF_TESTBED An anonymous client of the testbed testbed (temporarily) Used by the testbed REST API handler functions to make internal requests to restricted APIs on the behalf of the anonymous client. See Request::upgrade_from_anonymous.
ACTOR_DEF_ANON An anonymous client None Used for REST API calls that lack credentials or for which an error occurs during authentication. By still having an actor even in this case we can handle all API calls the same way. The anonymous actor has no role and so, unless overridden by a custom authorization policy, has no rights in Krill. It can thus only successfully request REST API endpoints that do not require authentication.

Debatable design choices

A couple of design properties that emerged and that should perhaps be revisited are:

  • Requests to the OpenIDConnectAuthProvider are handled in parallel, which can cause bursts of connectivity to the actual 3rd party OpenID Connect provider. It might make sense to use some sort of queue being serviced by workers to avoid this.

  • The AuthProviders are sync code while a lot of Krill is now async code.

  • There's a second reqwest dependency because using the main v0.10 reqwest dependency in the OpenIDConnectAuthProvider caused panics from its use of an internal async Tokio loop wrapper around its synchronous client implementation, while in earlier versions of reqwest the synchronous client is actually synchronous code.

  • Switching to the async reqwest client would require working around the problem that Rust async traits are not yet supported unless you use the async_trait crate.

  • If we continue to bundle more and more "plugin" implementations into Krill it will gain more and more crate dependencies, will increase in size, in compile time and test execution time. The more plugins that are added the more the separate FFI plugin dynamic library model might make sense.

  • The term "resource" in authorization was inherited from Oso but clashes with the arguably more important (for Krill) RPKI meaning of the term "resource".