mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 22:03:14 +00:00
Openstack Keystone integration - v1 keeps the same mechanism as (#1961)
Co-authored-by: loverustfs <hello@rustfs.com> Co-authored-by: 安正超 <anzhengchao@gmail.com>
This commit is contained in:
@@ -50,6 +50,7 @@ rustfs-credentials = { workspace = true }
|
||||
rustfs-ecstore = { workspace = true }
|
||||
rustfs-filemeta.workspace = true
|
||||
rustfs-iam = { workspace = true }
|
||||
rustfs-keystone = { workspace = true }
|
||||
rustfs-kms = { workspace = true }
|
||||
rustfs-lock.workspace = true
|
||||
rustfs-madmin = { workspace = true }
|
||||
|
||||
@@ -117,10 +117,32 @@ impl IAMAuth {
|
||||
#[async_trait::async_trait]
|
||||
impl S3Auth for IAMAuth {
|
||||
async fn get_secret_key(&self, access_key: &str) -> S3Result<SecretKey> {
|
||||
// NEW: Check if Keystone credentials are present in task-local storage
|
||||
// This handles pure X-Auth-Token requests without Authorization header
|
||||
use rustfs_keystone::KEYSTONE_CREDENTIALS;
|
||||
|
||||
if let Ok(Some(creds)) = KEYSTONE_CREDENTIALS.try_with(|c| c.clone()) {
|
||||
tracing::debug!("IAMAuth: Keystone credentials found in task-local storage for user {}", creds.parent_user);
|
||||
// Return empty secret key - Keystone uses token validation, not AWS signatures
|
||||
return Ok(SecretKey::from(String::new()));
|
||||
}
|
||||
|
||||
if access_key.is_empty() {
|
||||
return Err(s3_error!(UnauthorizedAccess, "Your account is not signed up"));
|
||||
}
|
||||
|
||||
// Check if this is a Keystone access key (from mixed auth scenario)
|
||||
// Keystone credentials use token authentication, not signature verification
|
||||
if access_key.starts_with("keystone:") {
|
||||
tracing::debug!(
|
||||
"IAMAuth: Keystone access key detected ({}), returning empty secret for token-based auth",
|
||||
access_key
|
||||
);
|
||||
// Return empty secret key - Keystone uses token validation, not AWS signatures
|
||||
// The actual credentials are stored in task-local storage by KeystoneAuthMiddleware
|
||||
return Ok(SecretKey::from(String::new()));
|
||||
}
|
||||
|
||||
if let Ok(key) = self.simple_auth.get_secret_key(access_key).await {
|
||||
return Ok(key);
|
||||
}
|
||||
@@ -155,6 +177,70 @@ impl S3Auth for IAMAuth {
|
||||
|
||||
// check_key_valid checks the key is valid or not. return the user's credentials and if the user is the owner.
|
||||
pub async fn check_key_valid(session_token: &str, access_key: &str) -> S3Result<(Credentials, bool)> {
|
||||
// KEYSTONE INTEGRATION: Check if Keystone credentials are present in task-local storage
|
||||
// This handles both:
|
||||
// 1. Pure X-Auth-Token requests (access_key may be empty)
|
||||
// 2. Keystone access keys formatted as "keystone:user_id"
|
||||
use crate::auth_keystone;
|
||||
use rustfs_keystone::KEYSTONE_CREDENTIALS;
|
||||
|
||||
// Try to get Keystone credentials from task-local storage first
|
||||
if let Ok(Some(credentials)) = KEYSTONE_CREDENTIALS.try_with(|creds| creds.clone()) {
|
||||
tracing::debug!("check_key_valid: Keystone credentials found in task-local storage");
|
||||
|
||||
if !auth_keystone::is_keystone_enabled() {
|
||||
return Err(s3_error!(InvalidAccessKeyId, "Keystone authentication is not enabled"));
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"check_key_valid: Retrieved Keystone credentials for user: {} (project: {})",
|
||||
credentials.parent_user,
|
||||
credentials
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|c| c.get("keystone_project_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
);
|
||||
|
||||
// Determine if user is admin (owner-level access)
|
||||
// Users with "admin" or "reseller_admin" role have owner permissions
|
||||
// Roles are stored in claims["keystone_roles"] by the middleware
|
||||
let is_owner = credentials
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("keystone_roles"))
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
tracing::debug!(
|
||||
"check_key_valid: Keystone user {} has owner permissions: {}",
|
||||
credentials.parent_user,
|
||||
is_owner
|
||||
);
|
||||
|
||||
return Ok((credentials, is_owner));
|
||||
}
|
||||
|
||||
// Legacy check for explicit "keystone:" prefix (for backwards compatibility)
|
||||
if access_key.starts_with("keystone:") {
|
||||
tracing::warn!(
|
||||
"check_key_valid: Keystone access key detected but no credentials in task-local storage. \
|
||||
This indicates middleware was bypassed or not configured."
|
||||
);
|
||||
|
||||
if !auth_keystone::is_keystone_enabled() {
|
||||
return Err(s3_error!(InvalidAccessKeyId, "Keystone authentication is not enabled"));
|
||||
}
|
||||
|
||||
return Err(s3_error!(InvalidAccessKeyId, "Keystone authentication requires X-Auth-Token header"));
|
||||
}
|
||||
|
||||
let Some(mut cred) = get_global_action_cred() else {
|
||||
return Err(S3Error::with_message(
|
||||
S3ErrorCode::InternalError,
|
||||
@@ -254,6 +340,39 @@ pub fn check_claims_from_token(token: &str, cred: &Credentials) -> S3Result<Hash
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
|
||||
/// Check for Keystone authentication headers and authenticate if present
|
||||
/// Returns Some((Credentials, is_owner)) if Keystone authentication succeeds
|
||||
/// Returns None if no Keystone headers present (fall back to standard auth)
|
||||
///
|
||||
/// Reserved for future use (alternative Keystone auth path)
|
||||
#[allow(dead_code)]
|
||||
pub async fn try_keystone_auth(headers: &HeaderMap) -> S3Result<Option<(Credentials, bool)>> {
|
||||
use crate::auth_keystone;
|
||||
|
||||
if !auth_keystone::is_keystone_enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match auth_keystone::authenticate_keystone(headers).await? {
|
||||
Some(cred) => {
|
||||
// Keystone credentials are never "owner" in the traditional sense
|
||||
// unless they have admin role
|
||||
let is_owner = cred
|
||||
.groups
|
||||
.as_ref()
|
||||
.map(|groups| {
|
||||
groups
|
||||
.iter()
|
||||
.any(|g| g.eq_ignore_ascii_case("admin") || g.eq_ignore_ascii_case("reseller_admin"))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(Some((cred, is_owner)))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_session_token<'a>(uri: &'a Uri, hds: &'a HeaderMap) -> Option<&'a str> {
|
||||
hds.get("x-amz-security-token")
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
@@ -1279,6 +1398,176 @@ mod tests {
|
||||
let conditions = get_condition_values(&headers, &cred, None, None, Some(remote_addr_v6));
|
||||
assert_eq!(conditions.get("SourceIp").unwrap()[0], "2001:db8::1");
|
||||
}
|
||||
|
||||
// ========== KEYSTONE AUTHENTICATION TESTS ==========
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_key_valid_keystone_not_enabled() {
|
||||
// Test that keystone: access key fails when Keystone is not enabled
|
||||
let result = check_key_valid("dummy-token", "keystone:user123").await;
|
||||
|
||||
// Should fail with InvalidAccessKeyId because Keystone is not enabled
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(*err.code(), s3s::S3ErrorCode::InvalidAccessKeyId);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_key_valid_keystone_no_credentials() {
|
||||
use rustfs_keystone::KEYSTONE_CREDENTIALS;
|
||||
|
||||
// Test behavior when Keystone would be enabled but no credentials in task-local
|
||||
// This simulates a request that bypassed middleware
|
||||
KEYSTONE_CREDENTIALS
|
||||
.scope(None, async {
|
||||
// Call function that checks for keystone: prefix
|
||||
// In real scenario, would check is_keystone_enabled() first
|
||||
let access_key = "keystone:user123";
|
||||
if access_key.starts_with("keystone:") {
|
||||
// Without credentials in task-local, this should fail
|
||||
let creds_result = KEYSTONE_CREDENTIALS.try_with(|c: &Option<Credentials>| c.clone());
|
||||
assert!(creds_result.is_ok()); // try_with succeeds
|
||||
assert!(creds_result.unwrap().is_none()); // but value is None
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystone_role_detection_admin() {
|
||||
// Test role detection logic for admin role
|
||||
let mut claims: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
claims.insert("roles".to_string(), json!(["admin", "member"]));
|
||||
|
||||
let is_owner = claims
|
||||
.get("roles")
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(is_owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystone_role_detection_reseller_admin() {
|
||||
// Test role detection logic for reseller_admin role
|
||||
let mut claims: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
claims.insert("roles".to_string(), json!(["reseller_admin"]));
|
||||
|
||||
let is_owner = claims
|
||||
.get("roles")
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(is_owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystone_role_detection_non_admin() {
|
||||
// Test role detection logic for non-admin roles
|
||||
let mut claims: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
claims.insert("roles".to_string(), json!(["member", "reader"]));
|
||||
|
||||
let is_owner = claims
|
||||
.get("roles")
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(!is_owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystone_role_detection_empty() {
|
||||
// Test role detection logic for empty roles
|
||||
let mut claims: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
claims.insert("roles".to_string(), json!([]));
|
||||
|
||||
let is_owner = claims
|
||||
.get("roles")
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(!is_owner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystone_role_detection_no_claim() {
|
||||
// Test role detection logic when roles claim is missing
|
||||
let claims: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
|
||||
let is_owner = claims
|
||||
.get("roles")
|
||||
.and_then(|roles| roles.as_array())
|
||||
.map(|roles| {
|
||||
roles
|
||||
.iter()
|
||||
.any(|role| role.as_str().map(|r| r == "admin" || r == "reseller_admin").unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(!is_owner);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_keystone_task_local_storage() {
|
||||
use rustfs_keystone::KEYSTONE_CREDENTIALS;
|
||||
|
||||
// Test that task-local storage properly stores and retrieves credentials
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("project_id".to_string(), json!("project123"));
|
||||
claims.insert("roles".to_string(), json!(["member"]));
|
||||
|
||||
let test_creds = Credentials {
|
||||
access_key: "keystone:testuser".to_string(),
|
||||
secret_key: String::new(),
|
||||
session_token: String::new(),
|
||||
expiration: None,
|
||||
status: "on".to_string(),
|
||||
parent_user: "testuser".to_string(),
|
||||
groups: None,
|
||||
claims: Some(claims),
|
||||
name: Some("Test User".to_string()),
|
||||
description: None,
|
||||
};
|
||||
|
||||
// Outside scope, should fail
|
||||
let result = KEYSTONE_CREDENTIALS.try_with(|c: &Option<Credentials>| c.clone());
|
||||
assert!(result.is_err());
|
||||
|
||||
// Inside scope, should succeed
|
||||
KEYSTONE_CREDENTIALS
|
||||
.scope(Some(test_creds.clone()), async {
|
||||
let result = KEYSTONE_CREDENTIALS.try_with(|c: &Option<Credentials>| c.clone());
|
||||
assert!(result.is_ok());
|
||||
let creds = result.unwrap();
|
||||
assert!(creds.is_some());
|
||||
assert_eq!(creds.unwrap().access_key, "keystone:testuser");
|
||||
})
|
||||
.await;
|
||||
|
||||
// After scope, should fail again
|
||||
let result = KEYSTONE_CREDENTIALS.try_with(|c: &Option<Credentials>| c.clone());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
// 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.
|
||||
|
||||
//! OpenStack Keystone authentication integration for RustFS
|
||||
|
||||
use http::HeaderMap;
|
||||
use rustfs_credentials::Credentials;
|
||||
use rustfs_keystone::{KeystoneAuthProvider, KeystoneClient, KeystoneConfig, KeystoneIdentityMapper};
|
||||
use s3s::{S3Result, s3_error};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
static KEYSTONE_AUTH: OnceLock<Arc<KeystoneAuthProvider>> = OnceLock::new();
|
||||
static KEYSTONE_MAPPER: OnceLock<Arc<KeystoneIdentityMapper>> = OnceLock::new();
|
||||
static KEYSTONE_CONFIG: OnceLock<KeystoneConfig> = OnceLock::new();
|
||||
|
||||
/// Initialize Keystone authentication
|
||||
pub async fn init_keystone_auth(config: KeystoneConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !config.enable {
|
||||
info!("Keystone authentication disabled");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Initializing Keystone authentication...");
|
||||
|
||||
// Validate configuration
|
||||
config.validate()?;
|
||||
|
||||
let version = config.get_version()?;
|
||||
let client = KeystoneClient::new(
|
||||
config.auth_url.clone(),
|
||||
version,
|
||||
config.admin_user.clone(),
|
||||
config.admin_password.clone(),
|
||||
config.admin_project.clone(),
|
||||
config.get_admin_domain(),
|
||||
config.verify_ssl,
|
||||
);
|
||||
|
||||
let auth_provider = KeystoneAuthProvider::new(client.clone(), config.cache_size, config.get_cache_ttl(), config.enable_cache);
|
||||
|
||||
let mut mapper = KeystoneIdentityMapper::new(Arc::new(client), config.enable_tenant_prefix);
|
||||
|
||||
// Add custom role mappings if configured
|
||||
if let Some(role_mappings) = &config.role_mappings {
|
||||
for mapping in role_mappings {
|
||||
mapper.add_role_mapping(mapping.keystone_role.clone(), mapping.rustfs_policy.clone());
|
||||
}
|
||||
}
|
||||
|
||||
KEYSTONE_AUTH
|
||||
.set(Arc::new(auth_provider))
|
||||
.map_err(|_| "Keystone auth already initialized")?;
|
||||
|
||||
KEYSTONE_MAPPER
|
||||
.set(Arc::new(mapper))
|
||||
.map_err(|_| "Keystone mapper already initialized")?;
|
||||
|
||||
KEYSTONE_CONFIG
|
||||
.set(config.clone())
|
||||
.map_err(|_| "Keystone config already initialized")?;
|
||||
|
||||
info!("Keystone authentication initialized successfully");
|
||||
info!(" Auth URL: {}", config.auth_url);
|
||||
info!(" Version: {}", config.version);
|
||||
info!(" Tenant prefix enabled: {}", config.enable_tenant_prefix);
|
||||
info!(" Token caching enabled: {}", config.enable_cache);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get Keystone auth provider
|
||||
pub fn get_keystone_auth() -> Option<Arc<KeystoneAuthProvider>> {
|
||||
KEYSTONE_AUTH.get().cloned()
|
||||
}
|
||||
|
||||
/// Get Keystone identity mapper
|
||||
///
|
||||
/// Reserved for future use (Swift API, tenant prefixing)
|
||||
#[allow(dead_code)]
|
||||
pub fn get_keystone_mapper() -> Option<Arc<KeystoneIdentityMapper>> {
|
||||
KEYSTONE_MAPPER.get().cloned()
|
||||
}
|
||||
|
||||
/// Get Keystone configuration
|
||||
///
|
||||
/// Reserved for future use (dynamic configuration updates)
|
||||
#[allow(dead_code)]
|
||||
pub fn get_keystone_config() -> Option<&'static KeystoneConfig> {
|
||||
KEYSTONE_CONFIG.get()
|
||||
}
|
||||
|
||||
/// Check if Keystone is enabled
|
||||
pub fn is_keystone_enabled() -> bool {
|
||||
KEYSTONE_CONFIG.get().map(|c| c.enable).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Authenticate request with Keystone
|
||||
///
|
||||
/// Checks for:
|
||||
/// 1. X-Auth-Token header (Keystone token)
|
||||
/// 2. X-Storage-Token header (Swift compatibility)
|
||||
///
|
||||
/// Returns Some(Credentials) if authenticated via Keystone,
|
||||
/// None if Keystone is disabled or no Keystone headers present
|
||||
///
|
||||
/// Reserved for future use (alternative auth path, Swift API)
|
||||
#[allow(dead_code)]
|
||||
pub async fn authenticate_keystone(headers: &HeaderMap) -> S3Result<Option<Credentials>> {
|
||||
let auth_provider = match get_keystone_auth() {
|
||||
Some(provider) => provider,
|
||||
None => return Ok(None), // Keystone not enabled
|
||||
};
|
||||
|
||||
// Check for X-Auth-Token header (Keystone v3)
|
||||
if let Some(token) = headers.get("X-Auth-Token").and_then(|v| v.to_str().ok()) {
|
||||
debug!("Found X-Auth-Token header, validating with Keystone");
|
||||
|
||||
return match auth_provider.authenticate_with_token(token).await {
|
||||
Ok(cred) => {
|
||||
info!("Keystone token authentication successful: user={}", cred.parent_user);
|
||||
Ok(Some(cred))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Keystone token authentication failed: {}", e);
|
||||
Err(s3_error!(InvalidToken, "Invalid Keystone token: {}", e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check for X-Storage-Token header (Swift compatibility)
|
||||
if let Some(token) = headers.get("X-Storage-Token").and_then(|v| v.to_str().ok()) {
|
||||
debug!("Found X-Storage-Token header, validating with Keystone");
|
||||
|
||||
return match auth_provider.authenticate_with_token(token).await {
|
||||
Ok(cred) => {
|
||||
info!("Keystone Swift token authentication successful: user={}", cred.parent_user);
|
||||
Ok(Some(cred))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Keystone Swift token authentication failed: {}", e);
|
||||
Err(s3_error!(InvalidToken, "Invalid Keystone token: {}", e))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// No Keystone headers found
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Apply tenant prefix to bucket name
|
||||
///
|
||||
/// Reserved for future use (multi-tenancy feature)
|
||||
#[allow(dead_code)]
|
||||
pub fn apply_tenant_prefix(bucket: &str, cred: &Credentials) -> String {
|
||||
let mapper = match get_keystone_mapper() {
|
||||
Some(m) => m,
|
||||
None => return bucket.to_string(),
|
||||
};
|
||||
|
||||
// Extract project_id from claims
|
||||
let project_id = cred
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("keystone_project_id"))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
mapper.apply_tenant_prefix(bucket, project_id)
|
||||
}
|
||||
|
||||
/// Remove tenant prefix from bucket name
|
||||
///
|
||||
/// Reserved for future use (multi-tenancy feature)
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_tenant_prefix(prefixed_bucket: &str, cred: &Credentials) -> String {
|
||||
let mapper = match get_keystone_mapper() {
|
||||
Some(m) => m,
|
||||
None => return prefixed_bucket.to_string(),
|
||||
};
|
||||
|
||||
let project_id = cred
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("keystone_project_id"))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
mapper.remove_tenant_prefix(prefixed_bucket, project_id)
|
||||
}
|
||||
|
||||
/// Check if bucket belongs to user's project
|
||||
///
|
||||
/// Reserved for future use (multi-tenancy feature)
|
||||
#[allow(dead_code)]
|
||||
pub fn is_user_bucket(bucket: &str, cred: &Credentials) -> bool {
|
||||
let mapper = match get_keystone_mapper() {
|
||||
Some(m) => m,
|
||||
None => return true,
|
||||
};
|
||||
|
||||
let project_id = cred
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("keystone_project_id"))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
mapper.is_project_bucket(bucket, project_id)
|
||||
}
|
||||
|
||||
/// Filter bucket list to only show user's project buckets
|
||||
///
|
||||
/// Reserved for future use (multi-tenancy feature)
|
||||
#[allow(dead_code)]
|
||||
pub fn filter_bucket_list(buckets: Vec<String>, cred: &Credentials) -> Vec<String> {
|
||||
let mapper = match get_keystone_mapper() {
|
||||
Some(m) => m,
|
||||
None => return buckets,
|
||||
};
|
||||
|
||||
if !mapper.is_tenant_prefix_enabled() {
|
||||
return buckets;
|
||||
}
|
||||
|
||||
let project_id = cred
|
||||
.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("keystone_project_id"))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
if let Some(proj_id) = project_id {
|
||||
let prefix = format!("{}:", proj_id);
|
||||
buckets
|
||||
.into_iter()
|
||||
.filter(|b| b.starts_with(&prefix))
|
||||
.map(|b| b[prefix.len()..].to_string())
|
||||
.collect()
|
||||
} else {
|
||||
// No project ID, return unprefixed buckets only
|
||||
buckets.into_iter().filter(|b| !b.contains(':')).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if credential is from Keystone
|
||||
///
|
||||
/// Reserved for future use (credential type detection)
|
||||
#[allow(dead_code)]
|
||||
pub fn is_keystone_credential(cred: &Credentials) -> bool {
|
||||
cred.claims
|
||||
.as_ref()
|
||||
.and_then(|claims| claims.get("auth_source"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s == "keystone")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn create_test_credentials(project_id: Option<&str>) -> Credentials {
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("auth_source".to_string(), json!("keystone"));
|
||||
if let Some(proj_id) = project_id {
|
||||
claims.insert("keystone_project_id".to_string(), json!(proj_id));
|
||||
}
|
||||
|
||||
Credentials {
|
||||
access_key: "test-access".to_string(),
|
||||
secret_key: "test-secret".to_string(),
|
||||
claims: Some(claims),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_keystone_credential() {
|
||||
let cred = create_test_credentials(Some("proj123"));
|
||||
assert!(is_keystone_credential(&cred));
|
||||
|
||||
let non_keystone_cred = Credentials::default();
|
||||
assert!(!is_keystone_credential(&non_keystone_cred));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
mod admin;
|
||||
mod app;
|
||||
mod auth;
|
||||
mod auth_keystone;
|
||||
mod config;
|
||||
mod error;
|
||||
mod init;
|
||||
@@ -368,6 +369,18 @@ async fn run(config: config::Config) -> Result<()> {
|
||||
init_iam_sys(store.clone()).await.map_err(Error::other)?;
|
||||
readiness.mark_stage(SystemStage::IamReady);
|
||||
|
||||
// 3a. Initialize Keystone authentication if enabled
|
||||
let keystone_config = rustfs_keystone::KeystoneConfig::from_env().map_err(Error::other)?;
|
||||
if keystone_config.enable {
|
||||
match auth_keystone::init_keystone_auth(keystone_config).await {
|
||||
Ok(_) => info!("Keystone authentication initialized successfully"),
|
||||
Err(e) => {
|
||||
error!("Failed to initialize Keystone authentication: {}", e);
|
||||
// Continue without Keystone - fall back to standard auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Initialize OIDC System (non-fatal if no providers configured)
|
||||
if let Err(e) = init_oidc_sys().await {
|
||||
warn!("OIDC initialization failed (non-fatal): {}", e);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// Import HTTP server components and compression configuration
|
||||
use crate::admin;
|
||||
use crate::auth::IAMAuth;
|
||||
use crate::auth_keystone;
|
||||
use crate::config;
|
||||
use crate::server::{
|
||||
ReadinessGateLayer, RemoteAddr, ServiceState, ServiceStateManager,
|
||||
@@ -37,6 +38,7 @@ use opentelemetry::global;
|
||||
use rustfs_common::GlobalReadiness;
|
||||
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||
use rustfs_ecstore::rpc::{TONIC_RPC_PREFIX, verify_rpc_signature};
|
||||
use rustfs_keystone::KeystoneAuthLayer;
|
||||
use rustfs_protos::proto_gen::node_service::node_service_server::NodeServiceServer;
|
||||
use rustfs_trusted_proxies::ClientInfo;
|
||||
use rustfs_utils::net::parse_and_resolve_address;
|
||||
@@ -617,6 +619,13 @@ fn process_connection(
|
||||
// CRITICAL: Insert ReadinessGateLayer before business logic
|
||||
// This stops requests from hitting IAMAuth or Storage if they are not ready.
|
||||
.layer(ReadinessGateLayer::new(readiness))
|
||||
// Add Keystone authentication middleware
|
||||
// This validates X-Auth-Token headers and stores credentials in task-local storage
|
||||
// Must be placed AFTER ReadinessGateLayer but BEFORE business logic
|
||||
.layer({
|
||||
let keystone_auth = auth_keystone::get_keystone_auth();
|
||||
KeystoneAuthLayer::new(keystone_auth)
|
||||
})
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &HttpRequest<_>| {
|
||||
|
||||
Reference in New Issue
Block a user