hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
pub mod action;
mod layout;
mod org;
pub mod permission;
pub mod staff;
mod user_group;

use action::*;
pub use layout::RoleMap;
pub use org::{StaffGov, StaffOrg};
pub use permission::UserPass;
pub use staff::{ProductionRole, Staff};
pub use user_group::UserGroup;

#[cfg(feature = "gui")]
use mktree::*;

use super::*;

#[cfg(feature = "ldap")]
use mkutil::{
    ldap,
    ldap3::{LdapConn, SearchEntry},
};

const MAX_STAFF_NAME_LENGTH: u8 = 20;

/// Interface to a Hunter user if performing actions related to the active production.
pub trait ActivePersonnel: DynClone + fmt::Debug + Send + Sync {
    fn name(&self) -> &String;

    fn name_mut(&mut self, name: &str);

    /// The `Staff` for this user -- as fetched from DB.
    fn as_staff(&self) -> &Staff;

    #[cfg(feature = "ldap")]
    fn person_entry_by_name(
        &self,
        conn: &mut LdapConn,
        base: &str,
        attrs: &[&str],
    ) -> AnyResult<SearchEntry>;

    #[cfg(all(feature = "ldap", feature = "image_processing"))]
    fn take_profile_img(&mut self) -> Option<AnyResult<RetainedImage>>;

    #[cfg(all(feature = "ldap", feature = "image_processing"))]
    fn profile_img_mut(&mut self, img: Option<AnyResult<RetainedImage>>);

    /// The role in the active production.
    fn working_role(&self) -> &ProductionRole;

    /// Whether the user is allowed to perform given action.
    fn is_authorized_for(&mut self, action: &Action) -> bool;

    /// Whether we can approximately deduct from the given name
    /// into what is identical to the current user.
    fn is_likely_self(&self, name: &str) -> bool;

    /// Updates [`ProductionRole`] and permission sum for the active production
    /// using the given, queried, [`Staff`].
    fn role_n_permission_mut(
        &mut self,
        staff: Staff,
        #[cfg(all(feature = "ldap", feature = "image_processing"))] profile_img: Option<
            AnyResult<RetainedImage>,
        >,
        clear_cache: bool,
    );

    fn clear_cache(&mut self) {}
}

dyn_clone::clone_trait_object!(ActivePersonnel);

// -------------------------------------------------------------------------------
pub trait PanProjectPersonnel: DynClone + fmt::Debug + Send + Sync {
    fn role_in(&self, project: &Project) -> ProductionRole;

    fn is_authorized_for_pp(&self, project: &Project, action: &Action) -> bool;

    /// Used to update cache.
    fn pp_staff_mut(&mut self, project: &Project, staff: &Staff);
}

dyn_clone::clone_trait_object!(PanProjectPersonnel);

// -------------------------------------------------------------------------------
pub trait Personnel: ActivePersonnel + PanProjectPersonnel {}

dyn_clone::clone_trait_object!(Personnel);

// -------------------------------------------------------------------------------
#[derive(Debug, Clone)]
/// A user queried from `USERNAME` of `USER` env var, with permission caching.
pub struct LocalUser {
    /// The current user as a `Staff` in the active project.
    staff: Staff,

    /// The current user as `Staff`s in various projects.
    pp_staff: HashMap<Project, (Staff, UserPass)>,

    /// User permissions (in `u16` values) of various actions
    /// in the active project.
    pass: UserPass,

    /// Cache of user permissions (in boolean values) of various actions
    /// in the active project.
    perm_cache: HashMap<Action, bool>,
}

impl LocalUser {
    pub fn from_env() -> Self {
        Self {
            staff: Staff::from_env(),
            pp_staff: HashMap::new(),
            pass: UserPass::empty(),
            perm_cache: HashMap::new(),
        }
    }
}

impl ActivePersonnel for LocalUser {
    fn name(&self) -> &String {
        self.staff.name_unwrap()
    }

    fn name_mut(&mut self, name: &str) {
        self.staff.name_mut(name);
    }

    fn as_staff(&self) -> &Staff {
        &self.staff
    }

    #[cfg(feature = "ldap")]
    fn person_entry_by_name(
        &self,
        conn: &mut LdapConn,
        base: &str,
        attrs: &[&str],
    ) -> AnyResult<SearchEntry> {
        // can be either "mail" or "uid"
        let person_filter = &ClientCfgCel::settings().as_ref()?.ldap_raw().person_filter;

        // NOTE: we're adding a wildcard character for email
        let filter = format!(
            "(&(objectClass=person)({}={}*))",
            person_filter,
            self.name()
        );

        info!("Using LDAP filter: {}", filter);

        Ok(
            ldap::search_subtree_for_entries(conn, base, &filter, attrs)?
                .into_iter()
                .next()
                .context(format!("No entry found that matches filter \"{}\"", filter))?,
        )
    }

