awol2005ex_kinit 0.0.1

Kerberos authentication using password or keytab
Documentation
// Copyright 2024 awol2005ex
//
// 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.

//! Kerberos configuration handling

use std::env;
use std::fs;
use std::error::Error;
use std::path::Path;

/// Get default keytab file path from environment or configuration
pub fn get_default_keytab_path() -> String {
    // Check KRB5_KTNAME environment variable first
    if let Ok(keytab_path) = env::var("KRB5_KTNAME") {
        if Path::new(&keytab_path).exists() {
            return keytab_path;
        }
    }
    
    // Try to get from Kerberos configuration
    if let Ok(config_keytab) = get_keytab_from_config() {
        if Path::new(&config_keytab).exists() {
            return config_keytab;
        }
    }
    
    // Use default locations
    let default_paths = vec![
        "/etc/krb5.keytab".to_string(),
        "/etc/krb5/krb5.keytab".to_string(),
        "C:\\Windows\\krb5.keytab".to_string(),
        format!("{}\\krb5.keytab", env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string())),
    ];
    
    for path in default_paths {
        if Path::new(&path).exists() {
            return path;
        }
    }
    
    // Return the first default path even if it doesn't exist
    "/etc/krb5.keytab".to_string()
}

/// Read keytab path from Kerberos configuration file
pub fn get_keytab_from_config() -> Result<String, Box<dyn Error>> {
    let config_paths = if let Ok(krb5_config) = env::var("KRB5_CONFIG") {
        vec![krb5_config]
    } else {
        vec![
            "/etc/krb5.conf".to_string(),
            "/etc/krb5/krb5.conf".to_string(),
            "C:\\Windows\\krb5.ini".to_string(),
            format!("{}\\krb5.ini", env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string())),
        ]
    };
    
    for config_path in config_paths {
        if let Ok(config_content) = fs::read_to_string(&config_path) {
            if let Some(keytab_path) = parse_krb5_conf_for_keytab(&config_content) {
                return Ok(keytab_path);
            }
        }
    }
    
    Err("Could not find keytab path in any krb5.conf file".into())
}

/// Parse krb5.conf content to find keytab path
pub fn parse_krb5_conf_for_keytab(config_content: &str) -> Option<String> {
    for line in config_content.lines() {
        let line = line.trim();
        
        // Look for default_keytab_name = <path>
        if line.starts_with("default_keytab_name") || line.contains("default_keytab_name =") {
            if let Some(keytab_part) = line.split('=').nth(1) {
                let keytab = keytab_part.trim().trim_matches('"').trim_matches('\'');
                if !keytab.is_empty() {
                    return Some(keytab.to_string());
                }
            }
        }
    }
    
    None
}

/// Read KDC IP from Kerberos configuration file (krb5.conf)
pub fn get_kdc_ip_from_config(realm: &str) -> Result<String, Box<dyn Error>> {
    // Get KRB5_CONFIG environment variable, or use default locations
    let config_paths = if let Ok(krb5_config) = env::var("KRB5_CONFIG") {
        vec![krb5_config]
    } else {
        // Default locations for krb5.conf
        vec![
            "/etc/krb5.conf".to_string(),
            "/etc/krb5/krb5.conf".to_string(),
            "C:\\Windows\\krb5.ini".to_string(),
            format!("{}\\krb5.ini", env::var("WINDIR").unwrap_or_else(|_| "C:\\Windows".to_string())),
        ]
    };
    
    for config_path in config_paths {
        if let Ok(config_content) = fs::read_to_string(&config_path) {
            if let Some(kdc_ip) = parse_krb5_conf_for_kdc(&config_content, realm) {
                return Ok(kdc_ip);
            }
        }
    }
    
    Err(format!("Could not find KDC for realm {} in any krb5.conf file", realm).into())
}

/// Parse krb5.conf content to find KDC for the specified realm
pub fn parse_krb5_conf_for_kdc(config_content: &str, realm: &str) -> Option<String> {
    let mut in_realms_section = false;
    let mut current_realm = String::new();
    
    for line in config_content.lines() {
        let line = line.trim();
        
        if line.starts_with('[') && line.ends_with(']') {
            let section = line[1..line.len()-1].trim();
            in_realms_section = section == "realms" || section.ends_with("realms");
            continue;
        }
        
        if !in_realms_section {
            continue;
        }
        
        if line.starts_with(&format!("{} =", realm)) || line.starts_with(&format!("{}=", realm)) {
            current_realm = realm.to_string();
            continue;
        }
        
        if !current_realm.is_empty() {
            if line.contains('{') {
                continue;
            }
            if line.contains('}') {
                current_realm.clear();
                continue;
            }
            
            // Look for kdc = <hostname_or_ip>
            if line.starts_with("kdc") || line.contains("kdc =") {
                if let Some(kdc_part) = line.split('=').nth(1) {
                    let kdc = kdc_part.trim().trim_matches('"').trim_matches('\'');
                    if !kdc.is_empty() {
                        return Some(kdc.to_string());
                    }
                }
            }
        }
    }
    
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_parse_krb5_conf_for_kdc() {
        let config_content = r#"
[libdefaults]
default_realm = EXAMPLE.COM

[realms]
EXAMPLE.COM = {
    kdc = kdc.example.com
    admin_server = kdc.example.com
}

OTHER.REALM = {
    kdc = 192.168.1.100
    admin_server = 192.168.1.100
}
"#;
        
        let kdc = parse_krb5_conf_for_kdc(config_content, "EXAMPLE.COM");
        assert_eq!(kdc, Some("kdc.example.com".to_string()));
        
        let kdc2 = parse_krb5_conf_for_kdc(config_content, "OTHER.REALM");
        assert_eq!(kdc2, Some("192.168.1.100".to_string()));
        
        let kdc3 = parse_krb5_conf_for_kdc(config_content, "NONEXISTENT.REALM");
        assert_eq!(kdc3, None);
    }
}