mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex-rs] enforce PAT workspace restrictions (#27450)
## Summary - validate a hydrated personal access token's workspace against `forced_chatgpt_workspace_id` before persisting `codex login --with-access-token` - apply the same PAT-only check when restricted auth managers load environment, ephemeral, or persisted credentials - enforce PAT workspace restrictions in the existing central login-restriction path - leave Agent Identity and cloud bootstrap behavior unchanged ## Scope This is intentionally the small PAT-only change. It does not attempt the broader auth-manager/bootstrap unification; that needs separate design work. ## Validation - `CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/tmp/codex-pat-target just test -p codex-login -p codex-cli` (410 passed) - `CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/tmp/codex-pat-target just fix -p codex-login -p codex-cli` - `just fmt` - `git diff --check` Context: https://openai.slack.com/archives/D0AUPLV03RQ/p1781138331548269
This commit is contained in:
1 parent
14df0e8833
commit
b5f1bb75d4
3 files changed
+241
-17
No files matched your search
@@ -279,6 +279,7 @@ impl CodexAuth {
|
||||
codex_home,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
auth_credentials_store_mode,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await
|
||||
@@ -621,11 +622,13 @@ pub async fn login_with_access_token(
|
||||
codex_home: &Path,
|
||||
access_token: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: Option<&[String]>,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = match classify_codex_access_token(access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
PersonalAccessTokenAuth::load(access_token).await?;
|
||||
let auth = PersonalAccessTokenAuth::load(access_token).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
AuthDotJson {
|
||||
// Infer PAT auth from the credential field so older Codex builds can still
|
||||
// deserialize auth.json after a rollback.
|
||||
@@ -658,6 +661,14 @@ pub async fn login_with_access_token(
|
||||
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)
|
||||
}
|
||||
|
||||
fn ensure_personal_access_token_workspace_allowed(
|
||||
expected_workspace_ids: Option<&[String]>,
|
||||
auth: &PersonalAccessTokenAuth,
|
||||
) -> std::io::Result<()> {
|
||||
crate::server::ensure_workspace_account_allowed(expected_workspace_ids, auth.account_id())
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::PermissionDenied, message))
|
||||
}
|
||||
|
||||
/// Writes an in-memory auth payload for externally managed ChatGPT tokens.
|
||||
pub fn login_with_chatgpt_auth_tokens(
|
||||
codex_home: &Path,
|
||||
@@ -714,6 +725,7 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
&config.codex_home,
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
config.auth_credentials_store_mode,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
config.chatgpt_base_url.as_deref(),
|
||||
)
|
||||
.await?
|
||||
@@ -754,10 +766,10 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
|
||||
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
|
||||
let chatgpt_account_id = match &auth {
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
| CodexAuth::BedrockApiKey(_) => return Ok(()),
|
||||
CodexAuth::AgentIdentity(_) => auth.get_account_id(),
|
||||
CodexAuth::ApiKey(_) | CodexAuth::BedrockApiKey(_) => return Ok(()),
|
||||
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => {
|
||||
auth.get_account_id()
|
||||
}
|
||||
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
|
||||
let token_data = match auth.get_token_data() {
|
||||
Ok(data) => data,
|
||||
@@ -833,6 +845,7 @@ async fn load_auth(
|
||||
codex_home: &Path,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: Option<&[String]>,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
) -> std::io::Result<Option<CodexAuth>> {
|
||||
// API key via env var takes precedence over any other auth method.
|
||||
@@ -854,15 +867,18 @@ async fn load_auth(
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, auth)?;
|
||||
}
|
||||
return Ok(Some(auth));
|
||||
}
|
||||
|
||||
if let Some(access_token) = read_codex_access_token_from_env() {
|
||||
return match classify_codex_access_token(&access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
CodexAuth::from_personal_access_token(access_token)
|
||||
.await
|
||||
.map(Some)
|
||||
let auth = PersonalAccessTokenAuth::load(access_token).await?;
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
Ok(Some(CodexAuth::PersonalAccessToken(auth)))
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
CodexAuth::from_agent_identity_jwt(jwt, chatgpt_base_url)
|
||||
@@ -891,6 +907,9 @@ async fn load_auth(
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await?;
|
||||
if let CodexAuth::PersonalAccessToken(auth) = &auth {
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, auth)?;
|
||||
}
|
||||
Ok(Some(auth))
|
||||
}
|
||||
|
||||
@@ -1438,11 +1457,29 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
) -> Self {
|
||||
Self::new_with_workspace_restriction(
|
||||
codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_workspace_restriction(
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
) -> Self {
|
||||
let managed_auth = load_auth(
|
||||
&codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
chatgpt_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
@@ -1458,7 +1495,7 @@ impl AuthManager {
|
||||
auth_change_tx,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
forced_chatgpt_workspace_id: RwLock::new(forced_chatgpt_workspace_id),
|
||||
chatgpt_base_url,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
@@ -1658,10 +1695,12 @@ impl AuthManager {
|
||||
}
|
||||
|
||||
async fn load_auth_from_storage(&self) -> Option<CodexAuth> {
|
||||
let forced_chatgpt_workspace_id = self.forced_chatgpt_workspace_id();
|
||||
load_auth(
|
||||
&self.codex_home,
|
||||
self.enable_codex_api_key_env,
|
||||
self.auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
@@ -1753,15 +1792,16 @@ impl AuthManager {
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
let auth_manager = Self::shared(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
Arc::new(
|
||||
Self::new_with_workspace_restriction(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
config.forced_chatgpt_workspace_id(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id());
|
||||
auth_manager
|
||||
}
|
||||
|
||||
pub fn unauthorized_recovery(self: &Arc<Self>) -> UnauthorizedRecovery {
|
||||
|
||||
Reference in new issue
Block a user