pdk-token-introspection-lib 1.7.0

PDK Token Introspection Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

#[derive(Debug, Clone, PartialEq, Eq)]
enum ValidationMode {
    Any,
    All,
}

/// Validates token scopes against required scopes.
///
/// Use [`ScopesValidator::any()`] or [`ScopesValidator::all()`] to create instances.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopesValidator {
    mode: ValidationMode,
    required_scopes: Vec<String>,
}

impl ScopesValidator {
    /// Creates a validator that requires ANY of the specified scopes to be present.
    ///
    /// Returns `true` if the token has at least one of the required scopes,
    /// or if no scopes are required.
    pub fn any(required_scopes: Vec<String>) -> Self {
        Self {
            mode: ValidationMode::Any,
            required_scopes,
        }
    }

    /// Creates a validator that requires ALL of the specified scopes to be present.
    ///
    /// Returns `true` only if the token has all required scopes.
    pub fn all(required_scopes: Vec<String>) -> Self {
        Self {
            mode: ValidationMode::All,
            required_scopes,
        }
    }

    /// Validates if the token scopes satisfy the requirements.
    pub fn valid_scopes(&self, token_scopes: &[String]) -> bool {
        match self.mode {
            ValidationMode::Any => {
                token_scopes
                    .iter()
                    .any(|scope| self.required_scopes.contains(scope))
                    || self.required_scopes.is_empty()
            }
            ValidationMode::All => self
                .required_scopes
                .iter()
                .all(|scope| token_scopes.contains(scope)),
        }
    }
}

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

    fn scopes(s: &[&str]) -> Vec<String> {
        s.iter().map(|x| x.to_string()).collect()
    }

    #[test]
    fn all_validator_requires_all_scopes() {
        let validator = ScopesValidator::all(scopes(&["read", "write"]));
        // Valid: has all required scopes
        assert!(validator.valid_scopes(&scopes(&["read", "write", "delete"])));
        assert!(validator.valid_scopes(&scopes(&["read", "write"])));
        // Invalid: missing at least one
        assert!(!validator.valid_scopes(&scopes(&["read"])));
        assert!(!validator.valid_scopes(&scopes(&["write"])));
        assert!(!validator.valid_scopes(&scopes(&[])));
    }

    #[test]
    fn any_validator_requires_any_scope() {
        let validator = ScopesValidator::any(scopes(&["read", "write"]));
        // Valid: has at least one required scope
        assert!(validator.valid_scopes(&scopes(&["read"])));
        assert!(validator.valid_scopes(&scopes(&["write"])));
        assert!(validator.valid_scopes(&scopes(&["read", "other"])));
        // Invalid: has none of the required
        assert!(!validator.valid_scopes(&scopes(&["delete"])));
        assert!(!validator.valid_scopes(&scopes(&["other", "another"])));
    }

    #[test]
    fn empty_required_scopes_behavior() {
        // ANY with empty required: always valid
        let any_validator = ScopesValidator::any(vec![]);
        assert!(any_validator.valid_scopes(&scopes(&[])));
        assert!(any_validator.valid_scopes(&scopes(&["any"])));

        // ALL with empty required: always valid (vacuously true)
        let all_validator = ScopesValidator::all(vec![]);
        assert!(all_validator.valid_scopes(&scopes(&[])));
        assert!(all_validator.valid_scopes(&scopes(&["any"])));

        // Non-empty required behaves differently
        let non_empty = ScopesValidator::all(scopes(&["required"]));
        assert!(!non_empty.valid_scopes(&scopes(&[])));
    }
}