Restore agent identity workspace enforcement

Update enforced ChatGPT workspace checks so AgentIdentity credentials are compared against the configured workspace allowlist instead of skipping workspace enforcement.

Add a regression test for AgentIdentity credentials that belong to a disallowed workspace and verify the auth file is removed.
This commit is contained in:
rreichel3-oai
2026-05-12 10:56:03 -04:00
parent 949f45e0e6
commit 1a7eeaaea8
2 changed files with 79 additions and 15 deletions

View File

@@ -894,6 +894,67 @@ async fn enforce_login_restrictions_allows_any_matching_workspace_in_list() {
.expect("any matching workspace in the allowed list should succeed");
}
#[tokio::test]
#[serial(codex_auth_env)]
async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch() {
let codex_home = tempdir().unwrap();
let _access_token_guard = remove_access_token_env_var();
let record = agent_identity_record("org_another_org");
let agent_identity =
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/agent-identities/jwks"))
.respond_with(ResponseTemplate::new(200).set_body_json(test_jwks_body()))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/v1/agent/agent-runtime-id/task/register"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"task_id": "task-123",
})))
.expect(1)
.mount(&server)
.await;
let chatgpt_base_url = format!("{}/backend-api", server.uri());
let _authapi_guard =
EnvVarGuard::set("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", &chatgpt_base_url);
save_auth(
codex_home.path(),
&AuthDotJson {
auth_mode: Some(ApiAuthMode::AgentIdentity),
openai_api_key: None,
tokens: None,
last_refresh: None,
agent_identity: Some(agent_identity),
},
AuthCredentialsStoreMode::File,
)
.expect("seed agent identity auth");
let config = AuthConfig {
codex_home: codex_home.path().to_path_buf(),
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_login_method: None,
forced_chatgpt_workspace_id: Some(vec!["org_mine".to_string()]),
chatgpt_base_url: Some(chatgpt_base_url),
};
let err = super::enforce_login_restrictions(&config)
.await
.expect_err("expected workspace mismatch to error");
assert!(
err.to_string()
.contains("current credentials belong to org_another_org")
);
assert!(
!codex_home.path().join("auth.json").exists(),
"auth.json should be removed on mismatch"
);
server.verify().await;
}
#[tokio::test]
async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set()
{

View File

@@ -654,25 +654,28 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
}
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
if !auth.is_chatgpt_auth() {
return Ok(());
}
let token_data = match auth.get_token_data() {
Ok(data) => data,
Err(err) => {
return logout_with_message(
&config.codex_home,
format!(
"Failed to load ChatGPT credentials while enforcing workspace restrictions: {err}. Logging out."
),
config.auth_credentials_store_mode,
);
let chatgpt_account_id = match &auth {
CodexAuth::ApiKey(_) => return Ok(()),
CodexAuth::AgentIdentity(_) => auth.get_account_id(),
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
let token_data = match auth.get_token_data() {
Ok(data) => data,
Err(err) => {
return logout_with_message(
&config.codex_home,
format!(
"Failed to load ChatGPT credentials while enforcing workspace restrictions: {err}. Logging out."
),
config.auth_credentials_store_mode,
);
}
};
token_data.id_token.chatgpt_account_id
}
};
// workspace is the external identifier for account id.
let chatgpt_account_id = token_data.id_token.chatgpt_account_id.as_deref();
let chatgpt_account_id = chatgpt_account_id.as_deref();
if !chatgpt_account_id.is_some_and(|actual| {
expected_account_ids
.iter()