use std::fmt;
use serde::{Deserialize, Serialize};
use crate::security::errors::{Result, SecurityError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum IntrospectionPolicy {
Allowed,
Disabled,
InternalOnly,
}
impl fmt::Display for IntrospectionPolicy {
#[cfg_attr(test, mutants::skip)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Allowed => write!(f, "Allowed"),
Self::Disabled => write!(f, "Disabled"),
Self::InternalOnly => write!(f, "InternalOnly"),
}
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionConfig {
pub detect_schema: bool,
pub detect_type: bool,
pub detect_typename: bool,
pub detect_directive: bool,
}
impl IntrospectionConfig {
#[must_use]
pub const fn all() -> Self {
Self {
detect_schema: true,
detect_type: true,
detect_typename: true,
detect_directive: true,
}
}
#[must_use]
pub const fn strict() -> Self {
Self {
detect_schema: true,
detect_type: true,
detect_typename: false,
detect_directive: true,
}
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionEnforcer {
policy: IntrospectionPolicy,
config: IntrospectionConfig,
}
impl IntrospectionEnforcer {
#[must_use]
pub const fn new(policy: IntrospectionPolicy) -> Self {
Self {
policy,
config: IntrospectionConfig::all(),
}
}
#[must_use]
pub const fn with_config(policy: IntrospectionPolicy, config: IntrospectionConfig) -> Self {
Self { policy, config }
}
#[must_use]
pub const fn allowed() -> Self {
Self::new(IntrospectionPolicy::Allowed)
}
#[must_use]
pub const fn disabled() -> Self {
Self::new(IntrospectionPolicy::Disabled)
}
#[must_use]
pub const fn internal_only() -> Self {
Self::new(IntrospectionPolicy::InternalOnly)
}
pub fn validate_query(&self, query: &str, authenticated_user_id: Option<&str>) -> Result<()> {
let is_introspection = self.is_introspection_query(query);
if !is_introspection {
return Ok(());
}
match self.policy {
IntrospectionPolicy::Allowed => {
Ok(())
},
IntrospectionPolicy::Disabled => {
Err(SecurityError::IntrospectionDisabled {
detail: "Introspection queries are disabled in this environment".to_string(),
})
},
IntrospectionPolicy::InternalOnly => {
if authenticated_user_id.is_some() {
Ok(())
} else {
Err(SecurityError::IntrospectionDisabled {
detail: "Introspection queries require authentication".to_string(),
})
}
},
}
}
pub(crate) fn is_introspection_query(&self, query: &str) -> bool {
let query_lower = query.to_lowercase();
if self.config.detect_schema && query_lower.contains("__schema") {
return true;
}
if self.config.detect_type && query_lower.contains("__type") {
return true;
}
if self.config.detect_typename && query_lower.contains("__typename") {
return true;
}
if self.config.detect_directive && query_lower.contains("__directive") {
return true;
}
false
}
#[must_use]
pub const fn policy(&self) -> IntrospectionPolicy {
self.policy
}
#[must_use]
pub const fn config(&self) -> &IntrospectionConfig {
&self.config
}
}