Administrator privileges to get and delete subscribers
Some checks failed
Rust / Test (push) Has been cancelled
Rust / Rustfmt (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Code coverage (push) Has been cancelled

This commit is contained in:
Alphonse Paix
2025-09-30 18:27:48 +02:00
parent b5b00152cd
commit 3e81c27ab3
12 changed files with 2790 additions and 54 deletions

View File

@@ -1,12 +1,9 @@
use crate::{
routes::AdminError, session_state::TypedSession, telemetry::spawn_blocking_with_tracing,
};
use crate::telemetry::spawn_blocking_with_tracing;
use anyhow::Context;
use argon2::{
Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,
password_hash::{SaltString, rand_core::OsRng},
};
use axum::{extract::Request, middleware::Next, response::Response};
use secrecy::{ExposeSecret, SecretString};
use sqlx::PgPool;
use uuid::Uuid;
@@ -60,8 +57,9 @@ fn compute_pasword_hash(password: SecretString) -> Result<SecretString, anyhow::
pub async fn validate_credentials(
Credentials { username, password }: Credentials,
connection_pool: &PgPool,
) -> Result<Uuid, AuthError> {
) -> Result<(Uuid, Role), AuthError> {
let mut user_id = None;
let mut role = None;
let mut expected_password_hash = SecretString::from(
"$argon2id$v=19$m=15000,t=2,p=1$\
gZiV/M1gPc22ElAH/Jh1Hw$\
@@ -69,13 +67,14 @@ CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
.to_string(),
);
if let Some((stored_user_id, stored_expected_password_hash)) =
if let Some((stored_user_id, stored_expected_password_hash, stored_role)) =
get_stored_credentials(&username, connection_pool)
.await
.context("Failed to retrieve credentials from database.")
.map_err(AuthError::UnexpectedError)?
{
user_id = Some(stored_user_id);
role = Some(stored_role);
expected_password_hash = stored_expected_password_hash;
}
@@ -86,12 +85,16 @@ CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
.ok_or_else(|| anyhow::anyhow!("Unknown username."))
.map_err(AuthError::InvalidCredentials)?;
let role = role
.ok_or_else(|| anyhow::anyhow!("Unknown role."))
.map_err(AuthError::UnexpectedError)?;
handle
.await
.context("Failed to spawn blocking task.")
.map_err(AuthError::UnexpectedError)?
.map_err(AuthError::InvalidCredentials)
.map(|_| uuid)
.map(|_| (uuid, role))
}
#[tracing::instrument(name = "Verify password", skip_all)]
@@ -113,10 +116,10 @@ fn verify_password_hash(
async fn get_stored_credentials(
username: &str,
connection_pool: &PgPool,
) -> Result<Option<(Uuid, SecretString)>, sqlx::Error> {
) -> Result<Option<(Uuid, SecretString, Role)>, sqlx::Error> {
let row = sqlx::query!(
r#"
SELECT user_id, password_hash
SELECT user_id, password_hash, role as "role: Role"
FROM users
WHERE username = $1
"#,
@@ -124,37 +127,26 @@ async fn get_stored_credentials(
)
.fetch_optional(connection_pool)
.await?
.map(|row| (row.user_id, SecretString::from(row.password_hash)));
.map(|row| (row.user_id, SecretString::from(row.password_hash), row.role));
Ok(row)
}
pub async fn require_auth(
session: TypedSession,
mut request: Request,
next: Next,
) -> Result<Response, AdminError> {
let user_id = session
.get_user_id()
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?
.ok_or(AdminError::NotAuthenticated)?;
let username = session
.get_username()
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?
.ok_or(AdminError::UnexpectedError(anyhow::anyhow!(
"Could not find username in session."
)))?;
request
.extensions_mut()
.insert(AuthenticatedUser { user_id, username });
Ok(next.run(request).await)
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "user_role", rename_all = "lowercase")]
pub enum Role {
Admin,
Writer,
}
#[derive(Clone)]
pub struct AuthenticatedUser {
pub user_id: Uuid,
pub username: String,
pub role: Role,
}
impl AuthenticatedUser {
pub fn is_admin(&self) -> bool {
matches!(self.role, Role::Admin)
}
}