mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Enforce managed authentication requirements locally (#37132)
## Why Authentication restrictions must apply before stored or environment-provided credentials can be used, including during bootstrap before cloud requirements are fetched. ## What changed - Add local `requirements.toml` allowlists for login methods and ChatGPT workspaces. Ignore these fields in cloud-provided requirements. - Combine managed workspace allowlists with existing workspace restrictions by intersection, and fail closed when the resulting policy permits no usable login method. - Centralize policy checks in the authentication manager so CLI, TUI, app-server, external-auth, and credential-loading paths consistently reject disallowed authentication before token hydration or network requests. ## Testing - Cover policy composition, workspace intersection, invalid stored and external credentials, bootstrap enforcement, and login endpoint restrictions. GitOrigin-RevId: efef22b248f3c3333e9aa55423e539efa2d2dd48
This commit is contained in:
@@ -6,6 +6,7 @@ use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::feature_for_key;
|
||||
@@ -171,21 +172,16 @@ impl ConfigManager {
|
||||
}
|
||||
|
||||
pub(crate) async fn load_default_config(&self) -> std::io::Result<Config> {
|
||||
let mut config = Config::load_default_with_cli_overrides_for_codex_home(
|
||||
self.codex_home.clone(),
|
||||
self.current_cli_overrides(),
|
||||
)
|
||||
.await?;
|
||||
if self.loader_overrides.user_config_path.is_some()
|
||||
|| self.loader_overrides.user_config_profile.is_some()
|
||||
{
|
||||
let user_config_path = self.loader_overrides.user_config_path(self.codex_home())?;
|
||||
config.config_layer_stack = config.config_layer_stack.with_user_config_profile(
|
||||
&user_config_path,
|
||||
self.loader_overrides.user_config_profile.as_ref(),
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
)?;
|
||||
}
|
||||
let mut loader_overrides = self.loader_overrides.clone();
|
||||
loader_overrides.ignore_user_config = true;
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(self.codex_home.clone())
|
||||
.cli_overrides(self.current_cli_overrides())
|
||||
.loader_overrides(loader_overrides)
|
||||
.fallback_cwd(Some(self.codex_home.clone()))
|
||||
.cloud_config_bundle(CloudConfigBundleLoader::default())
|
||||
.build()
|
||||
.await?;
|
||||
config.psp = self.psp;
|
||||
self.apply_runtime_feature_enablement(&mut config);
|
||||
self.apply_arg0_paths(&mut config);
|
||||
|
||||
@@ -671,9 +671,14 @@ async fn write_value_defaults_to_selected_user_config_path() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_default_config_preserves_selected_user_config_path_after_load_error() {
|
||||
async fn load_default_config_preserves_managed_requirements_and_selected_user_config_path() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"gpt-main\"").unwrap();
|
||||
std::fs::write(
|
||||
tmp.path().join("requirements.toml"),
|
||||
"allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"managed-workspace\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let selected_path = tmp.path().join("work.config.toml");
|
||||
std::fs::write(&selected_path, "not valid toml").unwrap();
|
||||
let selected_file =
|
||||
@@ -703,6 +708,61 @@ async fn load_default_config_preserves_selected_user_config_path_after_load_erro
|
||||
config.config_layer_stack.get_user_config_file(),
|
||||
Some(&selected_file)
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.managed_auth_policy(),
|
||||
codex_config::ManagedAuthPolicy {
|
||||
allowed_login_methods: Some(vec![codex_protocol::config_types::ForcedLoginMethod::Api]),
|
||||
allowed_chatgpt_workspaces: Some(vec!["managed-workspace".to_string()]),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_auth_policy_survives_unusable_requirements_file_changes() -> Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
|
||||
let requirements_path = tmp.path().join("requirements.toml");
|
||||
std::fs::write(
|
||||
&requirements_path,
|
||||
"allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"startup\"]\n",
|
||||
)?;
|
||||
let service = ConfigManager::new_for_tests(
|
||||
tmp.path().to_path_buf(),
|
||||
Vec::new(),
|
||||
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")),
|
||||
CloudConfigBundleLoader::default(),
|
||||
);
|
||||
let startup = service.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let auth_manager = codex_login::AuthManager::shared_from_config(
|
||||
&startup, /*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
std::fs::write(
|
||||
&requirements_path,
|
||||
"allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n",
|
||||
)?;
|
||||
for refreshed in [
|
||||
service.load_latest_config(/*fallback_cwd*/ None).await?,
|
||||
service.load_latest_config_for_thread(&startup).await?,
|
||||
] {
|
||||
assert_eq!(refreshed.forced_login_method, None);
|
||||
assert_eq!(refreshed.forced_chatgpt_workspace_id, None);
|
||||
}
|
||||
assert!(
|
||||
auth_manager.is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Api)
|
||||
);
|
||||
assert!(
|
||||
!auth_manager
|
||||
.is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Chatgpt)
|
||||
);
|
||||
assert_eq!(
|
||||
auth_manager.effective_chatgpt_workspaces(),
|
||||
Some(vec!["startup".to_string()])
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -403,6 +403,7 @@ async fn run_outbound_router(
|
||||
}
|
||||
|
||||
async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
|
||||
args.config.auth_config().validate()?;
|
||||
let channel_capacity = args.channel_capacity.max(1);
|
||||
let installation_id = resolve_installation_id(&args.config.codex_home).await?;
|
||||
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
|
||||
|
||||
@@ -542,6 +542,7 @@ pub async fn run_main_with_transport_options(
|
||||
})?
|
||||
}
|
||||
};
|
||||
config.auth_config().validate()?;
|
||||
let code_mode_session_provider: Option<Arc<dyn CodeModeSessionProvider>> =
|
||||
match &runtime_options.code_mode_host_transport {
|
||||
CodeModeHostTransport::Local => None,
|
||||
|
||||
@@ -347,10 +347,10 @@ impl AccountRequestProcessor {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self.config.forced_login_method,
|
||||
Some(ForcedLoginMethod::Chatgpt)
|
||||
) {
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"API key login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
@@ -402,10 +402,10 @@ impl AccountRequestProcessor {
|
||||
if self.auth_manager.is_external_chatgpt_auth_active() {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
if matches!(
|
||||
self.config.forced_login_method,
|
||||
Some(ForcedLoginMethod::Chatgpt)
|
||||
) {
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"Amazon Bedrock login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
@@ -463,7 +463,10 @@ impl AccountRequestProcessor {
|
||||
return Err(self.external_auth_active_error());
|
||||
}
|
||||
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"ChatGPT login is disabled. Use API key login instead.",
|
||||
));
|
||||
@@ -476,7 +479,7 @@ impl AccountRequestProcessor {
|
||||
..LoginServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
oauth_client_id(),
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
self.auth_manager.effective_chatgpt_workspaces(),
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
@@ -739,10 +742,10 @@ impl AccountRequestProcessor {
|
||||
chatgpt_account_id: String,
|
||||
chatgpt_plan_type: Option<String>,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
if matches!(
|
||||
self.config.forced_login_method,
|
||||
Some(ForcedLoginMethod::Api)
|
||||
) {
|
||||
if !self
|
||||
.auth_manager
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"External ChatGPT auth is disabled. Use API key login instead.",
|
||||
));
|
||||
@@ -756,7 +759,7 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(expected_workspaces) = self.config.forced_chatgpt_workspace_id.as_deref()
|
||||
if let Some(expected_workspaces) = self.auth_manager.effective_chatgpt_workspaces()
|
||||
&& !expected_workspaces.contains(&chatgpt_account_id)
|
||||
{
|
||||
return Err(invalid_request(format!(
|
||||
|
||||
@@ -31,3 +31,36 @@ foo = "bar"
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_auth_requirements_fail_closed_for_standalone_app_server() -> Result<()> {
|
||||
for requirements in [
|
||||
"allowed_login_methods = []\n",
|
||||
"allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n",
|
||||
] {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("requirements.toml"), requirements)?;
|
||||
|
||||
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
|
||||
.env("CODEX_HOME", codex_home.path())
|
||||
.env(
|
||||
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
|
||||
codex_home.path().join("managed_config.toml"),
|
||||
)
|
||||
.args(["--listen", "off"])
|
||||
.output()?;
|
||||
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8(output.stderr)?;
|
||||
assert!(
|
||||
stderr.contains("authentication requirements do not permit any usable login method"),
|
||||
"expected managed authentication error in stderr, got: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("using defaults"),
|
||||
"managed authentication requirements must not fall back to defaults"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -356,6 +356,51 @@ async fn logout_account_succeeds_when_config_reload_fails() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_enforces_local_auth_requirements_before_cloud_fetch() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mock_server = MockServer::start().await;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
"allowed_login_methods = [\"api\"]\n",
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.plan_type("enterprise")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123")
|
||||
.account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
mock_server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("recorded requests")
|
||||
.is_empty(),
|
||||
"disallowed ChatGPT auth must not fetch cloud requirements"
|
||||
);
|
||||
|
||||
assert_eq!(read_account(&mut mcp).await?.account, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
use codex_core::config::bootstrap_auth_config;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_bootstrap_auth_route_config;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
@@ -41,28 +40,9 @@ pub(super) async fn bootstrap_cloud_config_bundle(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
||||
auth_route_config,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
+41
-25
@@ -12,7 +12,6 @@ use codex_core::config::Config;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::login_with_access_token;
|
||||
use codex_login::login_with_api_key;
|
||||
@@ -169,15 +168,18 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting browser login flow");
|
||||
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
if !config
|
||||
.auth_config()
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
||||
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
|
||||
match login_with_chatgpt(
|
||||
config.codex_home.to_path_buf(),
|
||||
forced_chatgpt_workspace_id,
|
||||
effective_chatgpt_workspaces,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
@@ -203,7 +205,10 @@ pub async fn run_login_with_api_key(
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting api key login flow");
|
||||
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
|
||||
if !config
|
||||
.auth_config()
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
{
|
||||
eprintln!("{API_KEY_LOGIN_DISABLED_MESSAGE}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -233,17 +238,21 @@ pub async fn run_login_with_access_token(
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting access token login flow");
|
||||
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
if !config
|
||||
.auth_config()
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
eprintln!("{ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let auth_route_config = config.auth_route_config();
|
||||
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
|
||||
match login_with_access_token(
|
||||
&config.codex_home,
|
||||
&access_token,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.forced_chatgpt_workspace_id.as_deref(),
|
||||
effective_chatgpt_workspaces.as_deref(),
|
||||
Some(&config.chatgpt_base_url),
|
||||
config.auth_keyring_backend_kind(),
|
||||
&auth_route_config,
|
||||
@@ -311,7 +320,10 @@ pub async fn run_login_with_device_code(
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting device code login flow");
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
if !config
|
||||
.auth_config()
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -323,11 +335,11 @@ pub async fn run_login_with_device_code(
|
||||
&auth_route_config,
|
||||
)
|
||||
.await;
|
||||
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
||||
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
|
||||
let mut opts = ServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
effective_chatgpt_workspaces,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
auth_route_config,
|
||||
@@ -359,7 +371,10 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
let _login_log_guard = init_login_file_logging(&config);
|
||||
tracing::info!("starting login flow with device code fallback");
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
if !config
|
||||
.auth_config()
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -372,11 +387,11 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
)
|
||||
.await;
|
||||
|
||||
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
||||
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
|
||||
let mut opts = ServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
effective_chatgpt_workspaces,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
auth_route_config,
|
||||
@@ -423,16 +438,11 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
|
||||
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
let auth_route_config = config.auth_route_config();
|
||||
|
||||
match CodexAuth::from_auth_storage(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
config.auth_keyring_backend_kind(),
|
||||
&auth_route_config,
|
||||
)
|
||||
.await
|
||||
match config
|
||||
.auth_config()
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
{
|
||||
Ok(Some(auth)) => match auth.auth_mode() {
|
||||
AuthMode::ApiKey => match auth.get_token() {
|
||||
@@ -469,8 +479,8 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
eprintln!("Not logged in");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error checking login status: {e}");
|
||||
Err(err) => {
|
||||
eprintln!("Error checking login status: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -513,7 +523,13 @@ async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config
|
||||
};
|
||||
|
||||
match Config::load_with_cli_overrides(cli_overrides).await {
|
||||
Ok(config) => config,
|
||||
Ok(config) => match config.auth_config().validate() {
|
||||
Ok(()) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading configuration: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error loading configuration: {e}");
|
||||
std::process::exit(1);
|
||||
|
||||
@@ -1821,16 +1821,19 @@ async fn load_exec_server_remote_auth_provider(
|
||||
use_agent_identity_auth: bool,
|
||||
) -> anyhow::Result<codex_api::SharedAuthProvider> {
|
||||
if use_agent_identity_auth {
|
||||
let agent_identity_jwt = read_codex_access_token_from_env().ok_or_else(|| {
|
||||
read_codex_access_token_from_env().ok_or_else(|| {
|
||||
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
|
||||
})?;
|
||||
let auth_route_config = config.auth_route_config();
|
||||
let auth = CodexAuth::from_agent_identity_jwt(
|
||||
&agent_identity_jwt,
|
||||
Some(&config.chatgpt_base_url),
|
||||
&auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
|
||||
if !matches!(auth, CodexAuth::AgentIdentity(_)) {
|
||||
anyhow::bail!(
|
||||
"CODEX_ACCESS_TOKEN did not provide permitted Agent Identity authentication"
|
||||
);
|
||||
}
|
||||
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ use codex_config::ConfigLoadOptions;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::LoaderOverrides;
|
||||
use codex_core::config::bootstrap_auth_config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_bootstrap_auth_route_config;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
|
||||
@@ -34,29 +33,10 @@ pub(super) async fn load_mcp_config(
|
||||
)
|
||||
.await
|
||||
.context("failed to load bootstrap configuration")?;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)
|
||||
.context("failed to resolve cloud configuration authentication")?;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)
|
||||
.context("failed to resolve cloud configuration authentication")?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)
|
||||
.context("failed to resolve cloud configuration credential storage")?,
|
||||
bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy;
|
||||
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
|
||||
use codex_core_plugins::marketplace::MarketplacePluginSource;
|
||||
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::read_codex_api_key_from_env;
|
||||
use codex_login::AuthManager;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
@@ -600,22 +599,11 @@ async fn load_plugin_command_context(
|
||||
}
|
||||
|
||||
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode> {
|
||||
if let Some(api_key) = read_codex_api_key_from_env() {
|
||||
return Some(CodexAuth::from_api_key(&api_key).api_auth_mode());
|
||||
}
|
||||
|
||||
let auth_route_config = config.auth_route_config();
|
||||
CodexAuth::from_auth_storage(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
config.auth_keyring_backend_kind(),
|
||||
&auth_route_config,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|auth| auth.api_auth_mode())
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
|
||||
.await
|
||||
.auth()
|
||||
.await
|
||||
.map(|auth| auth.api_auth_mode())
|
||||
}
|
||||
|
||||
struct PluginSelection {
|
||||
|
||||
@@ -66,6 +66,21 @@ fn login_with_api_key_reads_stdin_and_writes_auth_json() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_status_reports_auth_storage_errors() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_file_auth_config(codex_home.path())?;
|
||||
std::fs::write(codex_home.path().join("auth.json"), "{invalid json")?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["login", "status"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Error checking login status:"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_access_token_rejects_invalid_jwt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -4,11 +4,9 @@ use crate::service::CloudConfigBundleService;
|
||||
use codex_config::CloudConfigBundleLoadError;
|
||||
use codex_config::CloudConfigBundleLoadErrorCode;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -59,24 +57,17 @@ pub fn cloud_config_bundle_loader(
|
||||
}
|
||||
|
||||
pub async fn cloud_config_bundle_loader_for_storage(
|
||||
codex_home: PathBuf,
|
||||
auth_config: AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
chatgpt_base_url: String,
|
||||
auth_route_config: AuthRouteConfig,
|
||||
) -> CloudConfigBundleLoader {
|
||||
let http_client_factory = auth_route_config.http_client_factory().clone();
|
||||
let auth_manager = AuthManager::shared(
|
||||
codex_home.clone(),
|
||||
enable_codex_api_key_env,
|
||||
credentials_store_mode,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
keyring_backend_kind,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
let codex_home = auth_config.codex_home.clone();
|
||||
let chatgpt_base_url = auth_config
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let http_client_factory = auth_config.auth_route_config.http_client_factory().clone();
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_auth_config(auth_config, enable_codex_api_key_env).await;
|
||||
cloud_config_bundle_loader(
|
||||
auth_manager,
|
||||
chatgpt_base_url,
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_core::config::Config;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_login::AuthManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn set_user_agent_suffix(suffix: &str) {
|
||||
if let Ok(mut guard) = codex_login::default_client::USER_AGENT_SUFFIX.lock() {
|
||||
@@ -45,7 +46,7 @@ pub fn normalize_base_url(input: &str) -> String {
|
||||
|
||||
pub async fn load_auth_manager(
|
||||
chatgpt_base_url: Option<String>,
|
||||
) -> (Option<AuthManager>, HttpClientFactory) {
|
||||
) -> (Option<Arc<AuthManager>>, HttpClientFactory) {
|
||||
// TODO: pass in cli overrides once cloud tasks properly support them.
|
||||
let config = match Config::load_with_cli_overrides(Vec::new()).await {
|
||||
Ok(config) => config,
|
||||
@@ -58,16 +59,10 @@ pub async fn load_auth_manager(
|
||||
}
|
||||
};
|
||||
let http_client_factory = config.http_client_factory();
|
||||
let auth_manager = AuthManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url.or(Some(config.chatgpt_base_url.clone())),
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await;
|
||||
let mut auth_config = config.auth_config();
|
||||
auth_config.chatgpt_base_url = chatgpt_base_url.or(Some(config.chatgpt_base_url.clone()));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_auth_config(auth_config, /*enable_codex_api_key_env*/ false).await;
|
||||
(Some(auth_manager), http_client_factory)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
/// Authentication restrictions supplied by locally managed requirements.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ManagedAuthPolicy {
|
||||
pub allowed_login_methods: Option<Vec<ForcedLoginMethod>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ManagedAuthPolicy {
|
||||
pub fn allows_login_method(
|
||||
&self,
|
||||
method: ForcedLoginMethod,
|
||||
forced_login_method: Option<ForcedLoginMethod>,
|
||||
forced_workspaces: Option<&[String]>,
|
||||
) -> bool {
|
||||
forced_login_method.is_none_or(|forced| forced == method)
|
||||
&& self
|
||||
.allowed_login_methods
|
||||
.as_ref()
|
||||
.is_none_or(|allowed| allowed.contains(&method))
|
||||
&& (method != ForcedLoginMethod::Chatgpt
|
||||
|| self
|
||||
.effective_chatgpt_workspaces(forced_workspaces)
|
||||
.is_none_or(|workspaces| !workspaces.is_empty()))
|
||||
}
|
||||
|
||||
pub fn allowed_login_methods(
|
||||
&self,
|
||||
forced_login_method: Option<ForcedLoginMethod>,
|
||||
forced_workspaces: Option<&[String]>,
|
||||
) -> Vec<ForcedLoginMethod> {
|
||||
[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]
|
||||
.into_iter()
|
||||
.filter(|method| {
|
||||
self.allows_login_method(*method, forced_login_method, forced_workspaces)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn effective_chatgpt_workspaces(
|
||||
&self,
|
||||
forced_workspaces: Option<&[String]>,
|
||||
) -> Option<Vec<String>> {
|
||||
match (
|
||||
forced_workspaces,
|
||||
self.allowed_chatgpt_workspaces.as_deref(),
|
||||
) {
|
||||
(Some(forced), Some(allowed)) => Some(
|
||||
forced
|
||||
.iter()
|
||||
.filter(|workspace| allowed.contains(workspace))
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
(Some(forced), None) => Some(forced.to_vec()),
|
||||
(None, Some(allowed)) => Some(allowed.to_vec()),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -19,6 +20,7 @@ use super::requirements_exec_policy::RequirementsExecPolicy;
|
||||
use super::requirements_exec_policy::RequirementsExecPolicyToml;
|
||||
use crate::Constrained;
|
||||
use crate::ConstraintError;
|
||||
use crate::ManagedAuthPolicy;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
use crate::config_toml::ConfigToml;
|
||||
use crate::mcp_requirements::McpServerRequirement;
|
||||
@@ -147,6 +149,8 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
|
||||
/// normalization.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConfigRequirements {
|
||||
pub allowed_login_methods: Option<Sourced<Vec<ForcedLoginMethod>>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Sourced<Vec<String>>>,
|
||||
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
|
||||
@@ -181,6 +185,8 @@ pub struct ConfigRequirements {
|
||||
impl Default for ConfigRequirements {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
allowed_login_methods: None,
|
||||
allowed_chatgpt_workspaces: None,
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
@@ -230,6 +236,24 @@ impl Default for ConfigRequirements {
|
||||
}
|
||||
|
||||
impl ConfigRequirements {
|
||||
pub fn managed_auth_policy(&self) -> ManagedAuthPolicy {
|
||||
ManagedAuthPolicy {
|
||||
allowed_login_methods: self
|
||||
.allowed_login_methods
|
||||
.as_ref()
|
||||
.map(|allowed| allowed.value.clone()),
|
||||
allowed_chatgpt_workspaces: self.allowed_chatgpt_workspaces.as_ref().map(|allowed| {
|
||||
allowed
|
||||
.value
|
||||
.iter()
|
||||
.map(|workspace| workspace.trim())
|
||||
.filter(|workspace| !workspace.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_policy_source(&self) -> Option<&RequirementSource> {
|
||||
self.exec_policy.as_ref().map(|policy| &policy.source)
|
||||
}
|
||||
@@ -873,6 +897,8 @@ pub(crate) fn merge_app_requirements_descending(
|
||||
/// Base config deserialized from system `requirements.toml` or MDM.
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsToml {
|
||||
pub allowed_login_methods: Option<Vec<ForcedLoginMethod>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Vec<String>>,
|
||||
pub sqlite_home: Option<AbsolutePathBuf>,
|
||||
pub log_dir: Option<AbsolutePathBuf>,
|
||||
pub model_catalog_json: Option<AbsolutePathBuf>,
|
||||
@@ -964,6 +990,8 @@ impl<T> std::ops::Deref for Sourced<T> {
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_login_methods: Option<Sourced<Vec<ForcedLoginMethod>>>,
|
||||
pub allowed_chatgpt_workspaces: Option<Sourced<Vec<String>>>,
|
||||
pub sqlite_home: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub log_dir: Option<Sourced<AbsolutePathBuf>>,
|
||||
pub model_catalog_json: Option<Sourced<AbsolutePathBuf>>,
|
||||
@@ -1015,6 +1043,8 @@ impl ConfigRequirementsWithSources {
|
||||
// Destructure without `..` so adding fields to `ConfigRequirementsToml`
|
||||
// forces this merge logic to be updated.
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods: _,
|
||||
allowed_chatgpt_workspaces: _,
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
@@ -1061,6 +1091,8 @@ impl ConfigRequirementsWithSources {
|
||||
other,
|
||||
source,
|
||||
{
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1104,6 +1136,8 @@ impl ConfigRequirementsWithSources {
|
||||
|
||||
pub fn into_toml(self) -> ConfigRequirementsToml {
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1136,6 +1170,8 @@ impl ConfigRequirementsWithSources {
|
||||
guardian_policy_config,
|
||||
} = self;
|
||||
ConfigRequirementsToml {
|
||||
allowed_login_methods: allowed_login_methods.map(|sourced| sourced.value),
|
||||
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces.map(|sourced| sourced.value),
|
||||
sqlite_home: sqlite_home.map(|sourced| sourced.value),
|
||||
log_dir: log_dir.map(|sourced| sourced.value),
|
||||
model_catalog_json: model_catalog_json.map(|sourced| sourced.value),
|
||||
@@ -1235,7 +1271,9 @@ impl ConfigRequirementsToml {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.sqlite_home.is_none()
|
||||
self.allowed_login_methods.is_none()
|
||||
&& self.allowed_chatgpt_workspaces.is_none()
|
||||
&& self.sqlite_home.is_none()
|
||||
&& self.log_dir.is_none()
|
||||
&& self.model_catalog_json.is_none()
|
||||
&& self.check_for_update_on_startup.is_none()
|
||||
@@ -1416,6 +1454,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
// defaults also remain there because they are initialization values,
|
||||
// not runtime constraints.
|
||||
let ConfigRequirementsWithSources {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1744,6 +1784,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
});
|
||||
let guardian_policy_config_source = guardian_policy_config.map(|sourced| sourced.source);
|
||||
Ok(ConfigRequirements {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1898,6 +1940,8 @@ mod tests {
|
||||
|
||||
fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources {
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -1931,6 +1975,10 @@ mod tests {
|
||||
guardian_policy_config,
|
||||
} = toml;
|
||||
ConfigRequirementsWithSources {
|
||||
allowed_login_methods: allowed_login_methods
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_chatgpt_workspaces: allowed_chatgpt_workspaces
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
sqlite_home: sqlite_home.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
log_dir: log_dir.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
model_catalog_json: model_catalog_json
|
||||
@@ -2201,6 +2249,8 @@ mod tests {
|
||||
// Intentionally constructed without `..Default::default()` so adding a new field to
|
||||
// `ConfigRequirementsToml` forces this test to be updated.
|
||||
let other = ConfigRequirementsToml {
|
||||
allowed_login_methods: Some(vec![ForcedLoginMethod::Chatgpt]),
|
||||
allowed_chatgpt_workspaces: Some(vec!["managed-workspace".to_string()]),
|
||||
sqlite_home: Some(sqlite_home.clone()),
|
||||
log_dir: Some(log_dir.clone()),
|
||||
model_catalog_json: Some(model_catalog_json.clone()),
|
||||
@@ -2239,6 +2289,14 @@ mod tests {
|
||||
assert_eq!(
|
||||
target,
|
||||
ConfigRequirementsWithSources {
|
||||
allowed_login_methods: Some(Sourced::new(
|
||||
vec![ForcedLoginMethod::Chatgpt],
|
||||
source.clone(),
|
||||
)),
|
||||
allowed_chatgpt_workspaces: Some(Sourced::new(
|
||||
vec!["managed-workspace".to_string()],
|
||||
source.clone(),
|
||||
)),
|
||||
sqlite_home: Some(Sourced::new(sqlite_home, source.clone())),
|
||||
log_dir: Some(Sourced::new(log_dir, source.clone())),
|
||||
model_catalog_json: Some(Sourced::new(model_catalog_json, source.clone())),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod auth_policy;
|
||||
mod cloud_config_bundle;
|
||||
mod cloud_config_layers;
|
||||
mod config_layer_source;
|
||||
@@ -34,6 +35,7 @@ pub mod types;
|
||||
|
||||
pub const CONFIG_TOML_FILE: &str = "config.toml";
|
||||
|
||||
pub use auth_policy::ManagedAuthPolicy;
|
||||
pub use cloud_config_bundle::CloudConfigBundle;
|
||||
pub use cloud_config_bundle::CloudConfigBundleLayers;
|
||||
pub use cloud_config_bundle::CloudConfigBundleLoadError;
|
||||
|
||||
@@ -192,7 +192,14 @@ pub async fn load_config_layers_state(
|
||||
requirements_layers.extend(managed_preferences_requirements_layer);
|
||||
}
|
||||
|
||||
let config_requirements_toml = compose_requirements(requirements_layers)?.unwrap_or_default();
|
||||
let mut config_requirements_toml =
|
||||
compose_requirements(requirements_layers)?.unwrap_or_default();
|
||||
// Remote app servers enforce auth policy for their workspaces; do not let local
|
||||
// requirements reintroduce authentication restrictions for those workspaces.
|
||||
if overrides.ignore_login_requirements {
|
||||
config_requirements_toml.allowed_login_methods = None;
|
||||
config_requirements_toml.allowed_chatgpt_workspaces = None;
|
||||
}
|
||||
|
||||
let thread_config_context = ThreadConfigContext {
|
||||
thread_id: None,
|
||||
|
||||
@@ -65,8 +65,18 @@ impl ComposableRequirementsLayer {
|
||||
let _guard = base_dir
|
||||
.as_ref()
|
||||
.map(|base_dir| AbsolutePathBufGuard::new(base_dir.as_path()));
|
||||
let regular_toml = parse_layer_toml(&toml, &source)?;
|
||||
let requirements = parse_layer_requirements(&toml, &source)?;
|
||||
let mut regular_toml = parse_layer_toml(&toml, &source)?;
|
||||
|
||||
// These fields can only be set locally; ignore them before validating cloud policy.
|
||||
if matches!(source, RequirementSource::EnterpriseManaged { .. }) {
|
||||
remove_top_level_field(&mut regular_toml, "allowed_login_methods");
|
||||
remove_top_level_field(&mut regular_toml, "allowed_chatgpt_workspaces");
|
||||
}
|
||||
|
||||
let requirements = parse_layer_requirements(
|
||||
&RequirementsLayerToml::Value(regular_toml.clone()),
|
||||
&source,
|
||||
)?;
|
||||
(regular_toml, requirements)
|
||||
};
|
||||
|
||||
|
||||
@@ -206,6 +206,8 @@ fn populate_merged_regular_fields_with_sources(
|
||||
// Destructure without `..` so every new requirements field must choose
|
||||
// whether it belongs in the regular TOML merge path or in a special merger.
|
||||
let ConfigRequirementsToml {
|
||||
allowed_login_methods,
|
||||
allowed_chatgpt_workspaces,
|
||||
sqlite_home,
|
||||
log_dir,
|
||||
model_catalog_json,
|
||||
@@ -239,6 +241,8 @@ fn populate_merged_regular_fields_with_sources(
|
||||
guardian_policy_config,
|
||||
} = requirements;
|
||||
|
||||
set_sourced!(allowed_login_methods, &["allowed_login_methods"]);
|
||||
set_sourced!(allowed_chatgpt_workspaces, &["allowed_chatgpt_workspaces"]);
|
||||
set_sourced!(sqlite_home, &["sqlite_home"]);
|
||||
set_sourced!(log_dir, &["log_dir"]);
|
||||
set_sourced!(model_catalog_json, &["model_catalog_json"]);
|
||||
|
||||
@@ -57,6 +57,25 @@ fn empty_layers_compose_to_none() {
|
||||
assert_eq!(composed, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloud_auth_requirements_do_not_override_local_or_discard_other_policy() {
|
||||
let local = RequirementsLayerEntry::from_toml(
|
||||
RequirementSource::Unknown,
|
||||
"allowed_login_methods = [\"api\"]",
|
||||
);
|
||||
let cloud = layer(
|
||||
"req_cloud",
|
||||
"Cloud policy",
|
||||
"allowed_login_methods = [\"saml\"]\nallowed_chatgpt_workspaces = \"invalid\"\nallow_login_shell = false",
|
||||
);
|
||||
assert_eq!(
|
||||
compose(vec![local, cloud]).expect("cloud auth cannot invalidate enterprise policy"),
|
||||
Some(expected_requirements(
|
||||
"allowed_login_methods = [\"api\"]\nallow_login_shell = false"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_values_use_toml_priority() {
|
||||
let composed = compose(vec![
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct LoaderOverrides {
|
||||
pub system_config_path: Option<PathBuf>,
|
||||
pub system_requirements_path: Option<PathBuf>,
|
||||
pub ignore_managed_requirements: bool,
|
||||
/// Remote app servers own their authentication policy independently.
|
||||
pub ignore_login_requirements: bool,
|
||||
pub ignore_user_config: bool,
|
||||
pub ignore_user_and_project_exec_policy_rules: bool,
|
||||
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
|
||||
@@ -68,6 +70,7 @@ impl LoaderOverrides {
|
||||
system_config_path: Some(base.join("config.toml")),
|
||||
system_requirements_path: Some(base.join("requirements.toml")),
|
||||
ignore_managed_requirements: false,
|
||||
ignore_login_requirements: false,
|
||||
ignore_user_config: false,
|
||||
ignore_user_and_project_exec_policy_rules: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use super::Config;
|
||||
use super::ConfigTomlLoadResult;
|
||||
use super::ManagedFeatures;
|
||||
use super::resolve_bootstrap_auth_route_config;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
use codex_features::Features;
|
||||
use codex_login::AuthConfig;
|
||||
use std::path::Path;
|
||||
|
||||
impl Config {
|
||||
pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind {
|
||||
@@ -13,6 +16,66 @@ impl Config {
|
||||
self.features.enabled(Feature::SecretAuthStorage),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn auth_config(&self) -> AuthConfig {
|
||||
AuthConfig {
|
||||
codex_home: self.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: self.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: self.auth_keyring_backend_kind(),
|
||||
forced_login_method: self.forced_login_method,
|
||||
chatgpt_base_url: Some(self.chatgpt_base_url.clone()),
|
||||
forced_chatgpt_workspace_id: self.forced_chatgpt_workspace_id.clone(),
|
||||
managed_auth_policy: self.config_layer_stack.requirements().managed_auth_policy(),
|
||||
auth_route_config: self.auth_route_config(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds authentication settings from the locally resolved bootstrap config.
|
||||
///
|
||||
/// Use this before fetching cloud requirements, when a full [`Config`] is not
|
||||
/// yet available. Preserves the configured credential store, keyring backend,
|
||||
/// ChatGPT base URL, auth routing, and managed login/workspace restrictions.
|
||||
pub fn bootstrap_auth_config(
|
||||
codex_home: &Path,
|
||||
bootstrap_config: &ConfigTomlLoadResult,
|
||||
) -> std::io::Result<AuthConfig> {
|
||||
let config = &bootstrap_config.config_toml;
|
||||
// Empty legacy workspace settings mean unrestricted, not an empty allowlist.
|
||||
let forced_chatgpt_workspace_id = config
|
||||
.forced_chatgpt_workspace_id
|
||||
.clone()
|
||||
.map(|workspaces| {
|
||||
workspaces
|
||||
.into_vec()
|
||||
.into_iter()
|
||||
.map(|workspace| workspace.trim().to_string())
|
||||
.filter(|workspace| !workspace.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|workspaces| !workspaces.is_empty());
|
||||
let auth_config = AuthConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store.unwrap_or_default(),
|
||||
keyring_backend_kind: resolve_bootstrap_auth_keyring_backend_kind(bootstrap_config)?,
|
||||
forced_login_method: config.forced_login_method,
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
forced_chatgpt_workspace_id,
|
||||
managed_auth_policy: bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.managed_auth_policy(),
|
||||
auth_route_config: resolve_bootstrap_auth_route_config(
|
||||
config,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?,
|
||||
};
|
||||
auth_config.validate()?;
|
||||
Ok(auth_config)
|
||||
}
|
||||
|
||||
/// Resolve the auth keyring backend from a partially loaded bootstrap config.
|
||||
|
||||
@@ -6,7 +6,9 @@ use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ForcedChatgptWorkspaceIds;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -60,6 +62,70 @@ fn resolve_bootstrap_auth_keyring_backend_kind_uses_secret_auth_storage_feature(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_auth_restrictions_intersect_workspaces_and_fail_closed() {
|
||||
let config = ConfigToml {
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(ForcedChatgptWorkspaceIds::Multiple(vec![
|
||||
" denied ".to_string(),
|
||||
" allowed ".to_string(),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
let mut requirements = ConfigRequirements {
|
||||
allowed_login_methods: Some(Sourced::new(
|
||||
vec![ForcedLoginMethod::Chatgpt],
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
allowed_chatgpt_workspaces: Some(Sourced::new(
|
||||
vec!["allowed".to_string()],
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let bootstrap_config = ConfigTomlLoadResult {
|
||||
config_toml: config.clone(),
|
||||
config_layer_stack: ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
requirements.clone(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("requirements should stack"),
|
||||
};
|
||||
let auth_config = bootstrap_auth_config(Path::new("codex-home"), &bootstrap_config)
|
||||
.expect("policy should resolve");
|
||||
assert_eq!(auth_config.forced_login_method, None);
|
||||
assert!(auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt));
|
||||
assert!(!auth_config.is_login_method_allowed(ForcedLoginMethod::Api));
|
||||
assert_eq!(
|
||||
auth_config.forced_chatgpt_workspace_id,
|
||||
Some(vec!["denied".to_string(), "allowed".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
auth_config.effective_chatgpt_workspaces(),
|
||||
Some(vec!["allowed".to_string()])
|
||||
);
|
||||
|
||||
requirements.allowed_chatgpt_workspaces =
|
||||
Some(Sourced::new(Vec::new(), RequirementSource::Unknown));
|
||||
let bootstrap_config = ConfigTomlLoadResult {
|
||||
config_toml: config,
|
||||
config_layer_stack: ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
requirements,
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("requirements should stack"),
|
||||
};
|
||||
assert_eq!(
|
||||
bootstrap_auth_config(Path::new("codex-home"), &bootstrap_config)
|
||||
.expect_err("ChatGPT-only policy without an allowed workspace must fail")
|
||||
.kind(),
|
||||
std::io::ErrorKind::PermissionDenied
|
||||
);
|
||||
}
|
||||
|
||||
fn config_toml_load_result(
|
||||
config_toml: ConfigToml,
|
||||
feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
|
||||
@@ -9446,6 +9446,8 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = codex_config::ConfigRequirementsToml {
|
||||
allowed_login_methods: None,
|
||||
allowed_chatgpt_workspaces: None,
|
||||
sqlite_home: None,
|
||||
log_dir: None,
|
||||
model_catalog_json: None,
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ConstrainedWithSource;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::ManagedAuthPolicy;
|
||||
use codex_config::McpServerRequirement;
|
||||
use codex_config::PluginRequirementsToml;
|
||||
use codex_config::ProfileV2Name;
|
||||
@@ -160,6 +161,7 @@ mod requirements;
|
||||
mod resolved_permission_profile;
|
||||
#[cfg(test)]
|
||||
mod schema;
|
||||
pub use auth_keyring::bootstrap_auth_config;
|
||||
pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
pub use codex_config::ConfigLoadOptions;
|
||||
pub use codex_config::Constrained;
|
||||
@@ -1337,10 +1339,18 @@ impl AuthManagerConfig for Config {
|
||||
Config::auth_keyring_backend_kind(self)
|
||||
}
|
||||
|
||||
fn forced_login_method(&self) -> Option<ForcedLoginMethod> {
|
||||
self.forced_login_method
|
||||
}
|
||||
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
|
||||
self.forced_chatgpt_workspace_id.clone()
|
||||
}
|
||||
|
||||
fn managed_auth_policy(&self) -> ManagedAuthPolicy {
|
||||
self.config_layer_stack.requirements().managed_auth_policy()
|
||||
}
|
||||
|
||||
fn chatgpt_base_url(&self) -> String {
|
||||
self.chatgpt_base_url.clone()
|
||||
}
|
||||
@@ -3198,10 +3208,11 @@ impl Config {
|
||||
config_layer_stack.requirements(),
|
||||
&mut startup_warnings,
|
||||
);
|
||||
|
||||
// Destructure every field to ensure ConfigRequirements additions are
|
||||
// either applied above or handled while constructing the final Config.
|
||||
let ConfigRequirements {
|
||||
allowed_login_methods: _,
|
||||
allowed_chatgpt_workspaces: _,
|
||||
sqlite_home: _,
|
||||
log_dir: _,
|
||||
model_catalog_json: _,
|
||||
|
||||
@@ -63,10 +63,9 @@ use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigTomlLoadResult;
|
||||
use codex_core::config::bootstrap_auth_config;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_bootstrap_auth_route_config;
|
||||
use codex_core::config::resolve_oss_provider;
|
||||
use codex_core::config::resolve_profile_v2_config_path;
|
||||
use codex_core::find_thread_meta_by_name_str;
|
||||
@@ -75,7 +74,6 @@ use codex_core::path_utils;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::default_client::set_default_client_residency_requirement;
|
||||
use codex_login::default_client::set_default_originator;
|
||||
use codex_login::enforce_login_restrictions;
|
||||
@@ -342,28 +340,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
)
|
||||
.await;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
|
||||
let chatgpt_base_url = bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
bootstrap_auth_config(&codex_home, &bootstrap_config)?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
let run_cli_overrides = cli_kv_overrides.clone();
|
||||
@@ -480,18 +459,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
|
||||
let auth_route_config = config.auth_route_config();
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
auth_route_config,
|
||||
})
|
||||
.await
|
||||
{
|
||||
if let Err(err) = enforce_login_restrictions(&config.auth_config()).await {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,33 @@ async fn login_with_access_token_writes_agent_identity_jwt() {
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_with_access_token_rejects_agent_identity_workspace_mismatch() {
|
||||
let dir = tempdir().unwrap();
|
||||
let record = agent_identity_record(WORKSPACE_ID_DISALLOWED);
|
||||
let agent_identity =
|
||||
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
|
||||
let server = MockServer::start().await;
|
||||
let chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let allowed_workspaces = [WORKSPACE_ID_ALLOWED.to_string()];
|
||||
|
||||
let err = super::login_with_access_token(
|
||||
dir.path(),
|
||||
&agent_identity,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some(&allowed_workspaces),
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::default(),
|
||||
&crate::test_support::transport_default_auth_route_config(),
|
||||
)
|
||||
.await
|
||||
.expect_err("agent identity workspace mismatch should fail");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
|
||||
assert!(!get_auth_file(dir.path()).exists());
|
||||
assert!(server.received_requests().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn stored_agent_identity_jwt_keeps_auth_json_unchanged() -> anyhow::Result<()> {
|
||||
@@ -178,6 +205,7 @@ async fn stored_agent_identity_jwt_keeps_auth_json_unchanged() -> anyhow::Result
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -366,6 +394,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -448,6 +477,7 @@ async fn chatgpt_auth_registers_agent_identity_when_enabled() -> anyhow::Result<
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -490,6 +520,7 @@ async fn chatgpt_auth_retries_transient_agent_identity_registration() -> anyhow:
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -556,6 +587,7 @@ async fn chatgpt_auth_registration_retry_exhaustion_is_fallback_eligible() -> an
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -617,6 +649,7 @@ async fn chatgpt_auth_task_registration_retry_exhaustion_is_fallback_eligible()
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -673,6 +706,7 @@ async fn chatgpt_auth_non_retryable_registration_error_is_hard_failure() -> anyh
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -816,6 +850,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -878,6 +913,7 @@ async fn loads_api_key_from_auth_json() {
|
||||
dir.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -974,6 +1010,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -1343,6 +1380,7 @@ async fn build_config(
|
||||
keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id,
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
auth_route_config: crate::test_support::transport_default_auth_route_config(),
|
||||
}
|
||||
@@ -1423,6 +1461,7 @@ async fn load_auth_reads_access_token_from_env() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
Some(&chatgpt_base_url),
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -1471,6 +1510,7 @@ async fn load_auth_reads_personal_access_token_from_env() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
auth_credentials_store_mode,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
@@ -1644,6 +1684,7 @@ async fn load_auth_keeps_codex_api_key_env_precedence() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -1687,6 +1728,162 @@ async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn auth_manager_rejects_disallowed_stored_and_external_auth() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let _access_token_guard = remove_access_token_env_var();
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("seed api key");
|
||||
let mut config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
)
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_login_methods = Some(vec![ForcedLoginMethod::Chatgpt]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
assert!(
|
||||
manager
|
||||
.set_external_auth(Arc::new(StaticExternalAuth(CodexAuth::from_api_key(
|
||||
"sk-external",
|
||||
))))
|
||||
.await
|
||||
.is_err(),
|
||||
"external auth cannot bypass managed login policy"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn api_only_policy_rejects_access_tokens_before_hydration() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let server = MockServer::start().await;
|
||||
let _authapi_guard = EnvVarGuard::set("CODEX_AUTHAPI_BASE_URL", &server.uri());
|
||||
let _access_token_guard = EnvVarGuard::set(CODEX_ACCESS_TOKEN_ENV_VAR, "at-rejected");
|
||||
let mut config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
)
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_login_methods = Some(vec![ForcedLoginMethod::Api]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
assert!(
|
||||
server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("inspect auth requests")
|
||||
.is_empty(),
|
||||
"rejected access tokens must not call whoami or register Agent Identity"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn workspace_policy_rejects_agent_identity_before_hydration() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let server = MockServer::start().await;
|
||||
let record = agent_identity_record(WORKSPACE_ID_DISALLOWED);
|
||||
let agent_identity =
|
||||
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
|
||||
let _authapi_guard = EnvVarGuard::set("CODEX_AUTHAPI_BASE_URL", &server.uri());
|
||||
let _access_token_reset = remove_access_token_env_var();
|
||||
let access_token_guard = EnvVarGuard::set(CODEX_ACCESS_TOKEN_ENV_VAR, &agent_identity);
|
||||
let mut config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
)
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_chatgpt_workspaces =
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
drop(access_token_guard);
|
||||
|
||||
for stored_agent_identity in [
|
||||
AgentIdentityStorage::Jwt(agent_identity),
|
||||
AgentIdentityStorage::Record(record),
|
||||
] {
|
||||
save_auth(
|
||||
codex_home.path(),
|
||||
&AuthDotJson {
|
||||
auth_mode: Some(AuthMode::AgentIdentity),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(stored_agent_identity),
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.expect("store agent identity");
|
||||
let mut config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
)
|
||||
.await;
|
||||
config.managed_auth_policy.allowed_chatgpt_workspaces =
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]);
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
assert_eq!(manager.auth().await, None);
|
||||
}
|
||||
|
||||
assert!(
|
||||
server.received_requests().await.unwrap().is_empty(),
|
||||
"rejected agent identities must not fetch JWKS or register"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn workspace_policy_checks_the_selected_request_account() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let _access_token_guard = remove_access_token_env_var();
|
||||
write_auth_file(
|
||||
AuthFileParams {
|
||||
openai_api_key: None,
|
||||
chatgpt_plan_type: Some("pro".to_string()),
|
||||
chatgpt_account_id: Some(WORKSPACE_ID_ALLOWED.to_string()),
|
||||
},
|
||||
codex_home.path(),
|
||||
)
|
||||
.expect("seed ChatGPT credentials");
|
||||
let auth_path = codex_home.path().join("auth.json");
|
||||
let mut stored: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(&auth_path).unwrap()).unwrap();
|
||||
stored["tokens"]["account_id"] = json!(WORKSPACE_ID_DISALLOWED);
|
||||
std::fs::write(&auth_path, serde_json::to_vec(&stored).unwrap()).unwrap();
|
||||
let config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
)
|
||||
.await;
|
||||
let manager =
|
||||
AuthManager::shared_from_auth_config(config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
assert_eq!(manager.auth().await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_auth_env)]
|
||||
async fn enforce_login_restrictions_logs_out_for_workspace_mismatch() {
|
||||
@@ -1756,6 +1953,7 @@ async fn enforce_login_restrictions_logs_out_for_personal_access_token_workspace
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
auth_route_config: crate::test_support::transport_default_auth_route_config(),
|
||||
};
|
||||
@@ -1880,6 +2078,7 @@ async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismat
|
||||
keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: Some(chatgpt_base_url),
|
||||
auth_route_config: crate::test_support::transport_default_auth_route_config(),
|
||||
};
|
||||
@@ -2162,6 +2361,7 @@ async fn plan_type_maps_known_plan() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -2194,6 +2394,7 @@ async fn plan_type_maps_self_serve_business_usage_based_plan() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -2229,6 +2430,7 @@ async fn plan_type_maps_enterprise_cbp_usage_based_plan() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -2264,6 +2466,7 @@ async fn plan_type_maps_unknown_to_unknown() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
@@ -2296,6 +2499,7 @@ async fn missing_plan_type_maps_to_unknown() {
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
|
||||
@@ -56,6 +56,7 @@ use crate::outbound_proxy::AuthRouteConfig;
|
||||
use crate::token_data::TokenData;
|
||||
use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
use crate::token_data::parse_jwt_expiration;
|
||||
use codex_config::ManagedAuthPolicy;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_http_client::HttpClient;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
@@ -361,6 +362,7 @@ impl CodexAuth {
|
||||
codex_home,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
auth_credentials_store_mode,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
@@ -940,7 +942,7 @@ pub async fn login_with_access_token(
|
||||
let auth_dot_json = match classify_codex_access_token(access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
let auth = PersonalAccessTokenAuth::load(access_token, auth_route_config).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, auth.account_id())?;
|
||||
AuthDotJson {
|
||||
// Infer PAT auth from the credential field so older Codex builds can still
|
||||
// deserialize auth.json after a rollback.
|
||||
@@ -954,6 +956,8 @@ pub async fn login_with_access_token(
|
||||
}
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
let record = AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, &record.account_id)?;
|
||||
let base_url = chatgpt_base_url
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
@@ -978,14 +982,33 @@ pub async fn login_with_access_token(
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_personal_access_token_workspace_allowed(
|
||||
fn ensure_auth_workspace_allowed(
|
||||
expected_workspace_ids: Option<&[String]>,
|
||||
auth: &PersonalAccessTokenAuth,
|
||||
account_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
crate::server::ensure_workspace_account_allowed(expected_workspace_ids, auth.account_id())
|
||||
crate::server::ensure_workspace_account_allowed(expected_workspace_ids, account_id)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::PermissionDenied, message))
|
||||
}
|
||||
|
||||
fn ensure_agent_identity_workspace_allowed(
|
||||
expected_workspace_ids: Option<&[String]>,
|
||||
agent_identity: &AgentIdentityStorage,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(expected_workspace_ids) = expected_workspace_ids else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match agent_identity {
|
||||
AgentIdentityStorage::Jwt(jwt) => {
|
||||
let record = AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
|
||||
ensure_auth_workspace_allowed(Some(expected_workspace_ids), &record.account_id)
|
||||
}
|
||||
AgentIdentityStorage::Record(record) => {
|
||||
ensure_auth_workspace_allowed(Some(expected_workspace_ids), &record.account_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an in-memory auth payload for externally managed ChatGPT tokens.
|
||||
pub fn login_with_chatgpt_auth_tokens(
|
||||
codex_home: &Path,
|
||||
@@ -1047,9 +1070,131 @@ pub struct AuthConfig {
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
pub managed_auth_policy: ManagedAuthPolicy,
|
||||
pub auth_route_config: AuthRouteConfig,
|
||||
}
|
||||
|
||||
impl AuthConfig {
|
||||
pub fn is_login_method_allowed(&self, method: ForcedLoginMethod) -> bool {
|
||||
self.managed_auth_policy.allows_login_method(
|
||||
method,
|
||||
self.forced_login_method,
|
||||
self.forced_chatgpt_workspace_id.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn effective_chatgpt_workspaces(&self) -> Option<Vec<String>> {
|
||||
self.managed_auth_policy
|
||||
.effective_chatgpt_workspaces(self.forced_chatgpt_workspace_id.as_deref())
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> std::io::Result<()> {
|
||||
if self.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
|| self.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"authentication requirements do not permit any usable login method",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allows_auth(&self, auth: &CodexAuth) -> bool {
|
||||
let allowed_login_methods = self.allowed_login_methods();
|
||||
let workspaces = self.effective_chatgpt_workspaces();
|
||||
validate_auth_restrictions(Some(&allowed_login_methods), workspaces.as_deref(), auth)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn load_auth(
|
||||
&self,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> std::io::Result<Option<CodexAuth>> {
|
||||
let allowed_login_methods = self.allowed_login_methods();
|
||||
let workspaces = self.effective_chatgpt_workspaces();
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(self.chatgpt_base_url.as_deref()).ok();
|
||||
let auth = load_auth(
|
||||
&self.codex_home,
|
||||
enable_codex_api_key_env,
|
||||
self.auth_credentials_store_mode,
|
||||
Some(&allowed_login_methods),
|
||||
workspaces.as_deref(),
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
self.keyring_backend_kind,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
&self.auth_route_config,
|
||||
)
|
||||
.await?;
|
||||
Ok(auth.filter(|auth| self.allows_auth(auth)))
|
||||
}
|
||||
|
||||
fn allowed_login_methods(&self) -> Vec<ForcedLoginMethod> {
|
||||
self.managed_auth_policy.allowed_login_methods(
|
||||
self.forced_login_method,
|
||||
self.forced_chatgpt_workspace_id.as_deref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_mode_is_allowed(
|
||||
allowed_login_methods: Option<&[ForcedLoginMethod]>,
|
||||
mode: AuthMode,
|
||||
) -> bool {
|
||||
let method = if mode.uses_codex_backend() {
|
||||
ForcedLoginMethod::Chatgpt
|
||||
} else {
|
||||
ForcedLoginMethod::Api
|
||||
};
|
||||
allowed_login_methods.is_none_or(|allowed| allowed.contains(&method))
|
||||
}
|
||||
|
||||
fn validate_auth_restrictions(
|
||||
allowed_login_methods: Option<&[ForcedLoginMethod]>,
|
||||
expected_workspaces: Option<&[String]>,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<(), String> {
|
||||
if !auth_mode_is_allowed(allowed_login_methods, auth.auth_mode()) {
|
||||
return Err(match allowed_login_methods {
|
||||
Some(methods) if methods.contains(&ForcedLoginMethod::Api) => {
|
||||
"API key login is required".to_string()
|
||||
}
|
||||
Some(_) => "ChatGPT login is required".to_string(),
|
||||
None => unreachable!("unrestricted login methods accept every auth mode"),
|
||||
});
|
||||
}
|
||||
|
||||
let Some(expected_workspaces) = expected_workspaces else {
|
||||
return Ok(());
|
||||
};
|
||||
if matches!(
|
||||
auth,
|
||||
CodexAuth::ApiKey(_) | CodexAuth::Headers(_) | CodexAuth::BedrockApiKey(_)
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let actual_workspace = auth.get_account_id().or_else(|| {
|
||||
auth.get_token_data()
|
||||
.ok()
|
||||
.and_then(|tokens| tokens.id_token.chatgpt_account_id)
|
||||
});
|
||||
if actual_workspace
|
||||
.as_ref()
|
||||
.is_some_and(|workspace| expected_workspaces.contains(workspace))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
let actual = actual_workspace.unwrap_or_else(|| "unknown".to_string());
|
||||
Err(format!(
|
||||
"Login is restricted to workspace(s) {}, but current credentials belong to {actual}",
|
||||
expected_workspaces.join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforces configured login restrictions using auth-owned HTTP settings.
|
||||
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
let agent_identity_authapi_base_url =
|
||||
@@ -1065,10 +1210,16 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
|
||||
config: &AuthConfig,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
) -> std::io::Result<()> {
|
||||
// Managed-only restrictions are enforced by AuthManager.
|
||||
if config.forced_login_method.is_none() && config.forced_chatgpt_workspace_id.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(auth) = load_auth(
|
||||
&config.codex_home,
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
config.auth_credentials_store_mode,
|
||||
/*allowed_login_methods*/ None,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
config.chatgpt_base_url.as_deref(),
|
||||
config.keyring_backend_kind,
|
||||
@@ -1218,6 +1369,7 @@ async fn load_auth(
|
||||
codex_home: &Path,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
allowed_login_methods: Option<&[ForcedLoginMethod]>,
|
||||
forced_chatgpt_workspace_id: Option<&[String]>,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
@@ -1225,7 +1377,10 @@ async fn load_auth(
|
||||
auth_route_config: &AuthRouteConfig,
|
||||
) -> std::io::Result<Option<CodexAuth>> {
|
||||
// API key via env var takes precedence over any other auth method.
|
||||
if enable_codex_api_key_env && let Some(api_key) = read_codex_api_key_from_env() {
|
||||
if enable_codex_api_key_env
|
||||
&& auth_mode_is_allowed(allowed_login_methods, AuthMode::ApiKey)
|
||||
&& let Some(api_key) = read_codex_api_key_from_env()
|
||||
{
|
||||
return Ok(Some(CodexAuth::from_api_key(api_key.as_str())));
|
||||
}
|
||||
|
||||
@@ -1236,7 +1391,12 @@ async fn load_auth(
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
if let Some(auth_dot_json) = ephemeral_storage.load()? {
|
||||
if let Some(auth_dot_json) = ephemeral_storage.load()?
|
||||
&& auth_mode_is_allowed(allowed_login_methods, auth_dot_json.resolved_mode())
|
||||
{
|
||||
if let Some(agent_identity) = auth_dot_json.agent_identity.as_ref() {
|
||||
ensure_agent_identity_workspace_allowed(forced_chatgpt_workspace_id, agent_identity)?;
|
||||
}
|
||||
let auth = CodexAuth::from_auth_dot_json(
|
||||
codex_home,
|
||||
auth_dot_json,
|
||||
@@ -1248,19 +1408,23 @@ async fn load_auth(
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, auth)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, auth.account_id())?;
|
||||
}
|
||||
return Ok(Some(auth));
|
||||
}
|
||||
|
||||
if let Some(access_token) = read_codex_access_token_from_env() {
|
||||
if auth_mode_is_allowed(allowed_login_methods, AuthMode::AgentIdentity)
|
||||
&& let Some(access_token) = read_codex_access_token_from_env()
|
||||
{
|
||||
return match classify_codex_access_token(&access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
let auth = PersonalAccessTokenAuth::load(access_token, auth_route_config).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, auth.account_id())?;
|
||||
Ok(Some(CodexAuth::PersonalAccessToken(auth)))
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
let record = AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, &record.account_id)?;
|
||||
CodexAuth::from_agent_identity_jwt_with_authapi_base_url(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
@@ -1288,6 +1452,12 @@ async fn load_auth(
|
||||
Some(auth) => auth,
|
||||
None => return Ok(None),
|
||||
};
|
||||
if !auth_mode_is_allowed(allowed_login_methods, auth_dot_json.resolved_mode()) {
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(agent_identity) = auth_dot_json.agent_identity.as_ref() {
|
||||
ensure_agent_identity_workspace_allowed(forced_chatgpt_workspace_id, agent_identity)?;
|
||||
}
|
||||
|
||||
let auth = CodexAuth::from_auth_dot_json(
|
||||
codex_home,
|
||||
@@ -1300,7 +1470,7 @@ async fn load_auth(
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, auth)?;
|
||||
ensure_auth_workspace_allowed(forced_chatgpt_workspace_id, auth.account_id())?;
|
||||
}
|
||||
Ok(Some(auth))
|
||||
}
|
||||
@@ -1773,7 +1943,9 @@ pub struct AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
forced_login_method: Option<ForcedLoginMethod>,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
|
||||
managed_auth_policy: ManagedAuthPolicy,
|
||||
chatgpt_base_url: Option<String>,
|
||||
agent_identity_authapi_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
@@ -1799,9 +1971,15 @@ pub trait AuthManagerConfig {
|
||||
/// Returns the backend to use when CLI auth keyring storage is selected.
|
||||
fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind;
|
||||
|
||||
/// Returns the resolved login-method restriction, if any.
|
||||
fn forced_login_method(&self) -> Option<ForcedLoginMethod>;
|
||||
|
||||
/// Returns the workspace IDs that ChatGPT auth should be restricted to, if any.
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>>;
|
||||
|
||||
/// Returns administrator-managed authentication restrictions.
|
||||
fn managed_auth_policy(&self) -> ManagedAuthPolicy;
|
||||
|
||||
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
|
||||
fn chatgpt_base_url(&self) -> String;
|
||||
|
||||
@@ -1820,10 +1998,12 @@ impl Debug for AuthManager {
|
||||
&self.auth_credentials_store_mode,
|
||||
)
|
||||
.field("keyring_backend_kind", &self.keyring_backend_kind)
|
||||
.field("forced_login_method", &self.forced_login_method)
|
||||
.field(
|
||||
"forced_chatgpt_workspace_id",
|
||||
&self.forced_chatgpt_workspace_id,
|
||||
)
|
||||
.field("managed_auth_policy", &self.managed_auth_policy)
|
||||
.field("chatgpt_base_url", &self.chatgpt_base_url)
|
||||
.field("auth_route_config", &self.auth_route_config)
|
||||
.field("has_external_auth", &self.has_external_auth())
|
||||
@@ -1849,21 +2029,40 @@ impl AuthManager {
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_route_config: AuthRouteConfig,
|
||||
) -> Self {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url.as_deref()).ok();
|
||||
let managed_auth = load_auth(
|
||||
&codex_home,
|
||||
Self::new_from_auth_config(
|
||||
AuthConfig {
|
||||
codex_home,
|
||||
auth_credentials_store_mode,
|
||||
keyring_backend_kind,
|
||||
forced_login_method: None,
|
||||
chatgpt_base_url,
|
||||
forced_chatgpt_workspace_id,
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
auth_route_config,
|
||||
},
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
chatgpt_base_url.as_deref(),
|
||||
keyring_backend_kind,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
&auth_route_config,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
}
|
||||
|
||||
async fn new_from_auth_config(auth_config: AuthConfig, enable_codex_api_key_env: bool) -> Self {
|
||||
let managed_auth = auth_config
|
||||
.load_auth(enable_codex_api_key_env)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let AuthConfig {
|
||||
codex_home,
|
||||
auth_credentials_store_mode,
|
||||
keyring_backend_kind,
|
||||
forced_login_method,
|
||||
chatgpt_base_url,
|
||||
forced_chatgpt_workspace_id,
|
||||
managed_auth_policy,
|
||||
auth_route_config,
|
||||
} = auth_config;
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url.as_deref()).ok();
|
||||
let (auth_change_tx, _auth_change_rx) = watch::channel(0);
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -1875,7 +2074,9 @@ impl AuthManager {
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
keyring_backend_kind,
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id: RwLock::new(forced_chatgpt_workspace_id),
|
||||
managed_auth_policy,
|
||||
chatgpt_base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1901,7 +2102,9 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1926,7 +2129,9 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1955,7 +2160,9 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: Some(
|
||||
agent_identity_authapi_base_url
|
||||
@@ -1982,7 +2189,9 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
managed_auth_policy: ManagedAuthPolicy::default(),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -2056,10 +2265,10 @@ impl AuthManager {
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let forced_chatgpt_workspace_id = self.forced_chatgpt_workspace_id();
|
||||
let effective_chatgpt_workspaces = self.effective_chatgpt_workspaces();
|
||||
let cooldown_key = ManagedChatGptAgentIdentityBinding::from_auth(
|
||||
&auth,
|
||||
forced_chatgpt_workspace_id.clone(),
|
||||
effective_chatgpt_workspaces.clone(),
|
||||
)
|
||||
.and_then(|binding| {
|
||||
self.agent_identity_authapi_base_url
|
||||
@@ -2079,7 +2288,7 @@ impl AuthManager {
|
||||
.agent_identity_auth(
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
forced_chatgpt_workspace_id,
|
||||
effective_chatgpt_workspaces,
|
||||
&self.auth_route_config,
|
||||
session_source,
|
||||
)
|
||||
@@ -2098,7 +2307,7 @@ impl AuthManager {
|
||||
auth.agent_identity_auth(
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
self.effective_chatgpt_workspaces(),
|
||||
&self.auth_route_config,
|
||||
session_source,
|
||||
)
|
||||
@@ -2209,12 +2418,14 @@ impl AuthManager {
|
||||
};
|
||||
}
|
||||
|
||||
let forced_chatgpt_workspace_id = self.forced_chatgpt_workspace_id();
|
||||
let allowed_login_methods = self.allowed_login_methods();
|
||||
let effective_chatgpt_workspaces = self.effective_chatgpt_workspaces();
|
||||
load_auth(
|
||||
&self.codex_home,
|
||||
self.enable_codex_api_key_env,
|
||||
self.auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
Some(&allowed_login_methods),
|
||||
effective_chatgpt_workspaces.as_deref(),
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
self.keyring_backend_kind,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
@@ -2223,6 +2434,14 @@ impl AuthManager {
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|auth| {
|
||||
validate_auth_restrictions(
|
||||
Some(&allowed_login_methods),
|
||||
effective_chatgpt_workspaces.as_deref(),
|
||||
auth,
|
||||
)
|
||||
.is_ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn set_cached_auth(&self, new_auth: Option<CodexAuth>) -> bool {
|
||||
@@ -2279,6 +2498,26 @@ impl AuthManager {
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
pub fn effective_chatgpt_workspaces(&self) -> Option<Vec<String>> {
|
||||
self.managed_auth_policy
|
||||
.effective_chatgpt_workspaces(self.forced_chatgpt_workspace_id().as_deref())
|
||||
}
|
||||
|
||||
pub fn is_login_method_allowed(&self, method: ForcedLoginMethod) -> bool {
|
||||
self.managed_auth_policy.allows_login_method(
|
||||
method,
|
||||
self.forced_login_method,
|
||||
self.forced_chatgpt_workspace_id().as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn allowed_login_methods(&self) -> Vec<ForcedLoginMethod> {
|
||||
self.managed_auth_policy.allowed_login_methods(
|
||||
self.forced_login_method,
|
||||
self.forced_chatgpt_workspace_id().as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn has_external_auth(&self) -> bool {
|
||||
self.external_auth().is_some()
|
||||
}
|
||||
@@ -2322,18 +2561,30 @@ impl AuthManager {
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
Self::shared(
|
||||
config.codex_home(),
|
||||
Self::shared_from_auth_config(
|
||||
AuthConfig {
|
||||
codex_home: config.codex_home(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode(),
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url()),
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id(),
|
||||
managed_auth_policy: config.managed_auth_policy(),
|
||||
auth_route_config: config.auth_route_config(),
|
||||
},
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
config.forced_chatgpt_workspace_id(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Builds a shared manager using restrictions resolved before authentication.
|
||||
pub async fn shared_from_auth_config(
|
||||
auth_config: AuthConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self::new_from_auth_config(auth_config, enable_codex_api_key_env).await)
|
||||
}
|
||||
|
||||
pub fn unauthorized_recovery(self: &Arc<Self>) -> UnauthorizedRecovery {
|
||||
UnauthorizedRecovery::new(Arc::clone(self))
|
||||
}
|
||||
@@ -2582,16 +2833,13 @@ impl AuthManager {
|
||||
}
|
||||
|
||||
fn validate_external_auth(&self, auth: &CodexAuth) -> Result<(), RefreshTokenError> {
|
||||
if let Some(account_id) = auth.get_account_id()
|
||||
&& let Some(expected_workspace_ids) = self.forced_chatgpt_workspace_id()
|
||||
&& !expected_workspace_ids.contains(&account_id)
|
||||
{
|
||||
return Err(RefreshTokenError::Transient(std::io::Error::other(
|
||||
format!(
|
||||
"external auth returned workspace {account_id:?}, expected one of {expected_workspace_ids:?}"
|
||||
),
|
||||
)));
|
||||
}
|
||||
let allowed_login_methods = self.allowed_login_methods();
|
||||
validate_auth_restrictions(
|
||||
Some(&allowed_login_methods),
|
||||
self.effective_chatgpt_workspaces().as_deref(),
|
||||
auth,
|
||||
)
|
||||
.map_err(std::io::Error::other)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ pub async fn run_main(
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
|
||||
})?;
|
||||
config.auth_config().validate()?;
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
let otel = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
|
||||
@@ -929,6 +929,8 @@ interrupt_message = false
|
||||
};
|
||||
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
allowed_login_methods: None,
|
||||
allowed_chatgpt_workspaces: None,
|
||||
sqlite_home: Some(sqlite_home),
|
||||
log_dir: Some(log_dir),
|
||||
model_catalog_json: Some(model_catalog_json),
|
||||
|
||||
+19
-46
@@ -7,8 +7,9 @@ use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::ConfigTomlLoadResult;
|
||||
use crate::legacy_core::config::bootstrap_auth_config;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
#[cfg(test)]
|
||||
use crate::legacy_core::config::resolve_bootstrap_http_client_factory;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
@@ -47,7 +48,6 @@ use codex_config::types::ResumeCwdMode;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_login::default_client::set_default_client_residency_requirement;
|
||||
use codex_login::enforce_login_restrictions;
|
||||
@@ -274,6 +274,17 @@ impl AppServerTarget {
|
||||
matches!(self, Self::Remote { .. })
|
||||
}
|
||||
|
||||
fn auth_config_for_cloud_loader(&self, mut auth_config: AuthConfig) -> AuthConfig {
|
||||
if self.uses_remote_workspace() {
|
||||
// Remove local auth restrictions before loading credentials for a remote
|
||||
// workspace; the remote app server enforces its own authentication policy.
|
||||
auth_config.forced_login_method = None;
|
||||
auth_config.forced_chatgpt_workspace_id = None;
|
||||
auth_config.managed_auth_policy = Default::default();
|
||||
}
|
||||
auth_config
|
||||
}
|
||||
|
||||
fn thread_params_mode(&self) -> ThreadParamsMode {
|
||||
if self.uses_remote_workspace() {
|
||||
ThreadParamsMode::Remote
|
||||
@@ -1007,6 +1018,7 @@ pub async fn run_main(
|
||||
loader_overrides.user_config_path = Some(user_config_path);
|
||||
loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
loader_overrides.ignore_login_requirements = app_server_target.uses_remote_workspace();
|
||||
|
||||
let bootstrap_config = load_bootstrap_config_or_exit(
|
||||
&codex_home,
|
||||
@@ -1018,30 +1030,10 @@ pub async fn run_main(
|
||||
)
|
||||
.await;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
|
||||
let chatgpt_base_url = bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let bootstrap_http_client_factory = resolve_bootstrap_http_client_factory(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let auth_route_config =
|
||||
AuthRouteConfig::from_http_client_factory(bootstrap_http_client_factory);
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
app_server_target
|
||||
.auth_config_for_cloud_loader(bootstrap_auth_config(&codex_home, &bootstrap_config)?),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1135,12 +1127,8 @@ pub async fn run_main(
|
||||
.await;
|
||||
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
config.codex_home.to_path_buf(),
|
||||
app_server_target.auth_config_for_cloud_loader(config.auth_config()),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
config.chatgpt_base_url.clone(),
|
||||
config.auth_route_config(),
|
||||
)
|
||||
.await;
|
||||
let environment_manager = Arc::new(
|
||||
@@ -1209,19 +1197,8 @@ pub async fn run_main(
|
||||
}
|
||||
|
||||
if !app_server_target.uses_remote_workspace() {
|
||||
let auth_route_config = config.auth_route_config();
|
||||
#[allow(clippy::print_stderr)]
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
auth_route_config,
|
||||
})
|
||||
.await
|
||||
{
|
||||
if let Err(err) = enforce_login_restrictions(&config.auth_config()).await {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -1491,12 +1468,8 @@ async fn run_ratatui_app(
|
||||
// status detection edge cases.
|
||||
if show_login_screen && !uses_remote_workspace {
|
||||
cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
initial_config.codex_home.to_path_buf(),
|
||||
initial_config.auth_config(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
initial_config.cli_auth_credentials_store_mode,
|
||||
initial_config.auth_keyring_backend_kind(),
|
||||
initial_config.chatgpt_base_url.clone(),
|
||||
initial_config.auth_route_config(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_app_server_protocol::CancelLoginAccountParams;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::read_openai_api_key_from_env;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -234,7 +235,7 @@ pub(crate) struct AuthModeWidget {
|
||||
pub sign_in_state: Arc<RwLock<SignInState>>,
|
||||
pub login_status: LoginStatus,
|
||||
pub app_server_request_handle: AppServerRequestHandle,
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub auth_config: AuthConfig,
|
||||
pub animations_enabled: bool,
|
||||
pub animations_suppressed: Cell<bool>,
|
||||
}
|
||||
@@ -308,11 +309,13 @@ impl AuthModeWidget {
|
||||
}
|
||||
|
||||
fn is_api_login_allowed(&self) -> bool {
|
||||
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Chatgpt))
|
||||
self.auth_config
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
}
|
||||
|
||||
fn is_chatgpt_login_allowed(&self) -> bool {
|
||||
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Api))
|
||||
self.auth_config
|
||||
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
||||
}
|
||||
|
||||
fn displayed_sign_in_options(&self) -> Vec<SignInOption> {
|
||||
@@ -1029,9 +1032,6 @@ mod tests {
|
||||
use codex_app_server_client::InProcessClientStartArgs;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
@@ -1059,7 +1059,7 @@ mod tests {
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
let auth_route_config = config.auth_route_config();
|
||||
let mut auth_config = config.auth_config();
|
||||
let client = InProcessAppServerClient::start(InProcessClientStartArgs {
|
||||
arg0_paths: Arg0DispatchPaths::default(),
|
||||
config: Arc::new(config),
|
||||
@@ -1067,12 +1067,8 @@ mod tests {
|
||||
loader_overrides: Default::default(),
|
||||
strict_config: false,
|
||||
cloud_config_bundle: cloud_config_bundle_loader_for_storage(
|
||||
codex_home_path.clone(),
|
||||
auth_config.clone(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
auth_route_config,
|
||||
)
|
||||
.await,
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
@@ -1094,6 +1090,7 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
auth_config.forced_login_method = Some(ForcedLoginMethod::Chatgpt);
|
||||
let widget = AuthModeWidget {
|
||||
request_frame: FrameRequester::test_dummy(),
|
||||
highlighted_mode: SignInOption::ChatGpt,
|
||||
@@ -1101,7 +1098,7 @@ mod tests {
|
||||
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
||||
login_status: LoginStatus::NotAuthenticated,
|
||||
app_server_request_handle: AppServerRequestHandle::InProcess(client.request_handle()),
|
||||
forced_login_method: Some(ForcedLoginMethod::Chatgpt),
|
||||
auth_config,
|
||||
animations_enabled: true,
|
||||
animations_suppressed: std::cell::Cell::new(false),
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ impl OnboardingScreen {
|
||||
config,
|
||||
} = args;
|
||||
let cwd = config.cwd.to_path_buf();
|
||||
let forced_login_method = config.forced_login_method;
|
||||
let auth_config = config.auth_config();
|
||||
let mut steps: Vec<Step> = Vec::new();
|
||||
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||
!matches!(login_status, LoginStatus::NotAuthenticated),
|
||||
@@ -119,10 +119,12 @@ impl OnboardingScreen {
|
||||
config.animations,
|
||||
)));
|
||||
if show_login_screen {
|
||||
let highlighted_mode = match forced_login_method {
|
||||
Some(ForcedLoginMethod::Api) => SignInOption::ApiKey,
|
||||
_ => SignInOption::ChatGpt,
|
||||
};
|
||||
let highlighted_mode =
|
||||
if auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt) {
|
||||
SignInOption::ChatGpt
|
||||
} else {
|
||||
SignInOption::ApiKey
|
||||
};
|
||||
if let Some(app_server_request_handle) = app_server_request_handle {
|
||||
steps.push(Step::Auth(AuthModeWidget {
|
||||
request_frame: tui.frame_requester(),
|
||||
@@ -131,7 +133,7 @@ impl OnboardingScreen {
|
||||
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
||||
login_status,
|
||||
app_server_request_handle,
|
||||
forced_login_method,
|
||||
auth_config,
|
||||
animations_enabled: config.animations,
|
||||
animations_suppressed: std::cell::Cell::new(false),
|
||||
}));
|
||||
|
||||
@@ -13,9 +13,8 @@ use crate::Cli;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::bootstrap_auth_config;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_bootstrap_http_client_factory;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
@@ -28,7 +27,6 @@ use codex_config::ConfigLoadOptions;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
@@ -380,6 +378,7 @@ async fn start_app_server_for_archive_command(
|
||||
));
|
||||
loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
loader_overrides.ignore_login_requirements = app_server_target.uses_remote_workspace();
|
||||
|
||||
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||
codex_home.as_path(),
|
||||
@@ -394,26 +393,12 @@ async fn start_app_server_for_archive_command(
|
||||
.await
|
||||
.wrap_err("failed to load config.toml")?;
|
||||
let config_toml = &bootstrap_config.config_toml;
|
||||
let chatgpt_base_url = config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
|
||||
let http_client_factory = resolve_bootstrap_http_client_factory(
|
||||
config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
let auth_route_config = AuthRouteConfig::from_http_client_factory(http_client_factory);
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
app_server_target.auth_config_for_cloud_loader(bootstrap_auth_config(
|
||||
codex_home.as_path(),
|
||||
&bootstrap_config,
|
||||
)?),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config_toml.cli_auth_credentials_store.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
auth_route_config,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user