cedarling 0.0.65

The Cedarling: a high-performance local authorization service powered by the Rust Cedar Engine.
Documentation
// This software is available under the Apache-2.0 license.
// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
//
// Copyright (c) 2024, Gluu, Inc.

use serde::{Deserialize, Deserializer};
use serde_json::Value;

use crate::jwt_config::{MIN_JWKS_REFRESH_SECS, normalize_status_list_refresh_interval_max};

/// Custom parser for an Option<String> which returns `None` if the string is empty.
pub(super) fn parse_option_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let value: Option<String> = deserialize_or_parse_string_as_json(deserializer)?;

    Ok(value.filter(|s| !s.is_empty()))
}

/// Helper function to parse string to json, and fix some possible moments
fn to_json(s: &str) -> Option<Value> {
    let mut json_string = s.trim().to_string();

    // convert Python-style list strings to JSON format
    if json_string.starts_with('[') && json_string.ends_with(']') {
        let json_like = json_string.replace('\'', "\""); // Replace single quotes with double quotes
        json_string = json_like;
    }

    // Validate that the result is valid JSON
    serde_json::from_str::<Value>(json_string.as_str()).ok()
}

/// Attempts to deserialize a value, falling back to JSON parsing if the value is a string.
/// Returns the deserialized value or the original error if both attempts fail.
pub(super) fn deserialize_or_parse_string_as_json<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    // First deserialize to serde_json::Value
    let value = Value::deserialize(deserializer)?;

    // If it's a string, try to parse it as JSON
    if let Value::String(s) = &value
        && let Some(parsed_value) = to_json(s)
        && let Ok(result) = T::deserialize(parsed_value)
    {
        return Ok(result);
    }

    // Try normal deserialization
    T::deserialize(value).map_err(serde::de::Error::custom)
}

pub(super) fn deserialize_jwks_refresh_interval<'de, D>(
    deserializer: D,
) -> Result<Option<u64>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value: Option<u64> = deserialize_or_parse_string_as_json(deserializer)?;
    Ok(value.map(|v| v.max(MIN_JWKS_REFRESH_SECS)))
}

pub(super) fn deserialize_status_list_refresh_interval_max<'de, D>(
    deserializer: D,
) -> Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value: Option<u64> = deserialize_or_parse_string_as_json(deserializer)?;
    // Missing variable is treated the same as an explicit `0`: defer to the
    // shared normalizer so this code path stays in sync with the loader.
    Ok(normalize_status_list_refresh_interval_max(
        value.unwrap_or(0),
    ))
}

