Skip to content
Snippets Groups Projects
nss.rs 6.94 KiB
Newer Older
/* 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,
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(),
            });
        }

    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(),
            });
        }

    }
}

libnss_passwd_hooks!(oidc, OidcPasswd);