759ae5e
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::HeaderMap;
use export::pagination::{Page, PageQuery, paginate};
use export::rest::{AppResponseData, NoData};
use jwt::Session;
use jwt::share::session::require_uid;
use kenauth::proto::common::v1::{PageRequest, SshKeyAlgorithm, SshKeyInfo};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::ApiError;
use crate::services::AuthService;
use crate::share::rpc;
const KEY_PAGE_SIZE: i32 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SshKeyType {
SshRsa,
SshEd25519,
EcdsaSha2Nistp256,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SshKeyDto {
pub id: Uuid,
pub name: String,
pub key_type: SshKeyType,
pub fingerprint: String,
pub created_at: OffsetDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AddSshKeyCmd {
pub name: String,
pub content: String,
}
fn key_type_of(algorithm: i32) -> SshKeyType {
match SshKeyAlgorithm::try_from(algorithm) {
Ok(SshKeyAlgorithm::Rsa) => SshKeyType::SshRsa,
Ok(SshKeyAlgorithm::Ecdsa) => SshKeyType::EcdsaSha2Nistp256,
_ => SshKeyType::SshEd25519,
}
}
fn key_dto(info: SshKeyInfo) -> Result<SshKeyDto, ApiError> {
let created_at = info
.created_at
.and_then(|ts| rpc::datetime(ts.seconds, ts.nanos))
.ok_or_else(|| ApiError::internal("kenoauth returned a key without created_at"))?;
Ok(SshKeyDto {
id: Uuid::parse_str(&info.key_id)
.map_err(|_| ApiError::internal("kenoauth returned a malformed key id"))?,
key_type: key_type_of(info.algorithm),
name: info.name,
fingerprint: info.fingerprint,
created_at,
})
}
#[utoipa::path(
get,
path = "/auth/ssh-keys",
tag = "security",
description = "列出当前账号已登记的 SSH 公钥。",
params(PageQuery),
responses(
(status = 200, description = "ssh public keys", body = AppResponseData<Page<SshKeyDto>>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn list_ssh_keys(
State(service): State<AuthService>,
session: Session,
Query(query): Query<PageQuery>,
) -> Result<AppResponseData<Page<SshKeyDto>>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let listed = client
.list_user_ssh_keys(
uid,
false,
PageRequest {
page_size: KEY_PAGE_SIZE,
page_token: String::new(),
},
)
.await
.map_err(|error| ApiError::internal(error.to_string()))?;
let mut keys = Vec::with_capacity(listed.keys.len());
for info in listed.keys {
keys.push(key_dto(info)?);
}
Ok(AppResponseData::ok(paginate(keys, &query)))
}
#[utoipa::path(
post,
path = "/auth/ssh-keys",
tag = "security",
description = "登记一把新的 SSH 公钥,用于 git over SSH。",
request_body = AddSshKeyCmd,
responses(
(status = 200, description = "ssh public key added", body = AppResponseData<SshKeyDto>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn add_ssh_key(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Json(cmd): Json<AddSshKeyCmd>,
) -> Result<AppResponseData<SshKeyDto>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let added = client
.add_ssh_key(
uid,
cmd.name.trim(),
cmd.content.trim(),
"",
&rpc::client_ip(&headers),
)
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
let key = added
.key
.ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
Ok(AppResponseData::ok(key_dto(key)?))
}
#[utoipa::path(
delete,
path = "/auth/ssh-keys/{key_id}",
tag = "security",
description = "吊销指定 SSH 公钥;密钥不存在或不属于当前账号时返回 404。",
params(("key_id" = Uuid, Path)),
responses(
(status = 200, description = "ssh public key revoked", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>),
(status = 404, description = "not found", body = AppResponseData<NoData>)
)
)]
pub async fn delete_ssh_key(
State(service): State<AuthService>,
session: Session,
Path(key_id): Path<Uuid>,
) -> Result<AppResponseData<NoData>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let revoked = client
.revoke_ssh_key(uid, &key_id.to_string(), "user_revoked", false)
.await
.map_err(|error| ApiError::internal(error.to_string()))?;
if !revoked.revoked {
return Err(ApiError::not_found("ssh key not found"));
}
Ok(AppResponseData::empty())
}