feat: add admin route policy inventory (#3286)

* feat: add admin route policy inventory

* fix: align table route policy actions

* fix: align load table route policy action
This commit is contained in:
安正超
2026-06-09 10:17:50 +08:00
committed by GitHub
parent aabda41ae5
commit b9a3a2245a
5 changed files with 920 additions and 29 deletions
Generated
+1
View File
@@ -9105,6 +9105,7 @@ dependencies = [
"rustfs-s3select-api",
"rustfs-s3select-query",
"rustfs-scanner",
"rustfs-security-governance",
"rustfs-signer",
"rustfs-targets",
"rustfs-tls-runtime",
+36 -29
View File
@@ -5,15 +5,17 @@ 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-security-governance-policy-types`
- Baseline: `upstream/main` at `7a9bf707ee66e779f85e6e00cedfaa10ec2af4c2`
- Branch: `overtrue/arch-admin-route-policy`
- Baseline: `origin/main` at `3d0e6ce0da93de0f4618beb194ae2241df71f344`
- PR type for this branch: `contract`
- Runtime behavior changes: none
- Rust code changes: add redaction, serde policy, and artifact integrity
contract modules to the pure rustfs-security-governance crate, with typed
validation errors and unit tests.
- Rust code changes: add `rustfs/src/admin/route_policy.rs` as a pure
admin-route policy inventory backed by `rustfs-security-governance` route
contract types, with explicit deferred entries for routes that need
contextual, S3-action, multi-action, credential-only, or not-implemented
contract support.
- CI/script changes: none
- Docs changes: record the Phase 1 policy contract handoff.
- Docs changes: record the S-006 route policy handoff.
## Phase 0 Tasks
@@ -88,52 +90,57 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block
errors model digest, signature, and provenance requirements without changing
release or CI behavior.
- Verification: `cargo test -p rustfs-security-governance`.
- [ ] `S-006` Add `rustfs/src/admin/route_policy.rs` backed by these contract
- [x] `S-006` Add `rustfs/src/admin/route_policy.rs` backed by these contract
types, without changing route registration or auth behavior.
- Acceptance: direct `AdminRouteSpec` entries cover routes with a single
stable admin policy action, deferred inventory records routes that need
richer contract support, and tests prove the combined inventory covers every
registered admin route.
## Next PRs
1. `contract`: add an admin route policy table that consumes the new admin
route matrix types while preserving route registration and auth behavior.
2. `contract`: add initial policy inventory tables for redaction, serde, or
1. `contract`: add initial policy inventory tables for redaction, serde, or
supply-chain governance only after the contract shape remains stable.
## Pre-Push Review Log
| Expert | Status | Notes |
|---|---|---|
| Quality/architecture | pass | Confirmed the staged diff includes the new modules, keeps a pure `contract` design, uses clear API names and typed errors, and does not introduce runtime/admin/router/auth/startup/storage/config/global-state integration |
| Migration preservation | pass | Confirmed this PR only completes `S-003` through `S-005`, does not shift away from backlog #660, does not touch storage hot paths or global-state migration, and does not need compatibility markers |
| Testing/verification | pass | Confirmed focused contract tests assert valid and invalid policy behavior, production logic was not changed to satisfy tests, focused checks passed, and full `make pre-commit` passed |
| Quality/architecture | pass | Pure contract inventory module; no runtime route registration, alias canonicalization, or handler auth mutation. Names and deferred reasons are explicit and avoid false policy precision. |
| Migration preservation | pass | Aligns with backlog #660: directory and contract boundary first, no global-state migration, no crate split, and no storage hot-path behavior drift. |
| Testing/verification | pass | Route-policy tests cover validation, public exceptions, table-catalog scope, deferred contextual routes, and every registered admin route. Focused checks, guard scripts, and full pre-commit pass. |
## Verification Notes
Passed:
- `cargo check -p rustfs-security-governance`
- `cargo test -p rustfs-security-governance`
- `cargo test -p rustfs admin::route_policy`
- `cargo fmt --all`
- `cargo fmt --all --check`
- `cargo tree -p rustfs-security-governance --edges normal`
- `cargo check -p rustfs`
- `./scripts/check_architecture_migration_rules.sh`
- `./scripts/check_layer_dependencies.sh`
- `./scripts/check_metrics_migration_refs.sh`
- `./scripts/check_layer_dependencies.sh`
- `git diff --check`
- Rust quality scans for production `unwrap`/`expect`, narrowing casts,
string errors, boxed dynamic errors, debug printing, and relaxed atomics in
`rustfs/src/admin/route_policy.rs`
- `make pre-commit`
Notes:
- `cargo test -p rustfs-security-governance` passed 20 unit tests.
- `make pre-commit` passed, including 5513 nextest tests, 105 skipped tests,
and workspace doctests.
- `cargo test -p rustfs admin::route_policy` passed 7 route-policy tests.
- `make pre-commit` passed all checks, including 5526 nextest tests and
workspace doctests.
## Handoff Notes
- 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.
- Keep this S-006 branch as a pure `contract` PR. Do not change
admin route registration, `/minio/admin` alias canonicalization, handler auth
enforcement, Config moves, Storage API moves, Runtime moves, or ECStore moves.
- `rustfs` may depend on `rustfs-security-governance` for contract metadata;
the security-governance crate 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 admin route policy PR may consume the contract types, but must
preserve current route registration, alias canonicalization, public
exceptions, and handler-level authorization behavior.
- Deferred route policy entries must remain explicit until the contract crate
gains richer support for contextual owner checks, S3 actions, multiple
accepted actions, credential-only gates, and not-implemented routes.
+1
View File
@@ -82,6 +82,7 @@ rustfs-protos = { workspace = true }
rustfs-rio.workspace = true
rustfs-s3-types = { workspace = true }
rustfs-s3-ops = { workspace = true }
rustfs-security-governance = { workspace = true }
rustfs-data-usage = { workspace = true }
rustfs-s3select-api = { workspace = true }
rustfs-s3select-query = { workspace = true }
+3
View File
@@ -16,6 +16,9 @@ mod auth;
pub mod console;
pub mod handlers;
mod plugin_contract;
// Contract inventory is validated by tests before later runtime integration.
#[allow(dead_code)]
pub(crate) mod route_policy;
pub mod router;
pub mod service;
pub mod site_replication_identity;
+879
View File
@@ -0,0 +1,879 @@
// 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 rustfs_security_governance::{
AdminActionRef, AdminRouteMatrixError, AdminRouteSpec, HttpMethod, PublicRouteKind, RouteRiskLevel,
validate_admin_route_specs,
};
const ALL_ADMIN: AdminActionRef = AdminActionRef::new("AllAdminActions");
const ADD_USER_TO_GROUP: AdminActionRef = AdminActionRef::new("AddUserToGroupAdminAction");
const ATTACH_POLICY: AdminActionRef = AdminActionRef::new("AttachPolicyAdminAction");
const CONFIG_UPDATE: AdminActionRef = AdminActionRef::new("ConfigUpdateAdminAction");
const COMMIT_TABLE: AdminActionRef = AdminActionRef::new("CommitTableAction");
const CREATE_POLICY: AdminActionRef = AdminActionRef::new("CreatePolicyAdminAction");
const CREATE_SERVICE_ACCOUNT: AdminActionRef = AdminActionRef::new("CreateServiceAccountAdminAction");
const CREATE_TABLE: AdminActionRef = AdminActionRef::new("CreateTableAction");
const DECOMMISSION: AdminActionRef = AdminActionRef::new("DecommissionAdminAction");
const DELETE_POLICY: AdminActionRef = AdminActionRef::new("DeletePolicyAdminAction");
const DELETE_TABLE: AdminActionRef = AdminActionRef::new("DeleteTableAction");
const DELETE_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("DeleteTableNamespaceAction");
const DELETE_USER: AdminActionRef = AdminActionRef::new("DeleteUserAdminAction");
const ENABLE_GROUP: AdminActionRef = AdminActionRef::new("EnableGroupAdminAction");
const ENABLE_USER: AdminActionRef = AdminActionRef::new("EnableUserAdminAction");
const EXPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ExportBucketMetadataAction");
const EXPORT_IAM: AdminActionRef = AdminActionRef::new("ExportIAMAction");
const GET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("GetBucketTargetAction");
const GET_GROUP: AdminActionRef = AdminActionRef::new("GetGroupAdminAction");
const GET_POLICY: AdminActionRef = AdminActionRef::new("GetPolicyAdminAction");
const GET_REPLICATION_METRICS: AdminActionRef = AdminActionRef::new("GetReplicationMetricsAction");
const GET_TABLE: AdminActionRef = AdminActionRef::new("GetTableAction");
const GET_TABLE_CATALOG: AdminActionRef = AdminActionRef::new("GetTableCatalogAction");
const GET_TABLE_METADATA: AdminActionRef = AdminActionRef::new("GetTableMetadataAction");
const GET_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("GetTableNamespaceAction");
const HEAL: AdminActionRef = AdminActionRef::new("HealAdminAction");
const IMPORT_BUCKET_METADATA: AdminActionRef = AdminActionRef::new("ImportBucketMetadataAction");
const IMPORT_IAM: AdminActionRef = AdminActionRef::new("ImportIAMAction");
const KMS_CREATE_KEY: AdminActionRef = AdminActionRef::new("KMSCreateKeyAdminAction");
const KMS_KEY_STATUS: AdminActionRef = AdminActionRef::new("KMSKeyStatusAdminAction");
const LIST_GROUPS: AdminActionRef = AdminActionRef::new("ListGroupsAdminAction");
const LIST_TEMPORARY_ACCOUNTS: AdminActionRef = AdminActionRef::new("ListTemporaryAccountsAdminAction");
const LIST_TIER: AdminActionRef = AdminActionRef::new("ListTierAction");
const LIST_USER_POLICIES: AdminActionRef = AdminActionRef::new("ListUserPoliciesAdminAction");
const LIST_USERS: AdminActionRef = AdminActionRef::new("ListUsersAdminAction");
const PROFILING: AdminActionRef = AdminActionRef::new("ProfilingAdminAction");
const REBALANCE: AdminActionRef = AdminActionRef::new("RebalanceAdminAction");
const REGISTER_TABLE: AdminActionRef = AdminActionRef::new("RegisterTableAction");
const REMOVE_USER_FROM_GROUP: AdminActionRef = AdminActionRef::new("RemoveUserFromGroupAdminAction");
const SERVER_INFO: AdminActionRef = AdminActionRef::new("ServerInfoAdminAction");
const SET_BUCKET_QUOTA: AdminActionRef = AdminActionRef::new("SetBucketQuotaAdminAction");
const SET_BUCKET_TARGET: AdminActionRef = AdminActionRef::new("SetBucketTargetAction");
const SET_TABLE: AdminActionRef = AdminActionRef::new("SetTableAction");
const SET_TABLE_NAMESPACE: AdminActionRef = AdminActionRef::new("SetTableNamespaceAction");
const SET_TIER: AdminActionRef = AdminActionRef::new("SetTierAction");
const SITE_REPLICATION_ADD: AdminActionRef = AdminActionRef::new("SiteReplicationAddAction");
const SITE_REPLICATION_INFO: AdminActionRef = AdminActionRef::new("SiteReplicationInfoAction");
const SITE_REPLICATION_OPERATION: AdminActionRef = AdminActionRef::new("SiteReplicationOperationAction");
const SITE_REPLICATION_REMOVE: AdminActionRef = AdminActionRef::new("SiteReplicationRemoveAction");
const SITE_REPLICATION_RESYNC: AdminActionRef = AdminActionRef::new("SiteReplicationResyncAction");
const STORAGE_INFO: AdminActionRef = AdminActionRef::new("StorageInfoAdminAction");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeferredRoutePolicyReason {
ContextualAuthorization,
CredentialOnly,
MultipleActions,
NotImplemented,
S3Action,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeferredAdminRoutePolicy {
method: HttpMethod,
path: &'static str,
reason: DeferredRoutePolicyReason,
}
impl DeferredAdminRoutePolicy {
pub const fn new(method: HttpMethod, path: &'static str, reason: DeferredRoutePolicyReason) -> Self {
Self { method, path, reason }
}
pub const fn method(self) -> HttpMethod {
self.method
}
pub const fn path(self) -> &'static str {
self.path
}
pub const fn reason(self) -> DeferredRoutePolicyReason {
self.reason
}
}
pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
public(HttpMethod::Get, "/health", PublicRouteKind::Health, RouteRiskLevel::Normal),
public(HttpMethod::Head, "/health", PublicRouteKind::Health, RouteRiskLevel::Normal),
public(HttpMethod::Get, "/health/ready", PublicRouteKind::Health, RouteRiskLevel::Normal),
public(HttpMethod::Head, "/health/ready", PublicRouteKind::Health, RouteRiskLevel::Normal),
public(HttpMethod::Post, "/", PublicRouteKind::StsFormPost, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/profile/cpu", PROFILING, RouteRiskLevel::High),
admin(HttpMethod::Get, "/profile/memory", PROFILING, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/is-admin", ALL_ADMIN, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/rustfs/admin/v3/list-users", LIST_USERS, RouteRiskLevel::Sensitive),
admin(HttpMethod::Delete, "/rustfs/admin/v3/remove-user", DELETE_USER, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/set-user-status", ENABLE_USER, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/groups", LIST_GROUPS, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/rustfs/admin/v3/group", GET_GROUP, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/group/{group}",
REMOVE_USER_FROM_GROUP,
RouteRiskLevel::High,
),
admin(HttpMethod::Put, "/rustfs/admin/v3/set-group-status", ENABLE_GROUP, RouteRiskLevel::High),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/update-group-members",
ADD_USER_TO_GROUP,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/temporary-account-info",
LIST_TEMPORARY_ACCOUNTS,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/add-service-accounts",
CREATE_SERVICE_ACCOUNT,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/add-service-account",
CREATE_SERVICE_ACCOUNT,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/export-iam", EXPORT_IAM, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/import-iam", IMPORT_IAM, RouteRiskLevel::High),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/list-canned-policies",
LIST_USER_POLICIES,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/info-canned-policy",
GET_POLICY,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Put, "/rustfs/admin/v3/add-canned-policy", CREATE_POLICY, RouteRiskLevel::High),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/remove-canned-policy",
DELETE_POLICY,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/set-user-or-group-policy",
ATTACH_POLICY,
RouteRiskLevel::High,
),
admin(HttpMethod::Put, "/rustfs/admin/v3/set-policy", ATTACH_POLICY, RouteRiskLevel::High),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/idp/builtin/policy/attach",
ATTACH_POLICY,
RouteRiskLevel::High,
),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/idp/builtin/policy/detach",
ATTACH_POLICY,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/target/list",
GET_BUCKET_TARGET,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/target/{target_type}/{target_name}",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/target/{target_type}/{target_name}/reset",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/target/arns",
GET_BUCKET_TARGET,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/info", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/rustfs/admin/v3/storageinfo", STORAGE_INFO, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/pools/decommission",
DECOMMISSION,
RouteRiskLevel::High,
),
admin(HttpMethod::Post, "/rustfs/admin/v3/pools/cancel", DECOMMISSION, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/rebalance/start", REBALANCE, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/rebalance/status", REBALANCE, RouteRiskLevel::Sensitive),
admin(HttpMethod::Post, "/rustfs/admin/v3/rebalance/stop", REBALANCE, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/", HEAL, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/{bucket}", HEAL, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/heal/{bucket}/{prefix}", HEAL, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/background-heal/status", HEAL, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/tier", LIST_TIER, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/rustfs/admin/v3/tier-stats", LIST_TIER, RouteRiskLevel::Sensitive),
admin(HttpMethod::Get, "/rustfs/admin/v3/tier/{tier}", LIST_TIER, RouteRiskLevel::Sensitive),
admin(HttpMethod::Delete, "/rustfs/admin/v3/tier/{tiername}", SET_TIER, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/tier", SET_TIER, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/tier/{tiername}", SET_TIER, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/tier/clear", SET_TIER, RouteRiskLevel::High),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/set-bucket-quota",
SET_BUCKET_QUOTA,
RouteRiskLevel::High,
),
admin(HttpMethod::Put, "/rustfs/admin/v3/quota/{bucket}", SET_BUCKET_QUOTA, RouteRiskLevel::High),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/quota/{bucket}",
SET_BUCKET_QUOTA,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/export-bucket-metadata",
EXPORT_BUCKET_METADATA,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/export-bucket-metadata",
EXPORT_BUCKET_METADATA,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/import-bucket-metadata",
IMPORT_BUCKET_METADATA,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/import-bucket-metadata",
IMPORT_BUCKET_METADATA,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/get-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/set-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Delete, "/rustfs/admin/v3/del-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/help-config-kv", CONFIG_UPDATE, RouteRiskLevel::High),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/list-config-history-kv",
CONFIG_UPDATE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/clear-config-history-kv",
CONFIG_UPDATE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/restore-config-history-kv",
CONFIG_UPDATE,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/scanner/status", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/audit/target/list",
GET_BUCKET_TARGET,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/audit/target/{target_type}/{target_name}",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/audit/target/{target_type}/{target_name}/reset",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/module-switches",
SERVER_INFO,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Put, "/rustfs/admin/v3/module-switches", CONFIG_UPDATE, RouteRiskLevel::High),
admin(
HttpMethod::Get,
"/rustfs/admin/v4/plugins/catalog",
SERVER_INFO,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v4/plugins/instances",
GET_BUCKET_TARGET,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v4/plugins/instances/{id}",
GET_BUCKET_TARGET,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v4/plugins/instances/{id}",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v4/plugins/instances/{id}",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/replicationmetrics",
GET_REPLICATION_METRICS,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/set-remote-target",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/remove-remote-target",
SET_BUCKET_TARGET,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/add",
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/remove",
SITE_REPLICATION_REMOVE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/site-replication/info",
SITE_REPLICATION_INFO,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/site-replication/metainfo",
SITE_REPLICATION_INFO,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/site-replication/status",
SITE_REPLICATION_INFO,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/site-replication/devnull",
SITE_REPLICATION_INFO,
RouteRiskLevel::High,
),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/site-replication/netperf",
SITE_REPLICATION_INFO,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/join",
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/bucket-ops",
SITE_REPLICATION_OPERATION,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/iam-item",
SITE_REPLICATION_OPERATION,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/bucket-meta",
SITE_REPLICATION_OPERATION,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/site-replication/peer/idp-settings",
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/edit",
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/edit",
SITE_REPLICATION_ADD,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/peer/remove",
SITE_REPLICATION_REMOVE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/resync/op",
SITE_REPLICATION_RESYNC,
RouteRiskLevel::High,
),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/site-replication/state/edit",
SITE_REPLICATION_OPERATION,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/debug/pprof/profile", PROFILING, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/debug/pprof/status", PROFILING, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/debug/tls/status", PROFILING, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/create-key", KMS_CREATE_KEY, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/key/create", KMS_CREATE_KEY, RouteRiskLevel::High),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/kms/describe-key",
KMS_KEY_STATUS,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/kms/key/status",
KMS_KEY_STATUS,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/kms/list-keys",
KMS_KEY_STATUS,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/kms/generate-data-key",
SERVER_INFO,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/status", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/status", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/config", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/clear-cache", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/configure", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/start", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/stop", SERVER_INFO, RouteRiskLevel::High),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/kms/service-status",
SERVER_INFO,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/reconfigure", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Post, "/rustfs/admin/v3/kms/keys", KMS_CREATE_KEY, RouteRiskLevel::High),
admin(HttpMethod::Delete, "/rustfs/admin/v3/kms/keys/delete", SERVER_INFO, RouteRiskLevel::High),
admin(
HttpMethod::Post,
"/rustfs/admin/v3/kms/keys/cancel-deletion",
SERVER_INFO,
RouteRiskLevel::High,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/kms/keys", KMS_KEY_STATUS, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/kms/keys/{key_id}",
KMS_KEY_STATUS,
RouteRiskLevel::Sensitive,
),
public(
HttpMethod::Get,
"/rustfs/admin/v3/oidc/providers",
PublicRouteKind::OidcBootstrap,
RouteRiskLevel::Sensitive,
),
public(
HttpMethod::Get,
"/rustfs/admin/v3/oidc/authorize/{provider_id}",
PublicRouteKind::OidcBootstrap,
RouteRiskLevel::Sensitive,
),
public(
HttpMethod::Get,
"/rustfs/admin/v3/oidc/callback/{provider_id}",
PublicRouteKind::OidcBootstrap,
RouteRiskLevel::Sensitive,
),
public(
HttpMethod::Get,
"/rustfs/admin/v3/oidc/logout",
PublicRouteKind::OidcBootstrap,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Get, "/rustfs/admin/v3/oidc/config", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Put,
"/rustfs/admin/v3/oidc/config/{provider_id}",
CONFIG_UPDATE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/rustfs/admin/v3/oidc/config/{provider_id}",
CONFIG_UPDATE,
RouteRiskLevel::High,
),
admin(HttpMethod::Post, "/rustfs/admin/v3/oidc/validate", SERVER_INFO, RouteRiskLevel::High),
admin(HttpMethod::Get, "/iceberg/v1/config", GET_TABLE_CATALOG, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Get,
"/iceberg/v1/{warehouse}/namespaces",
GET_TABLE_NAMESPACE,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Post,
"/iceberg/v1/{warehouse}/namespaces",
SET_TABLE_NAMESPACE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/iceberg/v1/{warehouse}/namespaces/{namespace}",
GET_TABLE_NAMESPACE,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Delete,
"/iceberg/v1/{warehouse}/namespaces/{namespace}",
DELETE_TABLE_NAMESPACE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables",
GET_TABLE,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Post,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables",
CREATE_TABLE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Post,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/register",
REGISTER_TABLE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Get,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
GET_TABLE_METADATA,
RouteRiskLevel::Sensitive,
),
admin(
HttpMethod::Post,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
COMMIT_TABLE,
RouteRiskLevel::High,
),
admin(
HttpMethod::Delete,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
DELETE_TABLE,
RouteRiskLevel::High,
),
];
pub const DEFERRED_ADMIN_ROUTE_POLICIES: &[DeferredAdminRoutePolicy] = &[
deferred(HttpMethod::Get, "/rustfs/admin/v3/accountinfo", DeferredRoutePolicyReason::S3Action),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/user-info",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Put,
"/rustfs/admin/v3/add-user",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Post,
"/rustfs/admin/v3/update-service-account",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/info-service-account",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/info-access-key",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/list-service-accounts",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/list-access-keys-bulk",
DeferredRoutePolicyReason::MultipleActions,
),
deferred(
HttpMethod::Delete,
"/rustfs/admin/v3/delete-service-accounts",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Delete,
"/rustfs/admin/v3/delete-service-account",
DeferredRoutePolicyReason::ContextualAuthorization,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/idp/builtin/policy-entities",
DeferredRoutePolicyReason::MultipleActions,
),
deferred(HttpMethod::Post, "/rustfs/admin/v3/service", DeferredRoutePolicyReason::NotImplemented),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/inspect-data",
DeferredRoutePolicyReason::NotImplemented,
),
deferred(
HttpMethod::Post,
"/rustfs/admin/v3/inspect-data",
DeferredRoutePolicyReason::NotImplemented,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/datausageinfo",
DeferredRoutePolicyReason::MultipleActions,
),
deferred(HttpMethod::Get, "/rustfs/admin/v3/metrics", DeferredRoutePolicyReason::CredentialOnly),
deferred(HttpMethod::Get, "/rustfs/admin/v3/pools/list", DeferredRoutePolicyReason::MultipleActions),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/pools/status",
DeferredRoutePolicyReason::MultipleActions,
),
deferred(HttpMethod::Get, "/rustfs/admin/v3/get-bucket-quota", DeferredRoutePolicyReason::S3Action),
deferred(HttpMethod::Get, "/rustfs/admin/v3/quota/{bucket}", DeferredRoutePolicyReason::S3Action),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/quota-stats/{bucket}",
DeferredRoutePolicyReason::S3Action,
),
deferred(
HttpMethod::Post,
"/rustfs/admin/v3/quota-check/{bucket}",
DeferredRoutePolicyReason::S3Action,
),
deferred(
HttpMethod::Get,
"/rustfs/admin/v3/list-remote-targets",
DeferredRoutePolicyReason::CredentialOnly,
),
];
pub fn validate_admin_route_policy_specs() -> Result<(), AdminRouteMatrixError> {
validate_admin_route_specs(ADMIN_ROUTE_POLICY_SPECS)
}
const fn admin(method: HttpMethod, path: &'static str, action: AdminActionRef, risk_level: RouteRiskLevel) -> AdminRouteSpec {
AdminRouteSpec::admin(method, path, action, risk_level)
}
const fn public(method: HttpMethod, path: &'static str, kind: PublicRouteKind, risk_level: RouteRiskLevel) -> AdminRouteSpec {
AdminRouteSpec::public(method, path, kind, risk_level)
}
const fn deferred(method: HttpMethod, path: &'static str, reason: DeferredRoutePolicyReason) -> DeferredAdminRoutePolicy {
DeferredAdminRoutePolicy::new(method, path, reason)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admin::router::{AdminOperation, S3Router};
use hyper::Method;
use serial_test::serial;
use std::collections::BTreeSet;
use temp_env::with_var;
#[test]
fn admin_route_policy_specs_validate() {
assert!(validate_admin_route_policy_specs().is_ok());
}
#[test]
fn route_policy_inventory_has_no_internal_overlap() {
let mut seen = BTreeSet::new();
for spec in ADMIN_ROUTE_POLICY_SPECS {
let key = route_key(spec.method(), spec.path());
assert!(seen.insert(key), "duplicate direct route policy for {}", spec.path());
}
for policy in DEFERRED_ADMIN_ROUTE_POLICIES {
let key = route_key(policy.method(), policy.path());
assert!(seen.insert(key), "deferred route overlaps direct policy for {}", policy.path());
}
}
#[test]
#[serial]
fn route_policy_inventory_covers_registered_routes() {
let router = with_var(rustfs_config::ENV_HEALTH_ENDPOINT_ENABLE, Some("true"), || {
let mut router = S3Router::<AdminOperation>::new(false);
super::super::register_admin_routes(&mut router).expect("register production admin routes");
router
});
let registered = router.registered_routes().iter().cloned().collect::<BTreeSet<_>>();
let inventory = route_policy_inventory_keys();
assert_eq!(inventory, registered, "admin route policy inventory must cover every registered route");
}
#[test]
fn route_policy_keeps_public_exceptions_explicit() {
assert_public(HttpMethod::Get, "/health", PublicRouteKind::Health);
assert_public(HttpMethod::Head, "/health/ready", PublicRouteKind::Health);
assert_public(HttpMethod::Post, "/", PublicRouteKind::StsFormPost);
assert_public(
HttpMethod::Get,
"/rustfs/admin/v3/oidc/authorize/{provider_id}",
PublicRouteKind::OidcBootstrap,
);
}
#[test]
fn route_policy_keeps_table_catalog_outside_admin_prefix() {
let table_specs = ADMIN_ROUTE_POLICY_SPECS
.iter()
.filter(|spec| spec.path().starts_with("/iceberg/v1"));
assert_eq!(table_specs.count(), 11);
assert_action(HttpMethod::Get, "/iceberg/v1/{warehouse}/namespaces", GET_TABLE_NAMESPACE);
assert_action(HttpMethod::Post, "/iceberg/v1/{warehouse}/namespaces/{namespace}/tables", CREATE_TABLE);
assert_action(
HttpMethod::Get,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
GET_TABLE_METADATA,
);
assert_action(
HttpMethod::Post,
"/iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}",
COMMIT_TABLE,
);
}
#[test]
fn route_policy_keeps_contextual_auth_deferred() {
assert_deferred(
HttpMethod::Get,
"/rustfs/admin/v3/info-service-account",
DeferredRoutePolicyReason::ContextualAuthorization,
);
assert_deferred(
HttpMethod::Get,
"/rustfs/admin/v3/datausageinfo",
DeferredRoutePolicyReason::MultipleActions,
);
assert_deferred(HttpMethod::Get, "/rustfs/admin/v3/accountinfo", DeferredRoutePolicyReason::S3Action);
}
fn route_policy_inventory_keys() -> BTreeSet<String> {
ADMIN_ROUTE_POLICY_SPECS
.iter()
.map(|spec| route_key(spec.method(), spec.path()))
.chain(
DEFERRED_ADMIN_ROUTE_POLICIES
.iter()
.map(|policy| route_key(policy.method(), policy.path())),
)
.collect()
}
fn route_key(method: HttpMethod, path: &str) -> String {
format!("{}|{}", method.as_str(), path)
}
fn assert_action(method: HttpMethod, path: &str, action: AdminActionRef) {
let spec = ADMIN_ROUTE_POLICY_SPECS
.iter()
.find(|spec| spec.method() == method && spec.path() == path)
.expect("expected direct route policy");
assert_eq!(spec.access().admin_action(), Some(action));
}
fn assert_public(method: HttpMethod, path: &str, kind: PublicRouteKind) {
let spec = ADMIN_ROUTE_POLICY_SPECS
.iter()
.find(|spec| spec.method() == method && spec.path() == path)
.expect("expected public route policy");
assert_eq!(spec.access().public_kind(), Some(kind));
}
fn assert_deferred(method: HttpMethod, path: &str, reason: DeferredRoutePolicyReason) {
let policy = DEFERRED_ADMIN_ROUTE_POLICIES
.iter()
.find(|policy| policy.method() == method && policy.path() == path)
.expect("expected deferred route policy");
assert_eq!(policy.reason(), reason);
}
#[test]
fn method_mapping_matches_registered_route_format() {
assert_eq!(route_key(HttpMethod::Get, "/health"), Method::GET.as_str().to_string() + "|/health");
}
}