Skip to content

Commit 6873833

Browse files
committed
use org_id on POST /v1/api_key
1 parent 29191c0 commit 6873833

5 files changed

Lines changed: 157 additions & 4 deletions

File tree

src/http.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub struct ApiClient {
2828
http: Client,
2929
base_url: String,
3030
api_key: String,
31+
org_id: String,
3132
org_name: String,
3233
}
3334

@@ -60,6 +61,7 @@ impl ApiClient {
6061
http,
6162
base_url: ctx.api_url.trim_end_matches('/').to_string(),
6263
api_key: ctx.login.api_key().context("login state missing API key")?,
64+
org_id: ctx.login.org_id().unwrap_or_default(),
6365
org_name: ctx.login.org_name().unwrap_or_default(),
6466
})
6567
}
@@ -77,6 +79,10 @@ impl ApiClient {
7779
&self.base_url
7880
}
7981

82+
pub fn org_id(&self) -> &str {
83+
&self.org_id
84+
}
85+
8086
pub fn org_name(&self) -> &str {
8187
&self.org_name
8288
}

src/setup/mod.rs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,13 @@ async fn maybe_create_api_key_for_oauth(base: &BaseArgs, client: &ApiClient) ->
17721772
key: String,
17731773
}
17741774

1775+
let org_id = client.org_id().trim();
1776+
if org_id.is_empty() {
1777+
bail!(
1778+
"setup could not determine the current org_id for API key creation; rerun with a direct API key or re-authenticate so setup can resolve the selected organization"
1779+
);
1780+
}
1781+
17751782
let existing: Vec<String> = client
17761783
.get::<ApiKeyList>("/v1/api_key")
17771784
.await
@@ -1790,7 +1797,7 @@ async fn maybe_create_api_key_for_oauth(base: &BaseArgs, client: &ApiClient) ->
17901797
.expect("name sequence is infinite")
17911798
};
17921799

1793-
let body = serde_json::json!({ "name": name, "org_name": client.org_name() });
1800+
let body = serde_json::json!({ "name": name, "org_id": org_id });
17941801
let created: CreatedKey = client.post("/v1/api_key", &body).await?;
17951802

