eeff292
//! auth.v1 的 access key 校验。
//!
//! 明文令牌只出现在请求里,本层立即摘要后丢弃;库内只存 sha256 hex。
//! 调用方:git(HTTP 传输的凭据校验)。
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_access_tokens, users};
use crate::protos::auth::common::v1::{AccessKeyInfo, AuthMethod, Code, Principal};
use crate::protos::auth::v1::access_key_service_server::{
AccessKeyService as AccessKeyTrait, AccessKeyServiceServer,
};
use crate::protos::auth::v1::{
CreateAccessKeyRequest, CreateAccessKeyResponse, ListAccessKeysRequest, ListAccessKeysResponse,
RevokeAccessKeyRequest, RevokeAccessKeyResponse, RotateAccessKeyRequest,
RotateAccessKeyResponse, VerifyAccessKeyRequest, VerifyAccessKeyResponse,
};
use crate::services::AuthService;
use crate::share::crypto::sha256_hex;
pub fn register(router: InternalRouter, service: &AuthService, gate: Gate) -> InternalRouter {
router.add_service(AccessKeyServiceServer::with_interceptor(
AccessKeyRpc {
service: service.clone(),
},
gate,
))
}
#[derive(Clone)]
struct AccessKeyRpc {
service: AuthService,
}
#[tonic::async_trait]
impl AccessKeyTrait for AccessKeyRpc {
async fn verify_access_key(
&self,
request: Request<VerifyAccessKeyRequest>,
) -> Result<Response<VerifyAccessKeyResponse>, Status> {
let _meta = internal::interceptor::meta(&request)?;
let request = request.into_inner();
let plaintext = request.access_key.trim();
if plaintext.is_empty() {
return Err(Status::invalid_argument("access key is required"));
}
let db = self.service.db().read();
let token = user_access_tokens::Entity::find()
.filter(user_access_tokens::Column::TokenHash.eq(sha256_hex(plaintext.as_bytes())))
.one(db)
.await
.map_err(|error| {
tracing::error!(%error, "access key lookup failed");
Status::internal("access key lookup failed")
})?;
let Some(token) = token else {
return Ok(Response::new(rejected(Code::Unauthenticated)));
};
if token.revoked_at.is_some() {
return Ok(Response::new(rejected(Code::TokenRevoked)));
}
if token.expires_at.is_some_and(|expiry| expiry <= Utc::now()) {
return Ok(Response::new(rejected(Code::TokenExpired)));
}
let user = users::Entity::find_by_id(token.user_id)
.one(db)
.await
.map_err(|error| {
tracing::error!(%error, "access key owner lookup failed");
Status::internal("access key owner lookup failed")
})?;
let Some(user) = user else {
return Ok(Response::new(rejected(Code::UserDisabled)));
};
if user.deleted_at.is_some() || !user.allow_use || user.forbid_login {
return Ok(Response::new(rejected(Code::UserDisabled)));
}
// token.version 记录签发时的 users.security_version。
if token.version < user.security_version {
return Ok(Response::new(rejected(Code::SecurityVersionStale)));
}
let scopes = scopes_of(&token.scopes);
if !scopes_cover(&scopes, &request.required_scopes) {
return Ok(Response::new(rejected(Code::ScopeInsufficient)));
}
let key = AccessKeyInfo {
access_key_id: token.id.to_string(),
name: token.name.clone(),
description: token.description.clone().unwrap_or_default(),
fingerprint: token.fingerprint.clone(),
scopes: scopes.clone(),
hash_key_id: token.hash_key_id.clone(),
created_at: Some(timestamp(token.created_at)),
expires_at: token.expires_at.map(timestamp),
last_used_at: token.last_used_at.map(timestamp),
revoked_at: token.revoked_at.map(timestamp),
};
Ok(Response::new(VerifyAccessKeyResponse {
valid: true,
reason: Code::Unspecified as i32,
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::AccessKey as i32,
access_key_id: token.id.to_string(),
scopes: scopes.clone(),
// 空 scope 等价于不限范围,此时不是有界令牌。
scopes_bounded: !scopes.is_empty(),
mfa_verified: false,
authenticated_at: Some(timestamp(Utc::now())),
bot: user.bot,
system: user.system,
..Default::default()
}),
key: Some(key),
current_security_version: user.security_version,
}))
}
async fn create_access_key(
&self,
_request: Request<CreateAccessKeyRequest>,
) -> Result<Response<CreateAccessKeyResponse>, Status> {
Err(Status::unimplemented(
"access key issuance is not exposed by this service",
))
}
async fn rotate_access_key(
&self,
_request: Request<RotateAccessKeyRequest>,
) -> Result<Response<RotateAccessKeyResponse>, Status> {
Err(Status::unimplemented(
"access key rotation is not exposed by this service",
))
}
async fn list_access_keys(
&self,
_request: Request<ListAccessKeysRequest>,
) -> Result<Response<ListAccessKeysResponse>, Status> {
Err(Status::unimplemented(
"access key listing is not exposed by this service",
))
}
async fn revoke_access_key(
&self,
_request: Request<RevokeAccessKeyRequest>,
) -> Result<Response<RevokeAccessKeyResponse>, Status> {
Err(Status::unimplemented(
"access key revocation is not exposed by this service",
))
}
}
/// 失败响应只给原因码,不回显令牌或用户是否存在。
fn rejected(reason: Code) -> VerifyAccessKeyResponse {
VerifyAccessKeyResponse {
valid: false,
reason: reason as i32,
..Default::default()
}
}
fn scopes_of(value: &serde_json::Value) -> Vec<i32> {
value
.as_array()
.map(|items| {
items
.iter()
.filter_map(|item| item.as_i64())
.map(|value| value as i32)
.collect()
})
.unwrap_or_default()
}
/// 调用方未声明范围时放行;令牌本身不限范围时也放行。
fn scopes_cover(granted: &[i32], required: &[i32]) -> bool {
required.is_empty()
|| granted.is_empty()
|| required.iter().all(|scope| granted.contains(scope))
}
fn timestamp(value: DateTime<Utc>) -> prost_types::Timestamp {
prost_types::Timestamp {
seconds: value.timestamp(),
nanos: value.timestamp_subsec_nanos() as i32,
}
}