7.7 KiB
Multi-User: Architecture
Contents:
- Key architectural decisions
- Primary components & flow
- Authentication
- ActorDef vs Actor vs LoggedInUser
- Built-in Actors
- Debatable design choices
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 byKrillServerto obtain the internal Krill actor to attribute internal Krill actions to, and by the test build API to upgrade an anonymous actor to a specialtestbedactor.
Authentication is delegated by the Authorizer to an AuthProvider which is a trait with three implementations:
AdminTokenAuthProviderConfigFileAuthProviderOpenIDConnectAuthProvider
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
OpenIDConnectAuthProviderhas 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
OpenIDConnectAuthProviderwhether or not it should be a hard failure or if it would be okay to try theAdminTokenAuthProvideras 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, OROk(Some<ActorDef>)if the credentials were good, ORErrif 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
OpenIDConnectAuthProviderare 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
synccode while a lot of Krill is nowasynccode. -
There's a second
reqwestdependency because using the main v0.10reqwestdependency in theOpenIDConnectAuthProvidercaused panics from its use of an internal async Tokio loop wrapper around its synchronous client implementation, while in earlier versions ofreqwestthe synchronous client is actually synchronous code. -
Switching to the
asyncreqwestclient would require working around the problem that Rustasynctraits are not yet supported unless you use theasync_traitcrate. -
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".