wasmer-deploy-schema 0.0.21

Utilty crate that holds shared types and logic used in Wasmer Deploy.
Documentation
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;

use crate::schema::WebcIdent;

use uuid::Uuid;

/// 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,
}

/// Claims for a JWT token that allows running a specific workload on Deploy.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
pub struct DeployWorkloadTokenV1 {
    /// Expiration time.
    #[serde(with = "time::serde::timestamp")]
    #[schemars(with = "u64")]
    pub exp: OffsetDateTime,

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

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

    /// jti (aka token id)
    ///
    /// This is a unique identifier for the token.
    /// This can be optional
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jti: Option<Uuid>,

    /// A manually specified webc.
    ///
    /// Note that usually this will be empty, and provided via [`Self::cfg::webc`] instead,
    /// since deployment configs are the common way to specify configurations.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub webc: Option<WebcIdent>,

    /// Packages that this deployment is allowed to use.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allowed_packages: Option<Vec<WebcIdent>>,
}

/// Claims for a JWT token that allows running a specific workload on Deploy.
///
/// Can represent different versions of the token.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
// untagged means try each variant, one by one.
// Order is important! Newest versions should be on top.
#[serde(untagged)]
pub enum DeployWorkloadTokenData {
    V1(DeployWorkloadTokenV1),
}

impl From<DeployWorkloadTokenV1> for DeployWorkloadTokenData {
    fn from(value: DeployWorkloadTokenV1) -> Self {
        Self::V1(value)
    }
}

/// Claims for a JWT token that allows running a specific workload on Deploy.
///
/// Can represent different versions of the token.
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
// untagged means try each variant, one by one.
// Order is important! Newest versions should be on top.
pub struct DeployWorkloadToken {
    /// Raw JWT token.
    pub raw: String,
    /// Claims data extracted from the token.
    pub data: DeployWorkloadTokenData,
}

impl DeployWorkloadTokenData {
    pub fn expires(&self) -> Option<&OffsetDateTime> {
        match self {
            DeployWorkloadTokenData::V1(v1) => Some(&v1.exp),
        }
    }

    pub fn issued_at(&self) -> &OffsetDateTime {
        match self {
            DeployWorkloadTokenData::V1(v1) => &v1.iat,
        }
    }

    pub fn subject(&self) -> &str {
        match self {
            DeployWorkloadTokenData::V1(v1) => &v1.sub,
        }
    }

    /// Direct webc spec for this workload.
    ///
    /// Note: do not confuse this witht he DeploymentConfig webc, which is
    /// available on [`Self::cfg`].
    pub fn webc_spec(&self) -> Option<&WebcIdent> {
        match self {
            Self::V1(v1) => v1.webc.as_ref(),
        }
    }

    pub fn jti(&self) -> Option<&Uuid> {
        match self {
            Self::V1(v) => v.jti.as_ref(),
        }
    }

    pub fn has_webc_spec(&self) -> bool {
        self.webc_spec().is_some()
    }

    pub fn as_v1(&self) -> &DeployWorkloadTokenV1 {
        match &self {
            DeployWorkloadTokenData::V1(x) => x,
        }
    }
}

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

    #[test]
    fn test_deser_tokens() {
        #[allow(deprecated)]
        let expected = DeployWorkloadTokenData::V1(DeployWorkloadTokenV1 {
            exp: OffsetDateTime::from_unix_timestamp(10000).unwrap(),
            iat: OffsetDateTime::from_unix_timestamp(20000).unwrap(),
            sub: "user-1".to_string(),
            jti: None,
            webc: Some(WebcIdent {
                repository: Some("https://registry.wapm.dev".parse().unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("1.2.3".to_string()),
            }),
            allowed_packages: Some(vec![WebcIdent {
                repository: Some("https://registry.wapm.dev".parse().unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("1.2.3".to_string()),
            }]),
        });

        let raw = r#"
{
  "exp": 10000,
  "iat": 20000,
  "sub": "user-1",
  "webc": {
    "repository": "https://registry.wapm.dev/",
    "namespace": "ns",
    "name": "name",
    "tag": "1.2.3",
    "hash": null,
    "download_url": "https://test.com/lala"
  },
  "cfg": {
    "uid": "123",
    "backend_url": "http://test.com"
  },
  "allowed_packages": [
    {
      "repository": "https://registry.wapm.dev/",
      "namespace": "ns",
      "name": "name",
      "tag": "1.2.3",
      "hash": null,
      "download_url": "https://test.com/lala"
    }
  ]
}
"#;

        let deser: DeployWorkloadTokenData =
            crate::schema::deserialize_json(raw.as_bytes()).unwrap();
        assert_eq!(
            deser
                .webc_spec()
                .unwrap()
                .build_download_url()
                .unwrap()
                .to_string(),
            "https://registry.wapm.dev/ns/name@1.2.3",
        );
        pretty_assertions::assert_eq!(deser, expected);
    }
}