//! auth.v1 的 SSH 公钥解析。 //! //! 调用方:ssh(连接认证,并对长连接的每条 exec 复验一次)。 use chrono::{DateTime, Utc}; use internal::Gate; use internal::server::InternalRouter; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use tonic::{Request, Response, Status}; use crate::models::users::user_ssh_keys::PublicKeyType; use crate::models::users::{user_ssh_keys, users}; use crate::protos::auth::common::v1::{AuthMethod, Principal, SshKeyAlgorithm, SshKeyInfo}; use crate::protos::auth::v1::ssh_key_service_server::{ SshKeyService as SshKeyTrait, SshKeyServiceServer, }; use crate::protos::auth::v1::{ AddSshKeyRequest, AddSshKeyResponse, ListUserSshKeysRequest, ListUserSshKeysResponse, LookupSshKeyRequest, LookupSshKeyResponse, RevokeSshKeyRequest, RevokeSshKeyResponse, }; use crate::services::AuthService; pub fn register(router: InternalRouter, service: &AuthService, gate: Gate) -> InternalRouter { router.add_service(SshKeyServiceServer::with_interceptor( SshKeyRpc { service: service.clone(), }, gate, )) } #[derive(Clone)] struct SshKeyRpc { service: AuthService, } #[tonic::async_trait] impl SshKeyTrait for SshKeyRpc { async fn lookup_ssh_key( &self, request: Request, ) -> Result, Status> { let _meta = internal::interceptor::meta(&request)?; let request = request.into_inner(); let fingerprint = request.fingerprint.trim(); let public_key = request.public_key.trim(); if fingerprint.is_empty() && public_key.is_empty() { return Err(Status::invalid_argument( "fingerprint or public_key is required", )); } let db = self.service.db().read(); let mut query = user_ssh_keys::Entity::find(); query = if fingerprint.is_empty() { query.filter(user_ssh_keys::Column::PublicKey.eq(public_key)) } else { query.filter(user_ssh_keys::Column::Fingerprint.eq(fingerprint)) }; let key = query.one(db).await.map_err(|error| { tracing::error!(%error, "ssh key lookup failed"); Status::internal("ssh key lookup failed") })?; let Some(key) = key else { return Ok(Response::new(missed())); }; if request.require_active && (key.revoked_at.is_some() || key.expires_at.is_some_and(|expiry| expiry <= Utc::now())) { return Ok(Response::new(missed())); } let user = users::Entity::find_by_id(key.user_id) .one(db) .await .map_err(|error| { tracing::error!(%error, "ssh key owner lookup failed"); Status::internal("ssh key owner lookup failed") })?; let Some(user) = user else { return Ok(Response::new(missed())); }; if user.deleted_at.is_some() || !user.allow_use || user.forbid_login { return Ok(Response::new(missed())); } Ok(Response::new(LookupSshKeyResponse { found: true, key: Some(SshKeyInfo { key_id: key.id.to_string(), user_id: key.user_id, name: key.name.clone(), description: key.description.clone().unwrap_or_default(), algorithm: algorithm(key.algorithm) as i32, public_key: key.public_key.clone(), fingerprint: key.fingerprint.clone(), created_at: Some(timestamp(key.created_at)), expires_at: key.expires_at.map(timestamp), last_used_at: key.last_used_at.map(timestamp), revoked_at: key.revoked_at.map(timestamp), }), principal: Some(Principal { user_id: user.id, username: user.username.clone(), displayname: user.displayname.clone().unwrap_or_default(), is_admin: user.is_admin, security_version: user.security_version, method: AuthMethod::SshKey as i32, authenticated_at: Some(timestamp(Utc::now())), bot: user.bot, system: user.system, ..Default::default() }), current_security_version: user.security_version, })) } async fn add_ssh_key( &self, _request: Request, ) -> Result, Status> { Err(Status::unimplemented( "ssh key registration is not exposed by this service", )) } async fn list_user_ssh_keys( &self, _request: Request, ) -> Result, Status> { Err(Status::unimplemented( "ssh key listing is not exposed by this service", )) } async fn revoke_ssh_key( &self, _request: Request, ) -> Result, Status> { Err(Status::unimplemented( "ssh key revocation is not exposed by this service", )) } } /// 未命中一律返回空响应,不区分「不存在」与「不可用」,避免成为探测接口。 fn missed() -> LookupSshKeyResponse { LookupSshKeyResponse { found: false, ..Default::default() } } fn algorithm(value: PublicKeyType) -> SshKeyAlgorithm { match value { PublicKeyType::RSA => SshKeyAlgorithm::Rsa, PublicKeyType::ED25519 => SshKeyAlgorithm::Ed25519, PublicKeyType::ECDSA => SshKeyAlgorithm::Ecdsa, } } fn timestamp(value: DateTime) -> prost_types::Timestamp { prost_types::Timestamp { seconds: value.timestamp(), nanos: value.timestamp_subsec_nanos() as i32, } }