refactor(gpodder): split repository for admin view
parent
669aa475ca
commit
2524eb5807
|
@ -0,0 +1,7 @@
|
|||
use crate::models;
|
||||
|
||||
/// Admin view of the repository, providing methods only allowed by admins
|
||||
pub struct AdminRepository<'a> {
|
||||
pub(crate) store: &'a (dyn super::GpodderStore + Send + Sync),
|
||||
pub(crate) user: &'a models::User,
|
||||
}
|
|
@ -1,108 +1,15 @@
|
|||
use std::{collections::HashSet, sync::Arc};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use rand::{Rng, rngs::OsRng};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{
|
||||
models,
|
||||
store::{AuthErr, GpodderStore},
|
||||
};
|
||||
|
||||
const MAX_SESSION_AGE: TimeDelta = TimeDelta::seconds(60 * 60 * 24 * 7);
|
||||
|
||||
/// Main abstraction over the database that provides API-compatible methods for querying and
|
||||
/// modifying the underlying database
|
||||
#[derive(Clone)]
|
||||
pub struct GpodderRepository {
|
||||
store: Arc<dyn GpodderStore + Send + Sync>,
|
||||
}
|
||||
use crate::{AuthErr, GpodderStore, models};
|
||||
|
||||
/// Authenticated view of the repository, providing methods that take the authenticated user
|
||||
/// explicitely into account
|
||||
pub struct AuthenticatedRepository<'a> {
|
||||
store: &'a (dyn GpodderStore + Send + Sync),
|
||||
user: &'a models::User,
|
||||
}
|
||||
|
||||
impl GpodderRepository {
|
||||
pub fn new(store: impl GpodderStore + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
store: Arc::new(store),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an authenticated view of the repository for the given user
|
||||
pub fn user<'a>(&'a self, user: &'a models::User) -> AuthenticatedRepository<'a> {
|
||||
AuthenticatedRepository {
|
||||
store: self.store.as_ref(),
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_session(&self, session_id: i64) -> Result<models::Session, AuthErr> {
|
||||
let session = self
|
||||
.store
|
||||
.get_session(session_id)?
|
||||
.ok_or(AuthErr::UnknownSession)?;
|
||||
|
||||
// Expired sessions still in the database are considered removed
|
||||
if Utc::now() - session.last_seen > MAX_SESSION_AGE {
|
||||
Err(AuthErr::UnknownSession)
|
||||
} else {
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user(&self, username: &str) -> Result<models::User, AuthErr> {
|
||||
self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)
|
||||
}
|
||||
|
||||
pub fn create_user(&self, username: &str, password: &str) -> Result<models::User, AuthErr> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let password_hash = Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
self.store.insert_user(username, &password_hash)
|
||||
}
|
||||
|
||||
pub fn validate_credentials(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<models::User, AuthErr> {
|
||||
let user = self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)?;
|
||||
|
||||
let password_hash = PasswordHash::new(&user.password_hash).unwrap();
|
||||
|
||||
if Argon2::default()
|
||||
.verify_password(password.as_bytes(), &password_hash)
|
||||
.is_ok()
|
||||
{
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(AuthErr::InvalidPassword)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> {
|
||||
let now = Utc::now();
|
||||
|
||||
self.store.refresh_session(session, now)
|
||||
}
|
||||
|
||||
pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
|
||||
self.store.remove_session(session_id)
|
||||
}
|
||||
|
||||
pub fn remove_old_sessions(&self) -> Result<usize, AuthErr> {
|
||||
let min_last_seen = Utc::now() - MAX_SESSION_AGE;
|
||||
|
||||
self.store.remove_old_sessions(min_last_seen)
|
||||
}
|
||||
pub(crate) store: &'a (dyn GpodderStore + Send + Sync),
|
||||
pub(crate) user: &'a models::User,
|
||||
}
|
||||
|
||||
impl<'a> AuthenticatedRepository<'a> {
|
||||
|
@ -115,7 +22,9 @@ impl<'a> AuthenticatedRepository<'a> {
|
|||
|
||||
// Users can't see sessions from other users, and expired sessions still in the database
|
||||
// are considered removed
|
||||
if session.user.id != self.user.id || Utc::now() - session.last_seen > MAX_SESSION_AGE {
|
||||
if session.user.id != self.user.id
|
||||
|| Utc::now() - session.last_seen > super::MAX_SESSION_AGE
|
||||
{
|
||||
Err(AuthErr::UnknownSession)
|
||||
} else {
|
||||
Ok(session)
|
|
@ -0,0 +1,120 @@
|
|||
mod admin;
|
||||
mod authenticated;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::{
|
||||
models,
|
||||
store::{AuthErr, GpodderStore},
|
||||
};
|
||||
|
||||
const MAX_SESSION_AGE: TimeDelta = TimeDelta::seconds(60 * 60 * 24 * 7);
|
||||
|
||||
/// Main abstraction over the database that provides API-compatible methods for querying and
|
||||
/// modifying the underlying database
|
||||
#[derive(Clone)]
|
||||
pub struct GpodderRepository {
|
||||
store: Arc<dyn GpodderStore + Send + Sync>,
|
||||
}
|
||||
|
||||
impl GpodderRepository {
|
||||
pub fn new(store: impl GpodderStore + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
store: Arc::new(store),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an authenticated view of the repository for the given user
|
||||
pub fn user<'a>(
|
||||
&'a self,
|
||||
user: &'a models::User,
|
||||
) -> authenticated::AuthenticatedRepository<'a> {
|
||||
authenticated::AuthenticatedRepository {
|
||||
store: self.store.as_ref(),
|
||||
user,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an admin view of the repository, if the user is an admin
|
||||
pub fn admin<'a>(
|
||||
&'a self,
|
||||
user: &'a models::User,
|
||||
) -> Result<admin::AdminRepository<'a>, AuthErr> {
|
||||
if user.admin {
|
||||
Ok(admin::AdminRepository {
|
||||
store: self.store.as_ref(),
|
||||
user,
|
||||
})
|
||||
} else {
|
||||
Err(AuthErr::NotAnAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_session(&self, session_id: i64) -> Result<models::Session, AuthErr> {
|
||||
let session = self
|
||||
.store
|
||||
.get_session(session_id)?
|
||||
.ok_or(AuthErr::UnknownSession)?;
|
||||
|
||||
// Expired sessions still in the database are considered removed
|
||||
if Utc::now() - session.last_seen > MAX_SESSION_AGE {
|
||||
Err(AuthErr::UnknownSession)
|
||||
} else {
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user(&self, username: &str) -> Result<models::User, AuthErr> {
|
||||
self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)
|
||||
}
|
||||
|
||||
pub fn create_user(&self, username: &str, password: &str) -> Result<models::User, AuthErr> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let password_hash = Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
self.store.insert_user(username, &password_hash)
|
||||
}
|
||||
|
||||
pub fn validate_credentials(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<models::User, AuthErr> {
|
||||
let user = self.store.get_user(username)?.ok_or(AuthErr::UnknownUser)?;
|
||||
|
||||
let password_hash = PasswordHash::new(&user.password_hash).unwrap();
|
||||
|
||||
if Argon2::default()
|
||||
.verify_password(password.as_bytes(), &password_hash)
|
||||
.is_ok()
|
||||
{
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(AuthErr::InvalidPassword)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> {
|
||||
let now = Utc::now();
|
||||
|
||||
self.store.refresh_session(session, now)
|
||||
}
|
||||
|
||||
pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
|
||||
self.store.remove_session(session_id)
|
||||
}
|
||||
|
||||
pub fn remove_old_sessions(&self) -> Result<usize, AuthErr> {
|
||||
let min_last_seen = Utc::now() - MAX_SESSION_AGE;
|
||||
|
||||
self.store.remove_old_sessions(min_last_seen)
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ pub enum AuthErr {
|
|||
UnknownSession,
|
||||
UnknownUser,
|
||||
InvalidPassword,
|
||||
NotAnAdmin,
|
||||
Other(Box<dyn std::error::Error + Sync + Send>),
|
||||
}
|
||||
|
||||
|
@ -17,6 +18,7 @@ impl Display for AuthErr {
|
|||
Self::UnknownUser => write!(f, "unknown user"),
|
||||
Self::UnknownSession => write!(f, "unknown session"),
|
||||
Self::InvalidPassword => write!(f, "invalid password"),
|
||||
Self::NotAnAdmin => write!(f, "not an admin"),
|
||||
Self::Other(err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,7 +123,8 @@ impl From<gpodder::AuthErr> for AppError {
|
|||
match value {
|
||||
gpodder::AuthErr::UnknownUser
|
||||
| gpodder::AuthErr::UnknownSession
|
||||
| gpodder::AuthErr::InvalidPassword => Self::Unauthorized,
|
||||
| gpodder::AuthErr::InvalidPassword
|
||||
| gpodder::AuthErr::NotAnAdmin => Self::Unauthorized,
|
||||
gpodder::AuthErr::Other(err) => Self::Other(err),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod sessions;
|
||||
mod users;
|
||||
|
||||
use axum::{
|
||||
Form, RequestExt, Router,
|
||||
|
|
Loading…
Reference in New Issue