feat: add security governance contract types (#3270)

This commit is contained in:
安正超
2026-06-08 07:14:11 +08:00
committed by GitHub
parent c03c0ebc36
commit 7a9bf707ee
6 changed files with 362 additions and 25 deletions
Generated
+7
View File
@@ -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"
+2
View File
@@ -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" }
+34
View File
@@ -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"
}
);
}
}
+20
View File
@@ -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,
};
+42 -25
View File
@@ -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.