Files
rustfs/crates/keystone/src/identity.rs
T
Zhengchao An a9691b6797 chore: adjudicate 19 bare dead_code allows across six leaf crates (#6161)
backlog#1823 step 10, batch 1 of the repo-wide item-allow sweep. 227 bare #[allow(dead_code)] remain across 83 files; this takes the 19 in utils, notify, checksums, policy, keystone and trusted-proxies, which are small enough to verify end to end.

Removing all 19 first, before writing any reason, matters: 8 of them suppress nothing. Every allow in utils, one in policy and three in notify sit on items that are publicly reachable, so dead_code never applied to them — the same shape as the swift module and kms's dek.rs. Writing a reason onto a no-op allow would dress noise up as considered judgement, so those are simply deleted.

Three items are genuinely dead and go with their allows: notify's new_target_id_set, the AWS metadata fetcher's get_metadata_token, and policy's empty `pub struct Value;`, none of which is referenced anywhere in the tree.

The remaining eight keep an allow, now saying why the item survives rather than who calls it. Two are exercised only by their own crate's tests (checksums' MD5_HEADER_NAME, policy's is_match_as_pattern_prefix). Four are fields written but never read back: keystone's verify_ssl, parsed from config after the reqwest client is already built; keystone's client handle, which keeps the Keystone client alive for the mapper's lifetime; the AWS IMDS endpoint, kept beside the client while requests build their own URLs; and notify's rules_map, whose own comment retains it for snapshot-time judgements no code performs.

checksums' Md5 needed the most care. Crc32, Sha256 and seven others each have an arm in ChecksumAlgorithm::into_impl, and Md5 has none, which reads like a missing algorithm. It is not: ChecksumAlgorithm has no Md5 variant at all. S3 carries Content-MD5 as its own header, separate from the x-amz-checksum-* family, and this impl exists so both paths share the Checksum trait. The reason records that, so the next reader does not re-derive it.

One measurement note for anyone continuing this sweep: cargo does not re-emit warnings for cached compilations, so a per-crate loop of `cargo check -p <crate>` under-reports. checksums showed zero that way while actually carrying three. Touch the sources and check the crates in one invocation, then attribute by path.

Verification: the six crates are warning-free under cargo check --tests; clippy --lib --tests -D warnings clean; cargo nextest run 1096 passed; make pre-commit exit 0.

Ref rustfs/backlog#1823 (step 10).
2026-08-17 11:34:47 +08:00

328 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.
use crate::KeystoneClient;
use rustfs_policy::policy::Policy;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info};
/// Maps Keystone identities to RustFS concepts
pub struct KeystoneIdentityMapper {
#[allow(
dead_code,
reason = "keeps the Keystone client alive for the mapper's lifetime; the mapping paths do not call through it yet (backlog#1823)"
)]
client: Arc<KeystoneClient>,
role_policy_map: HashMap<String, String>,
enable_tenant_prefix: bool,
}
impl KeystoneIdentityMapper {
/// Create new identity mapper
pub fn new(client: Arc<KeystoneClient>, enable_tenant_prefix: bool) -> Self {
let mut role_policy_map = HashMap::new();
// Default Keystone role mappings
role_policy_map.insert("admin".to_string(), "AdminPolicy".to_string());
role_policy_map.insert("Admin".to_string(), "AdminPolicy".to_string());
role_policy_map.insert("Member".to_string(), "ReadWritePolicy".to_string());
role_policy_map.insert("_member_".to_string(), "ReadOnlyPolicy".to_string());
role_policy_map.insert("ResellerAdmin".to_string(), "AdminPolicy".to_string());
role_policy_map.insert("SwiftOperator".to_string(), "ReadWritePolicy".to_string());
role_policy_map.insert("objectstore:admin".to_string(), "AdminPolicy".to_string());
role_policy_map.insert("objectstore:creator".to_string(), "ReadWritePolicy".to_string());
Self {
client,
role_policy_map,
enable_tenant_prefix,
}
}
/// Add custom role-to-policy mapping
pub fn add_role_mapping(&mut self, keystone_role: String, rustfs_policy: String) {
info!("Adding role mapping: {} -> {}", keystone_role, rustfs_policy);
self.role_policy_map.insert(keystone_role, rustfs_policy);
}
/// Add multiple role mappings
pub fn add_role_mappings(&mut self, mappings: Vec<(String, String)>) {
for (role, policy) in mappings {
self.add_role_mapping(role, policy);
}
}
/// Map Keystone roles to RustFS policy names
pub fn map_roles_to_policies(&self, roles: &[String]) -> Vec<String> {
let policies: Vec<String> = roles
.iter()
.filter_map(|role| self.role_policy_map.get(role).cloned())
.collect();
debug!("Mapped roles {:?} to policies {:?}", roles, policies);
policies
}
/// Generate tenant-prefixed bucket name
/// Format: <project_id>:<bucket_name>
pub fn apply_tenant_prefix(&self, bucket: &str, project_id: Option<&str>) -> String {
if !self.enable_tenant_prefix {
return bucket.to_string();
}
if let Some(proj_id) = project_id {
let prefixed = format!("{}:{}", proj_id, bucket);
debug!("Applied tenant prefix: {} -> {}", bucket, prefixed);
prefixed
} else {
bucket.to_string()
}
}
/// Remove tenant prefix from bucket name
pub fn remove_tenant_prefix(&self, prefixed_bucket: &str, project_id: Option<&str>) -> String {
if !self.enable_tenant_prefix {
return prefixed_bucket.to_string();
}
if let Some(proj_id) = project_id {
let prefix = format!("{}:", proj_id);
if prefixed_bucket.starts_with(&prefix) {
let unprefixed = prefixed_bucket[prefix.len()..].to_string();
debug!("Removed tenant prefix: {} -> {}", prefixed_bucket, unprefixed);
return unprefixed;
}
}
prefixed_bucket.to_string()
}
/// Check if bucket belongs to project
pub fn is_project_bucket(&self, bucket: &str, project_id: Option<&str>) -> bool {
if !self.enable_tenant_prefix {
return true; // No multi-tenancy, all buckets accessible
}
if let Some(proj_id) = project_id {
let prefix = format!("{}:", proj_id);
bucket.starts_with(&prefix)
} else {
!bucket.contains(':') // No project ID, only unprefixed buckets
}
}
/// Extract project ID from prefixed bucket name
pub fn extract_project_id(&self, bucket: &str) -> Option<String> {
if !self.enable_tenant_prefix {
return None;
}
bucket.find(':').map(|pos| bucket[..pos].to_string())
}
/// Create default policies for Keystone roles
pub fn create_default_policies(&self) -> HashMap<String, Policy> {
let mut policies = HashMap::new();
// Admin policy - full access
let admin_json = r#"{
"Version": "2012-10-17",
"ID": "AdminPolicy",
"Statement": [{
"Sid": "AdminFullAccess",
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::*"]
}]
}"#;
if let Ok(policy) = serde_json::from_str::<Policy>(admin_json) {
policies.insert("AdminPolicy".to_string(), policy);
}
// ReadWrite policy - read/write access
let readwrite_json = r#"{
"Version": "2012-10-17",
"ID": "ReadWritePolicy",
"Statement": [{
"Sid": "ReadWriteAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
"Resource": ["arn:aws:s3:::*"]
}]
}"#;
if let Ok(policy) = serde_json::from_str::<Policy>(readwrite_json) {
policies.insert("ReadWritePolicy".to_string(), policy);
}
// ReadOnly policy - read-only access
let readonly_json = r#"{
"Version": "2012-10-17",
"ID": "ReadOnlyPolicy",
"Statement": [{
"Sid": "ReadOnlyAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": ["arn:aws:s3:::*"]
}]
}"#;
if let Ok(policy) = serde_json::from_str::<Policy>(readonly_json) {
policies.insert("ReadOnlyPolicy".to_string(), policy);
}
policies
}
/// Check if user has permission based on Keystone roles
pub fn has_permission(&self, roles: &[String], action: &str, _resource: &str) -> bool {
// Admin always has access
if roles.iter().any(|r| r.eq_ignore_ascii_case("admin") || r == "ResellerAdmin") {
return true;
}
// Check role-based permissions
for role in roles {
if let Some(policy_name) = self.role_policy_map.get(role) {
match policy_name.as_str() {
"AdminPolicy" => return true,
"ReadWritePolicy" => {
if action.starts_with("s3:Get")
|| action.starts_with("s3:Put")
|| action.starts_with("s3:Delete")
|| action.starts_with("s3:List")
{
return true;
}
}
"ReadOnlyPolicy" => {
if action.starts_with("s3:Get") || action.starts_with("s3:List") {
return true;
}
}
_ => continue,
}
}
}
false
}
/// Check if tenant prefixing is enabled
pub fn is_tenant_prefix_enabled(&self) -> bool {
self.enable_tenant_prefix
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::KeystoneVersion;
fn create_mapper() -> KeystoneIdentityMapper {
let client = KeystoneClient::new(
"http://localhost:5000".to_string(),
KeystoneVersion::V3,
None,
None,
None,
"Default".to_string(),
true,
std::time::Duration::from_secs(30),
);
KeystoneIdentityMapper::new(Arc::new(client), true)
}
#[test]
fn test_tenant_prefix() {
let mapper = create_mapper();
let prefixed = mapper.apply_tenant_prefix("mybucket", Some("proj123"));
assert_eq!(prefixed, "proj123:mybucket");
let unprefixed = mapper.remove_tenant_prefix("proj123:mybucket", Some("proj123"));
assert_eq!(unprefixed, "mybucket");
// No project ID
let no_prefix = mapper.apply_tenant_prefix("mybucket", None);
assert_eq!(no_prefix, "mybucket");
}
#[test]
fn test_is_project_bucket() {
let mapper = create_mapper();
assert!(mapper.is_project_bucket("proj123:mybucket", Some("proj123")));
assert!(!mapper.is_project_bucket("proj456:mybucket", Some("proj123")));
assert!(!mapper.is_project_bucket("mybucket", Some("proj123")));
}
#[test]
fn test_extract_project_id() {
let mapper = create_mapper();
assert_eq!(mapper.extract_project_id("proj123:mybucket"), Some("proj123".to_string()));
assert_eq!(mapper.extract_project_id("mybucket"), None);
}
#[test]
fn test_role_mapping() {
let mapper = create_mapper();
let roles = vec!["Member".to_string(), "admin".to_string()];
let policies = mapper.map_roles_to_policies(&roles);
assert!(policies.contains(&"ReadWritePolicy".to_string()));
assert!(policies.contains(&"AdminPolicy".to_string()));
}
#[test]
fn test_has_permission() {
let mapper = create_mapper();
// Admin has all permissions
assert!(mapper.has_permission(&["admin".to_string()], "s3:DeleteBucket", ""));
// Member has read/write permissions
assert!(mapper.has_permission(&["Member".to_string()], "s3:PutObject", ""));
assert!(mapper.has_permission(&["Member".to_string()], "s3:GetObject", ""));
// _member_ has read-only permissions
assert!(mapper.has_permission(&["_member_".to_string()], "s3:GetObject", ""));
assert!(!mapper.has_permission(&["_member_".to_string()], "s3:PutObject", ""));
}
#[test]
fn test_add_role_mapping() {
let mut mapper = create_mapper();
mapper.add_role_mapping("CustomRole".to_string(), "CustomPolicy".to_string());
let policies = mapper.map_roles_to_policies(&["CustomRole".to_string()]);
assert_eq!(policies, vec!["CustomPolicy".to_string()]);
}
}