17961803
let explicitly_quiet = base.quiet && base.quiet_source.is_some();
@@ -4916,8 +4923,11 @@ fn print_mcp_human_report(
49164923
#[cfg(test)]
49174924
mod tests {
49184925
use super::*;
4926+
use crate::auth::LoginContext;
49194927
use std::env;
49204928
use std::ffi::OsString;
4929+
use std::io::{Read, Write};
4930+
use std::net::TcpListener;
49214931
use std::sync::{Mutex, OnceLock};
49224932
use std::time::{SystemTime, UNIX_EPOCH};
49234933

@@ -4970,6 +4980,146 @@ mod tests {
49704980
}
49714981
}
49724982

4983+
fn make_login_context(
4984+
api_url: String,
4985+
app_url: String,
4986+
org_id: &str,
4987+
org_name: &str,
4988+
) -> LoginContext {
4989+
let login = braintrust_sdk_rust::LoginState::new();
4990+
let _ = login.set(
4991+
"test-api-key".to_string(),
4992+
org_id.to_string(),
4993+
org_name.to_string(),
4994+
api_url.clone(),
4995+
app_url.clone(),
4996+
);
4997+
4998+
LoginContext {
4999+
login,
5000+
api_url,
5001+
app_url,
5002+
}
5003+
}
5004+
5005+
#[tokio::test]
5006+
async fn maybe_create_api_key_for_oauth_uses_org_id_in_request_body() {
5007+
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
5008+
let addr = listener.local_addr().expect("listener addr");
5009+
let server = std::thread::spawn(move || {
5010+
let (mut stream, _) = listener.accept().expect("accept list request");
5011+
let mut buffer = [0u8; 4096];
5012+
let read = stream.read(&mut buffer).expect("read list request");
5013+
let request = String::from_utf8_lossy(&buffer[..read]);
5014+
assert!(request.starts_with("GET /v1/api_key HTTP/1.1"));
5015+
let response = concat!(
5016+
"HTTP/1.1 200 OK\r\n",
5017+
"Content-Type: application/json\r\n",
5018+
"Content-Length: 14\r\n",
5019+
"Connection: close\r\n",
5020+
"\r\n",
5021+
"{\"objects\":[]}"
5022+
);
5023+
stream
5024+
.write_all(response.as_bytes())
5025+
.expect("write list response");
5026+
stream.flush().expect("flush list response");
5027+
drop(stream);
5028+
5029+
let (mut stream, _) = listener.accept().expect("accept create request");
5030+
let mut header_buf = Vec::new();
5031+
let mut temp = [0u8; 1024];
5032+
let header_end;
5033+
loop {
5034+
let read = stream.read(&mut temp).expect("read create request");
5035+
assert!(read > 0, "request closed before headers");
5036+
header_buf.extend_from_slice(&temp[..read]);
5037+
if let Some(pos) = header_buf.windows(4).position(|w| w == b"\r\n\r\n") {
5038+
header_end = pos + 4;
5039+
break;
5040+
}
5041+
}
5042+
let headers = String::from_utf8_lossy(&header_buf[..header_end]);
5043+
assert!(headers.starts_with("POST /v1/api_key HTTP/1.1"));
5044+
let content_length = headers
5045+
.split("\r\n")
5046+
.find_map(|line| {
5047+
let (name, value) = line.split_once(':')?;
5048+
name.eq_ignore_ascii_case("content-length")
5049+
.then(|| value.trim().parse::<usize>().expect("content length"))
5050+
})
5051+
.expect("content-length header");
5052+
let mut body = header_buf[header_end..].to_vec();
5053+
while body.len() < content_length {
5054+
let read = stream.read(&mut temp).expect("read request body");
5055+
assert!(read > 0, "request closed before body completed");
5056+
body.extend_from_slice(&temp[..read]);
5057+
}
5058+
let json: serde_json::Value =
5059+
serde_json::from_slice(&body[..content_length]).expect("parse request body");
5060+
assert_eq!(json.get("org_id").and_then(|v| v.as_str()), Some("org_123"));
5061+
assert!(json.get("org_name").is_none());
5062+
assert!(json.get("name").and_then(|v| v.as_str()).is_some());
5063+
5064+
let response = concat!(
5065+
"HTTP/1.1 200 OK\r\n",
5066+
"Content-Type: application/json\r\n",
5067+
"Content-Length: 17\r\n",
5068+
"Connection: close\r\n",
5069+
"\r\n",
5070+
"{\"key\":\"new-key\"}"
5071+
);
5072+
stream
5073+
.write_all(response.as_bytes())
5074+
.expect("write create response");
5075+
stream.flush().expect("flush create response");
5076+
});
5077+
5078+
let mut base = make_base_args();
5079+
base.quiet = true;
5080+
base.quiet_source = Some(ArgValueSource::CommandLine);
5081+
5082+
let api_url = format!("http://{addr}");
5083+
let ctx = make_login_context(
5084+
api_url,
5085+
"https://app.example.test".to_string(),
5086+
"org_123",
5087+
"Acme",
5088+
);
5089+
let client = ApiClient::new(&ctx).expect("client");
5090+
5091+
let key = maybe_create_api_key_for_oauth(&base, &client)
5092+
.await
5093+
.expect("create api key");
5094+
assert_eq!(key, "new-key");
5095+
5096+
server.join().expect("server join");
5097+
}
5098+
5099+
#[tokio::test]
5100+
async fn maybe_create_api_key_for_oauth_requires_org_id() {
5101+
let mut base = make_base_args();
5102+
base.quiet = true;
5103+
base.quiet_source = Some(ArgValueSource::CommandLine);
5104+
5105+
let ctx = make_login_context(
5106+
"https://api.example.test".to_string(),
5107+
"https://app.example.test".to_string(),
5108+
"",
5109+
"Acme",
5110+
);
5111+
let client = ApiClient::new(&ctx).expect("client");
5112+
5113+
let err = maybe_create_api_key_for_oauth(&base, &client)
5114+
.await
5115+
.expect_err("missing org_id should fail");
5116+
let err_text = format!("{err:#}");
5117+
assert!(
5118+
err_text.contains("org_id") && err_text.contains("API key creation"),
5119+
"unexpected error: {err_text}"
5120+
);
5121+
}
5122+
49735123
#[test]
49745124
fn single_path_agent_is_selected_by_default() {
49755125
let detected = vec![DetectionSignal {

src/sync.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use crate::experiments::api::create_experiment;
2626
use crate::http::ApiClient;
2727
use crate::projects::api::{create_project, list_projects, Project};
2828
use crate::ui::{animations_enabled, fuzzy_select, is_quiet};
29-
use crate::utils::parse_duration_to_seconds;
3029

3130
const STATE_SCHEMA_VERSION: u32 = 1;
3231
const DEFAULT_PULL_LIMIT: usize = 100;

src/traces.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ use crate::args::BaseArgs;
3636
use crate::auth::{self, login};
3737
use crate::http::ApiClient;
3838
use crate::ui::{fuzzy_select, is_interactive, with_spinner};
39-
use crate::utils::parse_duration_to_seconds;
4039

4140
const MAX_TRACE_SPANS: usize = 5000;
4241
const MAX_BTQL_PAGE_LIMIT: usize = 1000;

src/utils/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ mod ids;
55
mod json_object;
66
mod plurals;
77

8-
pub use duration::parse_duration_to_seconds;
98
pub use fs_atomic::write_text_atomic;
109
pub use git::GitRepo;
1110
pub(crate) use ids::new_uuid_id;

0 commit comments

Comments
 (0)