Skip to content
Snippets Groups Projects
Verified Commit 6bb37e7e authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

[NSS] Rewrite user loading to use jq instead of custom mapping config

parent ec186a4f
No related branches found
No related tags found
No related merge requests found
...@@ -26,6 +26,7 @@ log = "^0.4.11" ...@@ -26,6 +26,7 @@ log = "^0.4.11"
syslog = "^5.0.0" syslog = "^5.0.0"
xdg = "^2.2.0" xdg = "^2.2.0"
serde_json = "^1.0.64" serde_json = "^1.0.64"
jq-rs = "^0.4.1"
[profile.release] [profile.release]
opt-level = 'z' opt-level = 'z'
......
...@@ -108,6 +108,17 @@ from the API up to date. It handles the following data: ...@@ -108,6 +108,17 @@ from the API up to date. It handles the following data:
* User access tokens (using corresponding refresh tokens, if available) * User access tokens (using corresponding refresh tokens, if available)
* NSS data * 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 ## Credits
Special thanks to mirabilos in his position as Senior Unix System Development Special thanks to mirabilos in his position as Senior Unix System Development
......
...@@ -9,13 +9,17 @@ client_secret = "" ...@@ -9,13 +9,17 @@ client_secret = ""
[nss] [nss]
client_id = "z8Oz0tG56QRo9QEPUZTs5Eda410FMiJtYxlInxKE" client_id = "z8Oz0tG56QRo9QEPUZTs5Eda410FMiJtYxlInxKE"
client_secret = "" client_secret = ""
passwd_url = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/"
[nss.maps.passwd] urls.passwd = "https://ticdesk-dev.teckids.org/app/nis/api/passwd/"
name = { type = "rename", value = "username" }
passwd = { type = "static", value = "x" } maps.passwd = """
# uid left unchanged {
gid = { type = "rename", value = "primary_gid" } name: .username,
gecos = { type = "static", value = "Foo" } passwd: "x",
dir = { type = "rename", value = "home_directory" } uid: .uid,
shell = { type = "rename", value = "login_shell" } gid: .primary_gid,
gecos: "Foo",
dir: .home_directory,
shell: .login_shell
}
"""
...@@ -23,11 +23,8 @@ use crate::cache::get_cache; ...@@ -23,11 +23,8 @@ use crate::cache::get_cache;
use crate::logging::setup_log; use crate::logging::setup_log;
use crate::oauth::get_data; use crate::oauth::get_data_jq;
use std::collections::HashMap; use serde::{Serialize, Deserialize};
use serde_json::value::Value;
use std::fmt;
use std::convert::TryInto;
use libc::{getpwuid, geteuid}; use libc::{getpwuid, geteuid};
use std::ffi::CStr; use std::ffi::CStr;
...@@ -35,6 +32,19 @@ use std::ffi::CStr; ...@@ -35,6 +32,19 @@ use std::ffi::CStr;
use libnss::interop::Response; use libnss::interop::Response;
use libnss::passwd::{PasswdHooks, Passwd}; 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 { fn nss_hook_prepare() -> Config {
let conf = get_config(None); let conf = get_config(None);
...@@ -57,75 +67,6 @@ fn get_current_user() -> String { ...@@ -57,75 +67,6 @@ fn get_current_user() -> String {
euser.to_str().ok().unwrap().to_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; struct OidcPasswd;
impl PasswdHooks for OidcPasswd { impl PasswdHooks for OidcPasswd {
...@@ -143,20 +84,14 @@ 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, Ok(d) => d,
Err(_) => return Response::Unavail Err(_) => {
error!("Could not load JSON data for passwd");
return Response::Unavail;
}
}; };
Response::Success(data.into_iter().map(|p| p.0).collect())
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> { fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> {
......
...@@ -37,29 +37,32 @@ use oauth2::basic::{ ...@@ -37,29 +37,32 @@ use oauth2::basic::{
}; };
use oauth2::reqwest::http_client; use oauth2::reqwest::http_client;
use std::error;
use serde::Deserialize; use serde::Deserialize;
use reqwest; use reqwest;
fn full_key(prefix: &str, key: &str) -> String { use serde_json;
let parts = vec![prefix.to_string(), key.to_string()]; use jq_rs;
let full_key = parts.join(".");
return full_key; fn full_key(parts: Vec<&str>) -> String {
parts.join(".")
} }
fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<BasicClient, E> { 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_id = ClientId::new(get_or_error(&conf, &full_key(vec![prefix, "client_id"]), error_value)?);
let client_secret = match get_optional(&conf, &full_key(prefix, "client_secret")) { let client_secret = match get_optional(&conf, &full_key(vec![prefix, "client_secret"])) {
Some(v) => Some(ClientSecret::new(v)), Some(v) => Some(ClientSecret::new(v)),
None => None, 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, Ok(u) => u,
_ => { _ => {
error!("Could not parse authorization URL"); error!("Could not parse authorization URL");
return Err(error_value); 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) { Some(v) => match TokenUrl::new(v) {
Ok(u) => Some(u), Ok(u) => Some(u),
Err(_) => { Err(_) => {
...@@ -75,7 +78,7 @@ fn get_client<E: Copy>(conf: Config, prefix: &str, error_value: E) -> Result<Bas ...@@ -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> { 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, Some(v) => v,
None => vec![] None => vec![]
}; };
...@@ -103,7 +106,7 @@ pub fn get_access_token_client<E: Copy>(conf: Config, prefix: &str, error_value: ...@@ -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> { 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, Some(v) => v,
None => vec![] None => vec![]
}; };
...@@ -133,31 +136,32 @@ pub fn get_access_token_password<E: Copy>(conf: Config, prefix: &str, username: ...@@ -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 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 client = reqwest::blocking::Client::new();
let res = match client Ok(client
.get(&endpoint_url) .get(&endpoint_url)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)) .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token))
.send() { .send()?
Ok(r) => r, .text()?)
Err(e) => { }
error!("Could not complete HTTP request: {}", e);
return Err(error_value);
}
};
let data = match res.json() { 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>> {
Ok(d) => d, let res: Option<String> = get_optional(&conf, &full_key(vec![prefix, "maps", endpoint]));
Err(e) => { let jq_code = match res {
error!("Could not parse JSON response: {}", e); Some(s) => match multi {
return Err(error_value); 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); Ok(serde_json::from_str(&data_trans)?)
return Ok(data);
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment