Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 49 additions & 30 deletions crates/openshell-cli/src/commands/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,9 @@ pub async fn fetch_provider_profile_catalog(
.await
.into_diagnostic()?
.into_inner();
for profile in &response.profiles {
validate_profile_grant_types(profile).into_diagnostic()?;
}
profiles.extend(response.profiles);
if response.next_page_token.is_empty() {
break;
Expand Down Expand Up @@ -869,13 +872,25 @@ async fn fetch_provider_profile_exact(
})
.await
.and_then(|response| {
response
.into_inner()
.profile
.ok_or_else(|| Status::internal("provider profile missing from response"))
let profile = response.into_inner().profile.ok_or_else(|| {
Status::internal(format!(
"provider profile '{provider_type}' missing from response"
))
})?;
validate_profile_grant_types(&profile)?;
Ok(profile)
})
}

fn validate_profile_grant_types(profile: &ProviderProfile) -> std::result::Result<(), Status> {
if let Some(diagnostic) =
openshell_providers::unknown_token_grant_type_diagnostic("gateway", profile)
{
return Err(Status::failed_precondition(diagnostic.message));
}
Ok(())
}

async fn discover_existing_provider_data(
client: &mut crate::tls::GrpcClient,
provider_type: &str,
Expand Down Expand Up @@ -1679,17 +1694,9 @@ pub async fn provider_profile_describe_text(
tls: &TlsOptions,
) -> Result<String> {
let mut client = grpc_client(server, tls).await?;
let response = client
.get_provider_profile(GetProviderProfileRequest {
id: id.to_string(),
workspace_scope: provider_profile_workspace_scope(workspace),
})
let profile = fetch_provider_profile_exact(&mut client, id, workspace)
.await
.into_diagnostic()?;
let profile = response
.into_inner()
.profile
.ok_or_else(|| miette!("provider profile '{id}' not found"))?;

format_provider_profile_description(&profile, output)
}
Expand Down Expand Up @@ -1718,17 +1725,9 @@ pub async fn provider_profile_export_text(
tls: &TlsOptions,
) -> Result<String> {
let mut client = grpc_client(server, tls).await?;
let response = client
.get_provider_profile(GetProviderProfileRequest {
id: id.to_string(),
workspace_scope: provider_profile_workspace_scope(workspace),
})
let profile = fetch_provider_profile_exact(&mut client, id, workspace)
.await
.into_diagnostic()?;
let profile = response
.into_inner()
.profile
.ok_or_else(|| miette!("provider profile '{id}' not found"))?;
let profile = ProviderTypeProfile::from_proto(&profile);

match output {
Expand Down Expand Up @@ -1760,9 +1759,7 @@ pub async fn provider_profile_import(

let mut client = grpc_client(server, tls).await?;
if !items.is_empty() {
if profiles_require_stable_placeholders(&items) {
lint_profile_items(&mut client, items.clone(), workspace).await?;
}
check_profile_write_capabilities(&mut client, &items, workspace).await?;
let response = client
.import_provider_profiles(ImportProviderProfilesRequest {
request_id: String::new(),
Expand Down Expand Up @@ -1809,9 +1806,8 @@ pub async fn provider_profile_update(

let mut client = grpc_client(server, tls).await?;
if let Some(item) = items.pop() {
if profiles_require_stable_placeholders(std::slice::from_ref(&item)) {
lint_profile_items(&mut client, vec![item.clone()], workspace).await?;
}
check_profile_write_capabilities(&mut client, std::slice::from_ref(&item), workspace)
.await?;
let expected_resource_version = item
.profile
.as_ref()
Expand Down Expand Up @@ -2174,14 +2170,32 @@ fn profiles_require_stable_placeholders(items: &[ProviderProfileImportItem]) ->
})
}

// Check support before persisting an opt-in: older gateways discard unknown
// credential fields and can otherwise report a successful revision-scoped import.
fn profiles_require_gateway_assertion(items: &[ProviderProfileImportItem]) -> bool {
items
.iter()
.filter_map(|item| item.profile.as_ref())
.any(openshell_providers::profile_uses_gateway_assertion)
}

// Older gateways can accept a profile write without preserving the opt-in.
async fn check_profile_write_capabilities(
client: &mut crate::tls::GrpcClient,
items: &[ProviderProfileImportItem],
workspace: &str,
) -> Result<()> {
if profiles_require_stable_placeholders(items) || profiles_require_gateway_assertion(items) {
lint_profile_items(client, items.to_vec(), workspace).await?;
}
Ok(())
}

async fn lint_profile_items(
client: &mut crate::tls::GrpcClient,
items: Vec<ProviderProfileImportItem>,
workspace: &str,
) -> Result<openshell_core::proto::LintProviderProfilesResponse> {
let requires_stable = profiles_require_stable_placeholders(&items);
let requires_gateway_assertion = profiles_require_gateway_assertion(&items);
let response = client
.lint_provider_profiles(LintProviderProfilesRequest {
profiles: items,
Expand All @@ -2195,6 +2209,11 @@ async fn lint_profile_items(
"gateway does not support stable_placeholder; upgrade the gateway before enabling it"
));
}
if requires_gateway_assertion && !response.supports_openshell_gateway_assertion_source {
return Err(miette!(
"gateway does not support assertion_source openshell_gateway; upgrade the gateway before enabling it"
));
}
Ok(response)
}

Expand Down
14 changes: 8 additions & 6 deletions crates/openshell-cli/src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ pub type GrpcClient = openshell_sdk::raw::GrpcClient<
/// Interceptor stack applied to gateway requests from clients built by
/// [`grpc_client`].
///
/// Runs authentication header injection first, then passive W3C trace-context
/// forwarding so a CI pipeline's `TRACEPARENT` extends into the gateway's
/// spans. tonic allows a single interceptor per channel, so the two concerns
/// are composed here.
/// Attaches CLI authentication and provider-profile capabilities and forwards
/// W3C trace context from the environment.
#[derive(Clone)]
pub struct GatewayInterceptor {
auth: openshell_sdk::EdgeAuthInterceptor,
Expand All @@ -39,7 +37,11 @@ impl tonic::service::Interceptor for GatewayInterceptor {
&mut self,
request: tonic::Request<()>,
) -> std::result::Result<tonic::Request<()>, tonic::Status> {
let request = self.auth.call(request)?;
let mut request = self.auth.call(request)?;
request.metadata_mut().insert(
openshell_providers::GATEWAY_ASSERTION_PROFILE_CAPABILITY,
tonic::metadata::MetadataValue::from_static("1"),
);
self.trace.call(request)
}
}
Expand Down Expand Up @@ -342,7 +344,7 @@ pub async fn build_channel(server: &str, tls: &TlsOptions) -> Result<Channel> {
/// The returned client carries a [`GatewayInterceptor`] that injects
/// authentication headers (when a token is set) and passively forwards the
/// `TRACEPARENT`/`TRACESTATE` trace context from the environment on every
/// request. With no token and no trace context, the interceptor is a no-op.
/// request. It also advertises the profile grant types this CLI can preserve.
pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result<GrpcClient> {
let config = sdk_client_config(server, tls)?;
// Keep the SDK's transport state machine out of every command future.
Expand Down
12 changes: 3 additions & 9 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ use openshell_core::proto::{
AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateProviderRequest,
CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest,
DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse,
DetachSandboxProviderRequest, DetachSandboxProviderResponse,
ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent,
DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent,
ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest,
GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest,
GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest,
Expand Down Expand Up @@ -211,7 +210,7 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(ListSandboxesResponse::default()))
}

unimplemented_sandbox_template_rpcs!();
unused_gateway_rpcs!(sandbox_templates);

async fn list_sandbox_providers(
&self,
Expand Down Expand Up @@ -329,12 +328,7 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(RevokeSshSessionResponse::default()))
}

async fn exchange_provider_subject_token(
&self,
_request: tonic::Request<ExchangeProviderSubjectTokenRequest>,
) -> Result<Response<ExchangeProviderSubjectTokenResponse>, Status> {
Err(Status::unimplemented("unused"))
}
unused_gateway_rpcs!(provider_token_grants);

async fn create_provider(
&self,
Expand Down
90 changes: 2 additions & 88 deletions crates/openshell-cli/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,94 +22,8 @@ pub fn example_profiles() -> &'static [openshell_providers::ProviderTypeProfile]
.as_slice()
}

#[macro_export]
macro_rules! unimplemented_sandbox_template_rpcs {
() => {
fn create_sandbox_template<'life0, 'async_trait>(
&'life0 self,
_request: tonic::Request<openshell_core::proto::CreateSandboxTemplateRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
tonic::Response<openshell_core::proto::SandboxTemplateResponse>,
tonic::Status,
>,
> + Send
+ 'async_trait,
>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async { Err(tonic::Status::unimplemented("unused")) })
}

fn get_sandbox_template<'life0, 'async_trait>(
&'life0 self,
_request: tonic::Request<openshell_core::proto::GetSandboxTemplateRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
tonic::Response<openshell_core::proto::SandboxTemplateResponse>,
tonic::Status,
>,
> + Send
+ 'async_trait,
>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async { Err(tonic::Status::unimplemented("unused")) })
}

fn list_sandbox_templates<'life0, 'async_trait>(
&'life0 self,
_request: tonic::Request<openshell_core::proto::ListSandboxTemplatesRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
tonic::Response<openshell_core::proto::ListSandboxTemplatesResponse>,
tonic::Status,
>,
> + Send
+ 'async_trait,
>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async { Err(tonic::Status::unimplemented("unused")) })
}

fn delete_sandbox_template<'life0, 'async_trait>(
&'life0 self,
_request: tonic::Request<openshell_core::proto::DeleteSandboxTemplateRequest>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
tonic::Response<openshell_core::proto::DeleteSandboxTemplateResponse>,
tonic::Status,
>,
> + Send
+ 'async_trait,
>,
>
where
'life0: 'async_trait,
Self: 'async_trait,
{
Box::pin(async { Err(tonic::Status::unimplemented("unused")) })
}
};
}
#[path = "../../../../tests/support/unused_gateway_rpcs.rs"]
mod unused_gateway_rpcs;

use openshell_crypto::pki::Certificate;
use openshell_crypto::pki::KeyPair;
Expand Down
18 changes: 6 additions & 12 deletions crates/openshell-cli/tests/mtls_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ use openshell_cli::{
};
use openshell_core::proto::{
CreateProviderRequest, CreateSshSessionRequest, CreateSshSessionResponse,
DeleteProviderRequest, DeleteProviderResponse, ExchangeProviderSubjectTokenRequest,
ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest,
GetProviderRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse,
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, ServiceStatus,
UpdateProviderRequest,
DeleteProviderRequest, DeleteProviderResponse, ExecSandboxEvent, ExecSandboxInput,
ExecSandboxRequest, GetProviderRequest, HealthRequest, HealthResponse, ListProvidersRequest,
ListProvidersResponse, ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse,
ServiceStatus, UpdateProviderRequest,
open_shell_server::{OpenShell, OpenShellServer},
};
use tokio::net::TcpListener;
Expand Down Expand Up @@ -169,7 +168,7 @@ impl OpenShell for TestOpenShell {
))
}

unimplemented_sandbox_template_rpcs!();
unused_gateway_rpcs!(sandbox_templates);

async fn list_sandbox_providers(
&self,
Expand Down Expand Up @@ -300,12 +299,7 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(RevokeSshSessionResponse::default()))
}

async fn exchange_provider_subject_token(
&self,
_request: tonic::Request<ExchangeProviderSubjectTokenRequest>,
) -> Result<Response<ExchangeProviderSubjectTokenResponse>, Status> {
Err(Status::unimplemented("unused"))
}
unused_gateway_rpcs!(provider_token_grants);

async fn create_provider(
&self,
Expand Down
Loading
Loading