mesa/common/
authentication.rs

1use directories::ProjectDirs;
2use serde_json::Value;
3
4use dialoguer::{Input, Password};
5use std::{
6    collections::HashMap,
7    fs::{create_dir_all, File},
8    io::{Read, Write},
9    path::PathBuf,
10    time::Duration,
11};
12
13use termion::color;
14
15use crate::error::Error;
16
17/// docs --> https://cray-hpe.github.io/docs-csm/en-12/operations/security_and_authentication/api_authorization/
18///      --> https://cray-hpe.github.io/docs-csm/en-12/operations/security_and_authentication/retrieve_an_authentication_token/
19pub async fn get_api_token(
20    shasta_base_url: &str,
21    shasta_root_cert: &[u8],
22    keycloak_base_url: &str,
23    site_name: &str,
24) -> Result<String, Error> {
25    let mut shasta_token: String;
26
27    // Look for authentication token in environment variable
28    for (env, value) in std::env::vars() {
29        if env.eq_ignore_ascii_case("MANTA_CSM_TOKEN") {
30            log::info!(
31                "Looking for CSM authentication token in envonment variable 'MANTA_CSM_TOKEN'"
32            );
33
34            shasta_token = value;
35
36            match test_client_api(shasta_base_url, &shasta_token, shasta_root_cert).await {
37                Ok(_) => return Ok(shasta_token),
38                Err(_) => return Err(Error::Message("Authentication unsucessful".to_string())),
39            }
40        }
41    }
42
43    // Look for authentication token in fielsystem
44    log::info!("Looking for CSM authentication token in filesystem file");
45
46    let mut file;
47
48    let project_dirs = ProjectDirs::from(
49        "local", /*qualifier*/
50        "cscs",  /*organization*/
51        "manta", /*application*/
52    );
53
54    let mut path = PathBuf::from(project_dirs.unwrap().cache_dir());
55
56    let mut attempts = 0;
57
58    create_dir_all(&path)?;
59
60    path.push(site_name.to_string() + "_auth"); // ~/.cache/manta/<site name>_http is the file containing the Shasta authentication
61                                                // token
62    log::debug!("Cache file: {:?}", path);
63
64    shasta_token = if path.exists() {
65        get_token_from_local_file(path.as_os_str()).unwrap()
66    } else {
67        String::new()
68    };
69
70    while !test_client_api(shasta_base_url, &shasta_token, shasta_root_cert)
71        .await
72        .unwrap()
73        && attempts < 3
74    {
75        println!(
76            "Please type your {}Keycloak credentials{}",
77            color::Fg(color::Green),
78            color::Fg(color::Reset)
79        );
80        let username: String = Input::new().with_prompt("username").interact_text()?;
81        let password = Password::new().with_prompt("password").interact()?;
82
83        match get_token_from_shasta_endpoint(
84            keycloak_base_url,
85            shasta_root_cert,
86            &username,
87            &password,
88        )
89        .await
90        {
91            Ok(shasta_token_aux) => {
92                log::debug!("Shasta token received");
93                file = File::create(&path).expect("Error encountered while creating file!");
94                file.write_all(shasta_token_aux.as_bytes())
95                    .expect("Error while writing to file");
96                shasta_token = get_token_from_local_file(path.as_os_str()).unwrap();
97            }
98            Err(_) => {
99                eprintln!("Failed in getting token from Shasta API");
100            }
101        }
102
103        attempts += 1;
104    }
105
106    if attempts < 3 {
107        shasta_token = get_token_from_local_file(path.as_os_str()).unwrap();
108        Ok(shasta_token)
109    } else {
110        Err(Error::Message("Authentication unsucessful".to_string())) // Black magic conversion from Err(Box::new("my error msg")) which does not
111    }
112}
113
114pub fn get_token_from_local_file(path: &std::ffi::OsStr) -> Result<String, reqwest::Error> {
115    let mut shasta_token = String::new();
116    File::open(path)
117        .unwrap()
118        .read_to_string(&mut shasta_token)
119        .unwrap();
120    Ok(shasta_token.to_string())
121}
122
123pub async fn test_connectivity_to_backend(shasta_base_url: &str) -> bool {
124    let client;
125
126    let client_builder = reqwest::Client::builder().connect_timeout(Duration::new(3, 0));
127
128    // Build client
129    client = client_builder.build().unwrap();
130
131    let api_url = shasta_base_url.to_owned() + "/cfs/healthz";
132
133    log::info!("Validate Shasta token against {}", api_url);
134
135    let resp_rslt = client.get(api_url).send().await;
136
137    match resp_rslt {
138        Ok(_) => true,
139        Err(error) => !error.is_timeout(),
140    }
141}
142
143pub async fn test_client_api(
144    shasta_base_url: &str,
145    shasta_token: &str,
146    shasta_root_cert: &[u8],
147) -> Result<bool, reqwest::Error> {
148    let client;
149
150    let client_builder = reqwest::Client::builder()
151        .add_root_certificate(reqwest::Certificate::from_pem(shasta_root_cert)?);
152
153    // Build client
154    if std::env::var("SOCKS5").is_ok() {
155        // socks5 proxy
156        log::debug!("SOCKS5 enabled");
157        let socks5proxy = reqwest::Proxy::all(std::env::var("SOCKS5").unwrap())?;
158
159        // rest client to authenticate
160        client = client_builder.proxy(socks5proxy).build()?;
161    } else {
162        client = client_builder.build()?;
163    }
164
165    let api_url = shasta_base_url.to_owned() + "/cfs/healthz";
166
167    log::info!("Validate Shasta token against {}", api_url);
168
169    let resp_rslt = client.get(api_url).bearer_auth(shasta_token).send().await;
170
171    match resp_rslt {
172        Ok(resp) => {
173            if resp.status().is_success() {
174                log::info!("Shasta token is valid");
175                return Ok(true);
176            } else {
177                let payload = resp.text().await?;
178                log::error!("Token is not valid - {}", payload);
179                return Ok(false);
180            }
181        }
182        Err(error) => {
183            eprintln!("Error connecting to Shasta API. Reason:\n{:?}. Exit", error);
184            log::debug!("Response:\n{:#?}", error);
185            std::process::exit(1);
186        }
187    }
188}
189
190pub async fn get_token_from_shasta_endpoint(
191    keycloak_base_url: &str,
192    shasta_root_cert: &[u8],
193    username: &str,
194    password: &str,
195) -> Result<String, reqwest::Error> {
196    let mut params = HashMap::new();
197    params.insert("grant_type", "password");
198    params.insert("client_id", "shasta");
199    params.insert("username", username);
200    params.insert("password", password);
201
202    let client;
203
204    let client_builder = reqwest::Client::builder()
205        .add_root_certificate(reqwest::Certificate::from_pem(shasta_root_cert)?);
206
207    // Build client
208    if std::env::var("SOCKS5").is_ok() {
209        // socks5 proxy
210        let socks5proxy = reqwest::Proxy::all(std::env::var("SOCKS5").unwrap())?;
211
212        // rest client to authenticate
213        client = client_builder.proxy(socks5proxy).build()?;
214    } else {
215        client = client_builder.build()?;
216    }
217
218    let api_url = format!(
219        "{}/realms/shasta/protocol/openid-connect/token",
220        keycloak_base_url
221    );
222
223    log::debug!("Request to fetch authentication token: {}", api_url);
224
225    Ok(client
226        .post(api_url)
227        .form(&params)
228        .send()
229        .await?
230        .error_for_status()?
231        .json::<Value>()
232        .await?["access_token"]
233        .as_str()
234        .unwrap()
235        .to_string())
236}