b6f7f73
ZhenYi
refactor: reorganize configuration files and update module paths for improved structure
b6f7f73
//! OAuth 客户端注册表管理面(`auth.internal.v1.ClientService`)。
//!
//! 共享密钥门禁(`x-rpc-key`)放行后,所有写操作立即刷新进程内快照。
use sea_orm::EntityTrait;
use tonic::{Request, Response, Status};
use crate::models::users::oauth_clients::{
ClientKind as ModelKind, ClientSource as ModelSource, Entity as ClientEntity,
};
use crate::protos::auth::internal::v1::client_service_server::{
ClientService, ClientServiceServer,
};
use crate::protos::auth::internal::v1::*;
use crate::server::AuthSvc;
use crate::server::errors::AuthError;
use crate::server::grpc::{context, convert, error};
use crate::server::oauth::client::{ClientRecord, ClientSpec};
#[derive(Clone)]
pub struct ClientApi {
svc: AuthSvc,
}
impl ClientApi {
pub fn new(svc: AuthSvc) -> Self {
Self { svc }
}
pub fn server(
self,
) -> tonic::service::interceptor::InterceptedService<
ClientServiceServer<Self>,
context::Interceptor,
> {
let gate = context::Interceptor::new(self.svc.auth().oauth.rpc_key.clone());
ClientServiceServer::with_interceptor(self, gate)
}
}
fn kind_of(value: i32) -> Result<ModelKind, Status> {
match ClientKind::try_from(value) {
Ok(ClientKind::Public) => Ok(ModelKind::Public),
Ok(ClientKind::Confidential) => Ok(ModelKind::Confidential),
_ => Err(Status::invalid_argument("client kind is required")),
}
}
fn record_of(row: &crate::models::users::oauth_clients::Model) -> OAuthClientRecord {
OAuthClientRecord {
client_id: row.client_id.clone(),
name: row.name.clone(),
kind: match row.kind {
ModelKind::Public => ClientKind::Public as i32,
ModelKind::Confidential => ClientKind::Confidential as i32,
},
audience: row.audience.clone().unwrap_or_default(),
enabled: row.enabled,
redirect_uris: serde_json::from_value(row.redirect_uris.clone()).unwrap_or_default(),
post_logout_redirect_uris: serde_json::from_value(row.post_logout_redirect_uris.clone())
.unwrap_or_default(),
backchannel_logout_uris: serde_json::from_value(row.backchannel_logout_uris.clone())
.unwrap_or_default(),
scopes: serde_json::from_value(row.scopes.clone()).unwrap_or_default(),
source: match row.source {
ModelSource::Config => ClientSource::Config as i32,
ModelSource::Database => ClientSource::Database as i32,
},
has_secret: row.secret_hash.is_some(),
created_at: Some(convert::timestamp_from_datetime(row.created_at)),
updated_at: Some(convert::timestamp_from_datetime(row.updated_at)),
}
}
#[tonic::async_trait]
impl ClientService for ClientApi {
async fn list_clients(
&self,
request: Request<ListClientsRequest>,
) -> Result<Response<ListClientsResponse>, Status> {
let meta = context::meta(&request).await?;
let timer = crate::oltp::metrics::ActiveRpcGuard::new("auth.v1.ClientService");
let result = self
.list_clients_inner(meta.request_id(), request.into_inner())
.await;
timer.finish("ListClients", &result);
result
}
async fn get_client(
&self,
request: Request<GetClientRequest>,
) -> Result<Response<GetClientResponse>, Status> {
let meta = context::meta(&request).await?;
let timer = crate::oltp::metrics::ActiveRpcGuard::new("auth.v1.ClientService");
let result = self
.get_client_inner(meta.request_id(), request.into_inner())
.await;
timer.finish("GetClient", &result);
result
}
async fn upsert_client(
&self,
request: Request<UpsertClientRequest>,
) -> Result<Response<UpsertClientResponse>, Status> {
let meta = context::meta(&request).await?;
let timer = crate::oltp::metrics::ActiveRpcGuard::new("auth.v1.ClientService");
let result = self
.upsert_client_inner(meta.request_id(), request.into_inner())
.await;
timer.finish("UpsertClient", &result);
result
}
async fn delete_client(
&self,
request: Request<DeleteClientRequest>,
) -> Result<Response<DeleteClientResponse>, Status> {
let meta = context::meta(&request).await?;
let timer = crate::oltp::metrics::ActiveRpcGuard::new("auth.v1.ClientService");
let result = self
.delete_client_inner(meta.request_id(), request.into_inner())
.await;
timer.finish("DeleteClient", &result);
result
}
}
impl ClientApi {
#[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
async fn list_clients_inner(
&self,
request_id: Option<&str>,
request: ListClientsRequest,
) -> Result<Response<ListClientsResponse>, Status> {
let enabled_only = request.enabled_only;
let rows = ClientEntity::find()
.all(self.svc.db().read())
.await
.map_err(|error| error::status(&AuthError::from(error), request_id))?;
let clients: Vec<OAuthClientRecord> = rows
.iter()
.filter(|row| !enabled_only || row.enabled)
.map(record_of)
.collect();
Ok(Response::new(ListClientsResponse { clients }))
}
#[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
async fn get_client_inner(
&self,
request_id: Option<&str>,
request: GetClientRequest,
) -> Result<Response<GetClientResponse>, Status> {
let client_id = request.client_id;
let rows = ClientEntity::find()
.all(self.svc.db().read())
.await
.map_err(|error| error::status(&AuthError::from(error), request_id))?;
let Some(row) = rows.iter().find(|row| row.client_id == client_id) else {
return Err(error::status(&AuthError::AppNotFound, request_id));
};
Ok(Response::new(GetClientResponse {
client: Some(record_of(row)),
}))
}
#[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
async fn upsert_client_inner(
&self,
request_id: Option<&str>,
request: UpsertClientRequest,
) -> Result<Response<UpsertClientResponse>, Status> {
let body = request;
let kind = kind_of(body.kind)?;
let secret = (!body.secret.is_empty()).then_some(body.secret.as_str());
let current = self.svc.clients().get(&body.client_id);
let secret_hash = current
.as_ref()
.and_then(|record| record.secret_hash.clone());
// enabled 未显式给出:新建按启用、覆盖时保持原状态。
let enabled = body.enabled.unwrap_or_else(|| {
current
.as_ref()
.map(|record| record.enabled)
.unwrap_or(true)
});
let record = ClientRecord::from_spec(
ClientSpec {
client_id: &body.client_id,
name: &body.name,
kind,
audience: (!body.audience.is_empty()).then_some(body.audience.as_str()),
redirect_uris: body.redirect_uris,
post_logout_redirect_uris: body.post_logout_redirect_uris,
backchannel_logout_uris: body.backchannel_logout_uris,
scopes: body.scopes,
// 服务令牌 scope 是配置面概念:管理面写入保留原值,新建置空。
service_scopes: current
.as_ref()
.map(|record| record.service_scopes.clone())
.unwrap_or_default(),
source: ModelSource::Database,
enabled,
secret,
secret_hash,
},
self.svc.auth().oauth.allow_wildcard_redirect,
)
.map_err(|error| error::status(&error, request_id))?;
self.svc
.clients()
.upsert(self.svc.db(), &record)
.await
.map_err(|error| error::status(&error, request_id))?;
let rows = ClientEntity::find()
.all(self.svc.db().read())
.await
.map_err(|error| error::status(&AuthError::from(error), request_id))?;
let row = rows
.iter()
.find(|row| row.client_id == record.client_id)
.map(record_of);
Ok(Response::new(UpsertClientResponse {
client: row,
secret_rotated: secret.is_some(),
}))
}
#[tracing::instrument(skip_all, fields(request_id = %request_id.unwrap_or_default()))]
async fn delete_client_inner(
&self,
request_id: Option<&str>,
request: DeleteClientRequest,
) -> Result<Response<DeleteClientResponse>, Status> {
let client_id = request.client_id;
// 停用的客户端不在内存注册表里,删除前先按库里的行判来源。
let rows = ClientEntity::find()
.all(self.svc.db().read())
.await
.map_err(|error| error::status(&AuthError::from(error), request_id))?;
let Some(row) = rows.iter().find(|row| row.client_id == client_id) else {
return Err(error::status(&AuthError::AppNotFound, request_id));
};
let config_sourced = row.source == ModelSource::Config;
self.svc
.clients()
.delete(self.svc.db(), &client_id)
.await
.map_err(|error| error::status(&error, request_id))?;
Ok(Response::new(DeleteClientResponse {
removed: !config_sourced,
disabled: config_sourced,
}))
}
}