Skip to content
Clone
main
169 lines 5.8 KB Rust
//! 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<LookupSshKeyRequest>,
) -> Result<Response<LookupSshKeyResponse>, 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<AddSshKeyRequest>,
) -> Result<Response<AddSshKeyResponse>, Status> {
Err(Status::unimplemented(
"ssh key registration is not exposed by this service",
))
}
async fn list_user_ssh_keys(
&self,
_request: Request<ListUserSshKeysRequest>,
) -> Result<Response<ListUserSshKeysResponse>, Status> {
Err(Status::unimplemented(
"ssh key listing is not exposed by this service",
))
}
async fn revoke_ssh_key(
&self,
_request: Request<RevokeSshKeyRequest>,
) -> Result<Response<RevokeSshKeyResponse>, 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<Utc>) -> prost_types::Timestamp {
prost_types::Timestamp {
seconds: value.timestamp(),
nanos: value.timestamp_subsec_nanos() as i32,
}
}