From 6bb37e7e65969a60873e763e47756f55a6f094ac Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Tue, 11 May 2021 12:40:19 +0200 Subject: [PATCH] [NSS] Rewrite user loading to use jq instead of custom mapping config --- Cargo.toml | 1 + README.md | 11 ++++ etc/nss_pam_oidc.example.toml | 22 ++++--- src/nss.rs | 107 +++++++--------------------------- src/oauth.rs | 62 +++++++++++--------- 5 files changed, 79 insertions(+), 124 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cc64117..d651218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ log = "^0.4.11" syslog = "^5.0.0" xdg = "^2.2.0" serde_json = "^1.0.64" +jq-rs = "^0.4.1" [profile.release] opt-level = 'z' diff --git a/README.md b/README.md index 8755bc8..fe9dcc5 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,17 @@ from the API up to date. It handles the following data: * User access tokens (using corresponding refresh tokens, if available) * NSS data +## Installation + +### Building from source + +To build from source, development headers for `libjq` and `libonig` are +required. On Debian, install them with: + +```shell +sudo apt install libjq-dev libonig-dev +``` + ## Credits Special thanks to mirabilos in his position as Senior Unix System Development diff --git a/etc/nss_pam_oidc.example.toml b/etc/nss_pam_oidc.example.toml index f965805..ac4e203 100644 --- a/etc/nss_pam_oidc.example.toml +++ b/etc/nss_pam_oidc.example.toml @@ -9,13 +9,17 @@ client_secret = "" [nss] client_id = "z8Oz0tG56QRo9QEPUZTs5Eda410FMiJtYxlInxKE" client_secret = "" -passwd_url = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/" -[nss.maps.passwd] -name = { type = "rename", value = "username" } -passwd = { type = "static", value = "x" } -# uid left unchanged -gid = { type = "rename", value = "primary_gid" } -gecos = { type = "static", value = "Foo" } -dir = { type = "rename", value = "home_directory" } -shell = { type = "rename", value = "login_shell" } +urls.passwd = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/" + +maps.passwd = """ + { + name: .username, + passwd: "x", + uid: .uid, + gid: .primary_gid, + gecos: "Foo", + dir: .home_directory, + shell: .login_shell + } +""" diff --git a/src/nss.rs b/src/nss.rs index e0d5aca..66ab75f 100644 --- a/src/nss.rs +++ b/src/nss.rs @@ -23,11 +23,8 @@ 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 crate::oauth::get_data_jq; +use serde::{Serialize, Deserialize}; use libc::{getpwuid, geteuid}; use std::ffi::CStr; @@ -35,6 +32,19 @@ use std::ffi::CStr; use libnss::interop::Response; use libnss::passwd::{PasswdHooks, Passwd}; +#[derive(Serialize, Deserialize)] +#[serde(remote = "Passwd")] +struct PasswdDef { + name: String, + passwd: String, + uid: libc::uid_t, + gid: libc::gid_t, + gecos: String, + dir: String, + shell: String +} +#[derive(Deserialize)] struct PasswdHelper(#[serde(with = "PasswdDef")] Passwd); + fn nss_hook_prepare() -> Config { let conf = get_config(None); @@ -57,75 +67,6 @@ fn get_current_user() -> String { 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 { @@ -143,20 +84,14 @@ impl PasswdHooks for OidcPasswd { } }; - let mut data: Vec<HashMap<String, Value>> = match get_data(&conf, "nss", "passwd", token, "") { + let data: Vec<PasswdHelper> = match get_data_jq(&conf, "nss", "passwd", token, true) { Ok(d) => d, - Err(_) => return Response::Unavail + Err(_) => { + error!("Could not load JSON data for passwd"); + 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) + Response::Success(data.into_iter().map(|p| p.0).collect()) } fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> { diff --git a/src/oauth.rs b/src/oauth.rs index 71e8c04..385d41f 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -37,29 +37,32 @@ use oauth2::basic::{ }; use oauth2::reqwest::http_client; +use std::error; + use serde::Deserialize; use reqwest; -fn full_key(prefix: &str, key: &str) -> String { - let parts = vec![prefix.to_string(), key.to_string()]; - let full_key = parts.join("."); - return full_key; +use serde_json; +use jq_rs; + +fn full_key(parts: Vec<&str>) -> String { + parts.join(".") } fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<BasicClient, E> { - let client_id = ClientId::new(get_or_error(&conf, &full_key(prefix, "client_id"), error_value)?); - let client_secret = match get_optional(&conf, &full_key(prefix, "client_secret")) { + let client_id = ClientId::new(get_or_error(&conf, &full_key(vec![prefix, "client_id"]), error_value)?); + let client_secret = match get_optional(&conf, &full_key(vec![prefix, "client_secret"])) { Some(v) => Some(ClientSecret::new(v)), None => None, }; - let auth_url = match AuthUrl::new(get_or_error(&conf, &full_key(prefix, "auth_url"), error_value)?) { + let auth_url = match AuthUrl::new(get_or_error(&conf, &full_key(vec![prefix, "auth_url"]), error_value)?) { Ok(u) => u, _ => { error!("Could not parse authorization URL"); return Err(error_value); }, }; - let token_url = match get_optional(&conf, &full_key(prefix, "token_url")) { + let token_url = match get_optional(&conf, &full_key(vec![prefix, "token_url"])) { Some(v) => match TokenUrl::new(v) { Ok(u) => Some(u), Err(_) => { @@ -75,7 +78,7 @@ fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<Bas } pub fn get_access_token_client<E: Copy>(conf: Config, prefix: &str, error_value: E, unauth_value: E) -> Result<BasicTokenResponse, E> { - let scopes: Vec<String> = match get_optional(&conf, &full_key(prefix, "scopes")) { + let scopes: Vec<String> = match get_optional(&conf, &full_key(vec![prefix, "scopes"])) { Some(v) => v, None => vec![] }; @@ -103,7 +106,7 @@ pub fn get_access_token_client<E: Copy>(conf: Config, prefix: &str, error_value: } pub fn get_access_token_password<E: Copy>(conf: Config, prefix: &str, username: String, password: String, error_value: E, unauth_value: E) -> Result<BasicTokenResponse, E> { - let scopes: Vec<String> = match get_optional(&conf, &full_key(prefix, "scopes")) { + let scopes: Vec<String> = match get_optional(&conf, &full_key(vec![prefix, "scopes"])) { Some(v) => v, None => vec![] }; @@ -133,31 +136,32 @@ pub fn get_access_token_password<E: Copy>(conf: Config, prefix: &str, username: } } -pub fn get_data<T: for<'de> Deserialize<'de>, E: Copy>(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse, error_value: E) -> Result<T, E> { +fn get_data(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse) -> Result<String, Box<dyn error::Error>> { let access_token = token.access_token().secret(); - let endpoint_url: String = get_or_error(&conf, &full_key(prefix, &(endpoint.to_string() + "_url")), error_value)?; + let endpoint_url: String = get_or_error(&conf, &full_key(vec![prefix, "urls", endpoint]), "")?; - info!("Loading JSON data from {}", endpoint_url); + debug!("Loading text data from {}", endpoint_url); let client = reqwest::blocking::Client::new(); - let res = match client + Ok(client .get(&endpoint_url) .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() { - Ok(r) => r, - Err(e) => { - error!("Could not complete HTTP request: {}", e); - return Err(error_value); - } - }; + .send()? + .text()?) +} - let data = match res.json() { - Ok(d) => d, - Err(e) => { - error!("Could not parse JSON response: {}", e); - return Err(error_value); - } +pub fn get_data_jq<T: for<'de> Deserialize<'de>>(conf: &Config, prefix: &str, endpoint: &str, token: &BasicTokenResponse, multi: bool) -> Result<T, Box<dyn error::Error>> { + let res: Option<String> = get_optional(&conf, &full_key(vec![prefix, "maps", endpoint])); + let jq_code = match res { + Some(s) => match multi { + true => "map(".to_string() + &s + ")", + false => s + }, + None => ".".to_string() }; + let mut jq_prog = jq_rs::compile(&jq_code)?; + + let data_raw = get_data(&conf, prefix, endpoint, token)?; + let data_trans = jq_prog.run(&data_raw)?; - debug!("Successfully loaded JSON data from {}", endpoint_url); - return Ok(data); + Ok(serde_json::from_str(&data_trans)?) } -- GitLab