mockforge-collab 0.3.92

Cloud collaboration features for MockForge - team workspaces, real-time sync, and version control
Documentation
//! Permission checking and role-based access control

use crate::error::{CollabError, Result};
use crate::models::UserRole;
use serde::{Deserialize, Serialize};

/// Permission types in the system
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum Permission {
    /// Create workspaces
    WorkspaceCreate,
    /// Read workspace data
    WorkspaceRead,
    /// Update workspace settings
    WorkspaceUpdate,
    /// Delete workspaces
    WorkspaceDelete,
    /// Archive workspaces
    WorkspaceArchive,
    /// Manage workspace members
    WorkspaceManageMembers,

    /// Create mock routes
    MockCreate,
    /// Read mock routes
    MockRead,
    /// Update mock routes
    MockUpdate,
    /// Delete mock routes
    MockDelete,

    /// Invite members to workspace
    InviteMembers,
    /// Remove members from workspace
    RemoveMembers,
    /// Change member roles
    ChangeRoles,

    /// View workspace history
    ViewHistory,
    /// Create named snapshots
    CreateSnapshot,
    /// Restore from snapshots
    RestoreSnapshot,

    /// Manage workspace settings
    ManageSettings,
    /// Manage integrations
    ManageIntegrations,

    // Scenario-specific permissions
    /// Modify chaos rules for scenarios (typically QA only)
    ScenarioModifyChaosRules,
    /// Change reality-level defaults for scenarios (typically Platform team only)
    ScenarioModifyRealityDefaults,
    /// Promote scenarios between environments
    ScenarioPromote,
    /// Approve scenario promotions
    ScenarioApprove,
    /// Modify drift budgets for scenarios
    ScenarioModifyDriftBudgets,
}

impl std::fmt::Display for Permission {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::WorkspaceCreate => write!(f, "WorkspaceCreate"),
            Self::WorkspaceRead => write!(f, "WorkspaceRead"),
            Self::WorkspaceUpdate => write!(f, "WorkspaceUpdate"),
            Self::WorkspaceDelete => write!(f, "WorkspaceDelete"),
            Self::WorkspaceArchive => write!(f, "WorkspaceArchive"),
            Self::WorkspaceManageMembers => write!(f, "WorkspaceManageMembers"),
            Self::MockCreate => write!(f, "MockCreate"),
            Self::MockRead => write!(f, "MockRead"),
            Self::MockUpdate => write!(f, "MockUpdate"),
            Self::MockDelete => write!(f, "MockDelete"),
            Self::InviteMembers => write!(f, "InviteMembers"),
            Self::RemoveMembers => write!(f, "RemoveMembers"),
            Self::ChangeRoles => write!(f, "ChangeRoles"),
            Self::ViewHistory => write!(f, "ViewHistory"),
            Self::CreateSnapshot => write!(f, "CreateSnapshot"),
            Self::RestoreSnapshot => write!(f, "RestoreSnapshot"),
            Self::ManageSettings => write!(f, "ManageSettings"),
            Self::ManageIntegrations => write!(f, "ManageIntegrations"),
            Self::ScenarioModifyChaosRules => write!(f, "ScenarioModifyChaosRules"),
            Self::ScenarioModifyRealityDefaults => write!(f, "ScenarioModifyRealityDefaults"),
            Self::ScenarioPromote => write!(f, "ScenarioPromote"),
            Self::ScenarioApprove => write!(f, "ScenarioApprove"),
            Self::ScenarioModifyDriftBudgets => write!(f, "ScenarioModifyDriftBudgets"),
        }
    }
}

/// Role permissions mapping
pub struct RolePermissions;

impl RolePermissions {
    /// Get all permissions for a role
    #[must_use]
    pub fn get_permissions(role: UserRole) -> Vec<Permission> {
        match role {
            UserRole::Admin => vec![
                // Full access to everything
                Permission::WorkspaceCreate,
                Permission::WorkspaceRead,
                Permission::WorkspaceUpdate,
                Permission::WorkspaceDelete,
                Permission::WorkspaceArchive,
                Permission::WorkspaceManageMembers,
                Permission::MockCreate,
                Permission::MockRead,
                Permission::MockUpdate,
                Permission::MockDelete,
                Permission::InviteMembers,
                Permission::RemoveMembers,
                Permission::ChangeRoles,
                Permission::ViewHistory,
                Permission::CreateSnapshot,
                Permission::RestoreSnapshot,
                Permission::ManageSettings,
                Permission::ManageIntegrations,
                // Scenario permissions - admins have all
                Permission::ScenarioModifyChaosRules,
                Permission::ScenarioModifyRealityDefaults,
                Permission::ScenarioPromote,
                Permission::ScenarioApprove,
                Permission::ScenarioModifyDriftBudgets,
            ],
            UserRole::Editor => vec![
                // Can edit but not manage workspace or members
                Permission::WorkspaceRead,
                Permission::MockCreate,
                Permission::MockRead,
                Permission::MockUpdate,
                Permission::MockDelete,
                Permission::ViewHistory,
                Permission::CreateSnapshot,
                // Editors can promote scenarios but not approve or modify sensitive configs
                Permission::ScenarioPromote,
            ],
            UserRole::Viewer => vec![
                // Read-only access
                Permission::WorkspaceRead,
                Permission::MockRead,
                Permission::ViewHistory,
            ],
        }
    }

