241 lines
6.6 KiB
Rust
241 lines
6.6 KiB
Rust
use argon2::{
|
|
password_hash::{PasswordHasher, SaltString},
|
|
Argon2,
|
|
};
|
|
use chrono::{NaiveDateTime, Utc};
|
|
use diesel::prelude::*;
|
|
use rand::rngs::OsRng;
|
|
use rand::RngCore;
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
error::AppError,
|
|
models::{NewWebdavToken, WebdavToken},
|
|
schema::webdav_tokens,
|
|
state::PgPooledConnection,
|
|
tenants::{apply_webdav_token_prefix, clear_webdav_token_prefix},
|
|
};
|
|
|
|
const TOKEN_PREFIX_LENGTH: usize = 12;
|
|
const TOKEN_SECRET_LENGTH: usize = 32;
|
|
|
|
pub struct IssuedWebdavToken {
|
|
pub token: String,
|
|
pub record: WebdavToken,
|
|
}
|
|
|
|
pub fn create_webdav_token(
|
|
conn: &mut PgPooledConnection,
|
|
user_id: Uuid,
|
|
tenant_id: Uuid,
|
|
label: Option<String>,
|
|
expires_at: Option<NaiveDateTime>,
|
|
) -> Result<IssuedWebdavToken, AppError> {
|
|
let raw_secret = generate_secret()?;
|
|
let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string();
|
|
let token_hash = hash_secret(&raw_secret)?;
|
|
let new_token = NewWebdavToken {
|
|
id: Uuid::new_v4(),
|
|
user_id,
|
|
tenant_id,
|
|
token_prefix,
|
|
token_hash,
|
|
label,
|
|
expires_at,
|
|
};
|
|
|
|
let record = diesel::insert_into(webdav_tokens::table)
|
|
.values(&new_token)
|
|
.get_result::<WebdavToken>(conn)?;
|
|
|
|
Ok(IssuedWebdavToken {
|
|
token: raw_secret,
|
|
record,
|
|
})
|
|
}
|
|
|
|
pub fn list_webdav_tokens(
|
|
conn: &mut PgPooledConnection,
|
|
user_id: Uuid,
|
|
tenant_id: Option<Uuid>,
|
|
) -> Result<Vec<WebdavToken>, AppError> {
|
|
let mut query = webdav_tokens::table
|
|
.filter(webdav_tokens::user_id.eq(user_id))
|
|
.into_boxed();
|
|
|
|
if let Some(tenant_id) = tenant_id {
|
|
query = query.filter(webdav_tokens::tenant_id.eq(tenant_id));
|
|
}
|
|
|
|
let tokens = query
|
|
.order(webdav_tokens::created_at.asc())
|
|
.load::<WebdavToken>(conn)?;
|
|
|
|
Ok(tokens)
|
|
}
|
|
|
|
pub fn regenerate_webdav_token(
|
|
conn: &mut PgPooledConnection,
|
|
token_id: Uuid,
|
|
user_id: Uuid,
|
|
tenant_id: Option<Uuid>,
|
|
) -> Result<IssuedWebdavToken, AppError> {
|
|
let mut query = webdav_tokens::table
|
|
.filter(webdav_tokens::id.eq(token_id))
|
|
.filter(webdav_tokens::user_id.eq(user_id))
|
|
.into_boxed();
|
|
|
|
if let Some(tenant) = tenant_id {
|
|
query = query.filter(webdav_tokens::tenant_id.eq(tenant));
|
|
}
|
|
|
|
let record = query
|
|
.first::<WebdavToken>(conn)
|
|
.optional()
|
|
.map_err(AppError::from)?
|
|
.ok_or_else(AppError::not_found)?;
|
|
|
|
if record.revoked_at.is_some() {
|
|
return Err(AppError::bad_request(
|
|
"cannot regenerate a revoked WebDAV token",
|
|
));
|
|
}
|
|
|
|
let raw_secret = generate_secret()?;
|
|
let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string();
|
|
let token_hash = hash_secret(&raw_secret)?;
|
|
|
|
let updated = diesel::update(webdav_tokens::table.find(record.id))
|
|
.set((
|
|
webdav_tokens::token_prefix.eq(&token_prefix),
|
|
webdav_tokens::token_hash.eq(&token_hash),
|
|
webdav_tokens::last_used_at.eq::<Option<NaiveDateTime>>(None),
|
|
))
|
|
.get_result::<WebdavToken>(conn)?;
|
|
|
|
Ok(IssuedWebdavToken {
|
|
token: raw_secret,
|
|
record: updated,
|
|
})
|
|
}
|
|
|
|
pub fn find_active_token_by_secret(
|
|
conn: &mut PgPooledConnection,
|
|
user_id: Uuid,
|
|
tenant_id: Option<Uuid>,
|
|
secret: &str,
|
|
) -> Result<Option<WebdavToken>, AppError> {
|
|
if secret.len() < TOKEN_PREFIX_LENGTH {
|
|
return Ok(None);
|
|
}
|
|
|
|
let prefix = &secret[..TOKEN_PREFIX_LENGTH];
|
|
apply_webdav_token_prefix(conn, prefix)?;
|
|
let mut query = webdav_tokens::table
|
|
.filter(webdav_tokens::user_id.eq(user_id))
|
|
.filter(webdav_tokens::token_prefix.eq(prefix))
|
|
.filter(webdav_tokens::revoked_at.is_null())
|
|
.into_boxed();
|
|
|
|
let now = Utc::now().naive_utc();
|
|
query = query.filter(
|
|
webdav_tokens::expires_at
|
|
.is_null()
|
|
.or(webdav_tokens::expires_at.gt(now)),
|
|
);
|
|
|
|
if let Some(tenant_id) = tenant_id {
|
|
query = query.filter(webdav_tokens::tenant_id.eq(tenant_id));
|
|
}
|
|
|
|
let load_result = query.load::<WebdavToken>(conn);
|
|
let clear_result = clear_webdav_token_prefix(conn);
|
|
clear_result?;
|
|
let candidates = load_result?;
|
|
|
|
for token in candidates {
|
|
if verify_token_secret(secret, &token.token_hash)? {
|
|
return Ok(Some(token));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub fn revoke_webdav_token(
|
|
conn: &mut PgPooledConnection,
|
|
token_id: Uuid,
|
|
user_id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let affected = diesel::update(
|
|
webdav_tokens::table
|
|
.filter(webdav_tokens::id.eq(token_id))
|
|
.filter(webdav_tokens::user_id.eq(user_id)),
|
|
)
|
|
.set(webdav_tokens::revoked_at.eq(Utc::now().naive_utc()))
|
|
.execute(conn)?;
|
|
|
|
if affected == 0 {
|
|
return Err(AppError::not_found());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn touch_webdav_token(conn: &mut PgPooledConnection, token_id: Uuid) -> Result<(), AppError> {
|
|
diesel::update(webdav_tokens::table.filter(webdav_tokens::id.eq(token_id)))
|
|
.set(webdav_tokens::last_used_at.eq(Utc::now().naive_utc()))
|
|
.execute(conn)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result<bool, AppError> {
|
|
crate::auth::password::verify_password(secret, token_hash).map_err(|err| {
|
|
tracing::error!(error = ?err, "failed to verify token");
|
|
AppError::internal("failed to verify token")
|
|
})
|
|
}
|
|
|
|
fn generate_secret() -> Result<String, AppError> {
|
|
let mut buffer = [0u8; TOKEN_SECRET_LENGTH];
|
|
OsRng.try_fill_bytes(&mut buffer).map_err(|err| {
|
|
tracing::error!(error = ?err, "failed to generate token");
|
|
AppError::internal("failed to generate token")
|
|
})?;
|
|
Ok(hex::encode(buffer))
|
|
}
|
|
|
|
fn hash_secret(secret: &str) -> Result<String, AppError> {
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
let hash = Argon2::default()
|
|
.hash_password(secret.as_bytes(), &salt)
|
|
.map_err(|err| {
|
|
tracing::error!(error = ?err, "failed to hash token");
|
|
AppError::internal("failed to hash token")
|
|
})?;
|
|
Ok(hash.to_string())
|
|
}
|
|
|
|
fn _ensure_constants() {
|
|
assert!(TOKEN_PREFIX_LENGTH < TOKEN_SECRET_LENGTH * 2);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn generated_secret_has_expected_length() {
|
|
let secret = generate_secret().unwrap();
|
|
assert_eq!(secret.len(), TOKEN_SECRET_LENGTH * 2);
|
|
}
|
|
|
|
#[test]
|
|
fn hash_and_verify_secret_round_trip() {
|
|
let secret = generate_secret().unwrap();
|
|
let hash = hash_secret(&secret).unwrap();
|
|
assert!(verify_token_secret(&secret, &hash).unwrap());
|
|
assert!(!verify_token_secret("wrong", &hash).unwrap());
|
|
}
|
|
}
|