whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Resolved identity for permission checks.
//!
//! All subject input formats (username, UID, PID, service name) normalize
//! to a [`ResolvedSubject`] before entering the check pipeline.

use serde::Serialize;

use crate::state::Probe;

/// Resolved identity performing the operation.
///
/// Effective UID, primary GID, supplementary group list, and Linux
/// capability bitmask. Produced by gathering layer from any of the
/// four input formats. `capabilities` is `Probe::Known(cap_eff)` for
/// `pid:N` and `svc:` subjects (read from `/proc/<pid>/status`),
/// `Probe::Unknown` for username/UID resolution (no process context),
/// and `Probe::Inaccessible` when the process is in a non-initial
/// user namespace (`CapEff` bits are namespace-relative).
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ResolvedSubject {
    /// Effective user ID.
    pub uid: u32,
    /// Primary group ID.
    pub gid: u32,
    /// Supplementary group IDs (does not include primary GID).
    pub groups: Vec<u32>,
    /// Effective capability bitmask (`CapEff` from `/proc/<pid>/status`).
    pub capabilities: Probe<u64>,
}

impl ResolvedSubject {
    /// Returns `true` if subject is in the given group.
    ///
    /// Checks both primary GID and supplementary group list.
    #[must_use]
    pub fn in_group(&self, gid: u32) -> bool {
        self.gid == gid || self.groups.contains(&gid)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn in_group_matches_primary_gid() {
        let subject = ResolvedSubject {
            uid: 1000,
            gid: 1000,
            groups: vec![],
            capabilities: Probe::Unknown,
        };
        assert!(subject.in_group(1000));
    }

    #[test]
    fn in_group_matches_supplementary_group() {
        let subject = ResolvedSubject {
            uid: 1000,
            gid: 1000,
            groups: vec![33, 44],
            capabilities: Probe::Unknown,
        };
        assert!(subject.in_group(33));
    }

    #[test]
    fn in_group_returns_false_for_nonmember() {
        let subject = ResolvedSubject {
            uid: 1000,
            gid: 1000,
            groups: vec![33],
            capabilities: Probe::Unknown,
        };
        assert!(!subject.in_group(999));
    }
}