cts-common 0.4.1

Common types and traits used across the CipherStash ecosystem
Documentation
use std::fmt::Display;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// The Authorized Party (`azp`) claim indicates the type of credential that was used to federate the token.
/// `OIDC` indicates that the token was federated from an OIDC provider.
/// If an ID is also provided, this indicates that the token was federated from a specific provider, otherwise it was federated from the root provider.
///
/// ## Examples
///
/// | Value | Description |
/// | --- | --- |
/// | `OIDC` | Federated from the root provider. |
/// | `OIDC|123e4567-e89b-12d3-a456-426614174000` | Federated provider with ID `123e4567-e89b-12d3-a456-426614174000`. |
/// | `CSAK|NWMLYZSTECEIGLDL` | Federated from an Access Key with ID `123e4567-e89b-12d3-a456-426614174000`. |
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Azp {
    RootProvider,

    /// The OIDC provider ID used when federating a 3rd party token.
    Provider(Uuid),

    /// The Access Key ID used when federating a token from an Access Key.
    AccessKey(Uuid),
}

impl Azp {
    /// Returns `true` if the `azp` is the root provider.
    pub fn is_root_provider(&self) -> bool {
        matches!(self, Self::RootProvider)
    }

    /// Returns `true` if the `azp` is a provider with a specific ID.
    pub fn is_provider(&self) -> bool {
        matches!(self, Self::Provider(_))
    }

    /// Returns `true` if the `azp` is an access key.
    pub fn is_access_key(&self) -> bool {
        matches!(self, Self::AccessKey(_))
    }
}

impl Display for Azp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::RootProvider => write!(f, "OIDC"),
            Self::Provider(provider_id) => write!(f, "OIDC|{provider_id}"),
            Self::AccessKey(access_key_id) => write!(f, "CSAK|{access_key_id}"),
        }
    }
}

impl Serialize for Azp {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.to_string().as_str())
    }
}

impl<'de> Deserialize<'de> for Azp {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        if s == "OIDC" {
            Ok(Self::RootProvider)
        } else if let Some((prefix, id)) = s.split_once('|') {
            match prefix {
                "OIDC" => Ok(Self::Provider(
                    Uuid::parse_str(id).map_err(serde::de::Error::custom)?,
                )),
                "CSAK" => Ok(Self::AccessKey(
                    Uuid::parse_str(id).map_err(serde::de::Error::custom)?,
                )),
                _ => Err(serde::de::Error::custom(format!("Invalid azp value: {s}"))),
            }
        } else {
            Err(serde::de::Error::custom(format!("Invalid azp value: {s}")))
        }
    }
}

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

    mod root {
        use super::*;

        #[test]
        fn serializes_as_oidc() {
            let azp = Azp::RootProvider;
            let serialized = serde_json::to_value(azp).unwrap();
            assert_eq!(serialized, json!("OIDC"));
        }

        #[test]
        fn deserializes_from_oidc() {
            let json = json!("OIDC");
            let azp: Azp = serde_json::from_value(json).unwrap();
            assert_eq!(azp, Azp::RootProvider);
        }
    }

    mod provider {
        use super::*;

        #[test]
        fn serializes_with_provider_id() {
            let uuid = Uuid::new_v4();
            let azp = Azp::Provider(uuid);
            let serialized = serde_json::to_value(azp).unwrap();
            assert_eq!(serialized, json!(format!("OIDC|{uuid}")));
        }

        #[test]
        fn deserializes_with_provider_id() {
            let uuid = Uuid::new_v4();
            let json = json!(format!("OIDC|{uuid}"));
            let azp: Azp = serde_json::from_value(json).unwrap();
            assert_eq!(azp, Azp::Provider(uuid));
        }
    }

    mod access_key {
        use super::*;

        #[test]
        fn serializes_with_access_key_id() {
            let uuid = Uuid::new_v4();
            let azp = Azp::AccessKey(uuid);
            let serialized = serde_json::to_value(azp).unwrap();
            assert_eq!(serialized, json!(format!("CSAK|{uuid}")));
        }

        #[test]
        fn deserializes_with_access_key_id() {
            let uuid = Uuid::new_v4();
            let json = json!(format!("CSAK|{uuid}"));
            let azp: Azp = serde_json::from_value(json).unwrap();
            assert_eq!(azp, Azp::AccessKey(uuid));
        }
    }

    mod invalid {
        use super::*;

        #[test]
        fn deserializes_invalid_azp() {
            let json = json!("INVALID|123e4567-e89b-12d3-a456-426614174000");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }

        #[test]
        fn deserializes_invalid_format() {
            let json = json!("OIDC|INVALID_UUID");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }
    }

    mod empty {
        use super::*;

        #[test]
        fn fails_on_empty_string() {
            let json = json!("");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }

        #[test]
        fn fails_on_null() {
            let json = json!(null);
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }
    }

    mod malformed {
        use super::*;

        #[test]
        fn fails_on_malformed_string() {
            let json = json!("OIDC|123e4567-e89b-12d3-a456-426614174000|extra");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }

        #[test]
        fn fails_on_missing_prefix() {
            let json = json!("123e4567-e89b-12d3-a456-426614174000");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }

        #[test]
        fn fails_on_invalid_uuid() {
            let json = json!("OIDC|invalid-uuid");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }

        #[test]
        fn fails_on_invalid_access_key_uuid() {
            let json = json!("CSAK|invalid-uuid");
            let result: Result<Azp, _> = serde_json::from_value(json);
            assert!(result.is_err());
        }
    }
}