    #[cfg(all(feature = "ldap", feature = "image_processing"))]
    fn take_profile_img(&mut self) -> Option<AnyResult<RetainedImage>> {
        self.staff.profile_img.take()
    }

    #[cfg(all(feature = "ldap", feature = "image_processing"))]
    fn profile_img_mut(&mut self, img: Option<AnyResult<RetainedImage>>) {
        match &img {
            Some(Ok(_)) => {
                info!(
                    "{} {}",
                    "Setting profile image for".on_bright_magenta(),
                    self.name()
                );
            }
            Some(Err(e)) => {
                warn!("{}: {}", "Profile image error".on_bright_red(), e);
            }
            None => {
                warn!("{}", "Profile image is none".on_bright_blue());
            }
        }
        self.staff.profile_img = img;
    }

    fn working_role(&self) -> &ProductionRole {
        &self.staff.role
    }

    /// Checks for `UserPass::is_authorized_for` while performing caching.
    fn is_authorized_for(&mut self, action: &Action) -> bool {
        match self.perm_cache.get(action) {
            Some(authorized) => *authorized,
            None => {
                // key doesn't exist
                let authorized = self.pass.is_authorized_for(action);
                self.perm_cache.insert(action.clone(), authorized);
                authorized
            }
        }
    }

    fn is_likely_self(&self, name: &str) -> bool {
        name == self.name()
    }

    fn role_n_permission_mut(
        &mut self,
        staff: Staff,
        #[cfg(all(feature = "ldap", feature = "image_processing"))] profile_img: Option<
            AnyResult<RetainedImage>,
        >,
        clear_cache: bool,
    ) {
        debug!("Updated Staff: {:?}", staff);

        if clear_cache {
            self.clear_cache();
        };

        // recalculates permission sum from relevant role within active project
        self.pass = staff.role.user_pass();
        self.staff = staff;

        #[cfg(all(feature = "ldap", feature = "image_processing"))]
        self.profile_img_mut(profile_img);
    }

    fn clear_cache(&mut self) {
        debug!("Clearing permission cache");
        self.perm_cache = HashMap::new();
    }
}

impl PanProjectPersonnel for LocalUser {
    /// Just gets in cache, or returns `ProductionRole::Undefined` if not found.
    fn role_in(&self, project: &Project) -> ProductionRole {
        match self.pp_staff.get(project) {
            Some((staff, _)) => staff.role.clone(),
            None => ProductionRole::Undefined,
        }
    }

    fn is_authorized_for_pp(&self, project: &Project, action: &Action) -> bool {
        match self.pp_staff.get(project) {
            Some((_, user_pass)) => user_pass.is_authorized_for(action),
            None => false,
        }
    }

    fn pp_staff_mut(&mut self, project: &Project, staff: &Staff) {
        if !self.pp_staff.contains_key(project) {
            self.pp_staff
                .insert(project.clone(), (staff.clone(), staff.role.user_pass()));
        };
    }
}

impl Personnel for LocalUser {}

// ----------------------------------------------------------------------------
pub fn make_assignee_str<'a>(staves: impl Iterator<Item = &'a Staff>) -> String {
    let staves = staves
        .into_iter()
        .map(|a| a.name_unwrap().to_owned())
        .collect::<Vec<String>>();
    if staves.is_empty() {
        "!? UNASSIGNED 🎁".to_string()
    } else {
        format!("Assignees: {}", staves.join(", "))
    }
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use hconf::ClientCfgCel;

    #[test]
    #[cfg(feature = "ldap")]
    fn connect_ldap() {
        use hconf::ReadFromFiles;
        use mkutil::ldap;
        ClientCfgCel::pkg_test(true);

        let cfg = ClientCfgCel::settings().as_ref().unwrap();
        let auth = cfg.ldap_auth().unwrap();

        // eprintln!("LDAP auth: {:?}", auth);

        let (mut conn, mut _bind) =
            ldap::authenticate(&auth.url, &auth.distinguished_name, &auth.password).unwrap();

        let search = ldap::search_subtree_for_entries(
            &mut conn,
            &auth.search_base,
            "(objectClass=person)",
            &["givenName", "sn", "mail", "cn", "uid"],
        );

        assert!(search.is_ok());

        for e in &search {
            eprintln!("Search result: {:?}", e);
        }
    }
}