Skip to content
Clone
951a522
223 lines 7.4 KB Rust
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::{AccessKeyInfo, PageRequest, TokenScope};
use models::users::access_scope::AccessKeyScope;
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 SCOPE_PAGE_SIZE: i32 = 100;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateAccessKeyCmd {
pub name: String,
pub scope: AccessKeyScope,
#[serde(
default,
deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
)]
pub expires_at: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct AccessKeyDto {
pub id: Uuid,
pub name: String,
pub scope: AccessKeyScope,
pub created_at: OffsetDateTime,
pub expires_at: Option<OffsetDateTime>,
pub last_used_at: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreateAccessKeyResp {
pub key: AccessKeyDto,
pub token: String,
}
fn scopes_of(scope: AccessKeyScope) -> Vec<i32> {
match scope {
AccessKeyScope::Read => vec![TokenScope::RepoRead as i32],
AccessKeyScope::Write => vec![TokenScope::RepoRead as i32, TokenScope::RepoWrite as i32],
}
}
fn scope_of(info: &AccessKeyInfo) -> AccessKeyScope {
if info.scopes.contains(&(TokenScope::RepoWrite as i32)) {
AccessKeyScope::Write
} else {
AccessKeyScope::Read
}
}
fn key_dto(info: AccessKeyInfo) -> Result<AccessKeyDto, 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(AccessKeyDto {
id: Uuid::parse_str(&info.access_key_id)
.map_err(|_| ApiError::internal("kenoauth returned a malformed key id"))?,
scope: scope_of(&info),
name: info.name,
created_at,
expires_at: info
.expires_at
.and_then(|ts| rpc::datetime(ts.seconds, ts.nanos)),
last_used_at: info
.last_used_at
.and_then(|ts| rpc::datetime(ts.seconds, ts.nanos)),
})
}
#[utoipa::path(
get,
path = "/auth/access-keys",
tag = "security",
description = "列出当前账号的访问令牌(不含令牌明文)。",
params(PageQuery),
responses(
(status = 200, description = "access keys", body = AppResponseData<Page<AccessKeyDto>>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn list_access_keys(
State(service): State<AuthService>,
session: Session,
Query(query): Query<PageQuery>,
) -> Result<AppResponseData<Page<AccessKeyDto>>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let listed = client
.list_access_keys(
uid,
false,
PageRequest {
page_size: SCOPE_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/access-keys",
tag = "security",
description = "为当前账号签发新的访问令牌,令牌明文只在本响应出现一次。",
request_body = CreateAccessKeyCmd,
responses(
(status = 200, description = "access key created", body = AppResponseData<CreateAccessKeyResp>),
(status = 400, description = "request failed validation", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn create_access_key(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Json(cmd): Json<CreateAccessKeyCmd>,
) -> Result<AppResponseData<CreateAccessKeyResp>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let expires_in_secs = match cmd.expires_at {
Some(expires_at) => (expires_at - OffsetDateTime::now_utc()).whole_seconds(),
None => 0,
};
let created = client
.create_access_key(
uid,
cmd.name.trim(),
"",
scopes_of(cmd.scope),
expires_in_secs,
&rpc::client_ip(&headers),
)
.await
.map_err(|error| ApiError::bad_request(error.to_string()))?;
let key = created
.key
.ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
Ok(AppResponseData::ok(CreateAccessKeyResp {
key: key_dto(key)?,
token: created.token,
}))
}
#[utoipa::path(
delete,
path = "/auth/access-keys/{key_id}",
tag = "security",
description = "吊销指定访问令牌;令牌不存在或不属于当前账号时返回 404。",
params(("key_id" = Uuid, Path)),
responses(
(status = 200, description = "access 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_access_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_access_key(uid, &key_id.to_string(), "user_revoked")
.await
.map_err(|error| ApiError::internal(error.to_string()))?;
if !revoked.revoked && !revoked.already_revoked {
return Err(ApiError::not_found("access key not found"));
}
Ok(AppResponseData::empty())
}
#[utoipa::path(
post,
path = "/auth/access-keys/{key_id}/rotate",
tag = "auth",
description = "为指定访问令牌换发新明文,保留其编号、名称与范围。",
params(("key_id" = Uuid, Path)),
responses(
(status = 200, description = "access key rotated", body = AppResponseData<CreateAccessKeyResp>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>),
(status = 404, description = "not found", body = AppResponseData<NoData>)
)
)]
pub async fn rotate_access_key(
State(service): State<AuthService>,
session: Session,
headers: HeaderMap,
Path(key_id): Path<Uuid>,
) -> Result<AppResponseData<CreateAccessKeyResp>, ApiError> {
let uid = require_uid(&session)?;
let client = rpc::client(&service.state)?;
let rotated = client
.rotate_access_key(uid, &key_id.to_string(), &rpc::client_ip(&headers))
.await
.map_err(|_error| ApiError::not_found("access key not found"))?;
let key = rotated
.key
.ok_or_else(|| ApiError::internal("kenoauth returned no key"))?;
Ok(AppResponseData::ok(CreateAccessKeyResp {
key: key_dto(key)?,
token: rotated.token,
}))
}