/* Copyright 2021 Dominik George <dominik.george@teckids.org> * * 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::config::{ get_config, get_optional, get_or_error }; use config::Config; use crate::cache::get_cache; use crate::logging::setup_log; use crate::oauth::get_data; use std::collections::HashMap; use serde_json::value::Value; use std::fmt; use std::convert::TryInto; use libc::{getpwuid, geteuid}; use std::ffi::CStr; use libnss::interop::Response; use libnss::passwd::{PasswdHooks, Passwd}; fn nss_hook_prepare() -> Config { let conf = get_config(None); let mut log_level = log::LevelFilter::Error; if get_optional(&conf, "nss.debug").unwrap_or_default() { log_level = log::LevelFilter::Debug; } setup_log(log_level); return conf; } fn get_current_user() -> String { let euid; let euser; unsafe { euid = geteuid(); euser = CStr::from_ptr((*getpwuid(euid)).pw_name); }; euser.to_str().ok().unwrap().to_string() } // FIXME Provide more specific error types and messages #[derive(Debug, Clone)] struct TransformMappingError { msg: String, field: String } impl fmt::Display for TransformMappingError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid mapping configuration: field={} - {}", self.field, self.msg) } } fn transform_ent(conf: &Config, row: &mut HashMap<String, Value>, map_name: &str) -> Result<(), TransformMappingError> { let mapping: HashMap<String, HashMap<String, Value>> = get_optional(&conf, &("nss.maps.".to_string() + map_name)).unwrap_or_default(); for (field, rule) in &mapping { let type_: String = rule.get("type") .ok_or(TransformMappingError { field: field.to_string(), msg: "No type".to_string() })? .as_str().unwrap().to_string(); let value: &Value = rule.get("value") .ok_or(TransformMappingError { field: field.to_string(), msg: "No value".to_string() })?; if type_ == "static" { row.insert(field.to_string(), value.clone()); } else if type_ == "rename" { let old_value: Value = row.remove(&value.as_str().unwrap().to_string()) .ok_or(TransformMappingError { field: field.to_string(), msg: "No value to rename".to_string() })?; row.insert(field.to_string(), old_value); } else { return Err(TransformMappingError { field: field.to_string(), msg: (&("Unknown type ".to_string() + &type_)).to_string() }); }; } Ok(()) } fn map_to_passwd(conf: &Config, row: &mut HashMap<String, Value>) -> Result<Passwd, TransformMappingError> { transform_ent(&conf, row, "passwd")?; Ok(Passwd { name: row.get("name") .ok_or(TransformMappingError { field: "name".to_string(), msg: "No value in JSON data".to_string() })? .as_str().unwrap().to_string(), passwd: row.get("passwd") .ok_or(TransformMappingError { field: "passwd".to_string(), msg: "No value in JSON data".to_string() })? .as_str().unwrap().to_string(), uid: row.get("uid") .ok_or(TransformMappingError { field: "uid".to_string(), msg: "No value in JSON data".to_string() })? .as_u64() .ok_or(TransformMappingError { field: "uid".to_string(), msg: "Invalid integer".to_string() })? .try_into() .or(Err(TransformMappingError { field: "uid".to_string(), msg: "Overflow converting to u32".to_string() }))?, gid: row.get("gid") .ok_or(TransformMappingError { field: "gid".to_string(), msg: "No value in JSON data".to_string() })? .as_u64() .ok_or(TransformMappingError { field: "gid".to_string(), msg: "Invalid integer".to_string() })? .try_into() .or(Err(TransformMappingError { field: "gid".to_string(), msg: "Overflow converting to u32".to_string() }))?, gecos: row.get("gecos") .ok_or(TransformMappingError { field: "gecos".to_string(), msg: "No value in JSON data".to_string() })? .as_str().unwrap().to_string(), dir: row.get("dir") .ok_or(TransformMappingError { field: "dir".to_string(), msg: "No value in JSON data".to_string() })? .as_str().unwrap().to_string(), shell: row.get("shell") .ok_or(TransformMappingError { field: "shell".to_string(), msg: "No value in JSON data".to_string() })? .as_str().unwrap().to_string(), }) } struct OidcPasswd; impl PasswdHooks for OidcPasswd { fn get_all_entries() -> Response<Vec<Passwd>> { let conf = nss_hook_prepare(); let mut cache = get_cache(); let user = get_current_user(); let token = match cache.load_user_token(&user) { Some(t) => t, None => { // FIXME Implement fallback to system token error!("Could not find a user token for {} to request NSS data", user); return Response::Unavail; } }; let mut data: Vec<HashMap<String, Value>> = match get_data(&conf, "nss", "passwd", token, "") { Ok(d) => d, Err(_) => return Response::Unavail }; let mut passwd_vec: Vec<Passwd> = Vec::new(); for row in &mut data { match map_to_passwd(&conf, row) { Ok(p) => passwd_vec.push(p), Err(e) => error!("Error converting JSON to passwd entry: {}", e) }; } Response::Success(passwd_vec) } fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> { if uid == 1005 { return Response::Success(Passwd { name: "test".to_string(), passwd: "x".to_string(), uid: 1005, gid: 1005, gecos: "Test Account".to_string(), dir: "/home/test".to_string(), shell: "/bin/bash".to_string(), }); } Response::NotFound } fn get_entry_by_name(name: String) -> Response<Passwd> { if name == "test" { return Response::Success(Passwd { name: "test".to_string(), passwd: "x".to_string(), uid: 1005, gid: 1005, gecos: "Test Account".to_string(), dir: "/home/test".to_string(), shell: "/bin/bash".to_string(), }); } Response::NotFound } } libnss_passwd_hooks!(oidc, OidcPasswd);