gitdataai/libs/service/user/access_key.rs
ZhenYi b35d2d4fe7 refactor(access_key): migrate auth to Argon2 password verification
- 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
2026-05-15 11:48:46 +08:00

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(&params.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()
}
}