mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: Update plugin share settings with discoverability (#21637)
Requires discoverability on plugin/share/updateTargets so the server can manage workspace link access consistently, including auto-adding the workspace principal for UNLISTED. Also rejects LISTED on share creation and blocks client-supplied workspace principals while preserving response parsing for LISTED.
This commit is contained in:
@@ -35,6 +35,7 @@ pub use share::RemotePluginSharePrincipal;
|
||||
pub use share::RemotePluginSharePrincipalType;
|
||||
pub use share::RemotePluginShareSaveResult;
|
||||
pub use share::RemotePluginShareTarget;
|
||||
pub use share::RemotePluginShareUpdateDiscoverability;
|
||||
pub use share::RemotePluginShareUpdateTargetsResult;
|
||||
pub use share::delete_remote_plugin_share;
|
||||
pub use share::list_remote_plugin_shares;
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct RemotePluginShareAccessPolicy {
|
||||
pub share_targets: Option<Vec<RemotePluginShareTarget>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum RemotePluginShareDiscoverability {
|
||||
Listed,
|
||||
@@ -40,6 +40,13 @@ pub enum RemotePluginShareDiscoverability {
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum RemotePluginShareUpdateDiscoverability {
|
||||
Unlisted,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RemotePluginSharePrincipalType {
|
||||
@@ -64,6 +71,7 @@ pub struct RemotePluginSharePrincipal {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginShareUpdateTargetsResult {
|
||||
pub principals: Vec<RemotePluginSharePrincipal>,
|
||||
pub discoverability: RemotePluginShareDiscoverability,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
@@ -100,12 +108,14 @@ struct RemoteWorkspacePluginCreateResponse {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct RemotePluginShareUpdateTargetsRequest {
|
||||
discoverability: RemotePluginShareUpdateDiscoverability,
|
||||
targets: Vec<RemotePluginShareTarget>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemotePluginShareUpdateTargetsResponse {
|
||||
principals: Vec<RemotePluginSharePrincipal>,
|
||||
discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
}
|
||||
|
||||
pub async fn save_remote_plugin_share(
|
||||
@@ -137,6 +147,9 @@ pub async fn save_remote_plugin_share(
|
||||
.etag
|
||||
.ok_or(RemotePluginCatalogError::MissingUploadEtag)?;
|
||||
put_workspace_plugin_upload(&upload.upload_url, archive_bytes).await?;
|
||||
let share_targets = access_policy.share_targets;
|
||||
let share_targets =
|
||||
ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?;
|
||||
let response = finalize_workspace_plugin_upload(
|
||||
config,
|
||||
auth,
|
||||
@@ -145,7 +158,7 @@ pub async fn save_remote_plugin_share(
|
||||
file_id: upload.file_id,
|
||||
etag,
|
||||
discoverability: access_policy.discoverability,
|
||||
share_targets: access_policy.share_targets,
|
||||
share_targets,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -245,19 +258,64 @@ pub async fn update_remote_plugin_share_targets(
|
||||
auth: Option<&CodexAuth>,
|
||||
remote_plugin_id: &str,
|
||||
targets: Vec<RemotePluginShareTarget>,
|
||||
discoverability: RemotePluginShareUpdateDiscoverability,
|
||||
) -> Result<RemotePluginShareUpdateTargetsResult, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let target_discoverability = match discoverability {
|
||||
RemotePluginShareUpdateDiscoverability::Unlisted => {
|
||||
RemotePluginShareDiscoverability::Unlisted
|
||||
}
|
||||
RemotePluginShareUpdateDiscoverability::Private => {
|
||||
RemotePluginShareDiscoverability::Private
|
||||
}
|
||||
};
|
||||
let targets =
|
||||
ensure_unlisted_workspace_target(auth, Some(target_discoverability), Some(targets))?
|
||||
.unwrap_or_default();
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/public/plugins/{remote_plugin_id}/shares");
|
||||
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.put(&url), auth)?
|
||||
.json(&RemotePluginShareUpdateTargetsRequest { targets });
|
||||
let request = authenticated_request(client.put(&url), auth)?.json(
|
||||
&RemotePluginShareUpdateTargetsRequest {
|
||||
discoverability,
|
||||
targets,
|
||||
},
|
||||
);
|
||||
let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?;
|
||||
Ok(RemotePluginShareUpdateTargetsResult {
|
||||
principals: response.principals,
|
||||
// TODO: Remove this fallback once deployed plugin-service responses always include
|
||||
// discoverability per the API schema.
|
||||
discoverability: response.discoverability.unwrap_or(target_discoverability),
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_unlisted_workspace_target(
|
||||
auth: &CodexAuth,
|
||||
discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
targets: Option<Vec<RemotePluginShareTarget>>,
|
||||
) -> Result<Option<Vec<RemotePluginShareTarget>>, RemotePluginCatalogError> {
|
||||
if discoverability != Some(RemotePluginShareDiscoverability::Unlisted) {
|
||||
return Ok(targets);
|
||||
}
|
||||
let account_id = auth.get_account_id().ok_or_else(|| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(
|
||||
"workspace plugin share requires an account id".to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut targets = targets.unwrap_or_default();
|
||||
if !targets.iter().any(|target| {
|
||||
target.principal_type == RemotePluginSharePrincipalType::Workspace
|
||||
&& target.principal_id == account_id
|
||||
}) {
|
||||
targets.push(RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::Workspace,
|
||||
principal_id: account_id,
|
||||
});
|
||||
}
|
||||
Ok(Some(targets))
|
||||
}
|
||||
|
||||
async fn fetch_created_workspace_plugins(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
|
||||
@@ -204,7 +204,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
.and(body_json(json!({
|
||||
"file_id": "file_123",
|
||||
"etag": "\"upload_etag_123\"",
|
||||
"discoverability": "PRIVATE",
|
||||
"discoverability": "UNLISTED",
|
||||
"share_targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
@@ -212,7 +212,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "workspace-1",
|
||||
"principal_id": "account_id",
|
||||
},
|
||||
],
|
||||
})))
|
||||
@@ -231,17 +231,11 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
&plugin_path,
|
||||
/*remote_plugin_id*/ None,
|
||||
RemotePluginShareAccessPolicy {
|
||||
discoverability: Some(RemotePluginShareDiscoverability::Private),
|
||||
share_targets: Some(vec![
|
||||
RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
},
|
||||
RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::Workspace,
|
||||
principal_id: "workspace-1".to_string(),
|
||||
},
|
||||
]),
|
||||
discoverability: Some(RemotePluginShareDiscoverability::Unlisted),
|
||||
share_targets: Some(vec![RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
}]),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -401,10 +395,11 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
let auth = test_auth();
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/backend-api/public/plugins/plugins_123/shares"))
|
||||
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(json!({
|
||||
"discoverability": "UNLISTED",
|
||||
"targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
@@ -414,6 +409,10 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
"principal_type": "group",
|
||||
"principal_id": "group-1",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "account_id",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
@@ -429,6 +428,7 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
"name": "Engineering",
|
||||
},
|
||||
],
|
||||
"discoverability": "UNLISTED",
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
@@ -448,6 +448,7 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
principal_id: "group-1".to_string(),
|
||||
},
|
||||
],
|
||||
RemotePluginShareUpdateDiscoverability::Unlisted,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -467,6 +468,65 @@ async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
name: "Engineering".to_string(),
|
||||
},
|
||||
],
|
||||
discoverability: RemotePluginShareDiscoverability::Unlisted,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_remote_plugin_share_targets_falls_back_to_requested_discoverability() {
|
||||
let server = MockServer::start().await;
|
||||
let config = test_config(&server);
|
||||
let auth = test_auth();
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(json!({
|
||||
"discoverability": "PRIVATE",
|
||||
"targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"name": "Gavin",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let result = update_remote_plugin_share_targets(
|
||||
&config,
|
||||
Some(&auth),
|
||||
"plugins_123",
|
||||
vec![RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
}],
|
||||
RemotePluginShareUpdateDiscoverability::Private,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
RemotePluginShareUpdateTargetsResult {
|
||||
principals: vec![RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
}],
|
||||
discoverability: RemotePluginShareDiscoverability::Private,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user