gitdataai/libs/service/user/ssh_key.rs
2026-04-15 09:08:09 +08:00

397 lines
13 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use base64::Engine;
use base64::engine::general_purpose;
use chrono::Utc;
use models::users::{user_activity_log, user_ssh_key};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct AddSshKeyParams {
pub title: String,
pub public_key: String,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct UpdateSshKeyParams {
pub title: Option<String>,
pub expires_at: Option<chrono::DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct SshKeyResponse {
pub id: i64,
pub user_uid: Uuid,
pub title: String,
pub fingerprint: String,
pub key_type: String,
pub key_bits: Option<i32>,
pub is_verified: bool,
pub last_used_at: Option<chrono::DateTime<Utc>>,
pub expires_at: Option<chrono::DateTime<Utc>>,
pub is_revoked: bool,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct SshKeyListResponse {
pub keys: Vec<SshKeyResponse>,
pub total: usize,
}
#[derive(Debug)]
pub struct ParsedSshKey {
pub key_type: String,
pub key_data: Vec<u8>,
pub key_bits: Option<i32>,
pub comment: Option<String>,
}
impl AppService {
pub async fn user_add_ssh_key(
&self,
context: &Session,
params: AddSshKeyParams,
) -> Result<SshKeyResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let public_key = params.public_key.trim().to_string();
let parsed = self.user_parse_ssh_public_key(&public_key)?;
let fingerprint = self.user_generate_ssh_fingerprint(&parsed.key_data)?;
let existing: Option<user_ssh_key::Model> = user_ssh_key::Entity::find()
.filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint))
.one(&self.db)
.await?;
if existing.is_some() {
return Err(AppError::BadRequest("SSH key already exists".to_string()));
}
let count = user_ssh_key::Entity::find()
.filter(user_ssh_key::Column::User.eq(user_uid))
.filter(user_ssh_key::Column::IsRevoked.eq(false))
.count(&self.db)
.await?;
if count >= 50 {
return Err(AppError::BadRequest("Too many SSH keys".to_string()));
}
let now = Utc::now();
let ssh_key_model = user_ssh_key::ActiveModel {
user: Set(user_uid),
title: Set(params.title.clone()),
public_key: Set(public_key),
fingerprint: Set(fingerprint),
key_type: Set(parsed.key_type.clone()),
key_bits: Set(parsed.key_bits),
is_verified: Set(false),
last_used_at: Set(None),
expires_at: Set(None),
is_revoked: Set(false),
created_at: Set(now),
updated_at: Set(now),
..Default::default()
};
let created_key = ssh_key_model.insert(&self.db).await?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("ssh_key_add".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"key_id": created_key.id,
"key_title": params.title,
"key_type": parsed.key_type,
"fingerprint": created_key.fingerprint
})),
created_at: Set(now),
..Default::default()
}
.insert(&self.db)
.await;
Ok(self.user_model_to_response(created_key))
}
pub async fn user_list_ssh_keys(
&self,
context: &Session,
) -> Result<SshKeyListResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let keys: Vec<user_ssh_key::Model> = user_ssh_key::Entity::find()
.filter(user_ssh_key::Column::User.eq(user_uid))
.order_by_desc(user_ssh_key::Column::CreatedAt)
.all(&self.db)
.await?;
let total = keys.len();
let keys = keys
.into_iter()
.map(|k| self.user_model_to_response(k))
.collect();
Ok(SshKeyListResponse { keys, total })
}
pub async fn user_get_ssh_key(
&self,
context: &Session,
key_id: i64,
) -> Result<SshKeyResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id)
.filter(user_ssh_key::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
Ok(self.user_model_to_response(key))
}
pub async fn user_update_ssh_key(
&self,
context: &Session,
key_id: i64,
params: UpdateSshKeyParams,
) -> Result<SshKeyResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id)
.filter(user_ssh_key::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
let mut active_key: user_ssh_key::ActiveModel = key.into();
let updated_title = params.title.clone();
let updated_expires_at = params.expires_at;
if let Some(title) = params.title {
active_key.title = Set(title);
}
if let Some(expires_at) = params.expires_at {
active_key.expires_at = Set(Some(expires_at));
}
active_key.updated_at = Set(Utc::now());
let updated_key = active_key.update(&self.db).await?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("ssh_key_update".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"key_id": key_id,
"updated_fields": {
"title": updated_title,
"expires_at": updated_expires_at
}
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
Ok(self.user_model_to_response(updated_key))
}
pub async fn user_delete_ssh_key(
&self,
context: &Session,
key_id: i64,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id)
.filter(user_ssh_key::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("ssh_key_delete".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"key_id": key_id,
"key_title": key.title.clone(),
"fingerprint": key.fingerprint.clone()
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
user_ssh_key::Entity::delete(key.into_active_model())
.exec(&self.db)
.await?;
Ok(())
}
pub async fn user_revoke_ssh_key(
&self,
context: &Session,
key_id: i64,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id)
.filter(user_ssh_key::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
let mut active_key: user_ssh_key::ActiveModel = key.clone().into();
active_key.is_revoked = Set(true);
active_key.updated_at = Set(Utc::now());
active_key.update(&self.db).await?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("ssh_key_revoke".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"key_id": key_id,
"key_title": key.title,
"fingerprint": key.fingerprint
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
Ok(())
}
pub async fn user_verify_ssh_key_by_fingerprint(
&self,
fingerprint: &str,
) -> Result<user_ssh_key::Model, AppError> {
let key: user_ssh_key::Model = user_ssh_key::Entity::find()
.filter(user_ssh_key::Column::Fingerprint.eq(fingerprint))
.filter(user_ssh_key::Column::IsRevoked.eq(false))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
if let Some(expires_at) = key.expires_at {
let now = Utc::now();
if now > expires_at {
return Err(AppError::Unauthorized); // AppError::SshKeyExpired replaced with generic Unauthorized
}
}
Ok(key)
}
pub async fn user_touch_ssh_key_last_used(&self, key_id: i64) -> Result<(), AppError> {
let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id)
.one(&self.db)
.await?
.ok_or(AppError::NotFound("SSH key not found".to_string()))?;
let mut active_key: user_ssh_key::ActiveModel = key.into();
active_key.last_used_at = Set(Some(Utc::now()));
active_key.updated_at = Set(Utc::now());
active_key.update(&self.db).await?;
Ok(())
}
fn user_parse_ssh_public_key(&self, public_key: &str) -> Result<ParsedSshKey, AppError> {
let parts: Vec<&str> = public_key.split_whitespace().collect();
if parts.len() < 2 {
return Err(AppError::BadRequest("Invalid SSH key format".to_string()));
}
let key_type = parts[0];
let key_data_base64 = parts[1];
let comment: Option<String> = parts.get(2).map(|s| (*s).to_string());
let normalized_key_type = match key_type {
"ssh-rsa" => "rsa",
"ssh-ed25519" => "ed25519",
"ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => "ecdsa",
"ssh-dss" => "dsa",
_ => return Err(AppError::BadRequest("Unsupported SSH key type".to_string())),
};
let key_data = general_purpose::STANDARD
.decode(key_data_base64)
.map_err(|_| AppError::BadRequest("Invalid SSH key format".to_string()))?;
let key_bits = if normalized_key_type == "rsa" {
self.user_calculate_rsa_key_bits(&key_data)
} else {
None
};
Ok(ParsedSshKey {
key_type: normalized_key_type.to_string(),
key_data,
key_bits,
comment,
})
}
fn user_generate_ssh_fingerprint(&self, key_data: &[u8]) -> Result<String, AppError> {
let mut hasher = Sha256::new();
hasher.update(key_data);
let hash = hasher.finalize();
let fingerprint = format!("SHA256:{}", general_purpose::STANDARD_NO_PAD.encode(&hash));
Ok(fingerprint)
}
fn user_calculate_rsa_key_bits(&self, key_data: &[u8]) -> Option<i32> {
if key_data.len() < 256 {
Some(1024)
} else if key_data.len() < 512 {
Some(2048)
} else if key_data.len() < 1024 {
Some(4096)
} else {
Some(8192)
}
}
fn user_model_to_response(&self, model: user_ssh_key::Model) -> SshKeyResponse {
SshKeyResponse {
id: model.id,
user_uid: model.user,
title: model.title,
fingerprint: model.fingerprint,
key_type: model.key_type,
key_bits: model.key_bits,
is_verified: model.is_verified,
last_used_at: model.last_used_at,
expires_at: model.expires_at,
is_revoked: model.is_revoked,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}