Files
codex/codex-rs/workload-identity/src/lib.rs
T
acrognale-oai 22a3f6d5d8 Enforce application network policy across app-server requests (#47407)
## What changed

- Load `application.network` requirements at startup and explicit config/account reloads, applying local policy before authentication and cloud configuration bootstrap.
- Block traffic when policy loading fails, cancel requests that a new policy no longer permits, and reject configuration loads based on superseded policy snapshots. Local requirements edits take effect on the next explicit reload or restart.
- Bind authenticated clients to the current account and revoke them when account ownership changes. Carry policy enforcement through backend requests, realtime connections, workload identity exchanges, and code-mode gRPC streams while preserving gRPC trailers.
- Treat denied credential refreshes as policy errors without retrying them or invalidating stored credentials.

## Testing

Add regression coverage for startup restrictions, reload ordering, cancellation after restrictive or malformed requirements edits, account revocation, gRPC destination denial, workload identity cancellation, and credential preservation after denied OAuth refresh.

GitOrigin-RevId: 0cb4c720a906397e9df06675f01ecf60d741101f
2026-09-23 00:47:07 +00:00

94 lines
3.3 KiB
Rust

mod assertion;
mod exchange;
use std::path::PathBuf;
use std::sync::Arc;
pub use exchange::WorkloadIdentityExchange;
pub use exchange::WorkloadIdentityToken;
use thiserror::Error;
/// The inputs needed to exchange a file-backed assertion for ChatGPT auth.
#[derive(Clone)]
pub struct WorkloadIdentityConfig {
pub(crate) assertion_file: PathBuf,
pub(crate) federation_rule_id: String,
pub(crate) workload_identity_context: Option<String>,
}
impl WorkloadIdentityConfig {
pub fn new(
federation_rule_id: String,
assertion_file: PathBuf,
workload_identity_context: Option<String>,
) -> Result<Self, WorkloadIdentityError> {
let federation_rule_id = federation_rule_id.trim();
if federation_rule_id.is_empty() {
return Err(WorkloadIdentityError::InvalidFederationRuleId);
}
if !assertion_file.is_absolute() {
return Err(WorkloadIdentityError::AssertionFileMustBeAbsolute);
}
Ok(Self {
assertion_file,
federation_rule_id: federation_rule_id.to_string(),
workload_identity_context,
})
}
}
#[derive(Clone, Debug, Error)]
pub enum WorkloadIdentityError {
#[error(transparent)]
Policy(#[from] codex_http_client::NetworkPolicyDenied),
#[error("the workload identity federation rule ID must not be empty")]
InvalidFederationRuleId,
#[error("the workload identity assertion file path must be absolute")]
AssertionFileMustBeAbsolute,
#[error("the workload identity assertion is invalid")]
InvalidAssertion,
#[error("the workload identity assertion exceeds 16 KiB")]
AssertionTooLarge,
#[error("could not read workload identity assertion file {path}")]
AssertionFile {
path: PathBuf,
#[source]
source: Arc<std::io::Error>,
},
#[error("could not configure the workload identity HTTP client")]
HttpClientConfiguration,
#[error("the workload identity token URL must use HTTPS or loopback HTTP")]
InvalidTokenUrl,
#[error("the workload identity token exchange is unavailable")]
ExchangeUnavailable,
#[error("the workload identity token exchange was rejected with HTTP {0}")]
ExchangeRejected(u16),
#[error("the workload identity token exchange returned an invalid response")]
InvalidExchangeResponse,
}
impl WorkloadIdentityError {
/// Whether retrying the operation may succeed without changing configuration.
pub fn is_transient(&self) -> bool {
match self {
Self::AssertionFile { source, .. } => matches!(
source.kind(),
std::io::ErrorKind::Interrupted
| std::io::ErrorKind::NotFound
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::WouldBlock
),
Self::ExchangeUnavailable | Self::ExchangeRejected(408 | 429 | 500..=599) => true,
Self::Policy(_)
| Self::InvalidFederationRuleId
| Self::AssertionFileMustBeAbsolute
| Self::InvalidAssertion
| Self::AssertionTooLarge
| Self::HttpClientConfiguration
| Self::InvalidTokenUrl
| Self::ExchangeRejected(_)
| Self::InvalidExchangeResponse => false,
}
}
}