sakcl 0.1.0

OpenSSH AuthorizedKeysCmd provider utilizing LDAP
/*
 * Copyright 2018 Doug Goldstein <cardoe@cardoe.com>
 *
 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
 * http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
 * <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
 * option. This file may not be copied, modified, or distributed
 * except according to those terms.
 */

extern crate ldap3;
#[cfg(target_env = "musl")]
extern crate openssl_probe;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate structopt;
extern crate toml;

use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;

use ldap3::{LdapConn, Scope, SearchEntry};
use structopt::StructOpt;

macro_rules! texterr {
    ($($tt:tt)*) => {
        Err(From::from(format!($($tt)*)))
    }
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
enum LdapScope {
    Base,
    One,
    Subtree,
}

#[derive(Debug, Deserialize, PartialEq)]
struct Config {
    uri: String,
    base: String,
    binddn: Option<String>,
    bindpw: Option<String>,
    scope: LdapScope,
    filter: String,
    attr: String,
}

#[derive(StructOpt, Debug)]
#[structopt()]
struct Opt {
    /// configuration file
    #[structopt(
        short = "c", long = "config", parse(from_os_str), default_value = "/etc/sakcl.conf"
    )]
    config: PathBuf,

    /// username whose SSH public key will be fetched
    #[structopt(name = "USERNAME")]
    uid: String,
}

fn main() {
    let opt = Opt::from_args();

    if let Err(e) = run(opt) {
        eprintln!("{}", e);
        ::std::process::exit(1);
    }
}

#[cfg(target_env = "musl")]
fn openssl_env_init() {
    openssl_probe::init_ssl_cert_env_vars();
}

#[cfg(not(target_env = "musl"))]
fn openssl_env_init() {}

fn run(opt: Opt) -> Result<(), Box<Error>> {
    // read the config
    let cfg: Config = {
        let mut file = File::open(&opt.config)
            .map_err(|e| format!("Unable to open config at {}: {}", opt.config.display(), e))?;
        // check that our permissions are 0400
        // with the hope that it will make users
        // not expose a bind password
        let md = file.metadata()
            .map_err(|e| format!("Unable to read permissions on config file: {}", e))?;
        let mode = md.mode();
        if (mode & 0o377) > 0 {
            return texterr!(
                "insecure permissions on config file. Got {:o} and expected {:o}",
                mode,
                (0o777400 & mode)
            );
        }

        let mut s = String::new();
        file.read_to_string(&mut s)?;
        toml::from_str(&s).map_err(|e| format!("invalid config: {}", e))
    }?;

    // initialize OpenSSL if we need to
    openssl_env_init();

    // blocking connection to the LDAP server
    let ldap = LdapConn::new(&cfg.uri)?;

    // if a binddn was specified then perform a simple bind
    if let Some(binddn) = cfg.binddn {
        ldap.simple_bind(&binddn, &cfg.bindpw.unwrap_or_else(|| "".to_string()))?
            .success()?;
    }

    // compute the scope
    let scope = match cfg.scope {
        LdapScope::Base => Scope::Base,
        LdapScope::One => Scope::OneLevel,
        LdapScope::Subtree => Scope::Subtree,
    };

    // search for the SSH key that the user has
    let (rs, _res) = ldap.search(
        &cfg.base,
        scope,
        &cfg.filter.replace("*", &opt.uid),
        vec![&cfg.attr],
    )?
        .success()?;

    for entry in rs {
        let entry = SearchEntry::construct(entry);
        if let Some(keys) = entry.attrs.get(&cfg.attr) {
            for key in keys {
                println!("{}", key);
            }
        }
    }

    Ok(())
}