mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-27 16:48:58 +00:00
351 lines
11 KiB
Rust
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));
|
|
}
|
|
}
|