Skip to content

refactor(admin): route plugin handler auth through authorize_admin_request - #6247

Merged
overtrue merged 1 commit into
mainfrom
overtrue/backlog-1829-auth-sweep-plugins
Aug 19, 2026
Merged

refactor(admin): route plugin handler auth through authorize_admin_request#6247
overtrue merged 1 commit into
mainfrom
overtrue/backlog-1829-auth-sweep-plugins

Conversation

@overtrue

Copy link
Copy Markdown
Collaborator

Related Issues

rustfs/backlog#1829 (T3 batch P1 — plugin/extension family). Follows PR #6020 (T1, shared gate landed) and PR #6194 (T2, KMS management group).

Summary of Changes

The plugin/extension admin family carried seven byte-near copies of the admin auth preamble — extract req.credentials, check_key_valid(get_session_token(...)), dig RemoteAddr out of req.extensions, call validate_admin_request. Every copy is a place the gate can drift, and a drifted copy is an endpoint with different authorization than its siblings.

All seven are per-file authorize_* wrappers with no resource scope and no audit seam, so they fold onto the shared authorize_admin_request gate (rustfs/src/admin/auth.rs:302) without changing the decision. Only the wrapper bodies change; every handler call site is untouched.

Sites converted (7/7 of the batch classified in rustfs/backlog#1829):

File Wrapper Action set (unchanged) Missing-cred message (preserved)
handlers/extensions.rs authorize_extension_catalog_request ServerInfoAdminAction InvalidRequest "authentication required"
handlers/extensions.rs authorize_extension_instance_request GetBucketTargetAction InvalidRequest "authentication required"
handlers/plugins_catalog.rs authorize_plugin_catalog_request ServerInfoAdminAction InvalidRequest "authentication required"
handlers/plugins_instances.rs authorize_plugin_instance_request GetBucketTargetAction InvalidRequest "authentication required"
handlers/plugins_instances.rs authorize_plugin_instance_write_request SetBucketTargetAction InvalidRequest "authentication required"
handlers/object_data_cache.rs authorize (parameterized) caller-supplied AdminAction InvalidRequest "missing credentials"
handlers/cluster_snapshot.rs authorize_cluster_snapshot_request ServerInfoAdminAction InvalidRequest "authentication required"

The shared gate reports InvalidRequest "get cred failed" for a credential-less request, which differs from what all seven endpoints have always returned. Each wrapper therefore keeps an explicit if req.credentials.is_none() pre-check returning its own historical error, the pattern established by T2 in kms_management.rs:75-81. Five new tests pin those messages byte-for-byte.

Per-site response-equivalence argument

Three request classes, per site:

  1. No credentials (req.credentials == None). Before: s3_error!(InvalidRequest, "<site message>"). After: the pre-check returns that same expression before the shared gate is reached. Identical code, identical message. Pinned by the five new *_keeps_its_missing_credentials_message / *_keep_their_missing_credentials_message tests.
  2. Wrong credentials (check_key_valid fails). Before: check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?. After: authorize_admin_requestauthenticate_request(&req.headers, &req.uri, input_cred) (auth.rs:256), whose body is check_key_valid(get_session_token(uri, headers).unwrap_or_default(), &credentials.access_key).await and which returns result unmodified. Same token source, same access key, same S3Error propagated through ?. The only difference is that authenticate_request emits a debug!/warn! trace event — log output, not response bytes.
  3. Authenticated but unauthorized. Before: validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(X)], req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0))). After: the gate calls validate_admin_request(&req.headers, &cred, owner, false, actions, remote_addr) with remote_addr computed by the identical expression. Same six arguments, same deny_only = false, same action vector per site, same empty bucket/object resource slots. validate_admin_request returns S3Result<()>; the gate's extra Ok(cred) is dropped by .await?; Ok(()) in the wrapper, so the wrapper signature and its success/error values are unchanged.

No site gained or lost an action, changed an action, gained a resource scope, or changed deny_only. No status code changes: all three classes return exactly the error they returned before.

Sites deliberately excluded

None from this batch — all seven classified sites converted. For the record, the wider issue's excluded classes are untouched here: key/bucket-scoped gates (kms_keys, kms_key_lifecycle, kms_key_metadata, quota), audit-wrapped gates (KmsAdminAudit::gate/gate_admin), deny_only-dynamic sites (user.rs:228/628, sts.rs:195), and the E-class one-offs. Batches P2–P6 of the inventory remain to be done.

Verification

  • cargo check -p rustfs — clean
  • cargo clippy -p rustfs --lib --tests -- -D warnings — clean
  • cargo nextest run -p rustfs -E 'test(/admin/)' — 1435 passed, 0 failed
  • cargo nextest run -p rustfs -E 'binary(rustfs) and test(/admin::handlers::(plugins_catalog|plugins_instances|extensions|cluster_snapshot|object_data_cache)::/)' — 57 passed
  • cargo fmt --all --check
  • scripts/check_layer_dependencies.sh, scripts/check_architecture_migration_rules.sh, scripts/check_unsafe_code_allowances.sh, scripts/check_logging_guardrails.sh, scripts/check_no_planning_docs.sh — all pass

