Files
rustfs/rustfs/src/auth_keystone.rs
T

351 lines
11 KiB
Rust

// 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 rustfs_utils::MaskedAccessKey;
use s3s::{S3Result, s3_error};
use std::sync::{Arc, OnceLock};
use tracing::{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();
const LOG_COMPONENT_AUTH: &str = "auth";
const LOG_SUBSYSTEM_KEYSTONE: &str = "keystone";
/// Initialize Keystone authentication
pub async fn init_keystone_auth(config: KeystoneConfig) -> Result<(), Box<dyn std::error::Error>> {
if !config.enable {
info!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_state",
state = "disabled",
"Keystone authentication state changed"
);
return Ok(());
}
info!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_state",
state = "initializing",
auth_url = %config.auth_url,
version = %config.version,
enable_tenant_prefix = config.enable_tenant_prefix,
enable_cache = config.enable_cache,
"Keystone authentication state changed"
);
// 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,
config.get_timeout(),
);
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!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_state",
state = "initialized",
auth_url = %config.auth_url,
version = %config.version,
enable_tenant_prefix = config.enable_tenant_prefix,
enable_cache = config.enable_cache,
"Keystone authentication state changed"
);
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()) {
return match auth_provider.authenticate_with_token(token).await {
Ok(cred) => {
info!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_token_auth",
token_type = "x_auth_token",
principal = %MaskedAccessKey(&cred.parent_user),
result = "success",
"Keystone token authentication completed"
);
Ok(Some(cred))
}
Err(e) => {
error!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_token_auth",
token_type = "x_auth_token",
result = "failed",
error = %e,
"Keystone token authentication completed"
);
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()) {
return match auth_provider.authenticate_with_token(token).await {
Ok(cred) => {
info!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_token_auth",
token_type = "x_storage_token",
principal = %MaskedAccessKey(&cred.parent_user),
result = "success",
"Keystone token authentication completed"
);
Ok(Some(cred))
}
Err(e) => {
error!(
component = LOG_COMPONENT_AUTH,
subsystem = LOG_SUBSYSTEM_KEYSTONE,
event = "keystone_token_auth",
token_type = "x_storage_token",
result = "failed",
error = %e,
"Keystone token authentication completed"
);
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));
}
}