//! 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, ) -> Result, 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, ) -> Result, Status> { Err(Status::unimplemented( "access key issuance is not exposed by this service", )) } async fn rotate_access_key( &self, _request: Request, ) -> Result, Status> { Err(Status::unimplemented( "access key rotation is not exposed by this service", )) } async fn list_access_keys( &self, _request: Request, ) -> Result, Status> { Err(Status::unimplemented( "access key listing is not exposed by this service", )) } async fn revoke_access_key( &self, _request: Request, ) -> Result, 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 { 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) -> prost_types::Timestamp { prost_types::Timestamp { seconds: value.timestamp(), nanos: value.timestamp_subsec_nanos() as i32, } }