use std::fmt;
use serde::{Deserialize, Serialize};
use crate::{
graphql::{ParsedQuery, parse_query},
security::errors::{Result, SecurityError},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum IntrospectionPolicy {
Allowed,
Disabled,
InternalOnly,
}
impl IntrospectionPolicy {
#[must_use]
pub const fn from_config(enabled: bool, require_auth: bool) -> Self {
match (enabled, require_auth) {
(false, _) => Self::Disabled,
(true, true) => Self::InternalOnly,
(true, false) => Self::Allowed,
}
}
}
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,
}
impl IntrospectionConfig {
#[must_use]
pub const fn all() -> Self {
Self {
detect_schema: true,
detect_type: true,
}
}
#[must_use]
pub const fn strict() -> Self {
Self::all()
}
}
#[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 {
match parse_query(query) {
Ok(parsed) => self.is_introspection(&parsed),
Err(_) => false,
}
}
#[must_use]
pub fn is_introspection(&self, parsed: &ParsedQuery) -> bool {
parsed.selections.iter().any(|sel| {
(self.config.detect_schema && sel.name == "__schema")
|| (self.config.detect_type && sel.name == "__type")
})
}
#[must_use]
pub const fn policy(&self) -> IntrospectionPolicy {
self.policy
}
#[must_use]
pub const fn config(&self) -> &IntrospectionConfig {
&self.config
}
}