password-as-service 0.1.4

a very poorly considered http/rest service
Documentation
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
extern crate lib;

use lib::{
    api,
    model::{Group, GroupQueryBuilder, User, UserQueryBuilder},
    FileResource,
};
use query::MemberNames;
use rocket::State;
use rocket_contrib::json::Json;
use std::{fs::File, io::Write};

const DEFAULT_USERS_FILE: &str = "/etc/passwd";
const DEFAULT_GROUPS_FILE: &str = "/etc/group";

fn main() {
    let mut args = std::env::args().skip(1);
    let (users, groups): (FileResource<User>, FileResource<Group>) = match (args.next(), args.next()) {
        _ if cfg!(debug_assertions) => _debug_files(),
        (Some(users), Some(groups)) => (
            FileResource::new(&groups).unwrap_or_else(|_| panic!("could not load groups from {}", &groups)),
            FileResource::new(&users).unwrap_or_else(|_| panic!("could not load users from {}", &users)),
        ),
        (None, None) => (
            FileResource::new(DEFAULT_USERS_FILE)
                .unwrap_or_else(|_| panic!("could not load users from {}", &DEFAULT_USERS_FILE)),
            FileResource::new(DEFAULT_GROUPS_FILE)
                .unwrap_or_else(|_| panic!("could not load groups from {}", &DEFAULT_GROUPS_FILE)),
        ),
        _ => {
            eprintln!("must specify paths to both groups and user files, or none");
            std::process::exit(1);
        },
    };

    _main(groups, users)
}

fn _debug_files() -> (FileResource<User>, FileResource<Group>) {
    eprintln!("creating debug files");
    let dir = tempfile::tempdir().unwrap();
    let (mut users, mut groups) = (dir.as_ref().to_owned(), dir.as_ref().to_owned());
    users.push("user.json");
    groups.push("groups.json");

    write!(
        File::create(&groups).unwrap(),
        r#""
    )
    .unwrap();
    write!(
        File::create(&users).unwrap(),
        r#"[
    {{
        "name": "root",
        "uid": 0,
        "gid": 0,
        "comment": "",
        "home": "",
        "shell": ""
    }}
]"#
    )
    .unwrap();
    eprintln!("loading debug files");

    let users = FileResource::<User>::new(users).unwrap();
    let groups = FileResource::<Group>::new(groups).unwrap();
    (users, groups)
}

fn _main(groups: FileResource<Group>, users: FileResource<User>) {
    rocket::ignite()
        .mount(
            "/",
            routes![
                get_all_groups,
                get_all_users,
                get_group,
                get_user,
                get_query_matching_groups,
                get_query_matching_users,
                get_test
            ],
        )
        .manage(users)
        .manage(groups)
        .launch();
}

/// custom parsing logic for queries
mod query {
    use rocket::request::{FromQuery, Query};
    use std::borrow::Cow;
    /// MemberName is a query for one or more members with the given name;
    /// [&member=<mq1>[&member=<mq2>, etc
    pub struct MemberNames(pub Vec<String>);

    impl<'q> FromQuery<'q> for MemberNames {
        type Error = std::str::Utf8Error;

        fn from_query(query: Query<'q>) -> std::result::Result<Self, Self::Error> {
            let mut members: Vec<String> = Vec::new();
            for q in query.filter(|q| q.key == "member") {
                members.push(q.value.percent_decode()?.to_string())
            }
            Ok(Self(members))
        }
    }

}

/// type alias for State<'r, FileResource<User>
type Users<'r> = State<'r, FileResource<User>>;
/// type alias for State<'r, FileResource<Group>
type Groups<'r> = State<'r, FileResource<Group>>;
/// std::io::Result type
type Result<T> = std::io::Result<T>;

#[get("/test")]
pub fn get_test() -> Json<Vec<usize>> {
    Json(vec![2, 3, 5])
}
#[get("/users")]
/// route to [api::get_all_users]
pub fn get_all_users(users: Users) -> Option<Json<Vec<User>>> {
    Some(Json(api::get_all_users(users).ok()?))
}

#[get("/users/<uid>")]
/// route to [api::get_user]
pub fn get_user(users: Users, uid: u64) -> Option<Json<User>> {
    Some(Json(api::get_user(users, uid)?))
}

#[get("/groups/<gid>")]
/// route to [api::get_group]. get the group uniquely matching the gid
pub fn get_group(groups: Groups, gid: u64) -> Option<Json<Group>> {
    Some(Json(api::get_group(groups, gid)?))
}

#[get("/users/<uid>/groups")]
/// route to [api::get_groups_for_user]. get the groups that contain the user.
pub fn get_groups_for_user(groups: Groups, users: Users, uid: u64) -> Option<Json<Vec<Group>>> {
    Some(Json(api::get_groups_for_user(groups, users, uid)?))
}

#[get("/groups")]
/// route to [api::get_all_groups]. return a list of all groups.
pub fn get_all_groups(groups: Groups) -> Option<Json<Vec<Group>>> {
    Some(Json(api::get_all_groups(groups).ok()?))
}

#[get("/groups/query?<name>&<gid>&<member..>")]
/// route to [api::get_matching_groups]. get the groups that match the query.
pub fn get_query_matching_groups(
    groups: Groups,
    name: Option<String>,
    gid: Option<u64>,
    member: Option<MemberNames>,
) -> Option<Json<Vec<Group>>> {
    let query = GroupQueryBuilder {
        name,
        gid,
        members: member.and_then(|m| Some(m.0)),
    }
    .build()?;
    Some(Json(api::get_matching_groups(query, groups).ok()?))
}
#[get("/users/query?<name>&<uid>&<gid>&<comment>&<home>&<shell>")]
pub fn get_query_matching_users(
    users: Users,
    name: Option<String>,
    uid: Option<u64>,
    gid: Option<u64>,
    comment: Option<String>,
    home: Option<String>,
    shell: Option<String>,
) -> Option<Json<Vec<User>>> {
    let query = UserQueryBuilder {
        name,
        uid,
        gid,
        comment,
        home,
        shell,
    }
    .build()?;
    Some(Json(api::get_matching_users(query, users).ok()?))
}