From 44bde3bb76a6dbfea5b15bd067db94d2539e7bf3 Mon Sep 17 00:00:00 2001
From: Dominik George <dominik.george@teckids.org>
Date: Thu, 13 May 2021 18:44:30 +0200
Subject: [PATCH] [NSS] Implement reverse-mapping of getent search fields

Closes #7
---
 etc/nss_pam_oidc.example.toml |  3 +++
 src/nss.rs                    |  2 +-
 src/oauth.rs                  | 46 +++++++++++++++++++++++++++++------
 3 files changed, 43 insertions(+), 8 deletions(-)

diff --git a/etc/nss_pam_oidc.example.toml b/etc/nss_pam_oidc.example.toml
index a1d6291..2b37ec5 100644
--- a/etc/nss_pam_oidc.example.toml
+++ b/etc/nss_pam_oidc.example.toml
@@ -32,3 +32,6 @@ maps.passwd = """
         shell: .login_shell
     }
 """
+
+# Reverse mapping to make sure uid lookups on entries mapped above still work
+maps.rev.passwd.by_uid = ". - 10000"
diff --git a/src/nss.rs b/src/nss.rs
index b7bbad8..dca4b9f 100644
--- a/src/nss.rs
+++ b/src/nss.rs
@@ -119,7 +119,7 @@ impl PasswdHooks for OidcPasswd {
             }
         };
 
-        let data: Passwd = match get_data_jq(&conf, "nss", "passwd.by_uid", uid.to_string(), &token, false).map(|PasswdHelper(p)| p) {
+        let data: Passwd = match get_data_jq(&conf, "nss", "passwd.by_uid", uid, &token, false).map(|PasswdHelper(p)| p) {
             Ok(d) => d,
             Err(e) => {
                 error!("Could not load JSON data for passwd: {}", e);
diff --git a/src/oauth.rs b/src/oauth.rs
index 2e0a500..72f3f62 100644
--- a/src/oauth.rs
+++ b/src/oauth.rs
@@ -39,7 +39,7 @@ use oauth2::reqwest::http_client;
 
 use std::error;
 
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 use reqwest;
 
 use serde_json;
@@ -151,15 +151,23 @@ fn get_data(conf: &Config, prefix: &str, endpoint: &str, param: String, token: &
         .text()?)
 }
 
-fn get_jq_prog(conf: &Config, prefix: &str, endpoint: &str) -> Option<String> {
-    match get_optional(&conf, &full_key(vec![prefix, "maps", endpoint])) {
+fn get_jq_prog(conf: &Config, prefix: &str, endpoint: &str, rev: bool) -> Option<String> {
+    let prog_key = match rev {
+        false => full_key(vec![prefix, "maps", endpoint]),
+        true => full_key(vec![prefix, "maps.rev", endpoint]),
+    };
+    match get_optional(&conf, &prog_key) {
         Some(v) => Some (v),
         None => {
+            if rev {
+                // Do not fallback to more generic program for reverse mapping
+                return None;
+            }
             // Try falling back to more generic program
             match endpoint.find('.') {
                 Some(i) => {
                     debug!("JQ mapping program for {} not found; trying more generic definition", endpoint);
-                    get_jq_prog(conf, prefix, &endpoint[..i])
+                    get_jq_prog(conf, prefix, &endpoint[..i], rev)
                 },
                 None => None
             }
@@ -167,11 +175,11 @@ fn get_jq_prog(conf: &Config, prefix: &str, endpoint: &str) -> Option<String> {
     }
 }
 
-pub fn get_data_jq<T: for<'de> Deserialize<'de>>(conf: &Config, prefix: &str, endpoint: &str, param: String, token: &BasicTokenResponse, multi: bool) -> Result<T, Box<dyn error::Error>> {
-    let res: Option<String> = get_jq_prog(&conf, prefix, endpoint);
+pub fn get_data_jq<T: for<'de> Deserialize<'de>, V: Serialize>(conf: &Config, prefix: &str, endpoint: &str, param: V, token: &BasicTokenResponse, multi: bool) -> Result<T, Box<dyn error::Error>> {
+    let res: Option<String> = get_jq_prog(&conf, prefix, endpoint, false);
     let jq_code = match res {
         Some(s) => {
-            debug!("Found jq mapping program for endpoint {}", endpoint);
+            debug!("Found jq mapping program for endpoint {}: {}", endpoint, s);
             match multi {
                 true => "map(".to_string() + &s + ")",
                 false => s
@@ -183,6 +191,30 @@ pub fn get_data_jq<T: for<'de> Deserialize<'de>>(conf: &Config, prefix: &str, en
         }
     };
     let mut jq_prog = jq_rs::compile(&jq_code)?;
+    let res: Option<String> = get_jq_prog(&conf, prefix, endpoint, true);
+    let jq_code = match res {
+        Some(s) => {
+            debug!("Found jq reverse mapping program for endpoint {}: {}", endpoint, s);
+            s
+        },
+        None => {
+            debug!("No jq reverse mapping program for endpoint {}; using default (no-op)", endpoint);
+            ".".to_string()
+        }
+    };
+    let mut jq_prog_rev = jq_rs::compile(&jq_code)?;
+
+    // Convert and transform the passed param using the reverse JQ mapping program
+    //  1. Serialize into JSON value (atomic) to be bale to pass into jq
+    let param_serialized = serde_json::to_string(&param)?;
+    //  2. Transform using the JQ program loaded above
+    let param_trans = jq_prog_rev.run(&param_serialized)?.trim().to_string();
+    //  3. Deserialize into serde_json value so we get numbers as numbers, strings properly unquoted
+    let param_deserialized: serde_json::Value = serde_json::from_str(&param_trans)?;
+    let param = match param_deserialized {
+        serde_json::Value::String(v) => v,   // We want strings verbatim without JSON quoting
+        _ => param_deserialized.to_string()  // We want numbers converted to string
+    };
 
     let data_raw = get_data(&conf, prefix, endpoint, param, token)?;
     let data_trans = jq_prog.run(&data_raw)?;
-- 
GitLab