mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
feat: add http auth boundary
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -18,6 +18,7 @@ Update this file before each feature commit.
|
||||
| Translation | Completed | `eff182e` | `cargo test chat_completion_translates_to_codex_response_request`; full `fmt/test/clippy` before commit | Adds OpenAI Chat request types, Codex Responses request type, translation, and OpenAI-compatible error body helper. |
|
||||
| HTTP routes | Partial | `ab0ad8f` + `3830b21` | `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test models_route_returns_openai_compatible_codex_model_list`; full `fmt/test/clippy` before commit | Adds Axum router, health route, `/v1/models`, `/v1` unauthorized OpenAI error body, global request-id response header, and binary listener. Still missing admin auth middleware extraction, account/auth/API-key management routes, settings routes, diagnostics, usage stats, model catalog/detail/refresh routes, and additional Responses routes. |
|
||||
| Admin/frontend response contract | Completed | `1c34ea7` + `b0c10f2` | `cargo test admin_envelope_serializes_lower_camel_case_request_id`; `cargo test admin_logs_reject_missing_admin_session_cookie`; `cargo test v1_response_has_request_id_header_without_admin_body_field`; full `fmt/test/clippy` before commit | Adds reusable admin envelopes with body codes, lower camelCase JSON, paginated list shape, requestId bodies, admin session cookie enforcement, and keeps `/v1/*` OpenAI-compatible. |
|
||||
| HTTP auth boundary | Completed | pending | `cargo test --test http_auth_test`; `cargo test --test routes_responses_test`; `cargo test --test admin_logs_route_test`; full `fmt/test/clippy` before commit | Adds a real `src/http/auth.rs` boundary for client API key and admin session cookie parsing. `/v1/*` accepts only `Bearer cpr_...`, admin session parsing ignores client API keys, and route handlers no longer own ad hoc credential parsing. |
|
||||
| Upstream lifecycle | Partial | `88cad87` + `225fbff` | `cargo test responses_route_rejects_non_codex_provider_models`; `cargo test v1_requires_client_api_key_not_admin_cookie`; `cargo test --test codex_sse_usage_test`; full `fmt/test/clippy` before commit | `/v1` currently enforces client API key shape, rejects non-Codex provider models, returns OpenAI-compatible errors, and includes reusable SSE/usage helpers. It does not yet acquire accounts, forward to Codex, replay/capture Cookies, stream/collect upstream responses, capture rate-limit headers, retry across accounts, record usage, or preserve previous-response/session affinity. |
|
||||
| Fingerprint updates | Partial | `dd26d31` + `e32220a` | `cargo test update_manifest_updates_app_version_and_build_number`; `cargo test fingerprint_updater_fetches_manifest_and_persists_history`; full `fmt/test/clippy` before commit | Parses a JSON update manifest and persists app version/build number history to SQLite. Still missing original appcast XML polling, update-state persistence, runtime header fingerprint selection from the latest stored fingerprint, and optional local Codex.app Chromium extraction. |
|
||||
| Runtime docs and packaging | Completed | `33b63f3` | `cargo fmt --check`; `cargo clippy --all-targets --all-features --locked -- -D warnings`; `cargo test`; `cargo run`; `curl -s -i http://127.0.0.1:8080/health` | README scope, `.env.example`, Dockerfile, compose file, and health smoke test are complete. |
|
||||
|
||||
@@ -2956,6 +2956,7 @@ git commit -m "docs: document codex-proxy-rs scope"
|
||||
- [x] Replace the refresh policy shell with a native refresh scheduler. It refreshes before expiry, refreshes immediately after an upstream 401 trigger, preserves the existing refresh token when the refresh response omits one, maps refresh failures to account statuses, and enforces configured concurrency with a semaphore.
|
||||
- [x] Fill the previously empty `src/codex/sse.rs` and `src/codex/usage.rs` modules with tested SSE frame parsing/encoding and token usage extraction helpers.
|
||||
- [x] Add a tested SQLite account repository foundation. It encrypts access/refresh tokens, decrypts account reads, cursor-pages accounts, updates status/labels without rewriting token ciphertext, persists usage counters, and preserves the existing refresh token when token refresh responses omit a new one.
|
||||
- [x] Add a real HTTP auth boundary. `src/http/auth.rs` parses client API keys and admin session cookies as separate credential types, `/v1/*` uses only `Bearer cpr_...`, and admin session parsing ignores client API keys.
|
||||
|
||||
## 2026-06-11 Original Project Gap Audit
|
||||
|
||||
|
||||
+2
-6
@@ -14,7 +14,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::admin_session::verify_admin_password,
|
||||
http::middleware::RequestId,
|
||||
http::{auth::admin_session_id, middleware::RequestId},
|
||||
pagination::{clamp_limit, Page},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -348,9 +348,5 @@ async fn validate_admin_session(
|
||||
}
|
||||
|
||||
fn admin_session_cookie(headers: &HeaderMap) -> Option<&str> {
|
||||
let cookie = headers.get("cookie")?.to_str().ok()?;
|
||||
cookie.split(';').map(str::trim).find_map(|part| {
|
||||
let (name, value) = part.split_once('=')?;
|
||||
(name == "cpr_admin_session" && !value.is_empty()).then_some(value)
|
||||
})
|
||||
admin_session_id(headers)
|
||||
}
|
||||
|
||||
@@ -1 +1,24 @@
|
||||
use axum::http::HeaderMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ClientApiKey<'a>(&'a str);
|
||||
|
||||
impl<'a> ClientApiKey<'a> {
|
||||
pub fn as_str(self) -> &'a str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_api_key(headers: &HeaderMap) -> Option<ClientApiKey<'_>> {
|
||||
let auth = headers.get("authorization")?.to_str().ok()?.trim();
|
||||
let token = auth.strip_prefix("Bearer ")?.trim();
|
||||
token.starts_with("cpr_").then_some(ClientApiKey(token))
|
||||
}
|
||||
|
||||
pub fn admin_session_id(headers: &HeaderMap) -> Option<&str> {
|
||||
let cookie = headers.get("cookie")?.to_str().ok()?;
|
||||
cookie.split(';').map(str::trim).find_map(|part| {
|
||||
let (name, value) = part.split_once('=')?;
|
||||
(name == "cpr_admin_session" && !value.is_empty()).then_some(value)
|
||||
})
|
||||
}
|
||||
|
||||
+2
-6
@@ -7,7 +7,7 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::translation::codex_to_openai::openai_error;
|
||||
use crate::{http::auth::client_api_key, translation::codex_to_openai::openai_error};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ResponsesBody {
|
||||
@@ -67,9 +67,5 @@ pub async fn models(headers: HeaderMap) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
fn has_client_api_key(headers: &HeaderMap) -> bool {
|
||||
let auth = headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
auth.starts_with("Bearer cpr_")
|
||||
client_api_key(headers).is_some()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use axum::http::{HeaderMap, HeaderValue};
|
||||
|
||||
use codex_proxy_rs::http::auth::{admin_session_id, client_api_key};
|
||||
|
||||
#[test]
|
||||
fn client_api_key_should_accept_only_cpr_bearer_tokens() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("Bearer cpr_local_client_key"),
|
||||
);
|
||||
assert_eq!(
|
||||
client_api_key(&headers).unwrap().as_str(),
|
||||
"cpr_local_client_key"
|
||||
);
|
||||
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("Bearer sk-official-openai-key"),
|
||||
);
|
||||
assert!(client_api_key(&headers).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_session_id_should_ignore_client_api_key_and_extract_only_cookie() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("Bearer cpr_local_client_key"),
|
||||
);
|
||||
assert!(admin_session_id(&headers).is_none());
|
||||
|
||||
headers.insert(
|
||||
"cookie",
|
||||
HeaderValue::from_static("theme=dark; cpr_admin_session=sess_123; other=1"),
|
||||
);
|
||||
assert_eq!(admin_session_id(&headers), Some("sess_123"));
|
||||
}
|
||||
Reference in New Issue
Block a user