mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-27 06:18:43 +00:00
feat(oidc): make the Console session duration configurable (#8568)
* feat(oidc): make the Console session duration configurable The OIDC callback hard-coded the Console credential lifetime at 3600 seconds, so a Console session always ended after about an hour. RUSTFS_CONSOLE_AUTH_TIMEOUT, the documented knob next to this behaviour, is the Console HTTP request timeout and cannot extend a session. Resolve the lifetime from the process-level RUSTFS_CONSOLE_OIDC_SESSION_DURATION instead and forward it to complete_authorization_code(). Unset, unparseable, zero, and negative values keep DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS (3600), so existing deployments keep the previous behaviour and a misconfigured variable can never issue an already expired credential. Values above CONSOLE_OIDC_SESSION_DURATION_MAX_SECS (7 days) are clamped to that maximum with a warning, which also caps how far an operator can stretch a credential by accident. Keep the Console logout token for that same lifetime. OidcStateStore pinned the logout cache to a hard-coded hour, so a session configured to last longer than that dropped its logout token while the credential was still valid: RP-initiated logout could no longer resolve, and the browser was sent back to the Console login page with the IdP session still active. The logout cache now derives its TTL from the same console_session_duration_secs(), which applies the same clamp, and the callback reads its lifetime from that one function instead of a local copy, so the credential lifetime and the logout-token retention cannot drift apart. Add tests covering the default hour, an extended session, the maximum, and out-of-range values. The variable is deliberately not part of ENV_IDENTITY_OPENID_KEYS: that list is suffix-scanned per provider, and this setting applies to every provider. It has no identity_openid KVS equivalent and needs a restart, like the other process-level settings in the same document. Document the setting in the login flow, the process-level table, and the troubleshooting table, and clarify that RUSTFS_CONSOLE_AUTH_TIMEOUT is a request timeout rather than a session lifetime. * test(ecstore): retain offline marker fixtures across reconnect Keep simulated drive handles attached and reject I/O until explicit rejoin. Attempt reconnect in the current-marker case so the former CI race is deterministic while retaining physical cleanup and historical-data checks. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(oidc): align session diagnostics and lifetime regression tests Use structured IAM diagnostics for clamping and move configuration tests to their owning crate. Cover duration boundaries, logout retention and signed credential expiration through the federation binding. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: Chris <anzhengchao@gmail.com> Co-authored-by: Hauser <housemecn@gmail.com> Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
@@ -69,15 +69,17 @@ pub const ENV_CONSOLE_RATE_LIMIT_RPM: &str = "RUSTFS_CONSOLE_RATE_LIMIT_RPM";
|
||||
pub const DEFAULT_CONSOLE_RATE_LIMIT_RPM: u32 = 100;
|
||||
|
||||
/// Set the console authentication timeout in seconds
|
||||
/// Specifies how long a console authentication session remains valid
|
||||
/// Bounds how long the console waits for a single authenticated HTTP request before it is
|
||||
/// considered timed out. This is a request timeout, not the session lifetime: the Console
|
||||
/// credential lifetime issued by the OIDC callback is `RUSTFS_CONSOLE_OIDC_SESSION_DURATION`.
|
||||
/// Default: 3600 seconds (1 hour)
|
||||
/// Minimum: 300 seconds (5 minutes)
|
||||
/// Maximum: 86400 seconds (24 hours)
|
||||
pub const ENV_CONSOLE_AUTH_TIMEOUT: &str = "RUSTFS_CONSOLE_AUTH_TIMEOUT";
|
||||
|
||||
/// Default console authentication timeout in seconds
|
||||
/// This is the default timeout for console authentication sessions.
|
||||
/// After this timeout, users need to re-authenticate to access the console.
|
||||
/// This is the default timeout for console authentication requests.
|
||||
/// After this timeout, a pending console request is abandoned.
|
||||
/// Default value: 3600 seconds (1 hour)
|
||||
/// Environment variable: RUSTFS_CONSOLE_AUTH_TIMEOUT
|
||||
/// Command line argument: --console-auth-timeout
|
||||
@@ -85,6 +87,26 @@ pub const ENV_CONSOLE_AUTH_TIMEOUT: &str = "RUSTFS_CONSOLE_AUTH_TIMEOUT";
|
||||
/// Example: --console-auth-timeout 3600
|
||||
pub const DEFAULT_CONSOLE_AUTH_TIMEOUT: u64 = 3600;
|
||||
|
||||
/// Console credential lifetime, in seconds, for Console credentials issued by the OIDC callback.
|
||||
///
|
||||
/// Process-level, Console-scoped setting: it applies to every OIDC provider and deliberately
|
||||
/// lives outside the per-provider `RUSTFS_IDENTITY_OPENID_*` namespace, so it is never
|
||||
/// suffix-scanned into a provider id and has no `identity_openid` KVS equivalent. The same value
|
||||
/// also sets how long the Console logout token stays valid.
|
||||
pub const ENV_CONSOLE_OIDC_SESSION_DURATION: &str = "RUSTFS_CONSOLE_OIDC_SESSION_DURATION";
|
||||
|
||||
/// Default Console credential lifetime in seconds, used when the environment variable is unset,
|
||||
/// not a positive integer, or zero. Matches the value this path hard-coded before it was
|
||||
/// parameterised.
|
||||
pub const DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS: usize = 3600;
|
||||
|
||||
/// Upper bound on the Console credential lifetime in seconds (7 days).
|
||||
///
|
||||
/// Values above this are clamped with a warning. The bound keeps the moka logout cache's
|
||||
/// `time_to_live` well under its 1000-year assertion, and stops an operator from minting an
|
||||
/// effectively permanent Console STS credential on every node.
|
||||
pub const CONSOLE_OIDC_SESSION_DURATION_MAX_SECS: usize = 604_800;
|
||||
|
||||
/// Toggle update check
|
||||
/// It controls whether to check for newer versions of rustfs
|
||||
/// Default value: true
|
||||
|
||||
@@ -10550,8 +10550,9 @@ mod tests {
|
||||
disk.read_all(bucket, &format!("{object}/xl.meta")).await.unwrap(),
|
||||
));
|
||||
}
|
||||
for slot in 12..16 {
|
||||
set.disks.write().await[slot] = None;
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
disk.close().await.expect("stop stale member health monitoring");
|
||||
disk.force_offline_for_test();
|
||||
}
|
||||
for version in historical {
|
||||
store
|
||||
@@ -10592,7 +10593,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
*set.disks.write().await = before.clone();
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
disk.reset_health_for_store_init_retry();
|
||||
}
|
||||
store
|
||||
.recover_delete_transactions_at_incarnation(
|
||||
bucket,
|
||||
@@ -10611,8 +10614,9 @@ mod tests {
|
||||
.await
|
||||
.expect("durable retirement")
|
||||
);
|
||||
for slot in 12..16 {
|
||||
set.disks.write().await[slot] = None;
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
disk.close().await.expect("stop returning member health monitoring");
|
||||
disk.force_offline_for_test();
|
||||
}
|
||||
store
|
||||
.handle_make_bucket(bucket, &create)
|
||||
@@ -10646,7 +10650,9 @@ mod tests {
|
||||
tokio::fs::write(root.join("xl.meta"), bytes).await.unwrap();
|
||||
}
|
||||
}
|
||||
*set.disks.write().await = before.clone();
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
disk.reset_health_for_store_init_retry();
|
||||
}
|
||||
let heal = rustfs_heal_contracts::heal_channel::HealOpts {
|
||||
remove: true,
|
||||
scan_mode: rustfs_heal_contracts::heal_channel::HealScanMode::Deep,
|
||||
@@ -10730,14 +10736,16 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
set.disks.write().await[15] = None;
|
||||
let offline = before[15].as_ref().expect("cleanup target must exist");
|
||||
offline.close().await.expect("stop cleanup target health monitoring");
|
||||
offline.force_offline_for_test();
|
||||
let incomplete = store
|
||||
.heal_object_with_proof(bucket, object, &marker.to_string(), &heal)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(incomplete.error.as_ref().is_some_and(Error::is_retired_marker_deferred));
|
||||
assert!(incomplete.absence.is_none(), "offline targets cannot be omitted from a cleanup receipt");
|
||||
set.disks.write().await[15] = before[15].clone();
|
||||
offline.reset_health_for_store_init_retry();
|
||||
let failure =
|
||||
crate::set_disk::DanglingDeleteFailure::install(bucket, object, 15, crate::disk::error::DiskError::FaultyDisk);
|
||||
let partial = store
|
||||
@@ -10856,9 +10864,13 @@ mod tests {
|
||||
.expect("bucket incarnation");
|
||||
let set = &store.pools[0].disk_set[0];
|
||||
let before = set.disks.read().await.clone();
|
||||
for slot in 12..16 {
|
||||
set.disks.write().await[slot] = None;
|
||||
// Keep the handles attached so reconnect probes inherit the forced
|
||||
// outage instead of recreating healthy handles for these local paths.
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
disk.close().await.expect("stop stale member health monitoring");
|
||||
disk.force_offline_for_test();
|
||||
}
|
||||
set.connect_disks().await;
|
||||
store
|
||||
.delete_object(
|
||||
bucket,
|
||||
@@ -10871,8 +10883,6 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.expect("acknowledged delete should remove the marker from the online quorum");
|
||||
*set.disks.write().await = before.clone();
|
||||
|
||||
for disk in before.iter().take(12).flatten() {
|
||||
assert!(matches!(
|
||||
disk.read_version("", bucket, object, &marker.to_string(), &crate::disk::ReadOptions::default())
|
||||
@@ -10881,6 +10891,8 @@ mod tests {
|
||||
));
|
||||
}
|
||||
for disk in before.iter().skip(12).flatten() {
|
||||
assert_eq!(disk.runtime_state(), crate::disk::health_state::RuntimeDriveHealthState::Offline);
|
||||
disk.reset_health_for_store_init_retry();
|
||||
assert!(
|
||||
disk.read_version("", bucket, object, &marker.to_string(), &crate::disk::ReadOptions::default())
|
||||
.await
|
||||
|
||||
@@ -11,15 +11,20 @@
|
||||
// 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 crate::{EVENT_OIDC_DIAGNOSTICS, LOG_COMPONENT_IAM, LOG_SUBSYSTEM_OIDC};
|
||||
use base64_simd::URL_SAFE_NO_PAD;
|
||||
use rustfs_config::oidc::*;
|
||||
use rustfs_config::server_config::{Config as ServerConfig, KVS};
|
||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState};
|
||||
use rustfs_config::{
|
||||
CONSOLE_OIDC_SESSION_DURATION_MAX_SECS, DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS, DEFAULT_DELIMITER, ENABLE_KEY,
|
||||
ENV_CONSOLE_OIDC_SESSION_DURATION, EnableState,
|
||||
};
|
||||
use rustfs_utils::egress::{OutboundUrlError, validate_outbound_url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
pub(super) const REDACTED_SECRET: &str = "***redacted***";
|
||||
@@ -815,6 +820,38 @@ pub fn load_oidc_provider_configs_from_env() -> Vec<OidcProviderConfig> {
|
||||
parse_env_configs()
|
||||
}
|
||||
|
||||
/// Credential lifetime, in seconds, for Console credentials issued by the OIDC callback.
|
||||
///
|
||||
/// `RUSTFS_CONSOLE_OIDC_SESSION_DURATION` overrides the default for every provider. An unset,
|
||||
/// unparseable, or zero value keeps [`DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS`], so a
|
||||
/// misconfigured variable can never issue credentials that are already expired. Values above
|
||||
/// [`CONSOLE_OIDC_SESSION_DURATION_MAX_SECS`] are clamped to that maximum with a warning.
|
||||
///
|
||||
/// Both the credential lifetime and the Console logout-token retention derive from this single
|
||||
/// value, so they stay in step whichever way the variable is set.
|
||||
pub fn console_session_duration_secs() -> usize {
|
||||
let configured = rustfs_utils::get_env_opt_u64(ENV_CONSOLE_OIDC_SESSION_DURATION)
|
||||
.and_then(|seconds| usize::try_from(seconds).ok())
|
||||
.filter(|seconds| *seconds > 0)
|
||||
.unwrap_or(DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS);
|
||||
|
||||
if configured > CONSOLE_OIDC_SESSION_DURATION_MAX_SECS {
|
||||
warn!(
|
||||
event = EVENT_OIDC_DIAGNOSTICS,
|
||||
component = LOG_COMPONENT_IAM,
|
||||
subsystem = LOG_SUBSYSTEM_OIDC,
|
||||
state = "console_session_duration_clamped",
|
||||
config_key = ENV_CONSOLE_OIDC_SESSION_DURATION,
|
||||
configured,
|
||||
max = CONSOLE_OIDC_SESSION_DURATION_MAX_SECS,
|
||||
"Console OIDC session duration exceeds the maximum"
|
||||
);
|
||||
return CONSOLE_OIDC_SESSION_DURATION_MAX_SECS;
|
||||
}
|
||||
|
||||
configured
|
||||
}
|
||||
|
||||
pub fn load_oidc_provider_configs_from_server_config(cfg: &ServerConfig) -> Vec<OidcProviderConfig> {
|
||||
parse_persisted_configs(cfg)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,28 @@ use crate::oidc::test_support::*;
|
||||
use rustfs_config::server_config::{Config as ServerConfig, KVS};
|
||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY};
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn console_session_duration_follows_the_environment() {
|
||||
for (configured, expected) in [
|
||||
(None, DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
(Some("1"), 1),
|
||||
(Some("7200"), 7200),
|
||||
(Some("604800"), CONSOLE_OIDC_SESSION_DURATION_MAX_SECS),
|
||||
(Some("604801"), CONSOLE_OIDC_SESSION_DURATION_MAX_SECS),
|
||||
(Some("999999999"), CONSOLE_OIDC_SESSION_DURATION_MAX_SECS),
|
||||
(Some("18446744073709551615"), CONSOLE_OIDC_SESSION_DURATION_MAX_SECS),
|
||||
(Some("18446744073709551616"), DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
(Some("0"), DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
(Some("-1"), DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
(Some("60s"), DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
(Some(""), DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS),
|
||||
] {
|
||||
let observed = temp_env::with_var(ENV_CONSOLE_OIDC_SESSION_DURATION, configured, console_session_duration_secs);
|
||||
assert_eq!(observed, expected, "Console OIDC session duration for {configured:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_single_provider_no_config_url() {
|
||||
let config = parse_single_provider("_TEST_EMPTY", "test_empty");
|
||||
|
||||
@@ -25,9 +25,10 @@ mod test_support;
|
||||
pub use config::{
|
||||
OidcConfigError, OidcConfigQuery, OidcConfigSnapshot, OidcProviderConfig, OidcProviderConfigInput, OidcProviderConfigSource,
|
||||
OidcProviderValidationInput, OidcProviderValidationResult, OidcSiteReplicationProvider, OidcSiteReplicationSnapshot,
|
||||
SourcedOidcProviderConfig, build_upsert_provider_config, build_validation_provider_config, delete_persisted_provider_config,
|
||||
load_oidc_config_snapshot, load_oidc_provider_configs_from_env, load_oidc_provider_configs_from_server_config,
|
||||
persisted_provider_secret, upsert_persisted_provider_config, validate_mutable_provider_id,
|
||||
SourcedOidcProviderConfig, build_upsert_provider_config, build_validation_provider_config, console_session_duration_secs,
|
||||
delete_persisted_provider_config, load_oidc_config_snapshot, load_oidc_provider_configs_from_env,
|
||||
load_oidc_provider_configs_from_server_config, persisted_provider_secret, upsert_persisted_provider_config,
|
||||
validate_mutable_provider_id,
|
||||
};
|
||||
pub use provider::validate_oidc_provider_config_with_extra_root_ca;
|
||||
pub use runtime::{OidcSys, StandardOidcAdapter};
|
||||
|
||||
@@ -18,6 +18,8 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
|
||||
use super::config::console_session_duration_secs;
|
||||
|
||||
const OIDC_STATE_CAPACITY: u64 = 10_000;
|
||||
const OIDC_STATE_CAPACITY_WARNING: u64 = 9_000;
|
||||
const OIDC_STATE_CAPACITY_CRITICAL: u64 = 10_000;
|
||||
@@ -57,7 +59,10 @@ impl OidcStateStore {
|
||||
.build();
|
||||
let logout_cache = Cache::builder()
|
||||
.max_capacity(OIDC_STATE_CAPACITY)
|
||||
.time_to_live(Duration::from_secs(3600)) // 1 hour TTL to match console OIDC sessions
|
||||
// Keep the logout token alive for as long as the credentials it belongs to: if it
|
||||
// expired sooner, a session longer than the old hard-coded hour could no longer
|
||||
// trigger RP-initiated logout.
|
||||
.time_to_live(Duration::from_secs(console_session_duration_secs() as u64))
|
||||
.build();
|
||||
Self {
|
||||
cache,
|
||||
@@ -140,6 +145,8 @@ impl Default for OidcStateStore {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rustfs_config::ENV_CONSOLE_OIDC_SESSION_DURATION;
|
||||
use serial_test::serial;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
@@ -302,4 +309,26 @@ mod tests {
|
||||
|
||||
assert!(store.take_logout("logout_abc").await.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn logout_retention_follows_the_bounded_session_duration() {
|
||||
for (configured, expected_seconds) in [
|
||||
(None, 3600),
|
||||
(Some("1"), 1),
|
||||
(Some("7200"), 7200),
|
||||
(Some("604801"), 604_800),
|
||||
(Some("18446744073709551615"), 604_800),
|
||||
] {
|
||||
temp_env::with_var(ENV_CONSOLE_OIDC_SESSION_DURATION, configured, || {
|
||||
let store = OidcStateStore::new();
|
||||
assert_eq!(
|
||||
store.logout_cache.policy().time_to_live(),
|
||||
Some(Duration::from_secs(expected_seconds)),
|
||||
"logout retention for {configured:?} must match the issued session"
|
||||
);
|
||||
assert_eq!(store.cache.policy().time_to_live(), Some(Duration::from_secs(300)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ Login flow:
|
||||
3. The IdP redirects back to `/rustfs/admin/v3/oidc/callback/<provider_id>?code=...&state=...`.
|
||||
4. RustFS exchanges the code at the token endpoint, sending `client_id` and `client_secret` in the request body (`client_secret_post`) together with the PKCE verifier.
|
||||
5. RustFS validates the ID token signature (JWKS), issuer, audience, expiry, and nonce.
|
||||
6. RustFS maps claim values to policy names and issues one-hour STS credentials to the Console.
|
||||
6. RustFS maps claim values to policy names and issues STS credentials to the Console with the lifetime set by `RUSTFS_CONSOLE_OIDC_SESSION_DURATION` (default 3600 seconds, one hour).
|
||||
|
||||
In-flight `state` and PKCE verifiers are node-local: the authorize and callback requests must reach the same RustFS node.
|
||||
|
||||
@@ -47,6 +47,7 @@ Process-level settings (environment only, never suffixed per provider):
|
||||
| --- | --- |
|
||||
| `RUSTFS_BROWSER_REDIRECT_URL` | Public browser origin used for callback generation, Console success redirects, and logout fallback. |
|
||||
| `RUSTFS_OUTBOUND_ALLOW_ORIGINS` | Exact `scheme://host[:port]` origins RustFS may contact for discovery, JWKS, and token requests when the IdP resolves to a private, loopback, or container-network address. See [outbound-connection-policy.md](outbound-connection-policy.md). |
|
||||
| `RUSTFS_CONSOLE_OIDC_SESSION_DURATION` | Credential lifetime in seconds for Console credentials issued by this callback, for every provider. Default `3600`. Values that are missing, not a positive integer, or zero keep the default; values above `604800` (7 days) are clamped to `604800` with a warning. The same value also sets how long the Console logout token stays valid, so RP-initiated logout keeps working for the whole session. Note that `RUSTFS_CONSOLE_AUTH_TIMEOUT` does not control this: it is the Console HTTP request timeout. |
|
||||
|
||||
Named providers: to use provider id `<id>`, suffix every provider env var with `_<id>` (for example `RUSTFS_IDENTITY_OPENID_CLIENT_ID_keycloak`) and register the callback `/rustfs/admin/v3/oidc/callback/<id>`. Suffix scanning is `parse_single_provider` in `crates/iam/src/oidc/config.rs`.
|
||||
|
||||
@@ -137,6 +138,7 @@ curl -fsS "<ISSUER>/.well-known/openid-configuration" | jq '{issuer, authorizati
|
||||
| Login succeeds, access denied | no claim value matches a policy name | Emit `groups` or `roles` as a flat array equal to policy names; check for `/` prefixes. |
|
||||
| Console redirects to an internal host | `RUSTFS_BROWSER_REDIRECT_URL` unset or proxy headers wrong | Set `RUSTFS_BROWSER_REDIRECT_URL` to the public origin. |
|
||||
| Invalid or expired OIDC state | callback reached a different node | Configure load-balancer session affinity for authorize and callback. |
|
||||
| Console session expires after about an hour | callback issues credentials with the default lifetime | Set `RUSTFS_CONSOLE_OIDC_SESSION_DURATION` to the desired lifetime in seconds (max `604800`, 7 days) and restart RustFS. `RUSTFS_CONSOLE_AUTH_TIMEOUT` is a request timeout and does not extend the session. |
|
||||
|
||||
## Production checklist
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use hyper::Method;
|
||||
use matchit::Params;
|
||||
use rustfs_config::{ENV_RUSTFS_BROWSER_REDIRECT_URL, MAX_ADMIN_REQUEST_BODY_SIZE};
|
||||
use rustfs_iam::federation::{FederatedSessionBindingError, FederationError};
|
||||
use rustfs_iam::oidc::{OidcProviderConfigInput, OidcProviderValidationInput};
|
||||
use rustfs_iam::oidc::{OidcProviderConfigInput, OidcProviderValidationInput, console_session_duration_secs};
|
||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
|
||||
use serde::Serialize;
|
||||
@@ -450,8 +450,9 @@ impl Operation for OidcCallbackHandler {
|
||||
|
||||
let redirect_uri = derive_callback_uri(&req, provider_id)?;
|
||||
|
||||
let session_duration_seconds = console_session_duration_secs();
|
||||
let login = federation
|
||||
.complete_authorization_code(&state, &code, &redirect_uri, 3600, &DefaultFederatedSessionBinding)
|
||||
.complete_authorization_code(&state, &code, &redirect_uri, session_duration_seconds, &DefaultFederatedSessionBinding)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if let FederationError::CodeExchange(message) = &error {
|
||||
@@ -1375,4 +1376,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_callback_uses_the_configured_session_duration() {
|
||||
let production = include_str!("oidc.rs")
|
||||
.split("\n#[cfg(test)]\n")
|
||||
.next()
|
||||
.expect("production source must precede tests");
|
||||
let callback = production
|
||||
.split_once("impl Operation for OidcCallbackHandler")
|
||||
.expect("the OIDC callback handler must exist")
|
||||
.1;
|
||||
let callback = callback.split_whitespace().collect::<String>();
|
||||
|
||||
assert!(
|
||||
callback.contains("session_duration_seconds=console_session_duration_secs()"),
|
||||
"the callback must resolve the Console session duration from the environment"
|
||||
);
|
||||
assert!(
|
||||
callback.contains(".complete_authorization_code(&state,&code,&redirect_uri,session_duration_seconds,"),
|
||||
"the callback must hand the resolved session duration to the code exchange"
|
||||
);
|
||||
assert!(
|
||||
!callback.contains(".complete_authorization_code(&state,&code,&redirect_uri,3600,"),
|
||||
"the callback must not hard-code the session duration"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +571,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn console_session_duration_bounds_signed_credential_expiration() {
|
||||
let secret = "console-session-expiration-test-secret";
|
||||
for (configured, expected_seconds) in [(None, 3600), (Some("7200"), 7200), (Some("604801"), 604_800)] {
|
||||
temp_env::with_var(rustfs_config::ENV_CONSOLE_OIDC_SESSION_DURATION, configured, || {
|
||||
let mut transaction = transaction();
|
||||
transaction.duration_seconds = rustfs_iam::oidc::console_session_duration_secs();
|
||||
let before = OffsetDateTime::now_utc().unix_timestamp();
|
||||
let credentials = issue_credentials(&transaction, &["readonly".to_string()], Some(secret))
|
||||
.expect("configured Console session should issue credentials");
|
||||
let after = OffsetDateTime::now_utc().unix_timestamp();
|
||||
let claims = rustfs_iam::sys::get_claims_from_token_with_secret(&credentials.session_token, secret)
|
||||
.expect("Console credential signature should verify");
|
||||
let expiration = claims["exp"].as_i64().expect("signed expiration should be an integer");
|
||||
assert!(
|
||||
(before + expected_seconds..=after + expected_seconds).contains(&expiration),
|
||||
"signed expiration must reflect the bounded Console session for {configured:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
credentials
|
||||
.expiration
|
||||
.expect("Console credential must expire")
|
||||
.unix_timestamp(),
|
||||
expiration
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issued_credentials_and_replication_item_use_minio_parent_shape() {
|
||||
let transaction = transaction();
|
||||
|
||||
Reference in New Issue
Block a user