diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index be95843f6b..f5c7c94480 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -13114,6 +13114,95 @@ mod tests { assert!(error.message().contains("already defines endpoints")); } + #[tokio::test] + async fn provider_attachment_credential_inspection_uses_latest_policy() { + for (creation_has_rule, updated_has_rule) in [ + (true, Some(false)), + (false, Some(true)), + (true, None), + (false, None), + ] { + let state = test_server_state().await; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + let policy = |has_rule| { + if has_rule { + test_policy_with_rule("uninspected", "api.github.com") + } else { + ProtoSandboxPolicy::default() + } + }; + let creation_policy = policy(creation_has_rule); + let sandbox = test_sandbox( + "sb-attachment-inspection", + "attach-inspection", + creation_policy.clone(), + Vec::new(), + ); + state.store.put_message(&sandbox).await.unwrap(); + + if let Some(has_rule) = updated_has_rule { + handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(policy(has_rule)), + ..Default::default() + })), + ) + .await + .expect("policy update without an attached provider must succeed"); + } + + let result = super::super::sandbox::handle_attach_sandbox_provider( + &state, + authed_request(openshell_core::proto::AttachSandboxProviderRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + provider: "work-github".to_string(), + ..Default::default() + }), + ) + .await; + let should_reject = updated_has_rule.unwrap_or(creation_has_rule); + if should_reject { + let error = result.expect_err("current uninspected rule must prevent attachment"); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("L4-only")); + assert!(error.message().contains("uninspected")); + } else { + assert!( + result + .expect("no current uninspected rule") + .into_inner() + .attached + ); + } + + let stored = state + .store + .get_message::("sb-attachment-inspection") + .await + .unwrap() + .unwrap(); + let spec = stored.spec.unwrap(); + assert_eq!(spec.policy, Some(creation_policy)); + assert_eq!( + spec.providers, + if should_reject { + Vec::::new() + } else { + vec!["work-github".to_string()] + } + ); + state.store.close_for_test().await; + } + } + #[tokio::test] async fn update_config_gates_uninspected_endpointless_credential_binding() { use openshell_core::proto::{ProviderProfile, ProviderProfileCategory}; diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index c998372edb..86ff12b170 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1566,11 +1566,13 @@ pub(super) async fn handle_attach_sandbox_provider( &candidate_spec.providers, ) .await?; + let current_policy = + super::policy::current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; super::policy::validate_candidate_sandbox_credential_policy( state, &workspace, &candidate_spec.providers, - candidate_spec.policy.as_ref(), + Some(¤t_policy), ) .await?; diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index 73f209738e..bcfd476050 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -409,6 +409,12 @@ and `openshell sandbox provider detach`. See +Provider attachment checks credential inspection against the latest saved sandbox +policy, falling back to the creation policy only when no revision exists. If a +current rule permits uninspected traffic to a credentialed endpoint, configure +L7 inspection or remove the rule before attaching the provider. Use +`allow_uninspected_credentials: true` only for an intentional exception. + ### Auto-Discovery Shortcut Name a provider with `--provider`. If no provider by that name exists and the