Authentication, Authorization & Login
Quokka Admin comes with a pretty flexible authentication system which splits up functions for Authentication & Authorization. Additionally there is a separate way to handle/validate the login for users.
Login
That's the easiest part, all you have to do is to implement the quokka_admin::middleware::AdminLoginProvider for a struct that can handle
logins. It comes with the async fn do_login(&self, login_data: &LoginData) -> quokka::Result<Option<LoginResult>> which gets you the
entered username and password and expects you to return a LoginResult which will be stored in the session. The first login provider that
returns a LoginResult will be taken for granted and allows the user to login.
Quokka Admin already comes and registers it's own login provider based on PostgreSQL.
Example
This example allows a user with the username "Test" and password "Password" to log in.
struct TestLoginProvider;
impl AdminLoginProvider for TestLoginProvider {
async fn do_login(
&self,
login_data: &quokka_admin::middleware::LoginData,
) -> quokka::Result<Option<LoginResult>> {
if login_data.login_name == "Test" && login_data.password == "Password" {
tracing::debug!("Got a secure login from the test user!");
return Ok(Some(LoginResult {
user_identifier: "Test".to_string(),
}));
}
Ok(None)
}
}
If you want to register a more complex login provider which requires state data, you can simply derive the Clone, FromState traits and
construct it in the configure_state callback by building it there (Quokka does it like this let login_provider = DbLoginProvider:: from_state(state);.
Authentication & Authorization
Authentication and Authorization is, although two separate functions, one trait that has to be implemented AdminAuthProvider. The
separation of these two functions allows multiple modules to work together. So you can have one module that finds the user from one service
(like LDAP) in the authenticate function and validate the user's permission against LDAP or some web API within a second auth provider in
the authorize function. Each function will be called in separate batches on all providers. Here it is the same as with the login. Whatever
provides first response with a value will be taken for granted. So multiple providers checking permissions will always be additive.
Across the providers all the communication should be provider-agnostik, that's also why, by design, the traits do not communicate IDs but strings instead.
The PermissionContext comes with a verb and a resource. Which correspond to the HTTP request Method and the MatchedPath, which in
axum represents the registered URL inside the router, including the path placeholders (eg. /admin/entity/test/{:pks}).
Example
This example with authenticate the "Test" user from the login example above if the name matches and will grant him any permission.
impl<S: Send + Sync + 'static> AdminAuthProvider<S> for TestAuthProvider {
type AuthParams = Extension<Session>;
async fn authenticate(
&self,
params: Self::AuthParams,
) -> quokka::Result<Option<AuthenticatedUser>> {
let login_name = params
.0
.get_extension::<LoginResult>(ADMIN_USER_SESSION_KEY)?
.user_identifier;
if login_name == "Test" {
return Ok(Some(AuthenticatedUser {
name: "Test".to_string(),
groups: Default::default(),
context: Default::default(),
}));
}
Ok(None)
}
async fn authorize(
&self,
user: &AuthenticatedUser,
_: &PermissionContext,
) -> quokka::Result<bool> {
if user.name == "Test" {
return Ok(true);
}
Ok(false)
}
}