diff --git a/Cargo.lock b/Cargo.lock index 798bbfa31..4eaba5c1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9105,6 +9105,7 @@ dependencies = [ "rustfs-s3select-api", "rustfs-s3select-query", "rustfs-scanner", + "rustfs-security-governance", "rustfs-signer", "rustfs-targets", "rustfs-tls-runtime", diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index ba469e625..25f9fb10c 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -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()` 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. diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 5e636e963..c91a8215b 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -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 } diff --git a/rustfs/src/admin/mod.rs b/rustfs/src/admin/mod.rs index aa11825ce..c3fc6166d 100644 --- a/rustfs/src/admin/mod.rs +++ b/rustfs/src/admin/mod.rs @@ -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; diff --git a/rustfs/src/admin/route_policy.rs b/rustfs/src/admin/route_policy.rs new file mode 100644 index 000000000..693bca2d5 --- /dev/null +++ b/rustfs/src/admin/route_policy.rs @@ -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::::new(false); + super::super::register_admin_routes(&mut router).expect("register production admin routes"); + router + }); + + let registered = router.registered_routes().iter().cloned().collect::>(); + 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 { + 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"); + } +}