mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-25 06:42:52 +08:00
chore: merge updated configuration snapshot base
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
@@ -28,7 +28,7 @@ inputs:
|
||||
mise-version:
|
||||
description: mise release to install
|
||||
required: false
|
||||
default: v2026.4.25
|
||||
default: v2026.9.9
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
@@ -8,7 +8,7 @@ inputs:
|
||||
mise-version:
|
||||
description: mise release to install
|
||||
required: false
|
||||
default: v2026.4.25
|
||||
default: v2026.9.9
|
||||
podman-major:
|
||||
description: Expected Podman major version
|
||||
required: true
|
||||
|
||||
@@ -8,7 +8,7 @@ inputs:
|
||||
mise-version:
|
||||
description: mise release to install
|
||||
required: false
|
||||
default: v2026.4.25
|
||||
default: v2026.9.9
|
||||
rust-cache-key:
|
||||
description: Shared key for Rust test artifacts
|
||||
required: false
|
||||
|
||||
@@ -8,7 +8,7 @@ inputs:
|
||||
version:
|
||||
description: mise release to install
|
||||
required: false
|
||||
default: v2026.4.25
|
||||
default: v2026.9.9
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
@@ -56,7 +56,7 @@ on:
|
||||
description: "mise version to install on the bare Kubernetes e2e runner"
|
||||
required: false
|
||||
type: string
|
||||
default: "v2026.4.25"
|
||||
default: "v2026.9.9"
|
||||
gateway-artifact:
|
||||
description: "Optional prebuilt gateway artifact used to compose a local test image"
|
||||
required: false
|
||||
|
||||
@@ -44,6 +44,7 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
|
||||
| `crates/openshell-isolation-interface/` | Isolation backend interface | RFC 0012 `IsolationBackend` trait and types; the supervisor-facing runtime contract |
|
||||
| `crates/openshell-sandbox-backend/` | OpenShell sandbox backend | `OpenShellRuntimeBackend` and the authenticated OpenShell Sandbox Protocol shared with `openshell-sandbox` |
|
||||
| `crates/openshell-policy/` | Policy engine | Filesystem, network, and process constraints |
|
||||
| `crates/openshell-policy-schema/` | Authored policy schema | Dependency-light YAML/JSON representation, bounded parsing, and pure authored-language semantics |
|
||||
| `crates/openshell-bootstrap/` | Gateway metadata | Gateway registration metadata, auth token storage, mTLS bundle storage |
|
||||
| `crates/openshell-gateway-interceptors/` | Gateway interceptors | Intercepts and transforms configured gRPC requests at the gateway routing boundary |
|
||||
| `crates/openshell-ocsf/` | OCSF logging | OCSF v1.8.0 event types, builders, shorthand/JSONL formatters, tracing layers |
|
||||
|
||||
@@ -458,6 +458,7 @@ These are the primary `mise` tasks for day-to-day development:
|
||||
| Path | Purpose |
|
||||
| --------------- | --------------------------------------------- |
|
||||
| `crates/` | Rust crates |
|
||||
| `crates/openshell-policy-schema/` | Canonical authored policy DTOs and bounded YAML/JSON parser |
|
||||
| `python/` | Python SDK and bindings |
|
||||
| `sdk/go/` | Go SDK (types, gRPC clients, converters) |
|
||||
| `sdk/typescript/` | TypeScript SDK (Connect client and generated protobuf bindings) |
|
||||
|
||||
Generated
+14
-1
@@ -3988,6 +3988,7 @@ dependencies = [
|
||||
"openshell-policy",
|
||||
"openshell-providers",
|
||||
"openshell-sdk",
|
||||
"openshell-server",
|
||||
"openshell-tui",
|
||||
"owo-colors",
|
||||
"prost-types",
|
||||
@@ -4047,6 +4048,7 @@ dependencies = [
|
||||
"miette",
|
||||
"nix 0.29.0",
|
||||
"openshell-extension-core",
|
||||
"openshell-policy-schema",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
@@ -4445,13 +4447,23 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"hickory-proto",
|
||||
"miette",
|
||||
"noyalib",
|
||||
"openshell-core",
|
||||
"openshell-policy-schema",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-policy-schema"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"noyalib",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-prover"
|
||||
version = "0.0.0"
|
||||
@@ -4460,6 +4472,7 @@ dependencies = [
|
||||
"include_dir",
|
||||
"miette",
|
||||
"noyalib",
|
||||
"openshell-policy-schema",
|
||||
"owo-colors",
|
||||
"serde",
|
||||
"z3",
|
||||
|
||||
@@ -21,6 +21,25 @@ For the field-by-field YAML reference, use
|
||||
Filesystem and process policy are startup-time controls. Network policy is
|
||||
dynamic and can be hot-reloaded when the new policy validates successfully.
|
||||
|
||||
### Authored policy boundary
|
||||
|
||||
`openshell-policy-schema` is the sole owner of the authored YAML and JSON
|
||||
representation. It preserves authored distinctions such as an absent
|
||||
`filesystem_policy` versus an explicitly empty object, rejects duplicate keys,
|
||||
and applies parser budgets while noyalib constructs the document. It also owns
|
||||
pure language semantics such as access presets, MCP revision vocabulary,
|
||||
effective ports and rule names, protocol classification, and lexical policy
|
||||
path normalization.
|
||||
|
||||
Consumers project that syntax into purpose-specific models. `openshell-policy`
|
||||
owns protobuf conversion, composition, merge behavior, raw-protobuf checks, and
|
||||
validation that depends on runtime components. The existing prover retains its
|
||||
risk model but uses the same fail-closed parser as the runtime. The parser
|
||||
requires `version: 1` and rejects managed annotations and every unknown field
|
||||
before any consumer-specific projection runs. There is no permissive parsing
|
||||
profile: unsupported policy fields always invalidate the document. Middleware `config`, query and persisted-query names, and recursive MCP
|
||||
parameter names are open user-data maps rather than schema extensions.
|
||||
|
||||
Before applying Landlock, the supervisor enriches baseline filesystem paths that
|
||||
the runtime needs. Missing baseline paths are skipped so one absent runtime path
|
||||
does not weaken the whole ruleset. When GPU devices are present, GPU baseline
|
||||
|
||||
@@ -91,6 +91,7 @@ nix = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = { workspace = true }
|
||||
openshell-server = { path = "../openshell-server", features = ["test-support"] }
|
||||
rcgen = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1477,11 +1477,6 @@ pub async fn sandbox_sync_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a sandbox by name.
|
||||
///
|
||||
/// Policy always comes from [`GetSandboxConfig`] (effective active policy, sandbox
|
||||
/// or global). With `policy_only`, prints only that YAML to stdout; otherwise
|
||||
/// prints sandbox metadata and the same policy with formatted YAML.
|
||||
pub async fn sandbox_get(
|
||||
server: &str,
|
||||
name: &str,
|
||||
@@ -1490,6 +1485,44 @@ pub async fn sandbox_get(
|
||||
workspace: &str,
|
||||
tls: &TlsOptions,
|
||||
) -> Result<()> {
|
||||
let mut stdout = Vec::new();
|
||||
sandbox_get_to_writer(
|
||||
server,
|
||||
name,
|
||||
policy_only,
|
||||
output,
|
||||
workspace,
|
||||
tls,
|
||||
&mut stdout,
|
||||
)
|
||||
.await?;
|
||||
if !stdout.is_empty() {
|
||||
std::io::stdout()
|
||||
.lock()
|
||||
.write_all(&stdout)
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a sandbox by name.
|
||||
///
|
||||
/// Policy always comes from [`GetSandboxConfig`] (effective active policy, sandbox
|
||||
/// or global). With `policy_only`, prints only that YAML to stdout; otherwise
|
||||
/// prints sandbox metadata and the same policy with formatted YAML.
|
||||
#[doc(hidden)]
|
||||
pub async fn sandbox_get_to_writer<W>(
|
||||
server: &str,
|
||||
name: &str,
|
||||
policy_only: bool,
|
||||
output: &str,
|
||||
workspace: &str,
|
||||
tls: &TlsOptions,
|
||||
stdout: &mut W,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Send,
|
||||
{
|
||||
let mut client = grpc_client(server, tls).await?;
|
||||
|
||||
let response = client
|
||||
@@ -1524,7 +1557,7 @@ pub async fn sandbox_get(
|
||||
};
|
||||
let yaml_str = openshell_policy::serialize_sandbox_policy(policy)
|
||||
.wrap_err("failed to serialize policy to YAML")?;
|
||||
print!("{yaml_str}");
|
||||
stdout.write_all(yaml_str.as_bytes()).into_diagnostic()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::{build_ca, build_client_cert, build_server_cert};
|
||||
use openshell_cli::run;
|
||||
use openshell_cli::tls::{TlsOptions, grpc_client};
|
||||
use openshell_core::proto::open_shell_server::OpenShellServer;
|
||||
use openshell_core::proto::{CreateSandboxRequest, SandboxSpec};
|
||||
use openshell_server::test_support::{authenticate_as_dev_user, gateway_service_with_driver};
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_stream::wrappers::TcpListenerStream;
|
||||
use tonic::transport::{Certificate as TlsCertificate, Identity, Server, ServerTlsConfig};
|
||||
|
||||
struct TestServer {
|
||||
endpoint: String,
|
||||
tls: TlsOptions,
|
||||
_dir: TempDir,
|
||||
}
|
||||
|
||||
async fn run_server() -> TestServer {
|
||||
let (ca, ca_key) = build_ca();
|
||||
let (server_cert, server_key) = build_server_cert(&ca, &ca_key);
|
||||
let (client_cert, client_key) = build_client_cert(&ca, &ca_key);
|
||||
let ca_cert = ca.pem();
|
||||
|
||||
let tls_config = ServerTlsConfig::new()
|
||||
.identity(Identity::from_pem(server_cert, server_key))
|
||||
.client_ca_root(TlsCertificate::from_pem(ca_cert.clone()));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let service = gateway_service_with_driver("docker").await;
|
||||
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.tls_config(tls_config)
|
||||
.unwrap()
|
||||
.add_service(OpenShellServer::with_interceptor(
|
||||
service,
|
||||
authenticate_as_dev_user,
|
||||
))
|
||||
.serve_with_incoming(TcpListenerStream::new(listener))
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let ca_path = dir.path().join("ca.crt");
|
||||
let cert_path = dir.path().join("tls.crt");
|
||||
let key_path = dir.path().join("tls.key");
|
||||
std::fs::write(&ca_path, ca_cert).unwrap();
|
||||
std::fs::write(&cert_path, client_cert).unwrap();
|
||||
std::fs::write(&key_path, client_key).unwrap();
|
||||
|
||||
TestServer {
|
||||
endpoint: format!("https://localhost:{}", addr.port()),
|
||||
tls: TlsOptions::new(Some(ca_path), Some(cert_path), Some(key_path)),
|
||||
_dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn policy_only_preserves_all_process_identity_combinations_through_gateway() {
|
||||
let server = run_server().await;
|
||||
let cases = [
|
||||
("neither", "process: {}", None),
|
||||
(
|
||||
"user-only",
|
||||
"process:\n run_as_user: \"1500\"",
|
||||
Some(("1500", "")),
|
||||
),
|
||||
(
|
||||
"group-only",
|
||||
"process:\n run_as_group: \"1600\"",
|
||||
Some(("", "1600")),
|
||||
),
|
||||
(
|
||||
"both",
|
||||
"process:\n run_as_user: \"1500\"\n run_as_group: \"1600\"",
|
||||
Some(("1500", "1600")),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, process_yaml, expected) in cases {
|
||||
let authored = format!("version: 1\n{process_yaml}\n");
|
||||
let policy = openshell_policy::parse_sandbox_policy(&authored)
|
||||
.unwrap_or_else(|error| panic!("{name}: authored policy should parse: {error}"));
|
||||
|
||||
let mut client = grpc_client(&server.endpoint, &server.tls).await.unwrap();
|
||||
client
|
||||
.create_sandbox(CreateSandboxRequest {
|
||||
name: name.to_string(),
|
||||
spec: Some(SandboxSpec {
|
||||
policy: Some(policy),
|
||||
..Default::default()
|
||||
}),
|
||||
workspace_scope: Some(openshell_core::proto::workspace_selector("default")),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{name}: gateway create should succeed: {error}"));
|
||||
|
||||
let mut output = Vec::new();
|
||||
run::sandbox_get_to_writer(
|
||||
&server.endpoint,
|
||||
name,
|
||||
true,
|
||||
"table",
|
||||
"default",
|
||||
&server.tls,
|
||||
&mut output,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{name}: --policy-only should succeed: {error}"));
|
||||
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
let exported = openshell_policy::parse_sandbox_policy(&output)
|
||||
.unwrap_or_else(|error| panic!("{name}: exported YAML should parse: {error}"));
|
||||
match expected {
|
||||
None => {
|
||||
assert!(
|
||||
exported.process.is_none(),
|
||||
"{name}: empty process must be omitted"
|
||||
);
|
||||
assert!(!output.contains("process:"), "{name}: {output}");
|
||||
}
|
||||
Some((user, group)) => {
|
||||
let process = exported
|
||||
.process
|
||||
.expect("partial process should be retained");
|
||||
assert_eq!(process.run_as_user, user, "{name}: {output}");
|
||||
assert_eq!(process.run_as_group, group, "{name}: {output}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@ impl OpenShell for TestOpenShell {
|
||||
);
|
||||
Ok(Response::new(GetSandboxConfigResponse {
|
||||
policy: Some(SandboxPolicy {
|
||||
version: 9,
|
||||
version: 1,
|
||||
network_policies: [
|
||||
(
|
||||
"user_api".to_string(),
|
||||
@@ -464,7 +464,7 @@ impl OpenShell for TestOpenShell {
|
||||
assert!(!req.global);
|
||||
|
||||
let policy = SandboxPolicy {
|
||||
version: 7,
|
||||
version: 1,
|
||||
network_policies: std::iter::once((
|
||||
"api".to_string(),
|
||||
NetworkPolicyRule {
|
||||
|
||||
@@ -13,6 +13,7 @@ repository.workspace = true
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
openshell-extension-core = { path = "../openshell-extension-core" }
|
||||
openshell-policy-schema = { path = "../openshell-policy-schema" }
|
||||
glob = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
//! `OpenShell`-owned MCP protocol revisions and immutable batch-shape metadata.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::proto::{McpOptions, ProviderProfile};
|
||||
|
||||
/// Fixed `OpenShell` batch-member bound for the MCP `2025-03-26` wire profile.
|
||||
///
|
||||
/// The bound is distinct from the separately enforced request-body byte limit.
|
||||
pub const MAX_MCP_LEGACY_BATCH_MESSAGES: usize = 64;
|
||||
pub use openshell_policy_schema::{
|
||||
DEFAULT_MCP_PROTOCOL_VERSION, MAX_MCP_LEGACY_BATCH_MESSAGES, McpProtocolVersion,
|
||||
McpWireProfile, ParseMcpProtocolVersionError,
|
||||
};
|
||||
|
||||
/// Return whether a policy protocol name denotes MCP.
|
||||
///
|
||||
@@ -23,28 +21,6 @@ pub fn is_mcp_protocol(protocol: &str) -> bool {
|
||||
protocol.trim().eq_ignore_ascii_case("mcp")
|
||||
}
|
||||
|
||||
/// Stable MCP protocol revisions accepted by `OpenShell` policy configuration.
|
||||
///
|
||||
/// Variant order is the canonical semantic order used when normalizing policy
|
||||
/// allowlists.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum McpProtocolVersion {
|
||||
/// MCP protocol revision `2025-03-26`.
|
||||
V2025_03_26,
|
||||
/// MCP protocol revision `2025-06-18`.
|
||||
V2025_06_18,
|
||||
/// MCP protocol revision `2025-11-25`.
|
||||
V2025_11_25,
|
||||
}
|
||||
|
||||
/// Pinned MCP protocol revision used when policy authoring omits a revision.
|
||||
///
|
||||
/// This value is assigned directly rather than derived from
|
||||
/// [`McpProtocolVersion::ALL`] or its order. Adding support for a newer
|
||||
/// revision therefore cannot silently change versionless policies.
|
||||
pub const DEFAULT_MCP_PROTOCOL_VERSION: McpProtocolVersion = McpProtocolVersion::V2025_11_25;
|
||||
|
||||
/// Normalize only the MCP option fields in a provider profile.
|
||||
///
|
||||
/// MCP protocol matching is case-insensitive. Missing or empty version state
|
||||
@@ -94,116 +70,6 @@ pub fn normalize_provider_profile_mcp_fields(profile: &mut ProviderProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
impl McpProtocolVersion {
|
||||
/// Every supported MCP protocol revision in canonical semantic order.
|
||||
pub const ALL: &'static [Self] = &[Self::V2025_03_26, Self::V2025_06_18, Self::V2025_11_25];
|
||||
|
||||
/// Return the exact protocol identifier used on the MCP wire.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::V2025_03_26 => "2025-03-26",
|
||||
Self::V2025_06_18 => "2025-06-18",
|
||||
Self::V2025_11_25 => "2025-11-25",
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the fixed batch-shape metadata for this protocol revision.
|
||||
#[must_use]
|
||||
pub const fn wire_profile(self) -> McpWireProfile {
|
||||
match self {
|
||||
Self::V2025_03_26 => McpWireProfile {
|
||||
version: self,
|
||||
allows_json_rpc_batches: true,
|
||||
max_batch_messages: Some(MAX_MCP_LEGACY_BATCH_MESSAGES),
|
||||
},
|
||||
Self::V2025_06_18 | Self::V2025_11_25 => McpWireProfile {
|
||||
version: self,
|
||||
allows_json_rpc_batches: false,
|
||||
max_batch_messages: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for McpProtocolVersion {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for McpProtocolVersion {
|
||||
type Err = ParseMcpProtocolVersionError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"2025-03-26" => Ok(Self::V2025_03_26),
|
||||
"2025-06-18" => Ok(Self::V2025_06_18),
|
||||
"2025-11-25" => Ok(Self::V2025_11_25),
|
||||
_ => Err(ParseMcpProtocolVersionError {
|
||||
value: value.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when a string is not an exact supported MCP revision.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ParseMcpProtocolVersionError {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl ParseMcpProtocolVersionError {
|
||||
/// Return the exact input that failed MCP revision parsing.
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseMcpProtocolVersionError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"unsupported MCP protocol version '{}'",
|
||||
self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseMcpProtocolVersionError {}
|
||||
|
||||
/// Immutable batch-shape metadata for one exact MCP protocol revision.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct McpWireProfile {
|
||||
version: McpProtocolVersion,
|
||||
allows_json_rpc_batches: bool,
|
||||
max_batch_messages: Option<usize>,
|
||||
}
|
||||
|
||||
impl McpWireProfile {
|
||||
/// Return the protocol revision governed by this profile.
|
||||
#[must_use]
|
||||
pub const fn version(self) -> McpProtocolVersion {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Return whether the revision permits a top-level JSON-RPC batch array.
|
||||
#[must_use]
|
||||
pub const fn allows_json_rpc_batches(self) -> bool {
|
||||
self.allows_json_rpc_batches
|
||||
}
|
||||
|
||||
/// Return the maximum batch members when batching is permitted.
|
||||
///
|
||||
/// `None` means the profile forbids batches; it never represents an
|
||||
/// unbounded batch.
|
||||
#[must_use]
|
||||
pub const fn max_batch_messages(self) -> Option<usize> {
|
||||
self.max_batch_messages
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use prost::Message;
|
||||
@@ -394,13 +260,10 @@ mod tests {
|
||||
"draft",
|
||||
"latest",
|
||||
] {
|
||||
let error = unsupported.parse::<McpProtocolVersion>();
|
||||
assert_eq!(
|
||||
error,
|
||||
Err(ParseMcpProtocolVersionError {
|
||||
value: unsupported.to_string(),
|
||||
})
|
||||
);
|
||||
let error = unsupported
|
||||
.parse::<McpProtocolVersion>()
|
||||
.expect_err("unsupported MCP revision must be rejected");
|
||||
assert_eq!(error.value(), unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,41 +140,9 @@ pub fn is_file_permissions_too_open(path: &Path) -> bool {
|
||||
std::fs::metadata(path).is_ok_and(|m| m.permissions().mode() & 0o077 != 0)
|
||||
}
|
||||
|
||||
/// Normalize a filesystem path by collapsing redundant separators
|
||||
/// and removing trailing slashes, without requiring the path to exist on disk.
|
||||
///
|
||||
/// This is a lexical normalization only — it does NOT resolve symlinks or
|
||||
/// check the filesystem. `..` components are preserved verbatim; callers that
|
||||
/// need to reject parent traversal must validate separately. The normalized
|
||||
/// representation always uses `/` so sandbox policy paths are host-independent.
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::Component;
|
||||
|
||||
let p = Path::new(path);
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in p.components() {
|
||||
match component {
|
||||
Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
|
||||
#[allow(clippy::path_buf_push_overwrite)]
|
||||
Component::RootDir => normalized.push("/"),
|
||||
Component::CurDir => {} // skip "."
|
||||
Component::ParentDir => {
|
||||
// Keep ".." — validation will catch it separately
|
||||
normalized.push("..");
|
||||
}
|
||||
Component::Normal(c) => normalized.push(c),
|
||||
}
|
||||
}
|
||||
let normalized = normalized.to_string_lossy();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
normalized.replace('\\', "/")
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
normalized.into_owned()
|
||||
}
|
||||
}
|
||||
/// Compatibility re-export; authored policy path normalization is owned by
|
||||
/// `openshell-policy-schema`.
|
||||
pub use openshell_policy_schema::normalize_path;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "openshell-policy-schema"
|
||||
description = "Canonical authored policy schema for OpenShell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
miette = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yml = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,9 @@ repository.workspace = true
|
||||
[dependencies]
|
||||
hickory-proto = { workspace = true }
|
||||
openshell-core = { path = "../openshell-core", default-features = false }
|
||||
openshell-policy-schema = { path = "../openshell-policy-schema" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yml = { workspace = true }
|
||||
miette = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
|
||||
|
||||
+232
-431
@@ -5,9 +5,9 @@
|
||||
//!
|
||||
//! Provides bidirectional YAML↔proto conversion for sandbox policies.
|
||||
//!
|
||||
//! The serde types here are the **single canonical representation** of the YAML
|
||||
//! policy schema. Both parsing (YAML→proto) and serialization (proto→YAML) use
|
||||
//! these types, ensuring round-trip fidelity.
|
||||
//! The canonical authored representation and bounded parser live in
|
||||
//! `openshell-policy-schema`; this crate adapts that representation to the
|
||||
//! runtime protobuf model and owns runtime-dependent validation.
|
||||
|
||||
mod compose;
|
||||
mod l7_validate;
|
||||
@@ -31,7 +31,6 @@ use openshell_core::proto::{
|
||||
LandlockPolicy, McpOptions, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProcessPolicy,
|
||||
SandboxPolicy,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
pub use compose::{
|
||||
PROVIDER_RULE_NAME_PREFIX, ProviderPolicyLayer, compose_effective_policy,
|
||||
@@ -49,238 +48,24 @@ pub use middleware::middleware_host_matches;
|
||||
pub use middleware::validate_json as validate_network_middleware_json;
|
||||
pub use middleware::validate_json_with_config as validate_network_middleware_json_with_config;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// YAML serde types (canonical — used for both parsing and serialization)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PolicyFile {
|
||||
version: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
filesystem_policy: Option<FilesystemDef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
landlock: Option<LandlockDef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
process: Option<ProcessDef>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
network_policies: BTreeMap<String, NetworkPolicyRuleDef>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
network_middlewares: BTreeMap<String, middleware::NetworkMiddlewareConfigDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct FilesystemDef {
|
||||
#[serde(default)]
|
||||
include_workdir: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
read_only: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
read_write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum LandlockCompatibilityDef {
|
||||
#[default]
|
||||
BestEffort,
|
||||
HardRequirement,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LandlockDef {
|
||||
#[serde(default)]
|
||||
compatibility: LandlockCompatibilityDef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ProcessDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
run_as_user: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
run_as_group: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct NetworkPolicyRuleDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
endpoints: Vec<NetworkEndpointDef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
binaries: Vec<NetworkBinaryDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[allow(
|
||||
clippy::struct_excessive_bools,
|
||||
reason = "Endpoint DTO mirrors independent policy schema toggles."
|
||||
)]
|
||||
struct NetworkEndpointDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
host: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
path: String,
|
||||
/// Single port (backwards compat). Mutually exclusive with `ports`.
|
||||
/// Uses `u16` to reject invalid values >65535 at parse time.
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
port: u16,
|
||||
/// Multiple ports. When non-empty, this endpoint covers all listed ports.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
ports: Vec<u16>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
protocol: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
tls: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
enforcement: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
access: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
rules: Vec<L7RuleDef>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
allowed_ips: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
deny_rules: Vec<L7DenyRuleDef>,
|
||||
/// When true, percent-encoded `/` (`%2F`) is preserved in path segments
|
||||
/// rather than rejected by the L7 path canonicalizer. Required for
|
||||
/// upstreams like GitLab that embed `%2F` in namespaced resource paths.
|
||||
/// Defaults to false (strict).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
allow_encoded_slash: bool,
|
||||
/// When true, client-to-server WebSocket text messages on this REST
|
||||
/// endpoint rewrite credential placeholders after an allowed 101 upgrade.
|
||||
/// Defaults to false.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
websocket_credential_rewrite: bool,
|
||||
/// When true, supported textual REST request bodies rewrite credential
|
||||
/// placeholders before forwarding upstream. Defaults to false.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
request_body_credential_rewrite: bool,
|
||||
/// Explicitly permits credentials on traffic paths that `OpenShell` cannot
|
||||
/// inspect or rewrite. Defaults to false.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
allow_uninspected_credentials: bool,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
persisted_queries: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
graphql_persisted_queries: BTreeMap<String, GraphqlOperationDef>,
|
||||
#[serde(default, skip_serializing_if = "is_zero_u32")]
|
||||
graphql_max_body_bytes: u32,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
credential_signing: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
signing_service: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
signing_region: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
credential_binding: Option<NetworkCredentialBindingDef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
json_rpc: Option<JsonRpcConfigDef>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_non_null_optional_field",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
mcp: Option<McpConfigDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct NetworkCredentialBindingDef {
|
||||
provider: String,
|
||||
}
|
||||
|
||||
// Signature dictated by serde's `skip_serializing_if`, which requires `&T`.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_zero(v: &u16) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
// Signature dictated by serde's `skip_serializing_if`, which requires `&T`.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_zero_u32(v: &u32) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
fn deserialize_non_null_optional_field<'de, D, T>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
// Serde skips this function when the field is absent because the field has
|
||||
// `default`. When it is present, deserialize `T` directly so an explicit
|
||||
// YAML or JSON null is rejected instead of collapsing into omission.
|
||||
T::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct JsonRpcConfigDef {
|
||||
#[serde(default, skip_serializing_if = "is_zero_u32")]
|
||||
max_body_bytes: u32,
|
||||
}
|
||||
// The authored serde tree lives in `openshell-policy-schema`. These local
|
||||
// aliases keep the protobuf adapter readable while exposing consumer-facing
|
||||
// names from the schema crate.
|
||||
use openshell_policy_schema::{
|
||||
AnyMatcher as QueryAnyDef, FilesystemPolicy as FilesystemDef,
|
||||
GraphqlOperation as GraphqlOperationDef, JsonRpcConfig as JsonRpcConfigDef,
|
||||
L7Allow as L7AllowDef, L7DenyRule as L7DenyRuleDef, L7Rule as L7RuleDef,
|
||||
LandlockCompatibility as LandlockCompatibilityDef, LandlockPolicy as LandlockDef,
|
||||
MCP_VERSION_REMEDIATION, McpConfig as McpConfigDef, NetworkBinary as NetworkBinaryDef,
|
||||
NetworkCredentialBinding as NetworkCredentialBindingDef, NetworkEndpoint as NetworkEndpointDef,
|
||||
NetworkPolicyRule as NetworkPolicyRuleDef, ParameterMatcher as ParamMatcherDef,
|
||||
PolicyDocument as PolicyFile, ProcessPolicy as ProcessDef, QueryMatcher as QueryMatcherDef,
|
||||
};
|
||||
|
||||
fn json_rpc_config_from_proto(max_body_bytes: u32) -> Option<JsonRpcConfigDef> {
|
||||
(max_body_bytes > 0).then_some(JsonRpcConfigDef { max_body_bytes })
|
||||
}
|
||||
|
||||
// MCP rides the same HTTP/JSON-RPC inspection machinery at runtime, but it
|
||||
// gets its own policy stanza so user-authored YAML can name the primary
|
||||
// protocol instead of treating MCP as generic JSON-RPC.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct McpConfigDef {
|
||||
// Presence is retained until authored-policy validation so an omitted
|
||||
// allowlist can select the pinned default while an explicit empty list is
|
||||
// rejected as an authoring mistake.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_non_null_optional_field",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
versions: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "is_zero_u32")]
|
||||
max_body_bytes: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
strict_tool_names: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
allow_all_known_mcp_methods: Option<bool>,
|
||||
}
|
||||
|
||||
const MCP_VERSION_REMEDIATION: &str = "omit mcp.versions to use the pinned default revision, use an exact supported revision, or omit protocol and mcp for deliberate uninspected L4 passthrough only when that weaker boundary is acceptable";
|
||||
|
||||
fn validate_authored_mcp_versions(versions: Option<&[String]>, context: &str) -> Result<()> {
|
||||
let Some(versions) = versions else {
|
||||
return Ok(());
|
||||
};
|
||||
if versions.is_empty() {
|
||||
return Err(miette::miette!(
|
||||
"{context} has an empty mcp.versions list; omit mcp.versions to use the pinned default revision"
|
||||
));
|
||||
}
|
||||
|
||||
let mut seen = BTreeSet::new();
|
||||
for value in versions {
|
||||
let version = value
|
||||
.parse::<McpProtocolVersion>()
|
||||
.map_err(|error| miette::miette!("{context}: {error}; {MCP_VERSION_REMEDIATION}"))?;
|
||||
if !seen.insert(version) {
|
||||
return Err(miette::miette!(
|
||||
"{context} has duplicate protocol version '{value}'"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mcp_config_from_proto(max_body_bytes: u32, mcp: Option<&McpOptions>) -> Option<McpConfigDef> {
|
||||
let mut versions = mcp
|
||||
.map(|config| config.versions.clone())
|
||||
@@ -319,22 +104,16 @@ impl L7ConfigStanza {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an L7 nested config stanza and return the flattened runtime fields
|
||||
/// consumed by the supervisor policy engine.
|
||||
///
|
||||
/// The stanza schema stays tied to this crate's canonical serde definitions, so
|
||||
/// adding a new supported field requires updating this conversion next to the
|
||||
/// type that parses it. MCP revision fields are validated and materialized
|
||||
/// before the alias is flattened so both runtime ingress paths receive the
|
||||
/// same canonical allowlist.
|
||||
/// Parse an L7 config fragment using the canonical schema and return runtime
|
||||
/// fields consumed by the supervisor policy engine.
|
||||
pub fn l7_config_alias_runtime_fields(
|
||||
stanza: L7ConfigStanza,
|
||||
value: serde_json::Value,
|
||||
) -> Result<Vec<(&'static str, serde_json::Value)>> {
|
||||
match stanza {
|
||||
L7ConfigStanza::JsonRpc => {
|
||||
let JsonRpcConfigDef { max_body_bytes } = serde_json::from_value(value)
|
||||
.map_err(|error| miette::miette!("invalid json_rpc config: {error}"))?;
|
||||
let JsonRpcConfigDef { max_body_bytes } =
|
||||
openshell_policy_schema::parse_json_rpc_config(value)?;
|
||||
let mut fields = Vec::new();
|
||||
if max_body_bytes > 0 {
|
||||
fields.push(("json_rpc_max_body_bytes", serde_json::json!(max_body_bytes)));
|
||||
@@ -342,9 +121,7 @@ pub fn l7_config_alias_runtime_fields(
|
||||
Ok(fields)
|
||||
}
|
||||
L7ConfigStanza::Mcp => {
|
||||
let config: McpConfigDef = serde_json::from_value(value)
|
||||
.map_err(|error| miette::miette!("invalid mcp config: {error}"))?;
|
||||
validate_authored_mcp_versions(config.versions.as_deref(), "invalid mcp config")?;
|
||||
let config = openshell_policy_schema::parse_mcp_config(value)?;
|
||||
let McpConfigDef {
|
||||
versions,
|
||||
max_body_bytes,
|
||||
@@ -353,126 +130,20 @@ pub fn l7_config_alias_runtime_fields(
|
||||
} = config;
|
||||
let mut versions = versions.unwrap_or_else(default_mcp_versions);
|
||||
canonicalize_mcp_versions(&mut versions);
|
||||
let mut fields = Vec::new();
|
||||
fields.push(("mcp_versions", serde_json::json!(versions)));
|
||||
let mut fields = vec![("mcp_versions", serde_json::json!(versions))];
|
||||
if max_body_bytes > 0 {
|
||||
fields.push(("json_rpc_max_body_bytes", serde_json::json!(max_body_bytes)));
|
||||
}
|
||||
if let Some(strict_tool_names) = strict_tool_names {
|
||||
fields.push((
|
||||
"mcp_strict_tool_names",
|
||||
serde_json::json!(strict_tool_names),
|
||||
));
|
||||
if let Some(value) = strict_tool_names {
|
||||
fields.push(("mcp_strict_tool_names", serde_json::json!(value)));
|
||||
}
|
||||
if let Some(allow_all_known_mcp_methods) = allow_all_known_mcp_methods {
|
||||
fields.push((
|
||||
"mcp_allow_all_known_mcp_methods",
|
||||
serde_json::json!(allow_all_known_mcp_methods),
|
||||
));
|
||||
if let Some(value) = allow_all_known_mcp_methods {
|
||||
fields.push(("mcp_allow_all_known_mcp_methods", serde_json::json!(value)));
|
||||
}
|
||||
Ok(fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct GraphqlOperationDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_type: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_name: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct L7RuleDef {
|
||||
allow: L7AllowDef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct L7AllowDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
method: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
path: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
command: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
query: BTreeMap<String, QueryMatcherDef>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_type: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_name: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
fields: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tool: Option<QueryMatcherDef>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
params: BTreeMap<String, ParamMatcherDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum QueryMatcherDef {
|
||||
// Short form: `query: { repo: "NVIDIA/*" }`.
|
||||
Glob(String),
|
||||
// Expanded form: `query: { repo: { any: ["NVIDIA/*", "openai/*"] } }`.
|
||||
Any(QueryAnyDef),
|
||||
}
|
||||
|
||||
// MCP params can be authored as nested maps in YAML, but the runtime matcher
|
||||
// map remains flat so the Rego policy can share query-param matching.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ParamMatcherDef {
|
||||
Matcher(QueryMatcherDef),
|
||||
Object(BTreeMap<String, Self>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct QueryAnyDef {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
any: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct L7DenyRuleDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
method: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
path: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
command: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
query: BTreeMap<String, QueryMatcherDef>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_type: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
operation_name: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
fields: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tool: Option<QueryMatcherDef>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
params: BTreeMap<String, ParamMatcherDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct NetworkBinaryDef {
|
||||
path: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// YAML → proto conversion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn matcher_def_to_proto(matcher: QueryMatcherDef) -> L7QueryMatcher {
|
||||
match matcher {
|
||||
QueryMatcherDef::Glob(glob) => L7QueryMatcher { glob, any: vec![] },
|
||||
@@ -974,21 +645,37 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
let network_policies = policy
|
||||
.network_policies
|
||||
.iter()
|
||||
.map(|(key, rule)| {
|
||||
.map(|(key, rule)| -> Result<_> {
|
||||
let yaml_rule = NetworkPolicyRuleDef {
|
||||
name: rule.name.clone(),
|
||||
endpoints: rule
|
||||
.endpoints
|
||||
.iter()
|
||||
.map(|e| {
|
||||
.map(|e| -> Result<_> {
|
||||
// Use compact form: if ports has exactly 1 element,
|
||||
// emit port (scalar). If >1, emit ports (array).
|
||||
// Proto uses u32; YAML uses u16. Clamp at boundary.
|
||||
let clamp = |v: u32| -> u16 { v.min(65535) as u16 };
|
||||
// Proto uses u32; authored ports are u16. Reject an
|
||||
// invalid protobuf value instead of silently clamping.
|
||||
let checked = |value: u32| {
|
||||
u16::try_from(value).map_err(|_| {
|
||||
miette::miette!(
|
||||
"cannot serialize endpoint '{}': port {value} exceeds {}",
|
||||
e.host,
|
||||
u16::MAX
|
||||
)
|
||||
})
|
||||
};
|
||||
let (port, ports) = if e.ports.len() > 1 {
|
||||
(0, e.ports.iter().map(|&p| clamp(p)).collect())
|
||||
(
|
||||
0,
|
||||
e.ports
|
||||
.iter()
|
||||
.copied()
|
||||
.map(checked)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
)
|
||||
} else {
|
||||
(clamp(e.ports.first().copied().unwrap_or(e.port)), vec![])
|
||||
(checked(e.ports.first().copied().unwrap_or(e.port))?, vec![])
|
||||
};
|
||||
let protocol = e.protocol.clone();
|
||||
let mcp_allow_all_known_mcp_methods = !is_mcp_protocol(&protocol)
|
||||
@@ -1022,7 +709,7 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
} else {
|
||||
(json_rpc_config_from_proto(e.json_rpc_max_body_bytes), None)
|
||||
};
|
||||
NetworkEndpointDef {
|
||||
Ok(NetworkEndpointDef {
|
||||
host: e.host.clone(),
|
||||
path: e.path.clone(),
|
||||
port,
|
||||
@@ -1064,9 +751,9 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
}),
|
||||
json_rpc,
|
||||
mcp,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
binaries: rule
|
||||
.binaries
|
||||
.iter()
|
||||
@@ -1075,14 +762,21 @@ fn from_proto(policy: &SandboxPolicy) -> Result<PolicyFile> {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
(key.clone(), yaml_rule)
|
||||
Ok((key.clone(), yaml_rule))
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<BTreeMap<_, _>>>()?;
|
||||
|
||||
let network_middlewares = middleware::from_proto(&policy.network_middlewares);
|
||||
|
||||
Ok(PolicyFile {
|
||||
version: policy.version,
|
||||
// Proto3 scalar fields do not preserve presence. Treat zero as an
|
||||
// omitted authored version and materialize the only supported schema
|
||||
// version in canonical output.
|
||||
version: if policy.version == 0 {
|
||||
1
|
||||
} else {
|
||||
policy.version
|
||||
},
|
||||
filesystem_policy,
|
||||
landlock,
|
||||
process,
|
||||
@@ -1143,43 +837,18 @@ pub fn is_valid_sandbox_identity(value: &str) -> bool {
|
||||
// Validate raw authored values and their relationship to the endpoint protocol
|
||||
// before conversion. Keeping validation outside the Serde error wrapper makes
|
||||
// actionable MCP diagnostics the top-level user-facing error.
|
||||
fn validate_mcp_version_schema(policy: &PolicyFile) -> Result<()> {
|
||||
for (policy_key, rule) in &policy.network_policies {
|
||||
let policy_name = if rule.name.is_empty() {
|
||||
policy_key
|
||||
} else {
|
||||
&rule.name
|
||||
};
|
||||
for endpoint in &rule.endpoints {
|
||||
if is_mcp_protocol(&endpoint.protocol) {
|
||||
let context = format!(
|
||||
"network policy '{policy_name}': MCP endpoint '{}'",
|
||||
endpoint.host
|
||||
);
|
||||
validate_authored_mcp_versions(
|
||||
endpoint
|
||||
.mcp
|
||||
.as_ref()
|
||||
.and_then(|config| config.versions.as_deref()),
|
||||
&context,
|
||||
)?;
|
||||
} else if endpoint.mcp.is_some() {
|
||||
return Err(miette::miette!(
|
||||
"network policy '{policy_name}': non-MCP endpoint '{}' cannot configure mcp options",
|
||||
endpoint.host
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a sandbox policy from a YAML string.
|
||||
pub fn parse_sandbox_policy(yaml: &str) -> Result<SandboxPolicy> {
|
||||
let raw: PolicyFile = serde_yml::from_str(yaml)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to parse sandbox policy YAML")?;
|
||||
validate_mcp_version_schema(&raw)?;
|
||||
let raw = openshell_policy_schema::parse_policy(yaml)?;
|
||||
to_proto(raw)
|
||||
}
|
||||
|
||||
/// Parse a sandbox policy from a regular file using the shared bounded reader.
|
||||
pub fn parse_sandbox_policy_file(path: &Path) -> Result<SandboxPolicy> {
|
||||
let raw = openshell_policy_schema::parse_policy_file(
|
||||
path,
|
||||
openshell_policy_schema::ParseLimits::default(),
|
||||
)?;
|
||||
to_proto(raw)
|
||||
}
|
||||
|
||||
@@ -1189,12 +858,11 @@ pub fn parse_sandbox_policy(yaml: &str) -> Result<SandboxPolicy> {
|
||||
/// canonical YAML field names (e.g. `filesystem_policy`, not `filesystem`)
|
||||
/// and is round-trippable through `parse_sandbox_policy`.
|
||||
pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result<String> {
|
||||
validate_proto_version_for_authored_serialization(policy)?;
|
||||
let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone())
|
||||
.map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?;
|
||||
let yaml_repr = from_proto(&canonical)?;
|
||||
serde_yml::to_string(&yaml_repr)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to serialize policy to YAML")
|
||||
openshell_policy_schema::serialize_policy(&yaml_repr)
|
||||
}
|
||||
|
||||
/// Convert a proto sandbox policy into the canonical policy JSON representation.
|
||||
@@ -1202,12 +870,21 @@ pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result<String> {
|
||||
/// The shape mirrors the YAML schema used by [`serialize_sandbox_policy`], so
|
||||
/// automation can use the same documented field names in either format.
|
||||
pub fn sandbox_policy_to_json_value(policy: &SandboxPolicy) -> Result<serde_json::Value> {
|
||||
validate_proto_version_for_authored_serialization(policy)?;
|
||||
let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone())
|
||||
.map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?;
|
||||
let json_repr = from_proto(&canonical)?;
|
||||
serde_json::to_value(&json_repr)
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to serialize policy to JSON")
|
||||
openshell_policy_schema::policy_to_json_value(&json_repr)
|
||||
}
|
||||
|
||||
fn validate_proto_version_for_authored_serialization(policy: &SandboxPolicy) -> Result<()> {
|
||||
if !matches!(policy.version, 0 | 1) {
|
||||
miette::bail!(
|
||||
"cannot serialize unsupported protobuf policy version {}; expected 0 (omitted) or 1",
|
||||
policy.version
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Serialize a proto sandbox policy to a pretty-printed JSON string.
|
||||
@@ -1228,20 +905,14 @@ pub fn serialize_sandbox_policy_json(policy: &SandboxPolicy) -> Result<String> {
|
||||
/// caller to omit the policy and let the server / sandbox apply its own
|
||||
/// default.
|
||||
pub fn load_sandbox_policy(cli_path: Option<&str>) -> Result<Option<SandboxPolicy>> {
|
||||
let contents = if let Some(p) = cli_path {
|
||||
let path = Path::new(p);
|
||||
std::fs::read_to_string(path)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("failed to read sandbox policy from {}", path.display()))?
|
||||
let policy = if let Some(p) = cli_path {
|
||||
parse_sandbox_policy_file(Path::new(p))?
|
||||
} else if let Ok(policy_path) = std::env::var("OPENSHELL_SANDBOX_POLICY") {
|
||||
let path = Path::new(&policy_path);
|
||||
std::fs::read_to_string(path)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("failed to read sandbox policy from {}", path.display()))?
|
||||
parse_sandbox_policy_file(Path::new(&policy_path))?
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
parse_sandbox_policy(&contents).map(Some)
|
||||
Ok(Some(policy))
|
||||
}
|
||||
|
||||
/// Well-known path where a sandbox container image can ship a policy YAML file.
|
||||
@@ -2212,7 +1883,7 @@ fn truncate_for_display(s: &str) -> String {
|
||||
///
|
||||
/// Re-exported from `openshell-core` so existing call sites
|
||||
/// (`openshell_policy::normalize_path`) keep resolving.
|
||||
pub use openshell_core::paths::normalize_path;
|
||||
pub use openshell_policy_schema::normalize_path;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
@@ -2446,19 +2117,88 @@ network_policies:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_identity_omission_survives_yaml_round_trip() {
|
||||
let policy = parse_sandbox_policy("version: 1\nprocess:\n run_as_user: \"1234\"\n")
|
||||
.expect("partial process identity should parse");
|
||||
let process = policy.process.as_ref().expect("process section");
|
||||
assert_eq!(process.run_as_user, "1234");
|
||||
assert!(process.run_as_group.is_empty());
|
||||
assert!(validate_sandbox_policy(&policy).is_ok());
|
||||
fn canonical_serializers_materialize_omitted_proto_version() {
|
||||
let policy = SandboxPolicy::default();
|
||||
|
||||
let yaml = serialize_sandbox_policy(&policy).expect("partial identity should serialize");
|
||||
assert!(yaml.contains("run_as_user"));
|
||||
assert!(!yaml.contains("run_as_group"));
|
||||
let reparsed = parse_sandbox_policy(&yaml).expect("round trip should parse");
|
||||
assert!(reparsed.process.unwrap().run_as_group.is_empty());
|
||||
let yaml = serialize_sandbox_policy(&policy).expect("serialize default protobuf policy");
|
||||
assert_eq!(
|
||||
parse_sandbox_policy(&yaml)
|
||||
.expect("canonical YAML must round trip")
|
||||
.version,
|
||||
1
|
||||
);
|
||||
|
||||
let json =
|
||||
sandbox_policy_to_json_value(&policy).expect("serialize default protobuf policy");
|
||||
assert_eq!(json["version"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_serializers_reject_unsupported_proto_version() {
|
||||
let policy = SandboxPolicy {
|
||||
version: 2,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let error = serialize_sandbox_policy(&policy)
|
||||
.expect_err("unsupported protobuf versions must not serialize");
|
||||
assert!(error.to_string().contains("protobuf policy version 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_serializers_preserve_independent_process_identity_omission() {
|
||||
let cases = [
|
||||
("", "", None, None),
|
||||
("1500", "", Some("1500"), None),
|
||||
("", "1600", None, Some("1600")),
|
||||
("1500", "1600", Some("1500"), Some("1600")),
|
||||
];
|
||||
|
||||
for (user, group, expected_user, expected_group) in cases {
|
||||
let policy = SandboxPolicy {
|
||||
version: 1,
|
||||
process: Some(ProcessPolicy {
|
||||
run_as_user: user.to_owned(),
|
||||
run_as_group: group.to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let yaml = serialize_sandbox_policy(&policy)
|
||||
.expect("omitted process identity components must serialize to YAML");
|
||||
assert_eq!(yaml.contains("run_as_user:"), expected_user.is_some());
|
||||
assert_eq!(yaml.contains("run_as_group:"), expected_group.is_some());
|
||||
|
||||
let reparsed = parse_sandbox_policy(&yaml).expect("canonical YAML must round trip");
|
||||
let reparsed_user = reparsed
|
||||
.process
|
||||
.as_ref()
|
||||
.map(|process| process.run_as_user.as_str())
|
||||
.filter(|value| !value.is_empty());
|
||||
let reparsed_group = reparsed
|
||||
.process
|
||||
.as_ref()
|
||||
.map(|process| process.run_as_group.as_str())
|
||||
.filter(|value| !value.is_empty());
|
||||
assert_eq!(reparsed_user, expected_user);
|
||||
assert_eq!(reparsed_group, expected_group);
|
||||
|
||||
let json = sandbox_policy_to_json_value(&policy)
|
||||
.expect("omitted process identity components must serialize to JSON");
|
||||
let json_process = json.get("process");
|
||||
assert_eq!(
|
||||
json_process.and_then(|process| process.get("run_as_user")),
|
||||
expected_user.map(serde_json::Value::from).as_ref()
|
||||
);
|
||||
assert_eq!(
|
||||
json_process.and_then(|process| process.get("run_as_group")),
|
||||
expected_group.map(serde_json::Value::from).as_ref()
|
||||
);
|
||||
assert_eq!(
|
||||
json_process.is_some(),
|
||||
expected_user.is_some() || expected_group.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4853,6 +4593,45 @@ network_policies:
|
||||
assert_eq!(proto1, proto2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_preserves_any_as_an_mcp_parameter_name() {
|
||||
let yaml = r#"
|
||||
version: 1
|
||||
network_policies:
|
||||
mcp:
|
||||
endpoints:
|
||||
- host: mcp.example.com
|
||||
port: 443
|
||||
protocol: mcp
|
||||
mcp: {}
|
||||
rules:
|
||||
- allow:
|
||||
method: tools/call
|
||||
params:
|
||||
arguments:
|
||||
any: "first"
|
||||
other: "second"
|
||||
"#;
|
||||
|
||||
let proto = parse_sandbox_policy(yaml).expect("authored policy must parse");
|
||||
let params = &proto.network_policies["mcp"].endpoints[0].rules[0]
|
||||
.allow
|
||||
.as_ref()
|
||||
.expect("allow rule")
|
||||
.params;
|
||||
assert_eq!(params["arguments.any"].glob, "first");
|
||||
assert_eq!(params["arguments.other"].glob, "second");
|
||||
|
||||
let serialized = serialize_sandbox_policy(&proto).expect("protobuf policy must serialize");
|
||||
assert!(serialized.contains("arguments:"));
|
||||
assert!(serialized.contains("any: first"));
|
||||
assert!(serialized.contains("other: second"));
|
||||
|
||||
let reparsed =
|
||||
parse_sandbox_policy(&serialized).expect("serialized protobuf policy must parse again");
|
||||
assert_eq!(reparsed, proto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_unsupported_json_rpc_config_fields() {
|
||||
let yaml = r"
|
||||
@@ -5034,4 +4813,26 @@ network_policies:
|
||||
"port >65535 should fail to parse"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialization_rejects_proto_port_above_u16() {
|
||||
let mut policy = SandboxPolicy {
|
||||
version: 1,
|
||||
..Default::default()
|
||||
};
|
||||
policy.network_policies.insert(
|
||||
"too-wide".to_owned(),
|
||||
NetworkPolicyRule {
|
||||
endpoints: vec![NetworkEndpoint {
|
||||
host: "example.com".to_owned(),
|
||||
port: u32::from(u16::MAX) + 1,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let error =
|
||||
serialize_sandbox_policy(&policy).expect_err("wide protobuf port must not be clamped");
|
||||
assert!(error.to_string().contains("exceeds"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2064,21 +2064,14 @@ fn expand_existing_access(
|
||||
}
|
||||
|
||||
fn expand_access_preset(protocol: &str, access: &str) -> Option<Vec<L7Rule>> {
|
||||
let methods = match (protocol, access) {
|
||||
(_, "full") => vec!["*"],
|
||||
("websocket", "read-only") => vec!["GET"],
|
||||
("websocket", "read-write") => vec!["GET", "WEBSOCKET_TEXT"],
|
||||
(_, "read-only") => vec!["GET", "HEAD", "OPTIONS"],
|
||||
(_, "read-write") => vec!["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"],
|
||||
_ => return None,
|
||||
};
|
||||
let methods = openshell_policy_schema::expand_access_preset(protocol, access)?;
|
||||
|
||||
Some(
|
||||
methods
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|method| L7Rule {
|
||||
allow: Some(L7Allow {
|
||||
method: method.to_string(),
|
||||
method: (*method).to_string(),
|
||||
path: "**".to_string(),
|
||||
command: String::new(),
|
||||
query: HashMap::default(),
|
||||
|
||||
@@ -13,63 +13,39 @@ use openshell_core::proto::{
|
||||
use openshell_core::proto_struct::{
|
||||
ProtoStructError, json_object_to_struct, struct_to_json_object,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::PolicyViolation;
|
||||
|
||||
pub use openshell_core::host_pattern::host_matches as middleware_host_matches;
|
||||
use openshell_core::host_pattern::{HostPattern, HostSelector};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkMiddlewareConfigDef {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
name: String,
|
||||
middleware: String,
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
order: i32,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
config: BTreeMap<String, serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
on_error: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
endpoints: Option<MiddlewareEndpointSelectorDef>,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
||||
value == &T::default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct MiddlewareEndpointSelectorDef {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
include: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
exclude: Vec<String>,
|
||||
}
|
||||
use openshell_policy_schema::{
|
||||
MiddlewareEndpointSelector as MiddlewareEndpointSelectorDef,
|
||||
NetworkMiddleware as NetworkMiddlewareConfigDef,
|
||||
};
|
||||
|
||||
/// Middleware-relevant projection of the runtime policy JSON accepted by the
|
||||
/// supervisor's local-file mode. Unrelated network and L7 fields are ignored;
|
||||
/// middleware entries retain their strict canonical schema.
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct MiddlewareValidationPolicyDef {
|
||||
struct SupervisorRuntimePolicyJson {
|
||||
#[serde(default)]
|
||||
network_middlewares: BTreeMap<String, NetworkMiddlewareConfigDef>,
|
||||
#[serde(default)]
|
||||
network_policies: BTreeMap<String, MiddlewareValidationNetworkPolicyDef>,
|
||||
network_policies: BTreeMap<String, SupervisorRuntimeNetworkPolicyJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct MiddlewareValidationNetworkPolicyDef {
|
||||
struct SupervisorRuntimeNetworkPolicyJson {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
endpoints: Vec<MiddlewareValidationEndpointDef>,
|
||||
endpoints: Vec<SupervisorRuntimeEndpointJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct MiddlewareValidationEndpointDef {
|
||||
struct SupervisorRuntimeEndpointJson {
|
||||
#[serde(default)]
|
||||
host: String,
|
||||
#[serde(default)]
|
||||
@@ -155,7 +131,7 @@ pub fn validate_json_with_config<F>(
|
||||
where
|
||||
F: Fn(&str, &prost_types::Struct) -> Result<(), String>,
|
||||
{
|
||||
let definition: MiddlewareValidationPolicyDef = serde_json::from_value(data.clone())
|
||||
let definition: SupervisorRuntimePolicyJson = serde_json::from_value(data.clone())
|
||||
.map_err(|error| format!("failed to parse network middleware policy: {error}"))?;
|
||||
let network_middlewares = into_proto(definition.network_middlewares)
|
||||
.map_err(|error| format!("failed to convert network middleware config: {error}"))?;
|
||||
|
||||
@@ -16,6 +16,7 @@ prebuilt-z3 = ["z3/gh-release"]
|
||||
|
||||
[dependencies]
|
||||
z3 = { workspace = true }
|
||||
openshell-policy-schema = { path = "../openshell-policy-schema" }
|
||||
serde = { workspace = true }
|
||||
serde_yml = { workspace = true }
|
||||
miette = { workspace = true }
|
||||
|
||||
@@ -93,8 +93,12 @@ be additive — they don't displace existing categories.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **Policy** — a `SandboxPolicy` proto, parsed via
|
||||
`openshell-policy::parse_sandbox_policy`.
|
||||
- **Policy** — authored YAML decoded by `openshell-policy-schema` with the shared
|
||||
fail-closed parser, then projected into the prover-only `PolicyModel`.
|
||||
Missing `version` and unknown authored fields are rejected consistently with
|
||||
runtime parsing. An absent `filesystem_policy` uses the runtime-effective
|
||||
`include_workdir: true`; an explicitly present empty object remains `false`.
|
||||
Explicit `protocol: tcp` is treated as L4, matching runtime classification.
|
||||
- **Credential set** — built from the sandbox's attached providers in
|
||||
`crates/openshell-server/src/grpc/policy.rs::build_credential_set_for_sandbox`.
|
||||
v1 captures presence only (host-coarse); no scope modeling.
|
||||
@@ -103,6 +107,10 @@ be additive — they don't displace existing categories.
|
||||
the binary's protocols, `bypasses_l7` flag, and `can_exfiltrate`
|
||||
capability.
|
||||
|
||||
Accepted-risk files, credential descriptors, and binary registries are
|
||||
separate file formats. They continue to use their own serde definitions and do
|
||||
not form part of the authored policy schema.
|
||||
|
||||
## Outputs
|
||||
|
||||
- A list of `Finding` values, one per fired category. Each finding's
|
||||
|
||||
@@ -127,6 +127,35 @@ filesystem_policy:
|
||||
assert!(!readable.contains(&policy::WORKDIR_PATH_SYMBOL.to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_filesystem_uses_runtime_effective_workdir_default() {
|
||||
let model = policy::parse_policy_str("version: 1\n").expect("parse");
|
||||
assert!(model.filesystem_policy.include_workdir);
|
||||
assert!(
|
||||
model
|
||||
.filesystem_policy
|
||||
.readable_paths(None)
|
||||
.contains(&policy::WORKDIR_PATH_SYMBOL.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn policy_parser_requires_version_and_rejects_unknown_authority() {
|
||||
assert!(policy::parse_policy_str("network_policies: {}\n").is_err());
|
||||
assert!(policy::parse_policy_str("version: 1\nfuture_authority: true\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_tcp_is_l4_in_the_risk_projection() {
|
||||
let model = policy::parse_policy_str(
|
||||
"version: 1\nnetwork_policies:\n tcp:\n endpoints:\n - host: example.com\n port: 443\n protocol: tcp\n",
|
||||
)
|
||||
.expect("parse");
|
||||
let endpoint = &model.network_policies["tcp"].endpoints[0];
|
||||
assert!(!endpoint.is_l7_enforced());
|
||||
assert_eq!(endpoint.intent(), policy::PolicyIntent::L4Only);
|
||||
}
|
||||
|
||||
// 4. Workdir excluded when include_workdir: false.
|
||||
#[test]
|
||||
fn test_include_workdir_false() {
|
||||
|
||||
@@ -7,17 +7,13 @@
|
||||
//! types) because the prover needs fields like `access`, `protocol`, and
|
||||
//! individual L7 rules that the proto representation strips.
|
||||
|
||||
use openshell_policy_schema::{
|
||||
AccessPreset, L7Allow as AuthoredAllow, NetworkEndpoint as AuthoredEndpoint, ParseLimits,
|
||||
PolicyDocument,
|
||||
};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use serde::Deserialize;
|
||||
use serde::de::IgnoredAny;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Policy intent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The inferred access intent for an endpoint.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PolicyIntent {
|
||||
@@ -29,106 +25,22 @@ pub enum PolicyIntent {
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PolicyIntent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::L4Only => write!(f, "l4_only"),
|
||||
Self::ReadOnly => write!(f, "read_only"),
|
||||
Self::ReadWrite => write!(f, "read_write"),
|
||||
Self::Full => write!(f, "full"),
|
||||
Self::Custom => write!(f, "custom"),
|
||||
}
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
Self::L4Only => "l4_only",
|
||||
Self::ReadOnly => "read_only",
|
||||
Self::ReadWrite => "read_write",
|
||||
Self::Full => "full",
|
||||
Self::Custom => "custom",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP methods considered to be write operations.
|
||||
pub const WRITE_METHODS: &[&str] = &["POST", "PUT", "PATCH", "DELETE"];
|
||||
|
||||
/// All standard HTTP methods.
|
||||
const ALL_METHODS: &[&str] = &["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serde types — mirrors the YAML schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PolicyFile {
|
||||
#[allow(dead_code)]
|
||||
version: Option<u32>,
|
||||
#[serde(default)]
|
||||
filesystem_policy: Option<FilesystemDef>,
|
||||
#[serde(default)]
|
||||
network_policies: Option<BTreeMap<String, NetworkPolicyRuleDef>>,
|
||||
// Ignored fields the prover does not need.
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
landlock: Option<IgnoredAny>,
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
process: Option<IgnoredAny>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FilesystemDef {
|
||||
#[serde(default)]
|
||||
include_workdir: bool,
|
||||
#[serde(default)]
|
||||
read_only: Vec<String>,
|
||||
#[serde(default)]
|
||||
read_write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NetworkPolicyRuleDef {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
endpoints: Vec<EndpointDef>,
|
||||
#[serde(default)]
|
||||
binaries: Vec<BinaryDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EndpointDef {
|
||||
#[serde(default)]
|
||||
host: String,
|
||||
#[serde(default)]
|
||||
port: u16,
|
||||
#[serde(default)]
|
||||
ports: Vec<u16>,
|
||||
#[serde(default)]
|
||||
protocol: String,
|
||||
#[serde(default)]
|
||||
tls: String,
|
||||
#[serde(default)]
|
||||
enforcement: String,
|
||||
#[serde(default)]
|
||||
access: String,
|
||||
#[serde(default)]
|
||||
rules: Vec<L7RuleDef>,
|
||||
#[serde(default)]
|
||||
allowed_ips: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct L7RuleDef {
|
||||
allow: L7AllowDef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct L7AllowDef {
|
||||
#[serde(default)]
|
||||
method: String,
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BinaryDef {
|
||||
path: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public model types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -158,19 +70,19 @@ pub struct Endpoint {
|
||||
impl Endpoint {
|
||||
/// Whether this endpoint has L7 (protocol-level) enforcement.
|
||||
pub fn is_l7_enforced(&self) -> bool {
|
||||
!self.protocol.is_empty()
|
||||
!self.protocol.is_empty() && !self.protocol.eq_ignore_ascii_case("tcp")
|
||||
}
|
||||
|
||||
/// The inferred access intent.
|
||||
pub fn intent(&self) -> PolicyIntent {
|
||||
if self.protocol.is_empty() {
|
||||
if !self.is_l7_enforced() {
|
||||
return PolicyIntent::L4Only;
|
||||
}
|
||||
match self.access.as_str() {
|
||||
"read-only" => PolicyIntent::ReadOnly,
|
||||
"read-write" => PolicyIntent::ReadWrite,
|
||||
"full" => PolicyIntent::Full,
|
||||
_ => {
|
||||
match AccessPreset::parse(&self.access) {
|
||||
Some(AccessPreset::ReadOnly) => PolicyIntent::ReadOnly,
|
||||
Some(AccessPreset::ReadWrite) => PolicyIntent::ReadWrite,
|
||||
Some(AccessPreset::Full) => PolicyIntent::Full,
|
||||
None => {
|
||||
if self.rules.is_empty() {
|
||||
return PolicyIntent::Custom;
|
||||
}
|
||||
@@ -204,33 +116,32 @@ impl Endpoint {
|
||||
|
||||
/// The set of HTTP methods this endpoint allows. Empty means all (L4-only).
|
||||
pub fn allowed_methods(&self) -> HashSet<String> {
|
||||
if self.protocol.is_empty() {
|
||||
if !self.is_l7_enforced() {
|
||||
return HashSet::new(); // L4-only: all traffic passes
|
||||
}
|
||||
match self.access.as_str() {
|
||||
"read-only" => ["GET", "HEAD", "OPTIONS"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect(),
|
||||
"read-write" => ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect(),
|
||||
"full" => ALL_METHODS.iter().map(|s| (*s).to_owned()).collect(),
|
||||
_ => {
|
||||
if !self.rules.is_empty() {
|
||||
let mut methods = HashSet::new();
|
||||
for r in &self.rules {
|
||||
let m = r.method.to_uppercase();
|
||||
if m == "*" {
|
||||
return ALL_METHODS.iter().map(|s| (*s).to_owned()).collect();
|
||||
}
|
||||
methods.insert(m);
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
HashSet::new()
|
||||
if let Some(preset) = AccessPreset::parse(&self.access) {
|
||||
let methods = preset.methods(&self.protocol);
|
||||
if methods.contains(&"*") {
|
||||
ALL_METHODS
|
||||
.iter()
|
||||
.map(|method| (*method).to_owned())
|
||||
.collect()
|
||||
} else {
|
||||
methods.iter().map(|method| (*method).to_owned()).collect()
|
||||
}
|
||||
} else {
|
||||
if !self.rules.is_empty() {
|
||||
let mut methods = HashSet::new();
|
||||
for r in &self.rules {
|
||||
let m = r.method.to_uppercase();
|
||||
if m == "*" {
|
||||
return ALL_METHODS.iter().map(|s| (*s).to_owned()).collect();
|
||||
}
|
||||
methods.insert(m);
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
HashSet::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,80 +255,78 @@ impl PolicyModel {
|
||||
// Parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parse an `OpenShell` policy YAML file into a [`PolicyModel`].
|
||||
pub fn parse_policy(path: &Path) -> Result<PolicyModel> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("reading policy file {}", path.display()))?;
|
||||
parse_policy_str(&contents)
|
||||
/// Parse an `OpenShell` policy YAML file into a `PolicyModel`.
|
||||
pub fn parse_policy(path: &Path) -> miette::Result<PolicyModel> {
|
||||
let document = openshell_policy_schema::parse_policy_file(path, ParseLimits::default())?;
|
||||
Ok(project_policy(document))
|
||||
}
|
||||
|
||||
/// Parse a policy YAML string into a [`PolicyModel`].
|
||||
pub fn parse_policy_str(yaml: &str) -> Result<PolicyModel> {
|
||||
let raw: PolicyFile = serde_yml::from_str(yaml)
|
||||
.into_diagnostic()
|
||||
.wrap_err("parsing policy YAML")?;
|
||||
/// Parse a policy YAML string into a `PolicyModel`.
|
||||
pub fn parse_policy_str(yaml: &str) -> miette::Result<PolicyModel> {
|
||||
let document = openshell_policy_schema::parse_policy(yaml)?;
|
||||
Ok(project_policy(document))
|
||||
}
|
||||
|
||||
let fs = match raw.filesystem_policy {
|
||||
Some(fs_def) => FilesystemPolicy {
|
||||
include_workdir: fs_def.include_workdir,
|
||||
read_only: fs_def.read_only,
|
||||
read_write: fs_def.read_write,
|
||||
},
|
||||
None => FilesystemPolicy::default(),
|
||||
fn project_policy(document: PolicyDocument) -> PolicyModel {
|
||||
let filesystem = document.effective_filesystem_policy();
|
||||
let filesystem_policy = FilesystemPolicy {
|
||||
include_workdir: filesystem.include_workdir,
|
||||
read_only: filesystem.read_only,
|
||||
read_write: filesystem.read_write,
|
||||
};
|
||||
|
||||
let mut network_policies = BTreeMap::new();
|
||||
if let Some(np) = raw.network_policies {
|
||||
for (key, rule_raw) in np {
|
||||
let endpoints = rule_raw
|
||||
.endpoints
|
||||
.into_iter()
|
||||
.map(|ep_raw| {
|
||||
let rules = ep_raw
|
||||
.rules
|
||||
.into_iter()
|
||||
.map(|r| L7Rule {
|
||||
method: r.allow.method,
|
||||
path: r.allow.path,
|
||||
command: r.allow.command,
|
||||
})
|
||||
.collect();
|
||||
Endpoint {
|
||||
host: ep_raw.host,
|
||||
port: ep_raw.port,
|
||||
ports: ep_raw.ports,
|
||||
protocol: ep_raw.protocol,
|
||||
tls: ep_raw.tls,
|
||||
enforcement: ep_raw.enforcement,
|
||||
access: ep_raw.access,
|
||||
rules,
|
||||
allowed_ips: ep_raw.allowed_ips,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let binaries = rule_raw
|
||||
let network_policies = document
|
||||
.network_policies
|
||||
.into_iter()
|
||||
.map(|(key, rule)| {
|
||||
let name = rule.effective_name(&key).to_owned();
|
||||
let endpoints = rule.endpoints.into_iter().map(project_endpoint).collect();
|
||||
let binaries = rule
|
||||
.binaries
|
||||
.into_iter()
|
||||
.map(|b| Binary { path: b.path })
|
||||
.map(|binary| Binary { path: binary.path })
|
||||
.collect();
|
||||
|
||||
let name = rule_raw.name.unwrap_or_else(|| key.clone());
|
||||
network_policies.insert(
|
||||
(
|
||||
key,
|
||||
NetworkPolicyRule {
|
||||
name,
|
||||
endpoints,
|
||||
binaries,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(PolicyModel {
|
||||
version: raw.version.unwrap_or(1),
|
||||
filesystem_policy: fs,
|
||||
PolicyModel {
|
||||
version: document.version,
|
||||
filesystem_policy,
|
||||
network_policies,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn project_endpoint(endpoint: AuthoredEndpoint) -> Endpoint {
|
||||
let rules = endpoint
|
||||
.rules
|
||||
.into_iter()
|
||||
.map(|rule| project_allow(rule.allow))
|
||||
.collect();
|
||||
Endpoint {
|
||||
host: endpoint.host,
|
||||
port: endpoint.port,
|
||||
ports: endpoint.ports,
|
||||
protocol: endpoint.protocol,
|
||||
tls: endpoint.tls,
|
||||
enforcement: endpoint.enforcement,
|
||||
access: endpoint.access,
|
||||
rules,
|
||||
allowed_ips: endpoint.allowed_ips,
|
||||
}
|
||||
}
|
||||
|
||||
fn project_allow(allow: AuthoredAllow) -> L7Rule {
|
||||
L7Rule {
|
||||
method: allow.method,
|
||||
path: allow.path,
|
||||
command: allow.command,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,7 @@ mod tests {
|
||||
|
||||
async fn authenticator(driver: NoopTestDriver) -> ComputeDriverAuthenticator {
|
||||
let store = Arc::new(Store::connect("sqlite::memory:").await.unwrap());
|
||||
let compute =
|
||||
new_test_runtime_with_driver(store, "external-kubernetes", Arc::new(driver)).await;
|
||||
let compute = new_test_runtime_with_driver(store, "external-kubernetes", Arc::new(driver));
|
||||
ComputeDriverAuthenticator::new(compute)
|
||||
}
|
||||
|
||||
|
||||
@@ -4930,15 +4930,16 @@ fn is_terminal_failure_reason(reason: &str) -> bool {
|
||||
!transient_reasons.contains(&reason.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Debug)]
|
||||
pub struct NoopTestDriver {
|
||||
workspace_delete_failures: std::sync::atomic::AtomicUsize,
|
||||
sandbox_authentication: Option<Result<String, (Code, String)>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl NoopTestDriver {
|
||||
#[cfg(test)]
|
||||
pub fn failing_workspace_deletes(count: usize) -> Self {
|
||||
Self {
|
||||
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(count),
|
||||
@@ -4946,6 +4947,7 @@ impl NoopTestDriver {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn authenticating_sandbox(sandbox_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(0),
|
||||
@@ -4953,6 +4955,7 @@ impl NoopTestDriver {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn failing_sandbox_authentication(code: Code, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
workspace_delete_failures: std::sync::atomic::AtomicUsize::new(0),
|
||||
@@ -4961,7 +4964,7 @@ impl NoopTestDriver {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for NoopTestDriver {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -4971,7 +4974,7 @@ impl Default for NoopTestDriver {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[tonic::async_trait]
|
||||
impl ComputeDriver for NoopTestDriver {
|
||||
async fn authenticate_sandbox(
|
||||
@@ -5130,18 +5133,18 @@ impl ComputeDriver for NoopTestDriver {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn new_test_runtime(store: Arc<Store>) -> ComputeRuntime {
|
||||
new_test_runtime_for_driver(store, "test").await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn new_test_runtime_for_driver(store: Arc<Store>, driver_name: &str) -> ComputeRuntime {
|
||||
new_test_runtime_with_driver(store, driver_name, Arc::new(NoopTestDriver::default())).await
|
||||
new_test_runtime_with_driver(store, driver_name, Arc::new(NoopTestDriver::default()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn new_test_runtime_with_driver(
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new_test_runtime_with_driver(
|
||||
store: Arc<Store>,
|
||||
driver_name: &str,
|
||||
driver: Arc<NoopTestDriver>,
|
||||
|
||||
@@ -141,7 +141,7 @@ impl RootfsTarStagingRegistry {
|
||||
}
|
||||
|
||||
/// Registry for a driver that does not accept rootfs tar sources.
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn disabled() -> Self {
|
||||
Self::new(None, 0)
|
||||
}
|
||||
|
||||
@@ -961,7 +961,7 @@ pub mod test_support {
|
||||
);
|
||||
crate::ensure_default_workspace(&store).await.unwrap();
|
||||
let driver = Arc::new(NoopTestDriver::failing_workspace_deletes(failures));
|
||||
let compute = new_test_runtime_with_driver(store.clone(), "test", driver).await;
|
||||
let compute = new_test_runtime_with_driver(store.clone(), "test", driver);
|
||||
Arc::new(ServerState::new(
|
||||
Config::new(None)
|
||||
.with_database_url("sqlite::memory:?cache=shared")
|
||||
|
||||
@@ -3,7 +3,17 @@
|
||||
|
||||
//! Test fixtures for exercising gateway integration points.
|
||||
|
||||
use crate::ServerState;
|
||||
use crate::auth::identity::{Identity, IdentityProvider};
|
||||
use crate::auth::principal::{Principal, UserPrincipal};
|
||||
use crate::compute::{new_test_runtime, new_test_runtime_for_driver};
|
||||
use crate::persistence::Store;
|
||||
use crate::sandbox_index::SandboxIndex;
|
||||
use crate::sandbox_watch::SandboxWatchBus;
|
||||
use crate::supervisor_session::SupervisorSessionRegistry;
|
||||
use crate::tracing_bus::TracingLogBus;
|
||||
use futures::{Stream, stream};
|
||||
use openshell_core::Config;
|
||||
#[cfg(unix)]
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_core::proto::compute::v1::{
|
||||
@@ -34,6 +44,55 @@ use tonic::{Request, Response, Status};
|
||||
|
||||
type WatchStream = Pin<Box<dyn Stream<Item = Result<WatchSandboxesEvent, Status>> + Send>>;
|
||||
|
||||
/// Build a real in-memory gateway service backed by the requested test compute driver.
|
||||
///
|
||||
/// This fixture is intentionally narrow: integration tests can exercise the public
|
||||
/// gRPC service without exposing the gateway's internal state construction details.
|
||||
pub async fn gateway_service_with_driver(driver_name: &str) -> crate::OpenShellService {
|
||||
let store = Arc::new(
|
||||
Store::connect("sqlite::memory:?cache=shared")
|
||||
.await
|
||||
.expect("in-memory gateway store should open"),
|
||||
);
|
||||
crate::ensure_default_workspace(&store)
|
||||
.await
|
||||
.expect("default workspace should be created");
|
||||
let compute = if driver_name == "test" {
|
||||
new_test_runtime(store.clone()).await
|
||||
} else {
|
||||
new_test_runtime_for_driver(store.clone(), driver_name).await
|
||||
};
|
||||
let state = Arc::new(ServerState::new(
|
||||
Config::new(None)
|
||||
.with_database_url("sqlite::memory:?cache=shared")
|
||||
.with_credential_drivers(["test-static"]),
|
||||
store,
|
||||
compute,
|
||||
SandboxIndex::new(),
|
||||
SandboxWatchBus::new(),
|
||||
TracingLogBus::new(),
|
||||
Arc::new(SupervisorSessionRegistry::new()),
|
||||
None,
|
||||
));
|
||||
crate::OpenShellService::new(state)
|
||||
}
|
||||
|
||||
/// Tonic interceptor that authenticates integration-test requests as the dev user.
|
||||
pub fn authenticate_as_dev_user(mut request: Request<()>) -> Result<Request<()>, Status> {
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(Principal::User(UserPrincipal {
|
||||
identity: Identity {
|
||||
subject: "dev-user".to_string(),
|
||||
display_name: None,
|
||||
roles: vec!["openshell-admin".to_string(), "openshell-user".to_string()],
|
||||
scopes: vec![],
|
||||
provider: IdentityProvider::Oidc,
|
||||
},
|
||||
}));
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FakeComputeDriverCall {
|
||||
GetCapabilities,
|
||||
|
||||
@@ -2745,10 +2745,10 @@ fn discover_policy_from_disk_or_default() -> openshell_core::proto::SandboxPolic
|
||||
/// hardcoded restrictive default if the file is missing or invalid.
|
||||
fn discover_policy_from_path(path: &std::path::Path) -> openshell_core::proto::SandboxPolicy {
|
||||
use openshell_policy::{
|
||||
parse_sandbox_policy, restrictive_default_policy, validate_sandbox_policy,
|
||||
parse_sandbox_policy_file, restrictive_default_policy, validate_sandbox_policy,
|
||||
};
|
||||
|
||||
let Ok(yaml) = std::fs::read_to_string(path) else {
|
||||
if !path.exists() {
|
||||
ocsf_emit!(
|
||||
ConfigStateChangeBuilder::new(ocsf_ctx())
|
||||
.severity(SeverityId::Informational)
|
||||
@@ -2761,20 +2761,20 @@ fn discover_policy_from_path(path: &std::path::Path) -> openshell_core::proto::S
|
||||
.build()
|
||||
);
|
||||
return restrictive_default_policy();
|
||||
};
|
||||
ocsf_emit!(
|
||||
ConfigStateChangeBuilder::new(ocsf_ctx())
|
||||
.severity(SeverityId::Informational)
|
||||
.status(StatusId::Success)
|
||||
.state(StateId::Enabled, "loaded")
|
||||
.message(format!(
|
||||
"Loaded sandbox policy from container disk [path:{}]",
|
||||
path.display()
|
||||
))
|
||||
.build()
|
||||
);
|
||||
match parse_sandbox_policy(&yaml) {
|
||||
}
|
||||
match parse_sandbox_policy_file(path) {
|
||||
Ok(policy) => {
|
||||
ocsf_emit!(
|
||||
ConfigStateChangeBuilder::new(ocsf_ctx())
|
||||
.severity(SeverityId::Informational)
|
||||
.status(StatusId::Success)
|
||||
.state(StateId::Enabled, "loaded")
|
||||
.message(format!(
|
||||
"Loaded sandbox policy from container disk [path:{}]",
|
||||
path.display()
|
||||
))
|
||||
.build()
|
||||
);
|
||||
// Validate the disk-loaded policy for safety.
|
||||
if let Err(violations) = validate_sandbox_policy(&policy) {
|
||||
let messages: Vec<String> = violations.iter().map(ToString::to_string).collect();
|
||||
@@ -5468,6 +5468,21 @@ network_policies:
|
||||
assert!(policy.filesystem.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_policy_from_oversized_file_returns_restrictive_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("policy.yaml");
|
||||
let oversized = format!("version: 1\n{}", " ".repeat(4 * 1024 * 1024));
|
||||
std::fs::write(&path, oversized).unwrap();
|
||||
|
||||
let policy = discover_policy_from_path(&path);
|
||||
assert!(policy.network_policies.is_empty());
|
||||
assert!(
|
||||
policy.filesystem.is_some(),
|
||||
"an oversized otherwise-valid policy must fall back instead of parsing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_policy_from_unsafe_yaml_falls_back_to_default() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -67,7 +67,7 @@ RUN case "$TARGETARCH" in \
|
||||
| tar xz --strip-components=2 -C /usr/local/bin "gh_${GH_VERSION}_linux_${gh_arch}/bin/gh"
|
||||
|
||||
# Install mise (NOTE: keep this version in sync with mise.toml)
|
||||
ARG MISE_VERSION=v2026.4.25
|
||||
ARG MISE_VERSION=v2026.9.9
|
||||
RUN curl https://mise.run | MISE_VERSION=$MISE_VERSION sh
|
||||
|
||||
# Copy mise.toml and task includes, then install all tools via mise
|
||||
|
||||
@@ -36,6 +36,32 @@ The YAML root and each present top-level section listed as an object or map must
|
||||
|
||||
Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting.
|
||||
|
||||
## Parsing and representation
|
||||
|
||||
OpenShell uses one canonical authored-policy representation for YAML and JSON.
|
||||
Runtime policy loading and the policy prover both decode through the same
|
||||
bounded parser before projecting the document into their runtime-specific
|
||||
models. The parser requires `version: 1`, rejects duplicate mapping keys and
|
||||
YAML merge keys, and accepts one document of at most 4 MiB. It limits nesting
|
||||
to 64 levels, total AST nodes to 100,000, parser events to 300,000, cumulative
|
||||
scalar data to 4 MiB, alias expansions to 100 with a 5:1 alias-to-anchor ratio,
|
||||
and each mapping or sequence to 10,000 entries.
|
||||
|
||||
Unknown keys in closed schema objects are rejected with their field path before
|
||||
the policy is converted or analyzed. The following maps are intentionally
|
||||
open user namespaces, so their keys are preserved as data: middleware `config`,
|
||||
query matcher names, GraphQL persisted-query names, and recursively nested MCP
|
||||
`params` names.
|
||||
|
||||
An absent `filesystem_policy` resolves to the runtime-effective
|
||||
`include_workdir: true`. An explicitly present `filesystem_policy: {}` keeps
|
||||
`include_workdir: false`. During protobuf conversion, `ports` takes precedence
|
||||
over scalar `port`, one effective port serializes in compact scalar form, empty
|
||||
rule names fall back to their map key, and runtime-only provenance fields are
|
||||
omitted. Because proto3 scalar fields do not preserve presence, protobuf `version: 0` means omission. Canonical serialization materializes
|
||||
`version: 1`; any other unsupported protobuf version is rejected. A protobuf port above
|
||||
65535 is rejected rather than clamped.
|
||||
|
||||
## Version
|
||||
|
||||
The version field identifies which schema the policy uses:
|
||||
|
||||
+2
-1
@@ -542,7 +542,8 @@ run_variant() {
|
||||
-u CONTAINER_HOST -u CONTAINER_CONNECTION -u CONTAINERS_STORAGE_CONF \
|
||||
-u CONTAINERS_CONF -u CONTAINERS_REGISTRIES_CONF -u CONTAINERS_REGISTRIES_CONF_DIR \
|
||||
-u CONTAINERS_POLICY -u PODMAN_CONNECTIONS_CONF -u DOCKER_HOST \
|
||||
-u OPENSHELL_SANDBOX_IMAGE -u OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE \
|
||||
-u OPENSHELL_SANDBOX_IMAGE -u OPENSHELL_SANDBOX_RUNTIME_IMAGE \
|
||||
-u OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE \
|
||||
-u OPENSHELL_GRPC_ENDPOINT -u OPENSHELL_PODMAN_HOST_GATEWAY_IP \
|
||||
-u OPENSHELL_PODMAN_USERNS -u OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET \
|
||||
-u OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET -u OPENSHELL_APP_ARMOR_PROFILE \
|
||||
|
||||
+11
-6
@@ -97,6 +97,7 @@ for variable in \
|
||||
CONTAINER_HOST CONTAINER_CONNECTION CONTAINERS_STORAGE_CONF CONTAINERS_CONF \
|
||||
CONTAINERS_REGISTRIES_CONF CONTAINERS_REGISTRIES_CONF_DIR CONTAINERS_POLICY \
|
||||
PODMAN_CONNECTIONS_CONF DOCKER_HOST OPENSHELL_SANDBOX_IMAGE \
|
||||
OPENSHELL_SANDBOX_RUNTIME_IMAGE \
|
||||
OPENSHELL_GRPC_ENDPOINT OPENSHELL_PODMAN_HOST_GATEWAY_IP OPENSHELL_PODMAN_USERNS \
|
||||
OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET \
|
||||
OPENSHELL_APP_ARMOR_PROFILE OPENSHELL_SANDBOX_HTTPS_PROXY OPENSHELL_SANDBOX_NO_PROXY \
|
||||
@@ -109,8 +110,8 @@ expected_sandbox="ghcr.io/nvidia/openshell-community/sandboxes/base@sha256:$(pri
|
||||
[ "${OPENSHELL_E2E_REQUIRE_DIGEST_PINNED_SANDBOX_IMAGE:-0}" = 1 ] || exit 24
|
||||
[ "${OPENSHELL_E2E_PODMAN_SANDBOX_IMAGE:-}" = "${expected_sandbox}" ] || exit 25
|
||||
[ "${OPENSHELL_COMMUNITY_REGISTRY:-}" = "ghcr.io/nvidia/openshell-community/sandboxes" ] || exit 28
|
||||
expected_base="docker.io/library/alpine@sha256:$(printf '%064d' 0)"
|
||||
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE:-}" = alpine:3.22 ] || exit 26
|
||||
expected_base="docker.io/library/debian@sha256:$(printf '%064d' 0)"
|
||||
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_IMAGE:-}" = debian:bookworm-slim ] || exit 26
|
||||
[ "${OPENSHELL_E2E_SUPERVISOR_BASE_RUNTIME_IMAGE:-}" = "${expected_base}" ] || exit 27
|
||||
printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$OPENSHELL_PARITY_VARIANT" "$OPENSHELL_E2E_CONFIG_SCHEMA_VERSION" "$OPENSHELL_GATEWAY_BIN" "$OPENSHELL_BIN" "$OPENSHELL_CONFORMANCE_BIN" "$MISE_TRUSTED_CONFIG_PATHS" "${OPENSHELL_E2E_PODMAN_OPTION_PROFILE:-}" "${OPENSHELL_PARITY_ORACLE_RESULT:-}" "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-}" "${OPENSHELL_EXTERNAL_DRIVER_BIN:-}" "${OPENSHELL_E2E_SUPERVISOR_BIN:-}" >>"$OPENSHELL_PARITY_TEST_CALLS"
|
||||
mkdir -p "$XDG_DATA_HOME/containers/storage"
|
||||
@@ -127,8 +128,9 @@ external = os.environ.get("OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER") == "1"
|
||||
zero = "0" * 64
|
||||
image_digest = f"sha256:{zero}"
|
||||
sandbox_runtime = f"ghcr.io/nvidia/openshell-community/sandboxes/base@{image_digest}"
|
||||
sandbox_boundary = "localhost/openshell/sandbox:dev"
|
||||
supervisor_runtime = f"localhost/openshell/supervisor@{image_digest}"
|
||||
base_runtime = f"docker.io/library/alpine@{image_digest}"
|
||||
base_runtime = f"docker.io/library/debian@{image_digest}"
|
||||
pull_policy = "missing" if schema == 1 else "if_not_present"
|
||||
gateway_port = 18181
|
||||
callback = f"https://host.containers.internal:{gateway_port}"
|
||||
@@ -172,7 +174,7 @@ launch = {
|
||||
"supervisor_image_id": zero,
|
||||
"supervisor_image_digest": image_digest,
|
||||
"supervisor_runtime_image": supervisor_runtime,
|
||||
"supervisor_base_image": "alpine:3.22",
|
||||
"supervisor_base_image": "debian:bookworm-slim",
|
||||
"supervisor_base_image_id": zero,
|
||||
"supervisor_base_image_digest": image_digest,
|
||||
"supervisor_base_runtime_image": base_runtime,
|
||||
@@ -183,6 +185,7 @@ launch = {
|
||||
"sandbox_image_id": zero,
|
||||
"sandbox_image_digest": image_digest,
|
||||
"sandbox_runtime_image": sandbox_runtime,
|
||||
"sandbox_boundary_image": sandbox_boundary,
|
||||
"sandbox_client_image_alias": "ghcr.io/nvidia/openshell-community/sandboxes/base:latest",
|
||||
"sandbox_client_image_alias_id": zero,
|
||||
"gateway_sha256_before_execution": os.environ[
|
||||
@@ -220,6 +223,7 @@ if external:
|
||||
"OPENSHELL_PODMAN_SOCKET": podman_socket,
|
||||
"OPENSHELL_SANDBOX_IMAGE": sandbox_runtime,
|
||||
"OPENSHELL_SANDBOX_IMAGE_PULL_POLICY": pull_policy,
|
||||
"OPENSHELL_SANDBOX_RUNTIME_IMAGE": sandbox_boundary,
|
||||
"OPENSHELL_HEALTH_CHECK_INTERVAL_SECS": 10,
|
||||
"OPENSHELL_GRPC_ENDPOINT": callback,
|
||||
"OPENSHELL_GATEWAY_PORT": gateway_port,
|
||||
@@ -288,7 +292,7 @@ case "$1" in
|
||||
case "$4" in
|
||||
'{{.Id}}') printf 'sha256:%064d\n' 0 ;;
|
||||
'{{.Digest}}') printf 'sha256:%064d\n' 0 ;;
|
||||
'{{index .RepoDigests 0}}') printf 'docker.io/library/alpine@sha256:%064d\n' 0 ;;
|
||||
'{{index .RepoDigests 0}}') printf 'docker.io/library/debian@sha256:%064d\n' 0 ;;
|
||||
*) exit 19 ;;
|
||||
esac
|
||||
;;
|
||||
@@ -369,6 +373,7 @@ CONTAINERS_POLICY=/tmp/untrusted-policy.json \
|
||||
PODMAN_CONNECTIONS_CONF=/tmp/untrusted-connections.json \
|
||||
DOCKER_HOST=tcp://untrusted.invalid:2375 \
|
||||
OPENSHELL_SANDBOX_IMAGE=untrusted.invalid/sandbox:latest \
|
||||
OPENSHELL_SANDBOX_RUNTIME_IMAGE=untrusted.invalid/runtime:latest \
|
||||
OPENSHELL_GRPC_ENDPOINT=http://untrusted.invalid:1 \
|
||||
OPENSHELL_PODMAN_HOST_GATEWAY_IP=192.0.2.1 \
|
||||
OPENSHELL_PODMAN_USERNS=keep-id \
|
||||
@@ -402,7 +407,7 @@ assert_not_contains "${WORKDIR}/results/baseline.json" '"scenarios"'
|
||||
assert_contains "${WORKDIR}/results/baseline.log" '"scenarios"'
|
||||
assert_contains "${WORKDIR}/results/baseline.conformance.json" '"passed":true'
|
||||
assert_contains "${WORKDIR}/podman-calls" 'pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest'
|
||||
assert_contains "${WORKDIR}/podman-calls" 'pull alpine:3.22'
|
||||
assert_contains "${WORKDIR}/podman-calls" 'pull debian:bookworm-slim'
|
||||
assert_contains "${WORKDIR}/podman-calls" 'unshare rm -rf -- '
|
||||
assert_contains "${WORKDIR}/podman-calls" 'openshell-parity-run.'
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::time::{Duration, Instant};
|
||||
use base64::Engine as _;
|
||||
use openshell_e2e::harness::binary::openshell_cmd;
|
||||
use serde_json::Value;
|
||||
use serial_test::serial;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
@@ -64,6 +65,7 @@ struct LoginSession {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_list_sandboxes() {
|
||||
let session = login_identity(ADMIN).await;
|
||||
assert_allowed(
|
||||
@@ -75,6 +77,7 @@ async fn admin_can_list_sandboxes() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_report_gateway_validated_identity() {
|
||||
let session = login_identity(USER).await;
|
||||
let output = assert_allowed(
|
||||
@@ -102,6 +105,7 @@ async fn user_can_report_gateway_validated_identity() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_list_sandboxes() {
|
||||
const WORKSPACE: &str = "oidc-user-list-sb";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -118,6 +122,7 @@ async fn user_can_list_sandboxes() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_create_sandbox() {
|
||||
let session = login_identity(ADMIN).await;
|
||||
let _lifecycle = SANDBOX_LIFECYCLE_LOCK.lock().await;
|
||||
@@ -125,6 +130,7 @@ async fn admin_can_create_sandbox() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_create_sandbox() {
|
||||
const WORKSPACE: &str = "oidc-user-create";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -138,6 +144,7 @@ async fn user_can_create_sandbox() {
|
||||
/// Workspace users must be able to create sandboxes with inferred-provider
|
||||
/// commands (e.g. `claude`) without requiring Platform Admin access.
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_create_sandbox_with_inferred_provider_command() {
|
||||
const WORKSPACE: &str = "oidc-inferred-cmd";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -187,6 +194,7 @@ async fn user_can_create_sandbox_with_inferred_provider_command() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_delete_sandbox() {
|
||||
let session = login_identity(ADMIN).await;
|
||||
let _lifecycle = SANDBOX_LIFECYCLE_LOCK.lock().await;
|
||||
@@ -194,6 +202,7 @@ async fn admin_can_delete_sandbox() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_delete_sandbox() {
|
||||
const WORKSPACE: &str = "oidc-user-delete";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -205,6 +214,7 @@ async fn user_can_delete_sandbox() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_inspect_gateway() {
|
||||
let session = login_identity(ADMIN).await;
|
||||
let output = assert_allowed(&session, &["gateway", "info"], "inspect gateway info").await;
|
||||
@@ -219,6 +229,7 @@ async fn admin_can_inspect_gateway() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_cannot_inspect_gateway() {
|
||||
let session = login_identity(USER).await;
|
||||
let output = run_session_cli(&session, &["gateway", "info"]).await;
|
||||
@@ -235,6 +246,7 @@ async fn user_cannot_inspect_gateway() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_list_providers() {
|
||||
let session = login_identity(ADMIN).await;
|
||||
assert_allowed(
|
||||
@@ -246,6 +258,7 @@ async fn admin_can_list_providers() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_can_list_providers() {
|
||||
const WORKSPACE: &str = "oidc-user-list-pr";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -262,6 +275,7 @@ async fn user_can_list_providers() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_manage_provider() {
|
||||
const PROVIDER: &str = "oidc-pkce-admin-provider";
|
||||
let session = login_identity(ADMIN).await;
|
||||
@@ -298,6 +312,7 @@ async fn admin_can_manage_provider() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_cannot_create_provider() {
|
||||
const WORKSPACE: &str = "oidc-user-no-create";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -323,6 +338,7 @@ async fn user_cannot_create_provider() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_cannot_delete_provider() {
|
||||
const PROVIDER: &str = "oidc-pkce-user-delete-target";
|
||||
const WORKSPACE: &str = "oidc-user-no-delete";
|
||||
@@ -360,6 +376,7 @@ async fn user_cannot_delete_provider() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_create_workspace() {
|
||||
const WORKSPACE: &str = "oidc-admin-create";
|
||||
let admin = login_identity(ADMIN).await;
|
||||
@@ -381,6 +398,7 @@ async fn admin_can_create_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_cannot_create_workspace() {
|
||||
let user = login_identity(USER).await;
|
||||
let denied = run_session_cli(
|
||||
@@ -392,6 +410,7 @@ async fn user_cannot_create_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn admin_can_delete_workspace() {
|
||||
const WORKSPACE: &str = "oidc-admin-delete";
|
||||
let admin = login_identity(ADMIN).await;
|
||||
@@ -406,6 +425,7 @@ async fn admin_can_delete_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn user_cannot_delete_workspace() {
|
||||
const WORKSPACE: &str = "oidc-user-del-deny";
|
||||
let admin = login_identity(ADMIN).await;
|
||||
@@ -423,6 +443,7 @@ async fn user_cannot_delete_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_can_read_workspace() {
|
||||
const WORKSPACE: &str = "oidc-ws-user-read";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -454,6 +475,7 @@ async fn workspace_user_can_read_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_manage_members() {
|
||||
const WORKSPACE: &str = "oidc-ws-user-deny";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -479,6 +501,7 @@ async fn workspace_user_cannot_manage_members() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_read_workspace() {
|
||||
const WORKSPACE: &str = "oidc-wsa-read";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -496,6 +519,7 @@ async fn workspace_admin_can_read_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_create_sandbox() {
|
||||
const WORKSPACE: &str = "oidc-wsa-create-sb";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -507,6 +531,7 @@ async fn workspace_admin_can_create_sandbox() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_delete_sandbox() {
|
||||
const WORKSPACE: &str = "oidc-wsa-delete-sb";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -518,6 +543,7 @@ async fn workspace_admin_can_delete_sandbox() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_create_provider() {
|
||||
const WORKSPACE: &str = "oidc-wsa-create-pr";
|
||||
const PROVIDER: &str = "oidc-wsa-create-provider";
|
||||
@@ -552,6 +578,7 @@ async fn workspace_admin_can_create_provider() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_delete_provider() {
|
||||
const WORKSPACE: &str = "oidc-wsa-delete-pr";
|
||||
const PROVIDER: &str = "oidc-wsa-delete-provider";
|
||||
@@ -585,6 +612,7 @@ async fn workspace_admin_can_delete_provider() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_add_user_member() {
|
||||
const WORKSPACE: &str = "oidc-wsa-add-user";
|
||||
let workspace_admin = login_identity(USER).await;
|
||||
@@ -619,6 +647,7 @@ async fn workspace_admin_can_add_user_member() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_can_remove_user_member() {
|
||||
const WORKSPACE: &str = "oidc-wsa-rm-user";
|
||||
let workspace_admin = login_identity(USER).await;
|
||||
@@ -662,6 +691,7 @@ async fn workspace_admin_can_remove_user_member() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_grant_admin() {
|
||||
const WORKSPACE: &str = "oidc-ws-admin-deny";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -687,6 +717,7 @@ async fn workspace_admin_cannot_grant_admin() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_create_workspace() {
|
||||
const WORKSPACE: &str = "oidc-wsa-no-create";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -700,6 +731,7 @@ async fn workspace_admin_cannot_create_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_delete_workspace() {
|
||||
const WORKSPACE: &str = "oidc-wsa-no-delete";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -712,6 +744,7 @@ async fn workspace_admin_cannot_delete_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_inspect_gateway() {
|
||||
const WORKSPACE: &str = "oidc-wsa-no-gw-info";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -724,6 +757,7 @@ async fn workspace_admin_cannot_inspect_gateway() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_read_another_workspace() {
|
||||
const WORKSPACE_A: &str = "oidc-wsa-xread-a";
|
||||
const WORKSPACE_B: &str = "oidc-wsa-xread-b";
|
||||
@@ -738,6 +772,7 @@ async fn workspace_admin_cannot_read_another_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_manage_another_workspace_members() {
|
||||
const WORKSPACE_A: &str = "oidc-wsa-xmem-a";
|
||||
const WORKSPACE_B: &str = "oidc-wsa-xmem-b";
|
||||
@@ -769,6 +804,7 @@ async fn workspace_admin_cannot_manage_another_workspace_members() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_admin_cannot_manage_another_workspace_providers() {
|
||||
const WORKSPACE_A: &str = "oidc-wsa-xprov-a";
|
||||
const WORKSPACE_B: &str = "oidc-wsa-xprov-b";
|
||||
@@ -800,6 +836,7 @@ async fn workspace_admin_cannot_manage_another_workspace_providers() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn membership_removal_revokes_workspace_access() {
|
||||
const WORKSPACE: &str = "oidc-ws-revoke";
|
||||
let user = login_identity(USER).await;
|
||||
@@ -825,6 +862,7 @@ async fn membership_removal_revokes_workspace_access() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_read_another_users_workspace() {
|
||||
const WORKSPACE_A: &str = "oidc-xread-a";
|
||||
const WORKSPACE_B: &str = "oidc-xread-b";
|
||||
@@ -838,6 +876,7 @@ async fn workspace_user_cannot_read_another_users_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn second_workspace_user_cannot_read_first_users_workspace() {
|
||||
const WORKSPACE_A: &str = "oidc-xread2-a";
|
||||
const WORKSPACE_B: &str = "oidc-xread2-b";
|
||||
@@ -851,6 +890,7 @@ async fn second_workspace_user_cannot_read_first_users_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_list_hides_another_users_workspace() {
|
||||
const WORKSPACE_A: &str = "oidc-xlist-a";
|
||||
const WORKSPACE_B: &str = "oidc-xlist-b";
|
||||
@@ -877,6 +917,7 @@ async fn workspace_list_hides_another_users_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_list_another_workspace_sandboxes() {
|
||||
const WORKSPACE_A: &str = "oidc-xsbox-a";
|
||||
const WORKSPACE_B: &str = "oidc-xsbox-b";
|
||||
@@ -895,6 +936,7 @@ async fn workspace_user_cannot_list_another_workspace_sandboxes() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_create_sandbox_in_another_workspace() {
|
||||
const WORKSPACE_A: &str = "oidc-xcreate-a";
|
||||
const WORKSPACE_B: &str = "oidc-xcreate-b";
|
||||
@@ -922,6 +964,7 @@ async fn workspace_user_cannot_create_sandbox_in_another_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_list_another_workspace_providers() {
|
||||
const WORKSPACE_A: &str = "oidc-xprov-a";
|
||||
const WORKSPACE_B: &str = "oidc-xprov-b";
|
||||
@@ -940,6 +983,7 @@ async fn workspace_user_cannot_list_another_workspace_providers() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(oidc_pkce)]
|
||||
async fn workspace_user_cannot_list_another_workspace_members() {
|
||||
const WORKSPACE_A: &str = "oidc-xmember-a";
|
||||
const WORKSPACE_B: &str = "oidc-xmember-b";
|
||||
|
||||
+151
-128
@@ -19,28 +19,28 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -154,9 +154,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -175,15 +175,15 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -307,13 +307,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -324,9 +324,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
@@ -346,15 +346,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
@@ -391,36 +391,36 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -475,9 +475,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -534,9 +534,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.2"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
@@ -544,9 +544,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
@@ -554,9 +554,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -579,9 +579,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -737,9 +737,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
@@ -801,7 +801,7 @@ dependencies = [
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -820,7 +820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -864,9 +864,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -897,9 +897,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
@@ -909,9 +909,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
@@ -940,7 +940,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -960,9 +960,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
@@ -1005,9 +1005,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
@@ -1021,9 +1021,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
@@ -1067,6 +1067,7 @@ dependencies = [
|
||||
"miette",
|
||||
"nix",
|
||||
"openshell-extension-core",
|
||||
"openshell-policy-schema",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
@@ -1126,13 +1127,23 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"hickory-proto",
|
||||
"miette",
|
||||
"noyalib",
|
||||
"openshell-core",
|
||||
"openshell-policy-schema",
|
||||
"prost-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-policy-schema"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"noyalib",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-providers"
|
||||
version = "0.0.0"
|
||||
@@ -1235,7 +1246,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1278,14 +1289,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1317,7 +1328,7 @@ dependencies = [
|
||||
"pulldown-cmark",
|
||||
"pulldown-cmark-to-cmark",
|
||||
"regex",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@@ -1331,7 +1342,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1433,18 +1444,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-to-cmark"
|
||||
version = "22.0.0"
|
||||
version = "22.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90"
|
||||
checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
|
||||
dependencies = [
|
||||
"pulldown-cmark",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1505,9 +1516,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.4"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -1517,9 +1528,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -1582,9 +1593,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.41"
|
||||
version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
@@ -1618,18 +1629,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -1639,9 +1650,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -1698,9 +1709,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
@@ -1718,29 +1729,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1821,15 +1832,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1870,9 +1881,20 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1893,7 +1915,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1931,29 +1953,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.52"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e48db7b415311b615f910b3dcaa4557bcd4bf1982379c95c223fd8c2a20e210"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
@@ -1971,9 +1993,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.31"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
|
||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -2006,9 +2028,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@@ -2023,20 +2045,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
version = "0.26.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
@@ -2044,9 +2066,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -2055,13 +2077,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
@@ -2106,7 +2129,7 @@ dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2131,7 +2154,7 @@ dependencies = [
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
"tempfile",
|
||||
"tonic-build",
|
||||
]
|
||||
@@ -2197,7 +2220,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2355,7 +2378,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2499,7 +2522,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2520,7 +2543,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2541,7 +2564,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2574,11 +2597,11 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
+50
-9
@@ -810,6 +810,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -872,6 +874,12 @@ version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -985,6 +993,22 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noyalib"
|
||||
version = "0.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"indexmap",
|
||||
"libm",
|
||||
"memchr",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
@@ -1017,6 +1041,7 @@ dependencies = [
|
||||
"miette",
|
||||
"nix",
|
||||
"openshell-extension-core",
|
||||
"openshell-policy-schema",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
@@ -1050,6 +1075,16 @@ dependencies = [
|
||||
"tower",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-policy-schema"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"noyalib",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-supervisor-middleware-content-guard"
|
||||
version = "0.0.0"
|
||||
@@ -1386,6 +1421,12 @@ version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
@@ -1661,13 +1702,13 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2190,13 +2231,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.5",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2211,13 +2252,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.5",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
||||
@@ -445,15 +445,19 @@ backend = "aqua:GoogleContainerTools/skaffold"
|
||||
"vars.version_prefix" = "v"
|
||||
|
||||
[tools.skaffold."platforms.linux-arm64"]
|
||||
checksum = "sha256:0062e1ff61f35200168454fa5a836306dd30b51acf30999d5d8836770d70ad2f"
|
||||
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-arm64"
|
||||
|
||||
[tools.skaffold."platforms.linux-x64"]
|
||||
checksum = "sha256:de75d5697d4a14d6a5e87105671ef9a30990270ab14ad301913f8f8d41a1ecca"
|
||||
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-linux-amd64"
|
||||
|
||||
[tools.skaffold."platforms.macos-arm64"]
|
||||
checksum = "sha256:73478a431c17674d2f75d2c6e2fcbf35aa019de4ad6027e5573cc0f581767fbc"
|
||||
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-darwin-arm64"
|
||||
|
||||
[tools.skaffold."platforms.windows-x64"]
|
||||
checksum = "sha256:a257da2988782b4c1971bb58b9c7bb52176f8a0b587b6520dec0064f271fa416"
|
||||
url = "https://storage.googleapis.com/skaffold/releases/v2.20.0/skaffold-windows-amd64.exe"
|
||||
|
||||
[[tools.uv]]
|
||||
@@ -461,13 +465,15 @@ version = "0.10.12"
|
||||
backend = "aqua:astral-sh/uv"
|
||||
|
||||
[tools.uv."platforms.linux-arm64"]
|
||||
checksum = "sha256:55bd1c1c10ec8b95a8c184f5e18b566703c6ab105f0fc118aaa4d748aabf28e4"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
checksum = "sha256:0ed7d20f49f6b9b60d45fdfcac28f3ac01a671a6ef08672401ed2833423fea2a"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-unknown-linux-gnu.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491938"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.uv."platforms.linux-x64"]
|
||||
checksum = "sha256:adccf40b5d1939a5e0093081ec2307ea24235adf7c2d96b122c561fa37711c46"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
checksum = "sha256:ec72570c9d1f33021aa80b176d7baba390de2cfeb1abcbefca346d563bf17484"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-unknown-linux-gnu.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491996"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.uv."platforms.macos-arm64"]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Tasks are defined in tasks/*.toml — run `mise tasks` to list them all.
|
||||
|
||||
# NOTE: keep this version in sync with what CI uses in Dockerfile.ci
|
||||
min_version = { soft = "2026.4.25" }
|
||||
min_version = { soft = "2026.9.9" }
|
||||
|
||||
redactions = ["*_TOKEN", "*_PASSWORD"]
|
||||
|
||||
|
||||
+5
-1
@@ -6,6 +6,7 @@
|
||||
["python:dev"]
|
||||
description = "Install the Python SDK in development mode"
|
||||
depends = ["python:proto"]
|
||||
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
|
||||
run = "uv sync --group dev"
|
||||
hide = true
|
||||
|
||||
@@ -38,23 +39,26 @@ hide = true
|
||||
["python:lint"]
|
||||
description = "Lint Python code with ruff"
|
||||
depends = ["python:proto"]
|
||||
env = { UV_NO_SYNC = "1" }
|
||||
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0", UV_NO_SYNC = "1" }
|
||||
run = "uv run ruff check {{vars.python_paths}}"
|
||||
hide = true
|
||||
|
||||
["python:format"]
|
||||
description = "Format Python code with ruff"
|
||||
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
|
||||
run = "uv run ruff format {{vars.python_paths}}"
|
||||
hide = true
|
||||
|
||||
["python:format:check"]
|
||||
description = "Check Python formatting with ruff"
|
||||
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
|
||||
run = "uv run ruff format --check {{vars.python_paths}}"
|
||||
hide = true
|
||||
|
||||
["python:typecheck"]
|
||||
description = "Type check Python code with ty"
|
||||
depends = ["python:proto"]
|
||||
env = { SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL = "0.0.0" }
|
||||
run = "uv run ty check {{vars.python_paths}}"
|
||||
hide = true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user