- Replace custom hash check with Argon2 password verification - Scan all un-revoked tokens for matching access key - Add expiry validation per token with proper skip logic
247 lines
8.1 KiB
Rust
247 lines
8.1 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use argon2::Argon2;
|
|
use argon2::password_hash::{PasswordHash, PasswordVerifier};
|
|
use chrono::Utc;
|
|
use models::users::{user_activity_log, user_token};
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct CreateAccessKeyParams {
|
|
pub name: String,
|
|
pub scopes: Vec<String>,
|
|
pub expires_at: Option<chrono::DateTime<Utc>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct AccessKeyResponse {
|
|
pub id: i64,
|
|
pub name: String,
|
|
pub access_key: Option<String>,
|
|
pub scopes: Vec<String>,
|
|
pub expires_at: Option<chrono::DateTime<Utc>>,
|
|
pub is_revoked: bool,
|
|
pub created_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct AccessKeyListResponse {
|
|
pub access_keys: Vec<AccessKeyResponse>,
|
|
pub total: usize,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn user_create_access_key(
|
|
&self,
|
|
context: &Session,
|
|
params: CreateAccessKeyParams,
|
|
) -> Result<AccessKeyResponse, AppError> {
|
|
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
|
|
|
|
let access_key = self.user_generate_access_key();
|
|
let access_key_hash = self.user_hash_access_key(&access_key);
|
|
|
|
let access_key_model = user_token::ActiveModel {
|
|
user: Set(user_uid),
|
|
name: Set(params.name.clone()),
|
|
token_hash: Set(access_key_hash),
|
|
scopes: Set(serde_json::to_value(¶ms.scopes).unwrap_or(serde_json::json!([]))),
|
|
expires_at: Set(params.expires_at),
|
|
is_revoked: Set(false),
|
|
created_at: Set(Utc::now()),
|
|
updated_at: Set(Utc::now()),
|
|
..Default::default()
|
|
};
|
|
|
|
let created_access_key = access_key_model.insert(&self.db).await?;
|
|
|
|
let _ = user_activity_log::ActiveModel {
|
|
user_uid: Set(Some(user_uid)),
|
|
action: Set("access_key_create".to_string()),
|
|
ip_address: Set(context.ip_address()),
|
|
user_agent: Set(context.user_agent()),
|
|
details: Set(serde_json::json!({
|
|
"access_key_name": params.name.clone(),
|
|
"access_key_id": created_access_key.id,
|
|
"scopes": params.scopes.clone()
|
|
})),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
}
|
|
.insert(&self.db)
|
|
.await;
|
|
|
|
let scopes: Vec<String> =
|
|
serde_json::from_value(created_access_key.scopes).unwrap_or_default();
|
|
|
|
Ok(AccessKeyResponse {
|
|
id: created_access_key.id,
|
|
name: created_access_key.name,
|
|
access_key: Some(access_key),
|
|
scopes,
|
|
expires_at: created_access_key.expires_at,
|
|
is_revoked: created_access_key.is_revoked,
|
|
created_at: created_access_key.created_at,
|
|
})
|
|
}
|
|
|
|
pub async fn user_list_access_keys(
|
|
&self,
|
|
context: &Session,
|
|
) -> Result<AccessKeyListResponse, AppError> {
|
|
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
|
|
|
|
let access_keys = user_token::Entity::find()
|
|
.filter(user_token::Column::User.eq(user_uid))
|
|
.order_by_desc(user_token::Column::CreatedAt)
|
|
.all(&self.db)
|
|
.await?;
|
|
|
|
let total = access_keys.len();
|
|
let access_keys = access_keys
|
|
.into_iter()
|
|
.map(|ak| {
|
|
let scopes: Vec<String> = serde_json::from_value(ak.scopes).unwrap_or_default();
|
|
AccessKeyResponse {
|
|
id: ak.id,
|
|
name: ak.name,
|
|
access_key: None,
|
|
scopes,
|
|
expires_at: ak.expires_at,
|
|
is_revoked: ak.is_revoked,
|
|
created_at: ak.created_at,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(AccessKeyListResponse { access_keys, total })
|
|
}
|
|
|
|
pub async fn user_revoke_access_key(
|
|
&self,
|
|
context: &Session,
|
|
access_key_id: i64,
|
|
) -> Result<(), AppError> {
|
|
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
|
|
|
|
let access_key = user_token::Entity::find_by_id(access_key_id)
|
|
.filter(user_token::Column::User.eq(user_uid))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::NotFound("Access key not found".to_string()))?;
|
|
|
|
let mut active_access_key: user_token::ActiveModel = access_key.clone().into();
|
|
active_access_key.is_revoked = Set(true);
|
|
active_access_key.updated_at = Set(Utc::now());
|
|
|
|
active_access_key.update(&self.db).await?;
|
|
|
|
let _ = user_activity_log::ActiveModel {
|
|
user_uid: Set(Some(user_uid)),
|
|
action: Set("access_key_revoke".to_string()),
|
|
ip_address: Set(context.ip_address()),
|
|
user_agent: Set(context.user_agent()),
|
|
details: Set(serde_json::json!({
|
|
"access_key_name": access_key.name,
|
|
"access_key_id": access_key_id
|
|
})),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
}
|
|
.insert(&self.db)
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn user_delete_access_key(
|
|
&self,
|
|
context: &Session,
|
|
access_key_id: i64,
|
|
) -> Result<(), AppError> {
|
|
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
|
|
|
|
let access_key = user_token::Entity::find_by_id(access_key_id)
|
|
.filter(user_token::Column::User.eq(user_uid))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::NotFound("Access key not found".to_string()))?;
|
|
|
|
let _ = user_activity_log::ActiveModel {
|
|
user_uid: Set(Some(user_uid)),
|
|
action: Set("access_key_delete".to_string()),
|
|
ip_address: Set(context.ip_address()),
|
|
user_agent: Set(context.user_agent()),
|
|
details: Set(serde_json::json!({
|
|
"access_key_name": access_key.name.clone(),
|
|
"access_key_id": access_key_id
|
|
})),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
}
|
|
.insert(&self.db)
|
|
.await;
|
|
|
|
user_token::Entity::delete(access_key.into_active_model())
|
|
.exec(&self.db)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn user_verify_access_key(&self, access_key: String) -> Result<Uuid, AppError> {
|
|
let access_key_models = user_token::Entity::find()
|
|
.filter(user_token::Column::IsRevoked.eq(false))
|
|
.all(&self.db)
|
|
.await?;
|
|
|
|
for access_key_model in access_key_models {
|
|
if access_key_model
|
|
.expires_at
|
|
.map(|expires_at| expires_at < Utc::now())
|
|
.unwrap_or(false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let Ok(hash) = PasswordHash::new(&access_key_model.token_hash) else {
|
|
continue;
|
|
};
|
|
|
|
if Argon2::default()
|
|
.verify_password(access_key.as_bytes(), &hash)
|
|
.is_ok()
|
|
{
|
|
return Ok(access_key_model.user);
|
|
}
|
|
}
|
|
|
|
Err(AppError::Unauthorized)
|
|
}
|
|
|
|
fn user_generate_access_key(&self) -> String {
|
|
use rand::RngExt;
|
|
let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
let mut rng = rand::rng();
|
|
let mut access_key = String::with_capacity(68);
|
|
access_key.push_str("gda_");
|
|
for _ in 0..64 {
|
|
access_key.push(chars[rng.random_range(0..chars.len())] as char);
|
|
}
|
|
access_key
|
|
}
|
|
|
|
fn user_hash_access_key(&self, access_key: &str) -> String {
|
|
use argon2::Argon2;
|
|
use argon2::password_hash::{PasswordHasher, SaltString};
|
|
let salt = SaltString::generate(&mut rsa::rand_core::OsRng::default());
|
|
Argon2::default()
|
|
.hash_password(access_key.as_bytes(), &salt)
|
|
.expect("Argon2 hash should not fail for access key")
|
|
.to_string()
|
|
}
|
|
}
|