kirino 0.3.0

A customizable, zero-trust authentication framework.
Documentation
use anyhow::{anyhow, Result};

use super::store::ConstraintStore;

pub struct ConstraintValidator<S: ConstraintStore> {
    store: S,
}

impl<S: ConstraintStore> ConstraintValidator<S> {
    pub fn new(store: S) -> Self {
        Self { store }
    }

    pub async fn validate_ssd(&self, current_roles: &[String], new_role: &str) -> Result<()> {
        let policies = self.store.list_ssd_policies().await?;
        let mut test_roles = current_roles.to_vec();
        test_roles.push(new_role.to_string());

        for policy in &policies {
            if !policy.validate(&test_roles) {
                return Err(anyhow!(
                    "SSD policy '{}' violated: adding '{}' would exceed cardinality {}",
                    policy.name,
                    new_role,
                    policy.cardinality,
                ));
            }
        }
        Ok(())
    }

    pub async fn validate_dsd(&self, active_roles: &[String], new_role: &str) -> Result<()> {
        let policies = self.store.list_dsd_policies().await?;
        let mut test_roles = active_roles.to_vec();
        test_roles.push(new_role.to_string());

        for policy in &policies {
            if !policy.validate(&test_roles) {
                return Err(anyhow!(
                    "DSD policy '{}' violated: activating '{}' exceeds cardinality {}",
                    policy.name,
                    new_role,
                    policy.cardinality,
                ));
            }
        }
        Ok(())
    }

    pub async fn validate_cardinality(
        &self,
        role_name: &str,
        current_subject_count: usize,
    ) -> Result<()> {
        let constraints = self.store.list_cardinality_constraints().await?;
        for constraint in &constraints {
            if constraint.role_name == role_name && !constraint.validate(current_subject_count) {
                return Err(anyhow!(
                    "Cardinality constraint: role '{}' already has {} subjects (max {})",
                    role_name,
                    current_subject_count,
                    constraint.max_subjects,
                ));
            }
        }
        Ok(())
    }

    pub async fn validate_prerequisite(
        &self,
        role_name: &str,
        current_roles: &[String],
    ) -> Result<()> {
        let constraints = self.store.list_prerequisite_constraints().await?;
        for constraint in &constraints {
            if constraint.role_name == role_name && !constraint.validate(current_roles) {
                return Err(anyhow!(
                    "Prerequisite constraint: role '{}' requires '{}'",
                    role_name,
                    constraint.requires,
                ));
            }
        }
        Ok(())
    }

    pub async fn validate_assignment(
        &self,
        current_roles: &[String],
        new_role: &str,
        current_subject_count: usize,
    ) -> Result<()> {
        self.validate_ssd(current_roles, new_role).await?;
        self.validate_cardinality(new_role, current_subject_count)
            .await?;
        self.validate_prerequisite(new_role, current_roles).await?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rbac::constraints::policies::{
        CardinalityConstraint, DsdPolicy, PrerequisiteConstraint, SsdPolicy,
    };
    use crate::rbac::constraints::store::InMemoryConstraintStore;

    #[tokio::test]
    async fn test_validate_ssd_pass() {
        let store = InMemoryConstraintStore::new();
        store
            .add_ssd_policy(SsdPolicy::new(
                "exclusive",
                ["admin".to_string(), "auditor".to_string()].into(),
                2,
            ))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);
        assert!(validator
            .validate_ssd(&["viewer".to_string()], "admin")
            .await
            .is_ok());
    }

    #[tokio::test]
    async fn test_validate_ssd_fail() {
        let store = InMemoryConstraintStore::new();
        store
            .add_ssd_policy(SsdPolicy::new(
                "exclusive",
                ["admin".to_string(), "auditor".to_string()].into(),
                2,
            ))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);
        assert!(validator
            .validate_ssd(&["admin".to_string()], "auditor")
            .await
            .is_err());
    }

    #[tokio::test]
    async fn test_validate_dsd_fail() {
        let store = InMemoryConstraintStore::new();
        store
            .add_dsd_policy(DsdPolicy::new(
                "session_exclusive",
                ["ops".to_string(), "audit".to_string()].into(),
                2,
            ))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);
        assert!(validator
            .validate_dsd(&["ops".to_string()], "audit")
            .await
            .is_err());
    }

    #[tokio::test]
    async fn test_validate_cardinality_fail() {
        let store = InMemoryConstraintStore::new();
        store
            .add_cardinality_constraint(CardinalityConstraint::new("admin", 1))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);
        assert!(validator.validate_cardinality("admin", 1).await.is_err());
        assert!(validator.validate_cardinality("admin", 0).await.is_ok());
    }

    #[tokio::test]
    async fn test_validate_prerequisite_fail() {
        let store = InMemoryConstraintStore::new();
        store
            .add_prerequisite_constraint(PrerequisiteConstraint::new("admin", "operator"))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);
        assert!(validator
            .validate_prerequisite("admin", &["viewer".to_string()])
            .await
            .is_err());
        assert!(validator
            .validate_prerequisite("admin", &["operator".to_string()])
            .await
            .is_ok());
    }

    #[tokio::test]
    async fn test_validate_assignment_full() {
        let store = InMemoryConstraintStore::new();
        store
            .add_ssd_policy(SsdPolicy::new(
                "exclusive",
                ["admin".to_string(), "auditor".to_string()].into(),
                2,
            ))
            .await
            .unwrap();
        store
            .add_prerequisite_constraint(PrerequisiteConstraint::new("admin", "operator"))
            .await
            .unwrap();

        let validator = ConstraintValidator::new(store);

        assert!(validator
            .validate_assignment(&["operator".to_string()], "admin", 0)
            .await
            .is_ok());

        assert!(validator
            .validate_assignment(&["auditor".to_string()], "admin", 0)
            .await
            .is_err());

        assert!(validator
            .validate_assignment(&["viewer".to_string()], "admin", 0)
            .await
            .is_err());
    }
}