pub(super) fn deserialize_jwks_refresh_min_interval<'de, D>(
    deserializer: D,
) -> Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value: u64 = deserialize_or_parse_string_as_json(deserializer)?;
    Ok(value.max(MIN_JWKS_REFRESH_SECS))
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;
    use test_utils::assert_eq;

    /// Test structure used to verify `fallback_deserialize` functionality
    /// Contains fields of different types to test various scenarios:
    /// - value: i32 - tests number deserialization
    /// - optional: Option<String> - tests optional string handling
    /// - vector: Vec<i32> - tests array deserialization
    #[derive(Debug, Deserialize, PartialEq)]
    struct TestStruct {
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        value: i32,
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        optional: Option<String>,
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        vector_int: Vec<i32>,
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        vector_str: Vec<String>,
    }

    /// Additional test structure for more complex scenarios
    #[derive(Debug, Deserialize, PartialEq)]
    struct ComplexTestStruct {
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        boolean: bool,
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        float: f64,
        #[serde(deserialize_with = "deserialize_or_parse_string_as_json", default)]
        nested: Option<TestStruct>,
    }

    #[test]
    fn test_fallback_deserialize_basic_types() {
        let test_cases = vec![
            (
                r#"{"value": 42, "optional": "test", "vector_int": [1, 2, 3]}"#,
                TestStruct {
                    value: 42,
                    optional: Some("test".to_string()),
                    vector_int: vec![1, 2, 3],
                    vector_str: vec![],
                },
            ),
            (
                r#"{"value": "42", "optional": null, "vector_int": [4, 5, 6]}"#,
                TestStruct {
                    value: 42,
                    optional: None,
                    vector_int: vec![4, 5, 6],
                    vector_str: vec![],
                },
            ),
            (
                r#"{"value": 42, "optional": "null", "vector_int": [4, 5, 6]}"#,
                TestStruct {
                    value: 42,
                    optional: None,
                    vector_int: vec![4, 5, 6],
                    vector_str: vec![],
                },
            ),
            (
                r#"{"value": 42, "optional": null, "vector_int": "[4, 5, 6]"}"#,
                TestStruct {
                    value: 42,
                    optional: None,
                    vector_int: vec![4, 5, 6],
                    vector_str: vec![],
                },
            ),
            (
                r#"{"value": 42, "vector_int": []}"#,
                TestStruct {
                    value: 42,
                    optional: None,
                    vector_int: vec![],
                    vector_str: vec![],
                },
            ),
            (
                r#"{"value": 42, "optional": "test", "vector_int": []}"#,
                TestStruct {
                    value: 42,
                    optional: Some("test".to_string()),
                    vector_int: vec![],
                    vector_str: vec![],
                },
            ),
        ];

        for (json, expected) in test_cases {
            let result: TestStruct = serde_json::from_str(json).unwrap();
            assert_eq!(result, expected);
        }
    }

    #[test]
    fn test_fallback_deserialize_string_vectors() {
        let test_cases = vec![
            (
                r#"{"value": 42, "optional": null, "vector_str": ["a", "b", "c"]}"#,
                vec!["a".to_string(), "b".to_string(), "c".to_string()],
            ),
            (
                r#"{"value": 42, "optional": null, "vector_str": "[\"a\", \"b\", \"c\"]"}"#,
                vec!["a".to_string(), "b".to_string(), "c".to_string()],
            ),
            (
                r#"{"vector_str": "['sub', 'email']"}"#,
                vec!["sub".to_string(), "email".to_string()],
            ),
            (
                r#"{"vector_str": "['sub', 'email', 'username']"}"#,
                vec![
                    "sub".to_string(),
                    "email".to_string(),
                    "username".to_string(),
                ],
            ),
            (
                r#"{"value": 42, "optional": null, "vector_str": []}"#,
                vec![],
            ),
        ];

        for (json, expected_vec) in test_cases {
            let result: TestStruct = serde_json::from_str(json).unwrap();
            assert_eq!(result.vector_str, expected_vec);
        }
    }

    #[test]
    #[allow(clippy::approx_constant)]
    fn test_fallback_deserialize_complex_types() {
        let test_cases = vec![
            (
                r#"{"boolean": "true", "float": 3.14, "nested": null}"#,
                ComplexTestStruct {
                    boolean: true,
                    float: 3.14,
                    nested: None,
                },
            ),
            (
                r#"{"boolean": false, "float": "3.14", "nested": null}"#,
                ComplexTestStruct {
                    boolean: false,
                    float: 3.14,
                    nested: None,
                },
            ),
            (
                r#"{
                    "boolean": true,
                    "float": 1.23,
                    "nested": "{\"value\":42,\"optional\":\"test\",\"vector_int\":[1,2,3]}"
                }"#,
                ComplexTestStruct {
                    boolean: true,
                    float: 1.23,
                    nested: Some(TestStruct {
                        value: 42,
                        optional: Some("test".to_string()),
                        vector_int: vec![1, 2, 3],
                        vector_str: vec![],
                    }),
                },
            ),
        ];

        for (json, expected) in test_cases {
            let result: ComplexTestStruct = serde_json::from_str(json).unwrap();
            assert_eq!(result, expected);
        }
    }

    #[test]
    fn test_fallback_deserialize_error_cases() {
        let test_cases = vec![
            (
                r#"{"value": "not a number", "optional": null, "vector_int": [], "vector_str": []}"#,
                "non-numeric string as i32",
            ),
            (
                r#"{"value": 42, "optional": null, "vector_int": [], "vector_str": "invalid"}"#,
                "invalid string as Vec<String>",
            ),
            (
                r#"{"value": 42, "optional": null, "vector_int": [], "vector_str": "[sub, email]"}"#,
                "invalid Python-style list",
            ),
            (
                r#"{"value": 42, "optional": null, "vector_int": "invalid", "vector_str": []}"#,
                "invalid string as Vec<i32>",
            ),
        ];

        for (json, error_desc) in test_cases {
            let result: Result<TestStruct, _> = serde_json::from_str(json);
            result.expect_err(&format!("should fail to parse {error_desc}"));
        }
    }
}