mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Support multiple forced ChatGPT workspaces (#18161)
## Summary This change lets `forced_chatgpt_workspace_id` accept multiple workspace IDs instead of a single value. It keeps the existing config key name, adds backward-compatible parsing for a single string in `config.toml`, and normalizes the setting into an allowed workspace list across login enforcement, app-server config surfaces, and local ChatGPT auth helpers. ## Why Workspace-restricted deployments may need to allow more than one ChatGPT workspace without dropping the guardrail entirely. ## Server-side impact Codex's local server and app-server protocol needed changes because they previously assumed a single workspace ID. The local login flow now matches the auth backend interface by sending the allowed workspace list as a single comma-separated `allowed_workspace_id` query parameter. ## Validation This was tested with: - A single workspace config - With multi-workspace configs - With multiple workspaces in the config - The user only being a part of a subset of them All were successful. Automated coverage: - `cargo test -p codex-login` - `cargo test -p codex-app-server-protocol` - `cargo test -p codex-tui local_chatgpt_auth` - `cargo test --locked -p codex-app-server login_account_chatgpt_includes_forced_workspace_allowlist_query_param`
This commit is contained in:
1 parent
32b45a43e2
commit
02a7205250
22 files changed
+673
-117
No files matched your search
@@ -610,8 +610,8 @@ pub struct AuthConfig {
|
||||
pub codex_home: PathBuf,
|
||||
pub auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub forced_chatgpt_workspace_id: Option<String>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
@@ -653,9 +653,8 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(expected_account_id) = config.forced_chatgpt_workspace_id.as_deref() {
|
||||
// workspace is the external identifier for account id.
|
||||
let chatgpt_account_id = match auth {
|
||||
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
|
||||
let chatgpt_account_id = match &auth {
|
||||
CodexAuth::ApiKey(_) => return Ok(()),
|
||||
CodexAuth::AgentIdentity(_) => auth.get_account_id(),
|
||||
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
|
||||
@@ -674,13 +673,21 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
token_data.id_token.chatgpt_account_id
|
||||
}
|
||||
};
|
||||
if chatgpt_account_id.as_deref() != Some(expected_account_id) {
|
||||
|
||||
// workspace is the external identifier for account id.
|
||||
let chatgpt_account_id = chatgpt_account_id.as_deref();
|
||||
if !chatgpt_account_id.is_some_and(|actual| {
|
||||
expected_account_ids
|
||||
.iter()
|
||||
.any(|expected| expected == actual)
|
||||
}) {
|
||||
let expected_workspaces = expected_account_ids.join(", ");
|
||||
let message = match chatgpt_account_id {
|
||||
Some(actual) => format!(
|
||||
"Login is restricted to workspace {expected_account_id}, but current credentials belong to {actual}. Logging out."
|
||||
"Login is restricted to workspace(s) {expected_workspaces}, but current credentials belong to {actual}. Logging out."
|
||||
),
|
||||
None => format!(
|
||||
"Login is restricted to workspace {expected_account_id}, but current credentials lack a workspace identifier. Logging out."
|
||||
"Login is restricted to workspace(s) {expected_workspaces}, but current credentials lack a workspace identifier. Logging out."
|
||||
),
|
||||
};
|
||||
return logout_with_message(
|
||||
@@ -1247,7 +1254,7 @@ pub struct AuthManager {
|
||||
inner: RwLock<CachedAuth>,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<String>>,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
@@ -1266,8 +1273,8 @@ pub trait AuthManagerConfig {
|
||||
/// Returns the CLI auth credential storage mode for auth loading.
|
||||
fn cli_auth_credentials_store_mode(&self) -> AuthCredentialsStoreMode;
|
||||
|
||||
/// Returns the workspace ID that ChatGPT auth should be restricted to, if any.
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<String>;
|
||||
/// Returns the workspace IDs that ChatGPT auth should be restricted to, if any.
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>>;
|
||||
|
||||
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
|
||||
fn chatgpt_base_url(&self) -> String;
|
||||
@@ -1548,7 +1555,7 @@ impl AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<String>) {
|
||||
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<Vec<String>>) {
|
||||
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write()
|
||||
&& *guard != workspace_id
|
||||
{
|
||||
@@ -1556,7 +1563,7 @@ impl AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forced_chatgpt_workspace_id(&self) -> Option<String> {
|
||||
pub fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
|
||||
self.forced_chatgpt_workspace_id
|
||||
.read()
|
||||
.ok()
|
||||
@@ -1836,13 +1843,13 @@ impl AuthManager {
|
||||
"external auth refresh did not return ChatGPT metadata",
|
||||
)));
|
||||
};
|
||||
if let Some(expected_workspace_id) = forced_chatgpt_workspace_id.as_deref()
|
||||
&& chatgpt_metadata.account_id != expected_workspace_id
|
||||
if let Some(expected_workspace_ids) = forced_chatgpt_workspace_id.as_deref()
|
||||
&& !expected_workspace_ids.contains(&chatgpt_metadata.account_id)
|
||||
{
|
||||
return Err(RefreshTokenError::Transient(std::io::Error::other(
|
||||
format!(
|
||||
"external auth refresh returned workspace {:?}, expected {expected_workspace_id:?}",
|
||||
chatgpt_metadata.account_id,
|
||||
"external auth refresh returned workspace {:?}, expected one of {:?}",
|
||||
chatgpt_metadata.account_id, expected_workspace_ids,
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user