diff --git a/rustfs/src/admin/kms_contract.rs b/rustfs/src/admin/kms_contract.rs new file mode 100644 index 000000000..bcb6f4c71 --- /dev/null +++ b/rustfs/src/admin/kms_contract.rs @@ -0,0 +1,99 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Reviewable snapshot of the KMS admin route surface. +//! +//! `route_registration_test` already fails when a route appears or disappears, +//! and `route_policy` pins each route's action and risk with individual +//! assertions. Neither records the surface as a whole, so a change to an +//! existing route's action or risk lands as an edited assertion rather than as +//! a visible before/after — and the KMS routes are exactly where that matters, +//! since the action decides who may reach key material and the risk level +//! decides which confirmations the route demands. +//! +//! Every field here is derived from [`ADMIN_ROUTE_POLICY_SPECS`], so this file +//! cannot drift from the routing table: there is nothing to keep in sync by +//! hand. Per-key authorization scoping is deliberately *not* restated here — +//! it is enforced and tested where it is implemented, by +//! `single_key_endpoints_reject_a_key_outside_the_policy_scope` in +//! `handlers::kms_keys`, and a second hand-maintained list would be a claim +//! nothing checks. + +use serde::Serialize; + +use crate::admin::route_policy::ADMIN_ROUTE_POLICY_SPECS; + +/// Path fragment identifying a KMS admin route. +const KMS_PATH_MARKER: &str = "/kms/"; + +/// One KMS admin route as the authorization layer sees it. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +struct KmsRouteContract { + method: String, + path: String, + /// The action a caller's policy must allow, or `null` for a public route. + action: Option, + risk: String, +} + +/// Every KMS admin route, in a stable order. +fn kms_route_contract() -> Vec { + let mut routes: Vec = ADMIN_ROUTE_POLICY_SPECS + .iter() + .filter(|spec| spec.path().contains(KMS_PATH_MARKER)) + .map(|spec| KmsRouteContract { + method: format!("{:?}", spec.method()).to_uppercase(), + path: spec.path().to_string(), + action: spec.access().admin_action().map(|action| action.as_str().to_string()), + risk: format!("{:?}", spec.risk_level()), + }) + .collect(); + routes.sort(); + routes +} + +#[cfg(test)] +mod tests { + use super::{KMS_PATH_MARKER, kms_route_contract}; + + /// The reviewable record. A route whose action or risk changes shows up as + /// a snapshot diff instead of as an edited assertion buried in + /// `route_policy`. + #[test] + fn kms_admin_route_contract_is_stable() { + insta::assert_json_snapshot!("kms_admin_route_contract", kms_route_contract()); + } + + /// The snapshot is only evidence if it actually covers the surface. A KMS + /// route registered outside the policy table, or a path spelling the filter + /// does not match, would otherwise leave the snapshot green and the route + /// unrecorded. + #[test] + fn every_kms_route_is_covered_and_authorized() { + let routes = kms_route_contract(); + assert!(routes.len() > 20, "expected the full KMS surface, got {}", routes.len()); + + for route in &routes { + assert!(route.path.contains(KMS_PATH_MARKER), "unexpected path in the KMS contract: {route:?}"); + let action = route + .action + .as_deref() + .unwrap_or_else(|| panic!("no KMS route may be public: {route:?}")); + assert!( + action.starts_with("kms:"), + "a KMS route must gate on a dedicated kms:* action, not on {action}: {route:?}" + ); + } + } +} diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index 79e08f80a..1671ffa2b 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -30,6 +30,8 @@ pub mod utils; #[cfg(test)] mod console_test; #[cfg(test)] +mod kms_contract; +#[cfg(test)] mod route_registration_test; use handlers::{ diff --git a/rustfs/src/admin/snapshots/rustfs__admin__kms_contract__tests__kms_admin_route_contract.snap b/rustfs/src/admin/snapshots/rustfs__admin__kms_contract__tests__kms_admin_route_contract.snap new file mode 100644 index 000000000..1b64b8595 --- /dev/null +++ b/rustfs/src/admin/snapshots/rustfs__admin__kms_contract__tests__kms_admin_route_contract.snap @@ -0,0 +1,192 @@ +--- +source: rustfs/src/admin/kms_contract.rs +expression: kms_route_contract() +--- +[ + { + "method": "DELETE", + "path": "/rustfs/admin/v3/kms/keys/delete", + "action": "kms:DeleteKey", + "risk": "Critical" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/backup", + "action": "kms:Backup", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/config", + "action": "kms:Configure", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/describe-key", + "action": "kms:DescribeKey", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/key/status", + "action": "kms:DescribeKey", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/keys", + "action": "kms:ListKeys", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/keys/{key_id}", + "action": "kms:DescribeKey", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/list-keys", + "action": "kms:ListKeys", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/service-status", + "action": "kms:ServiceControl", + "risk": "Sensitive" + }, + { + "method": "GET", + "path": "/rustfs/admin/v3/kms/status", + "action": "kms:ServiceControl", + "risk": "Sensitive" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/backup", + "action": "kms:Backup", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/clear-cache", + "action": "kms:ClearCache", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/configure", + "action": "kms:Configure", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/create-key", + "action": "kms:Configure", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/generate-data-key", + "action": "kms:GenerateDataKey", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/key/create", + "action": "kms:Configure", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys", + "action": "kms:Configure", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/cancel-deletion", + "action": "kms:DeleteKey", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/disable", + "action": "kms:DisableKey", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/enable", + "action": "kms:EnableKey", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/rotate", + "action": "kms:RotateKey", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/tag", + "action": "kms:TagResource", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/untag", + "action": "kms:UntagResource", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/keys/update-description", + "action": "kms:UpdateKeyDescription", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/reconfigure", + "action": "kms:Configure", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/restore", + "action": "kms:Restore", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/restore/abort", + "action": "kms:Restore", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/restore/dry-run", + "action": "kms:Restore", + "risk": "Sensitive" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/start", + "action": "kms:ServiceControl", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/status", + "action": "kms:ServiceControl", + "risk": "High" + }, + { + "method": "POST", + "path": "/rustfs/admin/v3/kms/stop", + "action": "kms:ServiceControl", + "risk": "High" + } +]