include_str! source-text assertions (plugin-contract-guard)

All five touched files are read by their own include_str! contract tests. Every one was re-run and passes unmodified — the assertions target handler call sites (unchanged) and the AdminAction::* constant inside each wrapper body (retained):

  • extensions::tests::extension_handlers_require_admin_authorization_contract
  • plugins_catalog::tests::plugin_catalog_handlers_require_admin_authorization_contract
  • plugins_instances::tests::plugin_instance_handlers_require_admin_authorization_contract
  • object_data_cache::tests::stats_handler_requires_server_info_action
  • cluster_snapshot::tests::cluster_snapshot_handler_requires_server_info_admin_permission

No source-text assertion needed updating.

Impact

None user-facing. Authorization decisions, HTTP status codes, error codes, and error messages are unchanged for every one of the seven endpoints. The only observable difference is two additional tracing events (debug! on authentication attempt/success, warn! on authentication failure) emitted by the shared gate, matching the six admin files already routed through it. No API, config, deployment, or on-disk impact.

Additional Notes

Adversarial Validation — Standard tier + security reviewer

  • Correctness adversary — attacked the three request classes per site (absent / invalid / unauthorized credentials), the deny_only value, the RemoteAddr extension lookup (present, absent, and Some(None)), the wrapper return type (S3Result<()> vs the gate's S3Result<Credentials>), and gate-vs-body ordering (all seven wrappers are called at the top of call before req.input is consumed, and stay there). No break found.
  • Simplicity adversary — production code shrinks (seven ~15-line preamble copies become seven 4-line wrappers, plus four now-dead imports removed); no new helper, type, or branch is introduced, and the pre-check is not a speculative guard: it has a nameable trigger (an unsigned request reaching an admin route) and a named consequence (message drift). Attacked whether the pre-check could be dropped in favour of the shared gate's own None arm — it cannot without changing 7 responses. No break found.
  • Security reviewer — grepped each AdminAction::* constant against the operation its handler performs: catalogs and cluster snapshot are reads under ServerInfoAdminAction, instance reads under GetBucketTargetAction, instance writes under SetBucketTargetAction, matching the plugin-contract-guard read/write split; the object-data-cache stats/flush pair keeps its ServerInfoAdminAction/ConfigUpdateAdminAction asymmetry. No action set widened, no deny_only flipped, no scope dropped, no read endpoint downgraded to a credentials-exist check, and the AccessDenied → InvalidRequest status-code hazard flagged in the inventory does not arise here (none of the seven used AccessDenied). No break found.
  • Test-coverage skeptic — a revert of any wrapper to a different missing-credentials message is caught by the five new pin tests; a revert of the action constant is caught by the pre-existing include_str! contract tests; a dropped gate call is caught by the same contract tests. Residual risk: the authenticated-but-unauthorized path has no unit test here because validate_admin_request requires a ready IAM handle — it is covered by argument-identity inspection above and by auth::tests::authorize_admin_request_without_credentials_is_rejected plus the gate's own allow/deny unit suite in auth.rs. No break found.
  • Concurrency/durability, compatibility, and performance roles are not applicable: no shared state, no format or API-shape change, no per-request hot path (admin routes only).

…quest

The plugin/extension admin family carried seven byte-near copies of the
admin auth preamble (extract credentials, check_key_valid, read RemoteAddr
out of the extensions, call validate_admin_request). Each copy is a place
the gate can drift, which is exactly the review surface rustfs/backlog#1829
tracks.

Every one of the seven is a per-file wrapper with no resource scope and no
audit seam, so it folds onto the shared `authorize_admin_request` gate
without changing the decision. The wrappers keep their own missing-
credentials pre-check, following the pattern established in
`kms_management.rs`: the shared gate reports "get cred failed", while these
endpoints have always reported "authentication required" (six sites) and
"missing credentials" (object_data_cache), and that response must stay
byte-identical. New tests pin each message.

Action sets, `deny_only=false`, the RemoteAddr lookup, and the wrapper
signatures are unchanged, so authenticated-but-unauthorized and
wrong-credential responses are unchanged too.
@github-actions

Copy link
Copy Markdown
Contributor

CLA requirements are satisfied for this pull request.

@overtrue
overtrue enabled auto-merge (squash) August 19, 2026 03:03
@overtrue
overtrue merged commit f7073d0 into main Aug 19, 2026
32 of 34 checks passed
@overtrue
overtrue deleted the overtrue/backlog-1829-auth-sweep-plugins branch August 19, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant