edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
use std::collections::HashSet;

use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

/// Node API permission scopes.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeApiPermission {
    /// Superuser permission granting all other permissions.
    #[serde(rename = "all")]
    All,

    /// Check node health.
    #[serde(rename = "node.health_check")]
    NodeHealthCheck,

    /// Read information about workloads.
    #[serde(rename = "workload.read")]
    WorkloadRead,

    /// Modify workloads. (EG: terminate)
    #[serde(rename = "workload.write")]
    WorkloadWrite,

    // Confdb
    #[serde(rename = "confdb.entity.read")]
    ConfDbEntityRead,
    #[serde(rename = "confdb.entity.delete")]
    ConfDbEntityDelete,

    // TLS
    #[serde(rename = "tls_cert.read")]
    TlsCertRead,
    #[serde(rename = "tls_cert.delete")]
    TlsCertDelete,

    #[serde(rename = "dns_cache.read")]
    DnsCacheRead,
    #[serde(rename = "dns_cache.write")]
    DnsCacheWrite,

    #[serde(rename = "platform_config.read")]
    PlatformConfigRead,

    /// Generate new API tokens.
    #[serde(rename = "api_token.generate")]
    ApiTokenGenerate,

    // (Cron)job
    #[serde(rename = "job.read")]
    JobRead,

    #[serde(rename = "cluster_health.read")]
    ClusterHealthRead,

    #[serde(rename = "module_cache.read")]
    ModuleCacheRead,
    #[serde(rename = "module_cache.write")]
    ModuleCacheWrite,
}

impl NodeApiPermission {
    fn as_str(self) -> &'static str {
        match self {
            Self::All => "all",
            Self::NodeHealthCheck => "node.health_check",
            Self::WorkloadRead => "workload.read",
            Self::WorkloadWrite => "workload.write",
            Self::ConfDbEntityRead => "confdb.entity.read",
            Self::ConfDbEntityDelete => "confdb.entity.delete",
            Self::TlsCertRead => "tls_cert.read",
            Self::TlsCertDelete => "tls_cert.delete",
            Self::DnsCacheRead => "dns_cache.read",
            Self::DnsCacheWrite => "dns_cache.write",
            Self::PlatformConfigRead => "platform_config.read",
            Self::ApiTokenGenerate => "api_token.generate",
            Self::JobRead => "job.read",
            Self::ClusterHealthRead => "cluster_health.read",
            Self::ModuleCacheRead => "module_cache.read",
            Self::ModuleCacheWrite => "module_cache.write",
        }
    }
}

impl std::str::FromStr for NodeApiPermission {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "all" => Ok(NodeApiPermission::All),
            "node.health_check" => Ok(NodeApiPermission::NodeHealthCheck),
            "workload.read" => Ok(NodeApiPermission::WorkloadRead),
            "workload.write" => Ok(NodeApiPermission::WorkloadWrite),
            "confdb.entity.read" => Ok(NodeApiPermission::ConfDbEntityRead),
            "confdb.entity.delete" => Ok(NodeApiPermission::ConfDbEntityDelete),
            "tls_cert.read" => Ok(NodeApiPermission::TlsCertRead),
            "tls_cert.delete" => Ok(NodeApiPermission::TlsCertDelete),
            "dns_cache.read" => Ok(NodeApiPermission::DnsCacheRead),
            "dns_cache.write" => Ok(NodeApiPermission::DnsCacheWrite),
            "platform_config.read" => Ok(NodeApiPermission::PlatformConfigRead),
            "api_token.generate" => Ok(NodeApiPermission::ApiTokenGenerate),
            "job.read" => Ok(NodeApiPermission::JobRead),
            "cluster_health.read" => Ok(NodeApiPermission::ClusterHealthRead),
            "module_cache.read" => Ok(NodeApiPermission::ModuleCacheRead),
            "module_cache.write" => Ok(NodeApiPermission::ModuleCacheWrite),
            _ => Err(anyhow::anyhow!("invalid node api permission: {}", s)),
        }
    }
}

impl std::fmt::Display for NodeApiPermission {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Basic JWT claims.
///
/// Only default fields.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct BaseClaims {
    /// Expiration time.
    #[serde(with = "time::serde::timestamp")]
    pub exp: OffsetDateTime,

    /// Issued at.
    #[serde(with = "time::serde::timestamp")]
    pub iat: OffsetDateTime,

    /// Subject (aka user id)
    pub sub: String,

    /// Permissions for the node API.
    ///
    /// NOTE: for legacy reasons, `None` implies [`NodeApiPermission::All`],
    /// meaning a superuser token which grants all other permissions.
    /// This is needed because this field was added after the initial implementation.
    /// This will be a required field in the future.
    pub node_api_permissions: Option<HashSet<NodeApiPermission>>,
}

impl BaseClaims {
    pub fn has_node_api_permission(&self, perm: NodeApiPermission) -> bool {
        match &self.node_api_permissions {
            Some(perms) => perms.contains(&perm) || perms.contains(&NodeApiPermission::All),
            None => true,
        }
    }
}

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

    #[test]
    fn test_base_claims_has_permission() {
        // Specific set of permissions.
        let c = BaseClaims {
            exp: OffsetDateTime::now_utc(),
            iat: OffsetDateTime::now_utc(),
            sub: "test".to_string(),
            node_api_permissions: Some(
                vec![
                    NodeApiPermission::WorkloadRead,
                    NodeApiPermission::ConfDbEntityRead,
                ]
                .into_iter()
                .collect(),
            ),
        };

        assert!(c.has_node_api_permission(NodeApiPermission::WorkloadRead));
        assert!(c.has_node_api_permission(NodeApiPermission::ConfDbEntityRead));
        assert!(!c.has_node_api_permission(NodeApiPermission::WorkloadWrite));
        assert!(!c.has_node_api_permission(NodeApiPermission::All));

        // All permissions.
        let c = BaseClaims {
            node_api_permissions: Some(vec![NodeApiPermission::All].into_iter().collect()),
            ..c
        };
        assert!(c.has_node_api_permission(NodeApiPermission::WorkloadRead));
        assert!(c.has_node_api_permission(NodeApiPermission::ConfDbEntityRead));
        assert!(c.has_node_api_permission(NodeApiPermission::All));

        // Legacy all permissions (`None`).
        let c = BaseClaims {
            node_api_permissions: None,
            ..c
        };
        assert!(c.has_node_api_permission(NodeApiPermission::WorkloadRead));
        assert!(c.has_node_api_permission(NodeApiPermission::ConfDbEntityRead));
        assert!(c.has_node_api_permission(NodeApiPermission::All));
    }
}