mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
feat: add security governance contract types (#3270)
This commit is contained in:
Generated
+7
@@ -9971,6 +9971,13 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-security-governance"
|
||||
version = "1.0.0-beta.7"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfs-signer"
|
||||
version = "1.0.0-beta.7"
|
||||
|
||||
@@ -44,6 +44,7 @@ members = [
|
||||
"crates/s3select-api", # S3 Select API interface
|
||||
"crates/s3select-query", # S3 Select query engine
|
||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||
"crates/security-governance", # Security governance contracts
|
||||
"crates/signer", # client signer
|
||||
"crates/targets", # Target-specific configurations and utilities
|
||||
"crates/trusted-proxies", # Trusted proxies management
|
||||
@@ -108,6 +109,7 @@ rustfs-s3-ops = { path = "crates/s3-ops", version = "1.0.0-beta.7" }
|
||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-beta.7" }
|
||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.7" }
|
||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.7" }
|
||||
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-beta.7" }
|
||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.7" }
|
||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.7" }
|
||||
rustfs-targets = { path = "crates/targets", version = "1.0.0-beta.7" }
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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.
|
||||
|
||||
[package]
|
||||
name = "rustfs-security-governance"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
description = "Security governance contracts for RustFS."
|
||||
keywords = ["security", "governance", "admin", "rustfs"]
|
||||
categories = ["web-programming", "development-tools"]
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
@@ -0,0 +1,257 @@
|
||||
// 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.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum HttpMethod {
|
||||
Delete,
|
||||
Get,
|
||||
Head,
|
||||
Post,
|
||||
Put,
|
||||
}
|
||||
|
||||
impl HttpMethod {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Delete => "DELETE",
|
||||
Self::Get => "GET",
|
||||
Self::Head => "HEAD",
|
||||
Self::Post => "POST",
|
||||
Self::Put => "PUT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct AdminActionRef(&'static str);
|
||||
|
||||
impl AdminActionRef {
|
||||
pub const fn new(action: &'static str) -> Self {
|
||||
Self(action)
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PublicRouteKind {
|
||||
ConsoleAsset,
|
||||
Health,
|
||||
OidcBootstrap,
|
||||
StsFormPost,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum RouteRiskLevel {
|
||||
Normal,
|
||||
Sensitive,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AdminRouteAccess {
|
||||
AdminAction(AdminActionRef),
|
||||
Public(PublicRouteKind),
|
||||
}
|
||||
|
||||
impl AdminRouteAccess {
|
||||
pub const fn admin_action(self) -> Option<AdminActionRef> {
|
||||
match self {
|
||||
Self::AdminAction(action) => Some(action),
|
||||
Self::Public(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn public_kind(self) -> Option<PublicRouteKind> {
|
||||
match self {
|
||||
Self::AdminAction(_) => None,
|
||||
Self::Public(kind) => Some(kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct AdminRouteSpec {
|
||||
method: HttpMethod,
|
||||
path: &'static str,
|
||||
access: AdminRouteAccess,
|
||||
risk_level: RouteRiskLevel,
|
||||
}
|
||||
|
||||
impl AdminRouteSpec {
|
||||
pub const fn admin(method: HttpMethod, path: &'static str, action: AdminActionRef, risk_level: RouteRiskLevel) -> Self {
|
||||
Self {
|
||||
method,
|
||||
path,
|
||||
access: AdminRouteAccess::AdminAction(action),
|
||||
risk_level,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn public(method: HttpMethod, path: &'static str, kind: PublicRouteKind, risk_level: RouteRiskLevel) -> Self {
|
||||
Self {
|
||||
method,
|
||||
path,
|
||||
access: AdminRouteAccess::Public(kind),
|
||||
risk_level,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn method(self) -> HttpMethod {
|
||||
self.method
|
||||
}
|
||||
|
||||
pub const fn path(self) -> &'static str {
|
||||
self.path
|
||||
}
|
||||
|
||||
pub const fn access(self) -> AdminRouteAccess {
|
||||
self.access
|
||||
}
|
||||
|
||||
pub const fn risk_level(self) -> RouteRiskLevel {
|
||||
self.risk_level
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum AdminRouteMatrixError {
|
||||
#[error("admin route spec at index {index} has an empty path")]
|
||||
EmptyPath { index: usize },
|
||||
|
||||
#[error("admin route spec at index {index} for {method} {path} has an empty admin action")]
|
||||
EmptyAdminAction {
|
||||
index: usize,
|
||||
method: &'static str,
|
||||
path: &'static str,
|
||||
},
|
||||
|
||||
#[error("duplicate admin route spec for {method} {path}")]
|
||||
DuplicateRoute { method: &'static str, path: &'static str },
|
||||
}
|
||||
|
||||
pub fn validate_admin_route_specs(specs: &[AdminRouteSpec]) -> Result<(), AdminRouteMatrixError> {
|
||||
let mut routes = BTreeSet::new();
|
||||
|
||||
for (index, spec) in specs.iter().copied().enumerate() {
|
||||
if spec.path.trim().is_empty() {
|
||||
return Err(AdminRouteMatrixError::EmptyPath { index });
|
||||
}
|
||||
|
||||
if let Some(action) = spec.access.admin_action()
|
||||
&& action.as_str().trim().is_empty()
|
||||
{
|
||||
return Err(AdminRouteMatrixError::EmptyAdminAction {
|
||||
index,
|
||||
method: spec.method.as_str(),
|
||||
path: spec.path,
|
||||
});
|
||||
}
|
||||
|
||||
if !routes.insert((spec.method, spec.path)) {
|
||||
return Err(AdminRouteMatrixError::DuplicateRoute {
|
||||
method: spec.method.as_str(),
|
||||
path: spec.path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SERVER_INFO: AdminActionRef = AdminActionRef::new("ServerInfoAdminAction");
|
||||
|
||||
#[test]
|
||||
fn validates_admin_and_public_route_specs() {
|
||||
let specs = [
|
||||
AdminRouteSpec::admin(HttpMethod::Get, "/rustfs/admin/v3/info", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
AdminRouteSpec::public(HttpMethod::Post, "/", PublicRouteKind::StsFormPost, RouteRiskLevel::Sensitive),
|
||||
];
|
||||
|
||||
assert!(validate_admin_route_specs(&specs).is_ok());
|
||||
assert_eq!(specs[0].access().admin_action().expect("admin spec must expose action"), SERVER_INFO);
|
||||
assert_eq!(
|
||||
specs[1].access().public_kind().expect("public spec must expose public kind"),
|
||||
PublicRouteKind::StsFormPost
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_method_and_path() {
|
||||
let specs = [
|
||||
AdminRouteSpec::admin(HttpMethod::Get, "/rustfs/admin/v3/info", SERVER_INFO, RouteRiskLevel::Sensitive),
|
||||
AdminRouteSpec::admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info",
|
||||
AdminActionRef::new("StorageInfoAdminAction"),
|
||||
RouteRiskLevel::Sensitive,
|
||||
),
|
||||
];
|
||||
|
||||
let err = validate_admin_route_specs(&specs).expect_err("duplicate route spec should fail validation");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
AdminRouteMatrixError::DuplicateRoute {
|
||||
method: "GET",
|
||||
path: "/rustfs/admin/v3/info"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_paths() {
|
||||
let specs = [AdminRouteSpec::admin(
|
||||
HttpMethod::Get,
|
||||
" ",
|
||||
SERVER_INFO,
|
||||
RouteRiskLevel::Sensitive,
|
||||
)];
|
||||
|
||||
let err = validate_admin_route_specs(&specs).expect_err("empty path should fail validation");
|
||||
|
||||
assert_eq!(err, AdminRouteMatrixError::EmptyPath { index: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_admin_actions() {
|
||||
let specs = [AdminRouteSpec::admin(
|
||||
HttpMethod::Get,
|
||||
"/rustfs/admin/v3/info",
|
||||
AdminActionRef::new(" "),
|
||||
RouteRiskLevel::Sensitive,
|
||||
)];
|
||||
|
||||
let err = validate_admin_route_specs(&specs).expect_err("empty admin action should fail validation");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
AdminRouteMatrixError::EmptyAdminAction {
|
||||
index: 0,
|
||||
method: "GET",
|
||||
path: "/rustfs/admin/v3/info"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
pub mod admin_matrix;
|
||||
|
||||
pub use admin_matrix::{
|
||||
AdminActionRef, AdminRouteAccess, AdminRouteMatrixError, AdminRouteSpec, HttpMethod, PublicRouteKind, RouteRiskLevel,
|
||||
validate_admin_route_specs,
|
||||
};
|
||||
@@ -5,15 +5,14 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
||||
## Current Context
|
||||
|
||||
- Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660)
|
||||
- Branch: `overtrue/arch-admin-route-matrix-guard`
|
||||
- Baseline: `upstream/main` at `dee550a831cbfc24aa5cecd60f6f3a4cfe1a2e30`
|
||||
- PR type for this branch: `test-only`
|
||||
- Branch: `overtrue/arch-security-governance-types`
|
||||
- Baseline: `upstream/main` at `c03c0ebc363209a4820e6d6f2267e0342c6a7782`
|
||||
- PR type for this branch: `contract`
|
||||
- Runtime behavior changes: none
|
||||
- Rust code changes: add test-only admin route matrix coverage, test-only
|
||||
registered route tracking, and a private shared admin route registration helper
|
||||
so tests exercise the production registration sequence.
|
||||
- Rust code changes: add the pure `rustfs-security-governance` contract crate
|
||||
with admin route matrix metadata types, typed validation errors, and unit tests.
|
||||
- CI/script changes: none
|
||||
- Docs changes: record the route matrix guard handoff.
|
||||
- Docs changes: record the Phase 1 security-governance contract handoff.
|
||||
|
||||
## Phase 0 Tasks
|
||||
|
||||
@@ -63,43 +62,61 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
|
||||
when a register entry lacks a source marker, or when a source marker omits a
|
||||
removal condition.
|
||||
|
||||
## Phase 1 Security Governance Tasks
|
||||
|
||||
- [x] `S-001` Add `crates/security-governance`.
|
||||
- Acceptance: the crate is a workspace member and has no dependency on
|
||||
`rustfs`, `ecstore`, admin handlers, Axum, or runtime state.
|
||||
- Verification: `cargo check -p rustfs-security-governance`.
|
||||
- [x] `S-002` Add admin route matrix core types.
|
||||
- Acceptance: `AdminRouteSpec`, `AdminRouteAccess`, `AdminActionRef`,
|
||||
`PublicRouteKind`, `RouteRiskLevel`, and validation errors model route
|
||||
governance metadata without registering routes or enforcing auth.
|
||||
- Verification: `cargo test -p rustfs-security-governance`.
|
||||
- [ ] `S-003` Add redaction contract types.
|
||||
- [ ] `S-004` Add serde policy marker types.
|
||||
- [ ] `S-005` Add supply-chain policy contract types.
|
||||
- [ ] `S-006` Add `rustfs/src/admin/route_policy.rs` backed by these contract
|
||||
types, without changing route registration or auth behavior.
|
||||
|
||||
## Next PRs
|
||||
|
||||
1. `contract`: define the config-model contract surface while preserving the
|
||||
existing `Config`, `KV`, and `KVS` behavior.
|
||||
2. `ci-gate`: add focused checks for public re-export and storage trait coverage
|
||||
before pure moves.
|
||||
1. `contract`: add redaction, serde policy, or supply-chain governance contract
|
||||
modules inside rustfs-security-governance.
|
||||
2. `contract`: add an admin route policy table that consumes the new
|
||||
admin_matrix types while preserving route registration and auth behavior.
|
||||
|
||||
## Pre-Push Review Log
|
||||
|
||||
| Expert | Status | Notes |
|
||||
|---|---|---|
|
||||
| Quality/architecture | pass | Confirmed the matrix helpers keep the snapshot explicit, `S3Router` instrumentation is fully `cfg(test)`, the production registration order is shared through one private helper, and the non-test insertion path keeps existing route construction/insert behavior |
|
||||
| Migration preservation | pass | Confirmed this remains `test-only` in effect: no route handler/auth/alias semantics changed, no storage hot-path/global-state/crate-split changes, and MinIO admin alias coverage is expanded across all admin-prefix matrix entries |
|
||||
| Testing/verification | pass | Confirmed focused route tests pin `ENV_HEALTH_ENDPOINT_ENABLE=true`, use the production registration helper, and pass with formatting, non-test cargo check, migration guard scripts, diff check, full `make pre-commit`, and temporary unaccounted-route negative coverage |
|
||||
| Quality/architecture | pass | Confirmed this is a pure `contract` PR: the new crate has a narrow admin_matrix module, typed errors via thiserror, no dependency on implementation crates, and no route registration or auth integration |
|
||||
| Migration preservation | pass | Confirmed no runtime code paths changed: no admin handler/router edits, no startup/global-state/storage hot-path movement, no compatibility wrappers, and no behavior drift |
|
||||
| Testing/verification | pass | Confirmed focused unit tests cover valid admin/public specs, duplicate method/path rejection, empty path rejection, and empty action rejection; focused checks, migration guard scripts, dependency tree review, diff check, and full `make pre-commit` passed |
|
||||
|
||||
## Verification Notes
|
||||
|
||||
Passed:
|
||||
- `cargo check -p rustfs-security-governance`
|
||||
- `cargo test -p rustfs-security-governance`
|
||||
- `cargo fmt --all --check`
|
||||
- `cargo check -p rustfs --lib`
|
||||
- `cargo test -p rustfs admin::route_registration_test -- --nocapture`
|
||||
- `cargo tree -p rustfs-security-governance --edges normal`
|
||||
- `./scripts/check_architecture_migration_rules.sh`
|
||||
- `./scripts/check_layer_dependencies.sh`
|
||||
- `./scripts/check_metrics_migration_refs.sh`
|
||||
- `git diff --check`
|
||||
- temporary negative check for an unaccounted admin route registration
|
||||
- `make pre-commit`
|
||||
|
||||
## Handoff Notes
|
||||
|
||||
- Keep Phase 0 PRs small. Do not move Config, Storage API, Runtime, or ECStore
|
||||
code inside this `test-only` branch.
|
||||
- Route matrix instrumentation must remain test-only and must not alter dispatch
|
||||
or auth behavior.
|
||||
- Keep this Phase 1 branch as a pure `contract` PR. Do not add
|
||||
`rustfs/src/admin` integration, route registration changes, auth enforcement,
|
||||
Config moves, Storage API moves, Runtime moves, or ECStore moves.
|
||||
- The new crate is allowed to depend on generic Rust libraries such as
|
||||
`thiserror`, but must stay independent from implementation crates and runtime
|
||||
state.
|
||||
- Do not add temporary compatibility code without a matching
|
||||
`RUSTFS_COMPAT_TODO(<task-id>)` marker and cleanup-register entry.
|
||||
- The next config-model PR must preserve the current tuple-struct shapes and
|
||||
persistence behavior before introducing narrower provider contracts.
|
||||
- Do not move `config::com` until a dedicated helper-user inventory covers
|
||||
ECStore-internal persistence paths as well as adjacent non-model users.
|
||||
- The next admin route policy PR may consume the contract types, but must
|
||||
preserve current route registration, alias canonicalization, public
|
||||
exceptions, and handler-level authorization behavior.
|
||||
|
||||
Reference in New Issue
Block a user