mirror of
https://github.com/openai/codex.git
synced 2026-09-28 08:43:01 +08:00
Expose effective login methods in config requirements (#45495)
## Why Configuration requirements did not report which login methods the running app server permits after applying managed policy, forced login settings, and workspace restrictions. ## What changed - Add `allowedLoginMethods` to `configRequirements/read`, using the running authentication manager's effective policy rather than newly read authentication settings. - Return requirements when login methods are restricted even without managed requirements, while preserving `requirements: null` for the unrestricted default. - Update protocol schemas and generated TypeScript and Python types. An empty list permits no login method; older servers may omit the field. ## Testing Add coverage for managed and forced login restrictions, workspace intersections, policy reporting after requirements files change, invalid login methods, and API-only Amazon Bedrock without ChatGPT requests. Extend tests for conflicting authentication requirements and cloud policy precedence. GitOrigin-RevId: 56c0767a74143e793aac2ac165d0cbe98a09469b
This commit is contained in:
+10
@@ -9828,6 +9828,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedLoginMethods": {
|
||||
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ForcedLoginMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
|
||||
+10
@@ -5708,6 +5708,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedLoginMethods": {
|
||||
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ForcedLoginMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
|
||||
+17
@@ -414,6 +414,16 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedLoginMethods": {
|
||||
"description": "Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ForcedLoginMethod"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowedPermissionProfiles": {
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
@@ -762,6 +772,13 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ForcedLoginMethod": {
|
||||
"enum": [
|
||||
"chatgpt",
|
||||
"api"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"InAppBrowserRequirements": {
|
||||
"properties": {
|
||||
"allowExternalBrowserSettingsImport": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ForcedLoginMethod } from "../ForcedLoginMethod";
|
||||
import type { PathUri } from "../PathUri";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
@@ -22,4 +23,8 @@ export type ConfigRequirements = {/**
|
||||
modelProvider: string | null, /**
|
||||
* Complete required provider definitions, using config.toml field names.
|
||||
*/
|
||||
modelProviders: { [key in string]?: JsonValue } | null, cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, additionalDeveloperInstructions: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowBrowserAndComputerUse: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
|
||||
modelProviders: { [key in string]?: JsonValue } | null, /**
|
||||
* Effective login methods after managed, forced-login, and workspace restrictions.
|
||||
* An empty list permits no login method. Older servers may omit this field.
|
||||
*/
|
||||
allowedLoginMethods: Array<ForcedLoginMethod> | null, cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, additionalDeveloperInstructions: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowBrowserAndComputerUse: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
|
||||
|
||||
@@ -412,6 +412,9 @@ pub struct ConfigRequirements {
|
||||
pub model_provider: Option<String>,
|
||||
/// Complete required provider definitions, using config.toml field names.
|
||||
pub model_providers: Option<HashMap<String, JsonValue>>,
|
||||
/// Effective login methods after managed, forced-login, and workspace restrictions.
|
||||
/// An empty list permits no login method. Older servers may omit this field.
|
||||
pub allowed_login_methods: Option<Vec<ForcedLoginMethod>>,
|
||||
pub cli_auth_credentials_store: Option<CliAuthCredentialsStoreMode>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub additional_developer_instructions: Option<String>,
|
||||
|
||||
@@ -2120,6 +2120,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
||||
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
|
||||
model_provider: None,
|
||||
model_providers: None,
|
||||
allowed_login_methods: None,
|
||||
application: None,
|
||||
cli_auth_credentials_store: None,
|
||||
chatgpt_base_url: None,
|
||||
|
||||
@@ -2630,3 +2630,45 @@ exclude = ["AWS_*"]
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allowed_login_methods_follow_current_forced_workspaces() -> Result<()> {
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
let tmp = tempdir()?;
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
|
||||
std::fs::write(
|
||||
tmp.path().join("requirements.toml"),
|
||||
"allowed_chatgpt_workspaces = ['managed']",
|
||||
)?;
|
||||
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 config = service.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let auth = codex_login::AuthManager::shared_from_config(
|
||||
&config, /*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await?;
|
||||
for (workspaces, expected) in [
|
||||
(
|
||||
Some(vec!["managed".to_string()]),
|
||||
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
|
||||
),
|
||||
(
|
||||
Some(vec!["other".to_string()]),
|
||||
vec![ForcedLoginMethod::Api],
|
||||
),
|
||||
(Some(Vec::new()), vec![ForcedLoginMethod::Api]),
|
||||
(
|
||||
None,
|
||||
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
|
||||
),
|
||||
] {
|
||||
auth.set_forced_chatgpt_workspace_id(workspaces);
|
||||
assert_eq!(auth.allowed_login_methods(), expected);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ use codex_features::canonical_feature_for_key;
|
||||
use codex_features::feature_for_key;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
@@ -135,8 +136,11 @@ impl ConfigRequestProcessor {
|
||||
.config_manager
|
||||
.read_requirements()
|
||||
.await
|
||||
.map_err(map_error)?
|
||||
.map(map_requirements_toml_to_api);
|
||||
.map_err(map_error)?;
|
||||
let requirements = map_requirements_to_api(
|
||||
requirements,
|
||||
self.thread_manager.auth_manager().allowed_login_methods(),
|
||||
);
|
||||
|
||||
Ok(ConfigRequirementsReadResponse { requirements })
|
||||
}
|
||||
@@ -381,13 +385,23 @@ pub(super) async fn reload_user_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements {
|
||||
fn map_requirements_to_api(
|
||||
requirements: Option<ConfigRequirementsToml>,
|
||||
allowed_login_methods: Vec<ForcedLoginMethod>,
|
||||
) -> Option<ConfigRequirements> {
|
||||
let requirements = match requirements {
|
||||
Some(requirements) => requirements,
|
||||
None if allowed_login_methods == [ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt] => {
|
||||
return None;
|
||||
}
|
||||
None => ConfigRequirementsToml::default(),
|
||||
};
|
||||
let windows_sandbox_private_desktop = requirements
|
||||
.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.sandbox_private_desktop);
|
||||
|
||||
ConfigRequirements {
|
||||
Some(ConfigRequirements {
|
||||
model_provider: requirements.model_provider,
|
||||
model_providers: requirements.model_providers.map(|providers| {
|
||||
providers
|
||||
@@ -395,6 +409,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
.map(|(id, provider)| (id, serde_json::json!(provider)))
|
||||
.collect()
|
||||
}),
|
||||
allowed_login_methods: Some(allowed_login_methods),
|
||||
application: requirements.application.map(|application| {
|
||||
codex_app_server_protocol::ApplicationRequirements {
|
||||
network: application.network.map(|network| {
|
||||
@@ -522,7 +537,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
||||
enabled: feedback.enabled,
|
||||
}),
|
||||
windows_sandbox_private_desktop,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn map_computer_use_requirements_to_api(
|
||||
@@ -828,7 +843,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::map_requirements_toml_to_api;
|
||||
use super::map_requirements_to_api;
|
||||
use codex_app_server_protocol::AllowDenyRequirement;
|
||||
use codex_app_server_protocol::AutoReviewRequirements;
|
||||
use codex_app_server_protocol::BrowserUseAccessApprovalLifetime;
|
||||
@@ -854,15 +869,26 @@ mod tests {
|
||||
use codex_config::NewThreadModelDefaultsToml;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
use codex_config::types::FeedbackConfigToml;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn map_test_requirements(
|
||||
requirements: ConfigRequirementsToml,
|
||||
) -> codex_app_server_protocol::ConfigRequirements {
|
||||
map_requirements_to_api(
|
||||
Some(requirements),
|
||||
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
|
||||
)
|
||||
.expect("requirements")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_managed_hooks_only() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
allow_managed_hooks_only: Some(true),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
@@ -873,7 +899,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_permission_default_and_allowlist() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
allowed_permission_profiles: Some(BTreeMap::from([
|
||||
("managed-build".to_string(), false),
|
||||
("managed-standard".to_string(), true),
|
||||
@@ -897,7 +923,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_appshots() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
allow_appshots: Some(false),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
@@ -908,7 +934,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allow_remote_control() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
allow_remote_control: Some(false),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
@@ -918,7 +944,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_model_auto_review_and_new_thread_defaults() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
auto_review: Some(AutoReviewRequirementsToml {
|
||||
required_on_models: Some(vec!["gpt-protected".to_string()]),
|
||||
ignore_rules: Some(vec!["gpt-protected".to_string()]),
|
||||
@@ -952,7 +978,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_browser_and_computer_use_requirements() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
allow_browser_and_computer_use: Some(false),
|
||||
browser_use: Some(BrowserUseRequirementsToml {
|
||||
allow_webmcp: Some(true),
|
||||
@@ -1070,7 +1096,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_allowed_windows_sandbox_implementations() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
windows: Some(WindowsRequirementsToml {
|
||||
allowed_sandbox_implementations: Some(vec![
|
||||
codex_config::types::WindowsSandboxModeToml::Elevated,
|
||||
@@ -1100,7 +1126,7 @@ mod tests {
|
||||
let model_catalog_json =
|
||||
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json"))
|
||||
.expect("managed model catalog path should be absolute");
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
let mapped = map_test_requirements(ConfigRequirementsToml {
|
||||
sqlite_home: Some(sqlite_home.clone()),
|
||||
log_dir: Some(log_dir.clone()),
|
||||
model_catalog_json: Some(model_catalog_json.clone()),
|
||||
|
||||
@@ -103,12 +103,24 @@ foo = "bar"
|
||||
|
||||
#[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",
|
||||
for (requirements, config) in [
|
||||
("allowed_login_methods = []", ""),
|
||||
(
|
||||
"allowed_login_methods = ['chatgpt']\nallowed_chatgpt_workspaces = []",
|
||||
"",
|
||||
),
|
||||
(
|
||||
"allowed_login_methods = ['api']",
|
||||
"forced_login_method = 'chatgpt'",
|
||||
),
|
||||
(
|
||||
"allowed_login_methods = ['chatgpt']\nallowed_chatgpt_workspaces = ['managed']",
|
||||
"forced_chatgpt_workspace_id = ['other']",
|
||||
),
|
||||
] {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("requirements.toml"), requirements)?;
|
||||
std::fs::write(codex_home.path().join("config.toml"), config)?;
|
||||
|
||||
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
|
||||
.env("CODEX_HOME", codex_home.path())
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Requirements reads report the authentication policy enforced by the running server.
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::MockServer;
|
||||
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
|
||||
|
||||
async fn start_server(
|
||||
config: &str,
|
||||
requirements: Option<&str>,
|
||||
) -> Result<(TempDir, TestAppServer)> {
|
||||
let home = TempDir::new()?;
|
||||
std::fs::write(home.path().join("config.toml"), config)?;
|
||||
if let Some(requirements) = requirements {
|
||||
std::fs::write(home.path().join("requirements.toml"), requirements)?;
|
||||
}
|
||||
let server = TestAppServer::builder()
|
||||
.with_codex_home(home.path())
|
||||
.build_initialized_with_timeout(READ_TIMEOUT)
|
||||
.await?;
|
||||
Ok((home, server))
|
||||
}
|
||||
|
||||
async fn read_requirements(server: &mut TestAppServer) -> Result<Value> {
|
||||
let id = server.send_config_requirements_read_request().await?;
|
||||
timeout(READ_TIMEOUT, server.read_response(id)).await?
|
||||
}
|
||||
|
||||
#[test_case(""; "no_requirements")]
|
||||
#[test_case("forced_chatgpt_workspace_id = []"; "empty_forced_workspaces_are_unrestricted")]
|
||||
#[test_case("forced_chatgpt_workspace_id = ['managed']"; "forced_workspace_does_not_exclude_api")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_preserves_unrestricted_default(config: &str) -> Result<()> {
|
||||
let (_home, mut server) = start_server(config, /*requirements*/ None).await?;
|
||||
assert_eq!(
|
||||
read_requirements(&mut server).await?,
|
||||
json!({"requirements": null})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case("allow_remote_control = false", "", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "unrestricted_with_other_requirements")]
|
||||
#[test_case("allowed_login_methods = ['api']", "", &[ForcedLoginMethod::Api]; "managed_api")]
|
||||
#[test_case("allowed_login_methods = ['chatgpt']", "", &[ForcedLoginMethod::Chatgpt]; "managed_chatgpt")]
|
||||
#[test_case("allowed_login_methods = ['chatgpt', 'api', 'api']", "", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "both_normalized")]
|
||||
#[test_case("", "forced_login_method = 'api'", &[ForcedLoginMethod::Api]; "forced_api_without_requirements")]
|
||||
#[test_case("", "forced_login_method = 'chatgpt'", &[ForcedLoginMethod::Chatgpt]; "forced_chatgpt_without_requirements")]
|
||||
#[test_case("allowed_login_methods = ['chatgpt', 'api']", "forced_login_method = 'api'", &[ForcedLoginMethod::Api]; "forced_narrows_managed")]
|
||||
#[test_case("allowed_chatgpt_workspaces = []", "", &[ForcedLoginMethod::Api]; "empty_managed_workspaces")]
|
||||
#[test_case("allowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other']", &[ForcedLoginMethod::Api]; "disjoint_workspaces")]
|
||||
#[test_case("allowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other', 'managed']", &[ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt]; "overlapping_workspaces")]
|
||||
#[test_case("allowed_login_methods = ['api']\nallowed_chatgpt_workspaces = ['managed']", "forced_chatgpt_workspace_id = ['other']", &[ForcedLoginMethod::Api]; "api_only_ignores_workspace_mismatch")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_exposes_effective_login_methods(
|
||||
requirements: &str,
|
||||
config: &str,
|
||||
expected: &[ForcedLoginMethod],
|
||||
) -> Result<()> {
|
||||
let (_home, mut server) = start_server(config, Some(requirements)).await?;
|
||||
let wire = read_requirements(&mut server).await?;
|
||||
assert_eq!(wire["requirements"]["allowedLoginMethods"], json!(expected));
|
||||
if !expected.contains(&ForcedLoginMethod::Chatgpt) {
|
||||
let id = server.send_login_account_chatgpt_request().await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
server.read_stream_until_error_message(RequestId::Integer(id)),
|
||||
)
|
||||
.await??;
|
||||
assert!(error.error.message.contains("disabled"), "{error:?}");
|
||||
}
|
||||
if !expected.contains(&ForcedLoginMethod::Api) {
|
||||
let id = server.send_login_account_api_key_request("sk-test").await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
server.read_stream_until_error_message(RequestId::Integer(id)),
|
||||
)
|
||||
.await??;
|
||||
assert!(error.error.message.contains("disabled"), "{error:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(""; "removed")]
|
||||
#[test_case("allowed_login_methods = ['chatgpt']"; "changed")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_uses_running_auth_policy(refreshed: &str) -> Result<()> {
|
||||
let (home, mut server) = start_server("", Some("allowed_login_methods = ['api']")).await?;
|
||||
std::fs::write(home.path().join("requirements.toml"), refreshed)?;
|
||||
assert_eq!(
|
||||
read_requirements(&mut server).await?["requirements"]["allowedLoginMethods"],
|
||||
json!(["api"])
|
||||
);
|
||||
let id = server.send_login_account_chatgpt_request().await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
server.read_stream_until_error_message(RequestId::Integer(id)),
|
||||
)
|
||||
.await??;
|
||||
assert!(error.error.message.contains("disabled"), "{error:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_rejects_invalid_login_method() -> Result<()> {
|
||||
let (home, mut server) = start_server("", /*requirements*/ None).await?;
|
||||
std::fs::write(
|
||||
home.path().join("requirements.toml"),
|
||||
"allowed_login_methods = ['saml']",
|
||||
)?;
|
||||
let id = server.send_config_requirements_read_request().await?;
|
||||
let error = timeout(
|
||||
READ_TIMEOUT,
|
||||
server.read_stream_until_error_message(RequestId::Integer(id)),
|
||||
)
|
||||
.await??;
|
||||
assert!(
|
||||
error.error.message.contains("allowed_login_methods"),
|
||||
"{error:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_requirements_read_preserves_api_only_bedrock_without_chatgpt_requests() -> Result<()>
|
||||
{
|
||||
let backend = MockServer::start().await;
|
||||
let (_home, mut server) = start_server(
|
||||
&format!(
|
||||
r#"
|
||||
forced_login_method = "api"
|
||||
chatgpt_base_url = "{}/backend-api"
|
||||
model_provider = "amazon-bedrock"
|
||||
[model_providers.amazon-bedrock]
|
||||
base_url = "https://bedrock.example.com/v1"
|
||||
[model_providers.amazon-bedrock.auth]
|
||||
command = "print-token"
|
||||
"#,
|
||||
backend.uri()
|
||||
),
|
||||
/*requirements*/ None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
read_requirements(&mut server).await?["requirements"]["allowedLoginMethods"],
|
||||
json!(["api"])
|
||||
);
|
||||
let id = server
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let account: GetAccountResponse = timeout(READ_TIMEOUT, server.read_response(id)).await??;
|
||||
assert_eq!(
|
||||
account,
|
||||
GetAccountResponse {
|
||||
account: Some(Account::AmazonBedrock {
|
||||
uses_codex_managed_credentials: false
|
||||
}),
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
backend
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("recorded requests")
|
||||
.is_empty()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -21,6 +21,7 @@ mod config_requirements_browser_use;
|
||||
#[path = "config_requirements_exec_tests.rs"]
|
||||
mod config_requirements_exec;
|
||||
mod config_requirements_in_app_browser;
|
||||
mod config_requirements_login;
|
||||
mod config_rpc;
|
||||
#[cfg(unix)]
|
||||
#[path = "connection_handling_stdio_tests.rs"]
|
||||
|
||||
@@ -65,24 +65,30 @@ fn cloud_auth_requirements_do_not_override_local_or_discard_other_policy() {
|
||||
cli_auth_credentials_store = "keyring"
|
||||
chatgpt_base_url = "https://managed.example/backend-api/""#,
|
||||
);
|
||||
let cloud = layer(
|
||||
"req_cloud",
|
||||
"Cloud policy",
|
||||
for cloud_auth in [
|
||||
r#"allowed_login_methods = ["api", "chatgpt"]
|
||||
allowed_chatgpt_workspaces = ["other"]"#,
|
||||
r#"allowed_login_methods = ["saml"]
|
||||
allowed_chatgpt_workspaces = "invalid"
|
||||
cli_auth_credentials_store = "invalid"
|
||||
chatgpt_base_url = false
|
||||
allow_login_shell = false"#,
|
||||
);
|
||||
assert_eq!(
|
||||
compose(vec![local, cloud]).expect("cloud auth cannot invalidate enterprise policy"),
|
||||
Some(expected_requirements(
|
||||
r#"allowed_login_methods = ["api"]
|
||||
chatgpt_base_url = false"#,
|
||||
] {
|
||||
let cloud = layer(
|
||||
"req_cloud",
|
||||
"Cloud policy",
|
||||
&format!("{cloud_auth}\nallow_login_shell = false"),
|
||||
);
|
||||
assert_eq!(
|
||||
compose(vec![local.clone(), cloud])
|
||||
.expect("cloud auth cannot invalidate enterprise policy"),
|
||||
Some(expected_requirements(
|
||||
r#"allowed_login_methods = ["api"]
|
||||
cli_auth_credentials_store = "keyring"
|
||||
chatgpt_base_url = "https://managed.example/backend-api/"
|
||||
allow_login_shell = false"#
|
||||
))
|
||||
);
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2691,7 +2691,8 @@ impl AuthManager {
|
||||
)
|
||||
}
|
||||
|
||||
fn allowed_login_methods(&self) -> Vec<ForcedLoginMethod> {
|
||||
/// Returns the login methods permitted by the current effective authentication policy.
|
||||
pub 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(),
|
||||
|
||||
@@ -11280,6 +11280,13 @@ class ConfigRequirements(BaseModel):
|
||||
allowed_approval_policies: Annotated[
|
||||
list[AskForApproval] | None, Field(alias="allowedApprovalPolicies")
|
||||
] = None
|
||||
allowed_login_methods: Annotated[
|
||||
list[ForcedLoginMethod] | None,
|
||||
Field(
|
||||
alias="allowedLoginMethods",
|
||||
description="Effective login methods after managed, forced-login, and workspace restrictions. An empty list permits no login method. Older servers may omit this field.",
|
||||
),
|
||||
] = None
|
||||
allowed_permission_profiles: Annotated[
|
||||
dict[str, Any] | None, Field(alias="allowedPermissionProfiles")
|
||||
] = None
|
||||
|
||||
Reference in New Issue
Block a user