mirror of
https://github.com/openai/codex.git
synced 2026-05-28 06:55:01 +00:00
feat: Add plugin share access controls (#21124)
Extends `plugin/share/save` to accept optional discoverability and shareTargets while uploading plugin contents, and adds `plugin/share/updateTargets` for share-only target updates without re-uploading.
This commit is contained in:
committed by
Channing Conger
parent
5ef71a8e53
commit
a2485575d5
@@ -29,10 +29,17 @@ pub use remote_installed_plugin_sync::RemotePluginCacheMutationGuard;
|
||||
pub use remote_installed_plugin_sync::mark_remote_plugin_cache_mutation_in_flight;
|
||||
pub use remote_installed_plugin_sync::maybe_start_remote_installed_plugin_bundle_sync;
|
||||
pub use remote_installed_plugin_sync::sync_remote_installed_plugin_bundles_once;
|
||||
pub use share::RemotePluginShareAccessPolicy;
|
||||
pub use share::RemotePluginShareDiscoverability;
|
||||
pub use share::RemotePluginSharePrincipal;
|
||||
pub use share::RemotePluginSharePrincipalType;
|
||||
pub use share::RemotePluginShareSaveResult;
|
||||
pub use share::RemotePluginShareTarget;
|
||||
pub use share::RemotePluginShareUpdateTargetsResult;
|
||||
pub use share::delete_remote_plugin_share;
|
||||
pub use share::list_remote_plugin_shares;
|
||||
pub use share::save_remote_plugin_share;
|
||||
pub use share::update_remote_plugin_share_targets;
|
||||
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global";
|
||||
pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "chatgpt-workspace";
|
||||
|
||||
@@ -26,6 +26,46 @@ pub struct RemotePluginShareSaveResult {
|
||||
pub share_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct RemotePluginShareAccessPolicy {
|
||||
pub discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
pub share_targets: Option<Vec<RemotePluginShareTarget>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum RemotePluginShareDiscoverability {
|
||||
Listed,
|
||||
Unlisted,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RemotePluginSharePrincipalType {
|
||||
User,
|
||||
Group,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RemotePluginShareTarget {
|
||||
pub principal_type: RemotePluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct RemotePluginSharePrincipal {
|
||||
pub principal_type: RemotePluginSharePrincipalType,
|
||||
pub principal_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginShareUpdateTargetsResult {
|
||||
pub principals: Vec<RemotePluginSharePrincipal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct RemoteWorkspacePluginUploadUrlRequest<'a> {
|
||||
filename: &'a str,
|
||||
@@ -46,6 +86,10 @@ struct RemoteWorkspacePluginUploadUrlResponse {
|
||||
struct RemoteWorkspacePluginCreateRequest {
|
||||
file_id: String,
|
||||
etag: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
discoverability: Option<RemotePluginShareDiscoverability>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
share_targets: Option<Vec<RemotePluginShareTarget>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
@@ -54,12 +98,23 @@ struct RemoteWorkspacePluginCreateResponse {
|
||||
share_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct RemotePluginShareUpdateTargetsRequest {
|
||||
targets: Vec<RemotePluginShareTarget>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemotePluginShareUpdateTargetsResponse {
|
||||
principals: Vec<RemotePluginSharePrincipal>,
|
||||
}
|
||||
|
||||
pub async fn save_remote_plugin_share(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
codex_home: &Path,
|
||||
plugin_path: &AbsolutePathBuf,
|
||||
remote_plugin_id: Option<&str>,
|
||||
access_policy: RemotePluginShareAccessPolicy,
|
||||
) -> Result<RemotePluginShareSaveResult, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin_path_for_archive = plugin_path.as_path().to_path_buf();
|
||||
@@ -89,6 +144,8 @@ pub async fn save_remote_plugin_share(
|
||||
RemoteWorkspacePluginCreateRequest {
|
||||
file_id: upload.file_id,
|
||||
etag,
|
||||
discoverability: access_policy.discoverability,
|
||||
share_targets: access_policy.share_targets,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -173,6 +230,24 @@ pub async fn delete_remote_plugin_share(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_remote_plugin_share_targets(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
remote_plugin_id: &str,
|
||||
targets: Vec<RemotePluginShareTarget>,
|
||||
) -> Result<RemotePluginShareUpdateTargetsResult, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/public/plugins/{remote_plugin_id}/shares");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.put(&url), auth)?
|
||||
.json(&RemotePluginShareUpdateTargetsRequest { targets });
|
||||
let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?;
|
||||
Ok(RemotePluginShareUpdateTargetsResult {
|
||||
principals: response.principals,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_created_workspace_plugins(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
|
||||
@@ -202,6 +202,17 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
.and(body_json(json!({
|
||||
"file_id": "file_123",
|
||||
"etag": "\"upload_etag_123\"",
|
||||
"discoverability": "PRIVATE",
|
||||
"share_targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
{
|
||||
"principal_type": "workspace",
|
||||
"principal_id": "workspace-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
||||
"plugin_id": "plugins_123",
|
||||
@@ -217,6 +228,19 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
|
||||
codex_home.path(),
|
||||
&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(),
|
||||
},
|
||||
]),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -354,6 +378,7 @@ async fn save_remote_plugin_share_updates_existing_workspace_plugin() {
|
||||
codex_home.path(),
|
||||
&plugin_path,
|
||||
Some("plugins_123"),
|
||||
RemotePluginShareAccessPolicy::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -367,6 +392,83 @@ async fn save_remote_plugin_share_updates_existing_workspace_plugin() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_remote_plugin_share_targets_updates_targets() {
|
||||
let server = MockServer::start().await;
|
||||
let config = test_config(&server);
|
||||
let auth = test_auth();
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/backend-api/public/plugins/plugins_123/shares"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(body_json(json!({
|
||||
"targets": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
},
|
||||
{
|
||||
"principal_type": "group",
|
||||
"principal_id": "group-1",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"principals": [
|
||||
{
|
||||
"principal_type": "user",
|
||||
"principal_id": "user-1",
|
||||
"name": "Gavin",
|
||||
},
|
||||
{
|
||||
"principal_type": "group",
|
||||
"principal_id": "group-1",
|
||||
"name": "Engineering",
|
||||
},
|
||||
],
|
||||
})))
|
||||
.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(),
|
||||
},
|
||||
RemotePluginShareTarget {
|
||||
principal_type: RemotePluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
RemotePluginShareUpdateTargetsResult {
|
||||
principals: vec![
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::User,
|
||||
principal_id: "user-1".to_string(),
|
||||
name: "Gavin".to_string(),
|
||||
},
|
||||
RemotePluginSharePrincipal {
|
||||
principal_type: RemotePluginSharePrincipalType::Group,
|
||||
principal_id: "group-1".to_string(),
|
||||
name: "Engineering".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user