mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: add admin api key install sessions
This commit is contained in:
@@ -158,9 +158,21 @@ pub(super) fn classify_admin_observability_family_route(
|
|||||||
"admin:api_keys",
|
"admin:api_keys",
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
|
} else if method == http::Method::POST
|
||||||
|
&& normalized_path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||||
|
&& normalized_path_no_trailing.ends_with("/install-sessions")
|
||||||
|
&& normalized_path_no_trailing.matches('/').count() == 5
|
||||||
|
{
|
||||||
|
Some(classified(
|
||||||
|
"admin_proxy",
|
||||||
|
"api_keys_manage",
|
||||||
|
"create_api_key_install_session",
|
||||||
|
"admin:api_keys",
|
||||||
|
false,
|
||||||
|
))
|
||||||
} else if method == http::Method::GET
|
} else if method == http::Method::GET
|
||||||
&& normalized_path.starts_with("/api/admin/api-keys/")
|
&& normalized_path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||||
&& normalized_path.matches('/').count() == 4
|
&& normalized_path_no_trailing.matches('/').count() == 4
|
||||||
{
|
{
|
||||||
Some(classified(
|
Some(classified(
|
||||||
"admin_proxy",
|
"admin_proxy",
|
||||||
@@ -170,8 +182,8 @@ pub(super) fn classify_admin_observability_family_route(
|
|||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
} else if method == http::Method::PUT
|
} else if method == http::Method::PUT
|
||||||
&& normalized_path.starts_with("/api/admin/api-keys/")
|
&& normalized_path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||||
&& normalized_path.matches('/').count() == 4
|
&& normalized_path_no_trailing.matches('/').count() == 4
|
||||||
{
|
{
|
||||||
Some(classified(
|
Some(classified(
|
||||||
"admin_proxy",
|
"admin_proxy",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use http::Uri;
|
use http::Uri;
|
||||||
|
|
||||||
|
use crate::control::GatewayPublicRequestContext;
|
||||||
|
use crate::handlers::shared::local_proxy_route_requires_buffered_body;
|
||||||
|
|
||||||
use super::{classify_control_route, headers};
|
use super::{classify_control_route, headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -38,6 +41,50 @@ fn classifies_admin_api_keys_create_as_admin_proxy_route() {
|
|||||||
assert!(!decision.is_execution_runtime_candidate());
|
assert!(!decision.is_execution_runtime_candidate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_admin_api_key_install_session_create_as_admin_proxy_route() {
|
||||||
|
let headers = headers(&[]);
|
||||||
|
for path in [
|
||||||
|
"/api/admin/api-keys/key-123/install-sessions",
|
||||||
|
"/api/admin/api-keys/key-123/install-sessions/",
|
||||||
|
] {
|
||||||
|
let uri: Uri = path.parse().expect("uri should parse");
|
||||||
|
let decision = classify_control_route(&http::Method::POST, &uri, &headers)
|
||||||
|
.expect("route should classify");
|
||||||
|
|
||||||
|
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
|
||||||
|
assert_eq!(decision.route_family.as_deref(), Some("api_keys_manage"));
|
||||||
|
assert_eq!(
|
||||||
|
decision.route_kind.as_deref(),
|
||||||
|
Some("create_api_key_install_session")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decision.auth_endpoint_signature.as_deref(),
|
||||||
|
Some("admin:api_keys")
|
||||||
|
);
|
||||||
|
assert!(!decision.is_execution_runtime_candidate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_api_key_install_session_create_buffers_request_body() {
|
||||||
|
let headers = headers(&[]);
|
||||||
|
let uri: Uri = "/api/admin/api-keys/key-123/install-sessions"
|
||||||
|
.parse()
|
||||||
|
.expect("uri should parse");
|
||||||
|
let decision =
|
||||||
|
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||||
|
let context = GatewayPublicRequestContext::from_request_parts(
|
||||||
|
"trace-admin-install-session",
|
||||||
|
&http::Method::POST,
|
||||||
|
&uri,
|
||||||
|
&headers,
|
||||||
|
Some(decision),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(local_proxy_route_requires_buffered_body(&context));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_admin_api_keys_detail_as_admin_proxy_route() {
|
fn classifies_admin_api_keys_detail_as_admin_proxy_route() {
|
||||||
let headers = headers(&[]);
|
let headers = headers(&[]);
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use super::shared::{
|
||||||
|
admin_api_key_install_session_id_from_path, build_admin_api_keys_bad_request_response,
|
||||||
|
build_admin_api_keys_data_unavailable_response, build_admin_api_keys_not_found_response,
|
||||||
|
};
|
||||||
|
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||||
|
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||||
|
use crate::handlers::public::{
|
||||||
|
build_api_key_install_session_response, CreateApiKeyInstallSessionRequest,
|
||||||
|
};
|
||||||
|
use crate::GatewayError;
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) async fn build_admin_create_api_key_install_session_response(
|
||||||
|
state: &AdminAppState<'_>,
|
||||||
|
request_context: &AdminRequestContext<'_>,
|
||||||
|
request_headers: &http::HeaderMap,
|
||||||
|
request_body: Option<&axum::body::Bytes>,
|
||||||
|
) -> Result<Response<Body>, GatewayError> {
|
||||||
|
if !state.has_auth_api_key_data_reader() {
|
||||||
|
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(api_key_id) = admin_api_key_install_session_id_from_path(request_context.path())
|
||||||
|
else {
|
||||||
|
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||||
|
};
|
||||||
|
let Some(request_body) = request_body else {
|
||||||
|
return Ok(build_admin_api_keys_bad_request_response(
|
||||||
|
"请求数据验证失败",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let payload = match serde_json::from_slice::<CreateApiKeyInstallSessionRequest>(request_body) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => {
|
||||||
|
return Ok(build_admin_api_keys_bad_request_response(
|
||||||
|
"请求数据验证失败",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(record) = state
|
||||||
|
.find_auth_api_key_export_standalone_record_by_id(&api_key_id)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(build_admin_api_keys_not_found_response());
|
||||||
|
};
|
||||||
|
let Some(ciphertext) = record
|
||||||
|
.key_encrypted
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
else {
|
||||||
|
return Ok(build_admin_api_keys_bad_request_response(
|
||||||
|
"该密钥没有存储完整密钥信息",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Some(api_key) = state.decrypt_catalog_secret_with_fallbacks(ciphertext) else {
|
||||||
|
return Ok((
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
axum::Json(serde_json::json!({ "detail": "解密密钥失败" })),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = build_api_key_install_session_response(
|
||||||
|
state.app(),
|
||||||
|
request_context.public(),
|
||||||
|
request_headers,
|
||||||
|
record.api_key_id.clone(),
|
||||||
|
record.name.unwrap_or_else(|| "API Key".to_string()),
|
||||||
|
api_key,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(attach_admin_audit_response(
|
||||||
|
response,
|
||||||
|
"admin_standalone_api_key_install_session_created",
|
||||||
|
"create_standalone_api_key_install_session",
|
||||||
|
"api_key",
|
||||||
|
&api_key_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -18,11 +18,13 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
mod install_routes;
|
||||||
mod mutation_routes;
|
mod mutation_routes;
|
||||||
mod read_routes;
|
mod read_routes;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod shared;
|
mod shared;
|
||||||
|
|
||||||
|
use self::install_routes::build_admin_create_api_key_install_session_response;
|
||||||
use self::mutation_routes::{
|
use self::mutation_routes::{
|
||||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||||
@@ -39,8 +41,14 @@ use self::shared::{
|
|||||||
pub(crate) async fn maybe_build_local_admin_api_keys_response(
|
pub(crate) async fn maybe_build_local_admin_api_keys_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
|
request_headers: &http::HeaderMap,
|
||||||
request_body: Option<&axum::body::Bytes>,
|
request_body: Option<&axum::body::Bytes>,
|
||||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||||
routes::maybe_build_local_admin_api_keys_routes_response(state, request_context, request_body)
|
routes::maybe_build_local_admin_api_keys_routes_response(
|
||||||
.await
|
state,
|
||||||
|
request_context,
|
||||||
|
request_headers,
|
||||||
|
request_body,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::install_routes::build_admin_create_api_key_install_session_response;
|
||||||
use super::mutation_routes::{
|
use super::mutation_routes::{
|
||||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||||
@@ -11,6 +12,7 @@ use axum::{body::Body, http, response::Response};
|
|||||||
pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
|
request_headers: &http::HeaderMap,
|
||||||
request_body: Option<&axum::body::Bytes>,
|
request_body: Option<&axum::body::Bytes>,
|
||||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||||
let Some(decision) = request_context.decision() else {
|
let Some(decision) = request_context.decision() else {
|
||||||
@@ -22,8 +24,10 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let path = request_context.path();
|
let path = request_context.path();
|
||||||
|
let path_no_trailing = path.trim_end_matches('/');
|
||||||
let is_api_keys_route = matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/")
|
let is_api_keys_route = matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/")
|
||||||
|| (path.starts_with("/api/admin/api-keys/") && path.matches('/').count() == 4);
|
|| (path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||||
|
&& matches!(path_no_trailing.matches('/').count(), 4 | 5));
|
||||||
|
|
||||||
if !is_api_keys_route {
|
if !is_api_keys_route {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -54,6 +58,21 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
|||||||
build_admin_create_api_key_response(state, request_context, request_body).await?,
|
build_admin_create_api_key_response(state, request_context, request_body).await?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
Some("create_api_key_install_session")
|
||||||
|
if request_context.method() == http::Method::POST
|
||||||
|
&& path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||||
|
&& path_no_trailing.ends_with("/install-sessions") =>
|
||||||
|
{
|
||||||
|
Ok(Some(
|
||||||
|
build_admin_create_api_key_install_session_response(
|
||||||
|
state,
|
||||||
|
request_context,
|
||||||
|
request_headers,
|
||||||
|
request_body,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
Some("update_api_key")
|
Some("update_api_key")
|
||||||
if request_context.method() == http::Method::PUT
|
if request_context.method() == http::Method::PUT
|
||||||
&& path.starts_with("/api/admin/api-keys/") =>
|
&& path.starts_with("/api/admin/api-keys/") =>
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ pub(super) fn admin_api_keys_id_from_path(request_path: &str) -> Option<String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn admin_api_key_install_session_id_from_path(request_path: &str) -> Option<String> {
|
||||||
|
let raw = request_path
|
||||||
|
.strip_prefix("/api/admin/api-keys/")?
|
||||||
|
.trim()
|
||||||
|
.trim_matches('/');
|
||||||
|
let mut segments = raw.split('/').map(str::trim);
|
||||||
|
let api_key_id = segments.next()?.to_string();
|
||||||
|
let suffix = segments.next()?;
|
||||||
|
(suffix == "install-sessions" && segments.next().is_none()).then_some(api_key_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn admin_api_keys_operator_id(
|
pub(super) fn admin_api_keys_operator_id(
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub(crate) async fn maybe_build_local_admin_auth_response(
|
|||||||
if let Some(response) = api_keys::maybe_build_local_admin_api_keys_response(
|
if let Some(response) = api_keys::maybe_build_local_admin_api_keys_response(
|
||||||
&request.state(),
|
&request.state(),
|
||||||
&request.request_context(),
|
&request.request_context(),
|
||||||
|
request.request_headers(),
|
||||||
request.request_body(),
|
request.request_body(),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{AdminAppState, AdminRequestContext};
|
use super::{AdminAppState, AdminRequestContext};
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
use axum::body::{Body, Bytes};
|
use axum::body::{Body, Bytes};
|
||||||
use axum::http::Response;
|
use axum::http::{HeaderMap, Response};
|
||||||
|
|
||||||
pub(crate) enum AdminCancelVideoTaskError {
|
pub(crate) enum AdminCancelVideoTaskError {
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -14,6 +14,7 @@ pub(crate) enum AdminCancelVideoTaskError {
|
|||||||
pub(crate) struct AdminRouteRequest<'a> {
|
pub(crate) struct AdminRouteRequest<'a> {
|
||||||
state: AdminAppState<'a>,
|
state: AdminAppState<'a>,
|
||||||
request_context: AdminRequestContext<'a>,
|
request_context: AdminRequestContext<'a>,
|
||||||
|
request_headers: &'a HeaderMap,
|
||||||
request_body: Option<&'a Bytes>,
|
request_body: Option<&'a Bytes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +22,13 @@ impl<'a> AdminRouteRequest<'a> {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
state: &'a AppState,
|
state: &'a AppState,
|
||||||
request_context: &'a crate::control::GatewayPublicRequestContext,
|
request_context: &'a crate::control::GatewayPublicRequestContext,
|
||||||
|
request_headers: &'a HeaderMap,
|
||||||
request_body: Option<&'a Bytes>,
|
request_body: Option<&'a Bytes>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: AdminAppState::new(state),
|
state: AdminAppState::new(state),
|
||||||
request_context: AdminRequestContext::new(request_context),
|
request_context: AdminRequestContext::new(request_context),
|
||||||
|
request_headers,
|
||||||
request_body,
|
request_body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +41,10 @@ impl<'a> AdminRouteRequest<'a> {
|
|||||||
self.request_context
|
self.request_context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request_headers(self) -> &'a HeaderMap {
|
||||||
|
self.request_headers
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn request_body(self) -> Option<&'a Bytes> {
|
pub(crate) fn request_body(self) -> Option<&'a Bytes> {
|
||||||
self.request_body
|
self.request_body
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub(super) async fn maybe_build_local_internal_proxy_response(
|
|||||||
pub(super) async fn maybe_build_local_admin_proxy_response(
|
pub(super) async fn maybe_build_local_admin_proxy_response(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
request_context: &GatewayPublicRequestContext,
|
request_context: &GatewayPublicRequestContext,
|
||||||
|
request_headers: &http::HeaderMap,
|
||||||
request_body: Option<&Bytes>,
|
request_body: Option<&Bytes>,
|
||||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||||
@@ -49,6 +50,7 @@ pub(super) async fn maybe_build_local_admin_proxy_response(
|
|||||||
admin_api::maybe_build_local_admin_response(admin_api::AdminRouteRequest::new(
|
admin_api::maybe_build_local_admin_response(admin_api::AdminRouteRequest::new(
|
||||||
state,
|
state,
|
||||||
request_context,
|
request_context,
|
||||||
|
request_headers,
|
||||||
request_body,
|
request_body,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -874,9 +874,13 @@ pub(crate) async fn proxy_request(
|
|||||||
request_permit.take(),
|
request_permit.take(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(response) =
|
if let Some(response) = maybe_build_local_admin_proxy_response(
|
||||||
maybe_build_local_admin_proxy_response(&state, &request_context, local_proxy_body.as_ref())
|
&state,
|
||||||
.await?
|
&request_context,
|
||||||
|
&parts.headers,
|
||||||
|
local_proxy_body.as_ref(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
{
|
{
|
||||||
let execution_path =
|
let execution_path =
|
||||||
resolve_local_proxy_execution_path(&response, EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH);
|
resolve_local_proxy_execution_path(&response, EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH);
|
||||||
|
|||||||
@@ -307,7 +307,11 @@ pub(crate) fn admin_proxy_local_requires_buffered_body(
|
|||||||
| (Some("payments_manage"), http::Method::POST, Some("credit_order"))
|
| (Some("payments_manage"), http::Method::POST, Some("credit_order"))
|
||||||
| (Some("payments_manage"), http::Method::POST, Some("create_redeem_code_batch"))
|
| (Some("payments_manage"), http::Method::POST, Some("create_redeem_code_batch"))
|
||||||
| (Some("payments_manage"), http::Method::POST, Some("delete_redeem_code_batch"))
|
| (Some("payments_manage"), http::Method::POST, Some("delete_redeem_code_batch"))
|
||||||
| (Some("api_keys_manage"), http::Method::POST, Some("create_api_key"))
|
| (
|
||||||
|
Some("api_keys_manage"),
|
||||||
|
http::Method::POST,
|
||||||
|
Some("create_api_key" | "create_api_key_install_session"),
|
||||||
|
)
|
||||||
| (Some("api_keys_manage"), http::Method::PUT, Some("update_api_key"))
|
| (Some("api_keys_manage"), http::Method::PUT, Some("update_api_key"))
|
||||||
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
|
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
|
||||||
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
|
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
|
||||||
|
|||||||
@@ -228,8 +228,13 @@ async fn gateway_handles_admin_api_keys_list_locally_with_trusted_admin_principa
|
|||||||
.await
|
.await
|
||||||
.expect("request should succeed");
|
.expect("request should succeed");
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
let status = response.status();
|
||||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.expect("response body should be readable");
|
||||||
|
assert_eq!(status, StatusCode::OK, "unexpected response body: {body}");
|
||||||
|
let payload: serde_json::Value = serde_json::from_str(&body).expect("json body should parse");
|
||||||
assert_eq!(payload["total"], json!(1));
|
assert_eq!(payload["total"], json!(1));
|
||||||
assert_eq!(payload["limit"], json!(10));
|
assert_eq!(payload["limit"], json!(10));
|
||||||
assert_eq!(payload["skip"], json!(0));
|
assert_eq!(payload["skip"], json!(0));
|
||||||
@@ -300,8 +305,13 @@ async fn gateway_handles_admin_api_keys_detail_locally_with_trusted_admin_princi
|
|||||||
.await
|
.await
|
||||||
.expect("request should succeed");
|
.expect("request should succeed");
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
let status = response.status();
|
||||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.expect("response body should be readable");
|
||||||
|
assert_eq!(status, StatusCode::OK, "unexpected response body: {body}");
|
||||||
|
let payload: serde_json::Value = serde_json::from_str(&body).expect("json body should parse");
|
||||||
assert_eq!(payload["id"], json!("key-1"));
|
assert_eq!(payload["id"], json!("key-1"));
|
||||||
assert_eq!(payload["user_id"], json!("user-1"));
|
assert_eq!(payload["user_id"], json!("user-1"));
|
||||||
assert_eq!(payload["wallet"]["id"], json!("wallet-key-1"));
|
assert_eq!(payload["wallet"]["id"], json!("wallet-key-1"));
|
||||||
@@ -412,6 +422,78 @@ async fn gateway_handles_admin_api_keys_full_key_locally_with_trusted_admin_prin
|
|||||||
upstream_handle.abort();
|
upstream_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gateway_handles_admin_api_key_install_session_locally_with_trusted_admin_principal() {
|
||||||
|
let (upstream_url, upstream_hits, upstream_handle) =
|
||||||
|
start_api_keys_upstream("/api/admin/api-keys/key-1/install-sessions").await;
|
||||||
|
let auth_repository = Arc::new(
|
||||||
|
InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||||
|
None,
|
||||||
|
sample_standalone_api_key_snapshot("key-1", "user-1", true),
|
||||||
|
)])
|
||||||
|
.with_export_records([sample_standalone_export_record(
|
||||||
|
"key-1",
|
||||||
|
"user-1",
|
||||||
|
"sk-key-1-plaintext",
|
||||||
|
true,
|
||||||
|
)]),
|
||||||
|
);
|
||||||
|
let gateway = build_router_with_state(
|
||||||
|
AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_data_state_for_tests(
|
||||||
|
GatewayDataState::with_auth_api_key_repository_for_tests(auth_repository)
|
||||||
|
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
|
|
||||||
|
let response = admin_request(reqwest::Client::new().post(format!(
|
||||||
|
"{gateway_url}/api/admin/api-keys/key-1/install-sessions/"
|
||||||
|
)))
|
||||||
|
.header("x-forwarded-host", "aether.example")
|
||||||
|
.header("x-forwarded-proto", "https")
|
||||||
|
.json(&json!({
|
||||||
|
"target_cli": "codex_cli",
|
||||||
|
"target_system": "linux",
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.expect("response body should be readable");
|
||||||
|
assert_eq!(status, StatusCode::OK, "unexpected response body: {body}");
|
||||||
|
let payload: serde_json::Value = serde_json::from_str(&body).expect("json body should parse");
|
||||||
|
let install_code = payload["install_code"]
|
||||||
|
.as_str()
|
||||||
|
.expect("install code should be returned");
|
||||||
|
assert_eq!(install_code.len(), 24);
|
||||||
|
assert_eq!(payload["expires_in_seconds"], json!(15 * 60));
|
||||||
|
assert_eq!(payload["target_cli"], json!("codex_cli"));
|
||||||
|
assert_eq!(payload["target_system"], json!("linux"));
|
||||||
|
assert!(payload["expires_at_unix_secs"].is_number());
|
||||||
|
assert_eq!(
|
||||||
|
payload["unix_command"],
|
||||||
|
json!(format!(
|
||||||
|
"curl -fsSL https://aether.example/install/{install_code} | sh"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["powershell_command"],
|
||||||
|
json!(format!(
|
||||||
|
"irm https://aether.example/install/{install_code}.ps1 | iex"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
|
upstream_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_handles_admin_api_keys_create_locally_with_trusted_admin_principal() {
|
async fn gateway_handles_admin_api_keys_create_locally_with_trusted_admin_principal() {
|
||||||
let (upstream_url, upstream_hits, upstream_handle) =
|
let (upstream_url, upstream_hits, upstream_handle) =
|
||||||
|
|||||||
Reference in New Issue
Block a user