    /// Check if a role has a specific permission
    #[must_use]
    pub fn has_permission(role: UserRole, permission: Permission) -> bool {
        Self::get_permissions(role).contains(&permission)
    }
}

/// Permission checker for authorization
pub struct PermissionChecker;

impl PermissionChecker {
    /// Check if a user has permission to perform an action
    ///
    /// # Errors
    ///
    /// Returns an error if the role lacks the required permission.
    pub fn check(user_role: UserRole, required_permission: Permission) -> Result<()> {
        if RolePermissions::has_permission(user_role, required_permission) {
            Ok(())
        } else {
            Err(CollabError::AuthorizationFailed(format!(
                "Role {user_role:?} does not have permission {required_permission:?}"
            )))
        }
    }

    /// Check multiple permissions (must have all)
    ///
    /// # Errors
    ///
    /// Returns an error if the role lacks any required permission.
    pub fn check_all(user_role: UserRole, required_permissions: &[Permission]) -> Result<()> {
        for permission in required_permissions {
            Self::check(user_role, *permission)?;
        }
        Ok(())
    }

    /// Check multiple permissions (must have at least one)
    ///
    /// # Errors
    ///
    /// Returns an error if the role lacks all required permissions.
    pub fn check_any(user_role: UserRole, required_permissions: &[Permission]) -> Result<()> {
        for permission in required_permissions {
            if RolePermissions::has_permission(user_role, *permission) {
                return Ok(());
            }
        }
        Err(CollabError::AuthorizationFailed(format!(
            "Role {user_role:?} does not have any of the required permissions"
        )))
    }
}

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

    #[test]
    fn test_admin_permissions() {
        let permissions = RolePermissions::get_permissions(UserRole::Admin);
        assert!(permissions.contains(&Permission::WorkspaceDelete));
        assert!(permissions.contains(&Permission::MockCreate));
        assert!(permissions.contains(&Permission::ChangeRoles));
    }

    #[test]
    fn test_editor_permissions() {
        let permissions = RolePermissions::get_permissions(UserRole::Editor);
        assert!(permissions.contains(&Permission::MockCreate));
        assert!(permissions.contains(&Permission::MockUpdate));
        assert!(!permissions.contains(&Permission::WorkspaceDelete));
        assert!(!permissions.contains(&Permission::ChangeRoles));
    }

    #[test]
    fn test_viewer_permissions() {
        let permissions = RolePermissions::get_permissions(UserRole::Viewer);
        assert!(permissions.contains(&Permission::WorkspaceRead));
        assert!(permissions.contains(&Permission::MockRead));
        assert!(!permissions.contains(&Permission::MockCreate));
        assert!(!permissions.contains(&Permission::MockUpdate));
    }

    #[test]
    fn test_permission_check() {
        assert!(PermissionChecker::check(UserRole::Admin, Permission::WorkspaceDelete).is_ok());
        assert!(PermissionChecker::check(UserRole::Editor, Permission::MockCreate).is_ok());
        assert!(PermissionChecker::check(UserRole::Viewer, Permission::MockCreate).is_err());
    }

    #[test]
    fn test_check_all() {
        let permissions = vec![Permission::MockRead, Permission::MockCreate];
        assert!(PermissionChecker::check_all(UserRole::Editor, &permissions).is_ok());
        assert!(PermissionChecker::check_all(UserRole::Viewer, &permissions).is_err());
    }

    #[test]
    fn test_check_any() {
        let permissions = vec![Permission::MockCreate, Permission::WorkspaceDelete];
        assert!(PermissionChecker::check_any(UserRole::Editor, &permissions).is_ok());

        let admin_only = vec![Permission::WorkspaceDelete, Permission::ChangeRoles];
        assert!(PermissionChecker::check_any(UserRole::Viewer, &admin_only).is_err());
    }
}