use std::{ ffi::{CStr, CString, OsStr}, fmt, fs::File, io::{self, Cursor, Read as _}, os::unix::ffi::OsStrExt as _, }; use curl::easy::{Easy, List}; use serde::Deserialize; use crate::{ ext::{CStrExt as _, StrExt}, keycloak::api::TokenResponse, libc as mylibc, }; pub mod api; #[allow(clippy::unsafe_derive_deserialize)] // seriously? -_- #[derive(Debug, Deserialize)] pub struct KeycloakAPI { base_url: String, default_realm: String, client_id: String, client_secret: String, unix_group: CString, admin_username: String, admin_password: String, } #[derive(Debug, thiserror::Error)] pub enum LoadError { #[error("opening configuration: {0}")] OpenConfig(#[from] io::Error), #[error("parsing configuration: {0}")] ParseConfig(#[from] serde_json::Error), } #[derive(Debug, thiserror::Error)] pub enum GetCredentialsError { #[error("on request: {0}")] Fetch(#[from] curl::Error), #[error("unexpected API response: {0}")] InvalidAPIResponse(#[from] serde_json::Error), #[error("expected 1 result, found none or more")] NonUnitResults, #[error("server error response")] ErrorResponse, } #[derive(Debug, thiserror::Error)] pub enum SetCredentialsError { #[error("on request: {0}")] Fetch(#[from] curl::Error), #[error("serializing request body")] SerializeError(#[from] serde_json::Error), #[error("server error response")] ErrorResponse, } #[derive(Debug, thiserror::Error)] pub enum LoginError { #[error("on POST request: {0}")] Fetch(#[from] curl::Error), #[error("unexpected API response: {0}")] InvalidAPIResponse(#[from] serde_json::Error), #[error("from the API: {0}")] InvalidLogin(#[from] api::AuthError), } #[derive(Clone)] pub enum Method<'a> { GET, POST(Option<&'a [u8]>), PUT(Option>), } impl fmt::Debug for Method<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::GET => write!(f, "GET"), Self::POST(None) => write!(f, "POST"), Self::POST(Some(_)) => write!(f, "POST"), Self::PUT(None) => write!(f, "PUT"), Self::PUT(Some(_)) => write!(f, "PUT"), } } } #[derive(Debug, Clone, Copy)] pub struct CurlStatus(u32); impl CurlStatus { const fn is_ok(self) -> bool { matches!(self.0, 200..300) } } fn curl_request( method: Method, uri: &str, headers: &[&str], debug: bool, ) -> Result<(CurlStatus, Vec), curl::Error> { if debug { eprintln!("{method:?} {uri} -H \"{headers:?}\""); } let mut easy = Easy::new(); let mut curl_headers = List::new(); for header in headers { curl_headers.append(header)?; } easy.http_headers(curl_headers)?; easy.url(uri)?; match method { Method::GET => { easy.get(true)?; } Method::POST(body) => { easy.post(true)?; if let Some(body) = body { easy.post_fields_copy(body)?; } } Method::PUT(body) => { easy.put(true)?; if let Some(body) = body { easy.upload(true)?; easy.in_filesize(body.len() as u64)?; let mut cur = Cursor::new(body); easy.read_function(move |buf| Ok(cur.read(buf).unwrap_or(0)))?; } } } let mut response_data = Vec::new(); { let mut transfer = easy.transfer(); transfer.write_function(|data| { response_data.extend_from_slice(data); Ok(data.len()) })?; transfer.perform()?; } Ok((CurlStatus(easy.response_code()?), response_data)) } impl KeycloakAPI { pub fn load_from_configuration(cfg: &OsStr) -> Result { let f = File::open(cfg)?; Ok(serde_json::from_reader(f)?) } pub fn should_skip(&self, username: &OsStr) -> Result { mylibc::getgrnam_use(&self.unix_group, |group| { let mut members_ptr = group.gr_mem; // SAFETY: The array pointer CANNOT be null, even if incremented. while let member_cstr_ptr = unsafe { *members_ptr } && !member_cstr_ptr.is_null() { // SAFETY: its guaranteed to be a CStr and its non-null let member_osstr = unsafe { CStr::from_ptr(member_cstr_ptr) }.to_os_str(); if member_osstr == username { return false; } // SAFETY: if current pointer wasn't NULL, we can increase to the next one members_ptr = unsafe { members_ptr.add(1) }; } true }) } pub fn get_admin_token(&self, debug: bool) -> Result { self.get_admincli_oidc_token( self.admin_username.as_os_str(), self.admin_password.as_os_str(), debug, ) } pub fn get_admincli_oidc_token( &self, admin_username: &OsStr, admin_password: &OsStr, debug: bool, ) -> Result { let body = format!( "grant_type=password&client_id=admin-cli&username={USERNAME}&password={PASSWORD}", USERNAME = urlencoding::encode_binary(admin_username.as_bytes()), PASSWORD = urlencoding::encode_binary(admin_password.as_bytes()), ); self.get_token_from_body(&body, debug) } pub fn get_oidc_token( &self, username: &OsStr, password: &OsStr, debug: bool, ) -> Result { let body = format!( "grant_type=password&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&username={USERNAME}&password={PASSWORD}", CLIENT_ID = urlencoding::encode(&self.client_id), CLIENT_SECRET = urlencoding::encode(&self.client_secret), USERNAME = urlencoding::encode_binary(username.as_bytes()), PASSWORD = urlencoding::encode_binary(password.as_bytes()), ); self.get_token_from_body(&body, debug) } pub fn get_token_from_body( &self, body: &str, debug: bool, ) -> Result { let (status_code, body) = curl_request( Method::POST(Some(body.as_bytes())), &format!( "{}/realms/{}/protocol/openid-connect/token", self.base_url, self.default_realm, ), &[ "Content-Type: application/x-www-form-urlencoded", "Accept: application/json", ], debug, )?; if status_code.is_ok() { Ok(serde_json::from_slice(&body)?) } else { Err(LoginError::InvalidLogin(serde_json::from_slice(&body)?)) } } pub fn get_user_credentials( &self, username: &str, admin_token: &str, debug: bool, ) -> Result<(String, Vec), GetCredentialsError> { let (status_code, body) = curl_request( Method::GET, &format!( "{}/admin/realms/{}/users?username={username}", self.base_url, self.default_realm ), &[&format!("Authorization: Bearer {admin_token}")], debug, )?; if !status_code.is_ok() { if debug { eprintln!("got error: {}", String::from_utf8_lossy(&body)); } return Err(GetCredentialsError::ErrorResponse); } let mut found_users: Vec = serde_json::from_slice(&body)?; if debug { eprintln!("matched users = {found_users:#?}"); } let [first_user] = &mut found_users[..] else { return Err(GetCredentialsError::NonUnitResults); }; let (status_code, body) = curl_request( Method::GET, &format!( "{}/admin/realms/{}/users/{}/credentials", self.base_url, self.default_realm, first_user.id ), &[ &format!("Authorization: Bearer {admin_token}"), "Accept: application/json", ], debug, )?; if !status_code.is_ok() { if debug { eprintln!("got error: {}", String::from_utf8_lossy(&body)); } return Err(GetCredentialsError::ErrorResponse); } Ok(( std::mem::take(&mut first_user.id), serde_json::from_slice(&body)?, )) } pub fn set_user_credentials( &self, user_id: &str, new_password: &str, admin_token: &str, debug: bool, ) -> Result<(), SetCredentialsError> { let request_body = serde_json::to_vec(&api::ResetPasswordRequest { temporary: false, type_: "password", value: new_password, }) .map_err(SetCredentialsError::SerializeError)?; let (status_code, body) = curl_request( Method::PUT(Some(request_body.into_boxed_slice())), &format!( // "http://127.0.0.1:3000/admin/realms/master/users/79149d47-17e6-42be-9fd8-47b04127f162/reset-password", "{}/admin/realms/{}/users/{user_id}/reset-password", self.base_url, self.default_realm ), &[ &format!("Authorization: Bearer {admin_token}"), "Content-Type: application/json", ], debug, )?; if !status_code.is_ok() { if debug { eprintln!("got error: {}", String::from_utf8_lossy(&body)); } return Err(SetCredentialsError::ErrorResponse); } Ok(()) } }