#[cfg(test)]
mod tests;
pub mod metadata;
use std::sync::Arc;
use crate::pipeline::authorization::metadata::AuthorizationMetadataExt;
use crate::pipeline::error::PipelineError;
use crate::pipeline::normalize::GraphQLNormalizationPayload;
use crate::pipeline::nullify::rebuilder::{
rebuild_nulled_operation, rebuild_nulled_projection_plan,
};
use crate::pipeline::trie::Trie;
use crate::utils::StrByAddr;
use ahash::HashMap;
use hive_router_config::authorization::UnauthorizedMode;
use hive_router_config::HiveRouterConfig;
use hive_router_internal::authorization::metadata::{AuthorizationMetadata, AuthorizationRule};
use hive_router_plan_executor::execution::client_request_details::JwtRequestDetails;
use hive_router_plan_executor::execution::plan::CoerceVariablesPayload;
use hive_router_plan_executor::introspection::schema::SchemaMetadata;
use hive_router_plan_executor::operation_filter::{OperationFilter, Selection};
use hive_router_plan_executor::projection::plan::FieldProjectionPlan;
use hive_router_plan_executor::response::graphql_error::GraphQLError;
use hive_router_query_planner::ast::operation::OperationDefinition;
use hive_router_internal::telemetry::traces::spans::graphql::GraphQLAuthorizeSpan;
pub use metadata::{AuthorizationMetadataError, UserAuthContext};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizationError {
pub path: String,
}
#[derive(Debug)]
pub enum AuthorizationDecision {
NoChange,
Modified {
new_operation_definition: OperationDefinition,
new_projection_plan: Vec<FieldProjectionPlan>,
errors: Vec<AuthorizationError>,
},
Reject { errors: Vec<AuthorizationError> },
}
impl From<&AuthorizationError> for GraphQLError {
fn from(auth_error: &AuthorizationError) -> Self {
GraphQLError::from_message_and_code(
"Unauthorized field or type",
"UNAUTHORIZED_FIELD_OR_TYPE",
)
.add_affected_path(&auth_error.path)
}
}
impl From<&GraphQLError> for AuthorizationError {
fn from(error: &GraphQLError) -> Self {
AuthorizationError {
path: error.extensions.affected_path.clone().unwrap_or_default(),
}
}
}
fn unauthorized_error() -> GraphQLError {
GraphQLError::from_message_and_code("Unauthorized field or type", "UNAUTHORIZED_FIELD_OR_TYPE")
}
struct AuthorizationChecker<'a, 'op> {
auth_metadata: &'a AuthorizationMetadata,
user_context: &'a UserAuthContext,
cache: HashMap<StrByAddr<'op>, bool>,
}
impl<'op> AuthorizationChecker<'_, 'op> {
fn parent_has_auth(&self, parent_type_name: &str) -> bool {
self.auth_metadata
.type_has_any_auth
.get(parent_type_name)
.copied()
.unwrap_or(true)
}
fn is_type_authorized(&mut self, type_name: &'op str) -> bool {
let key = StrByAddr(type_name);
if let Some(&authorized) = self.cache.get(&key) {
return authorized;
}
let authorized = self
.auth_metadata
.type_rules
.get(type_name)
.is_none_or(|rule| self.is_rule_satisfied(rule));
self.cache.insert(key, authorized);
authorized
}
fn is_field_authorized(&self, type_name: &str, field_name: &str) -> bool {
let Some(rule) = self
.auth_metadata
.field_rules
.get(type_name)
.and_then(|fields| fields.get(field_name))
else {
return true;
};
self.is_rule_satisfied(rule)
}
fn is_rule_satisfied(&self, rule: &AuthorizationRule) -> bool {
match rule {
AuthorizationRule::Authenticated => self.user_context.is_authenticated,
AuthorizationRule::RequiresScopes(scopes) => {
self.user_context.is_authenticated
&& scopes.0.iter().any(|and_group| {
and_group
.0
.iter()
.all(|scope_id| self.user_context.scope_ids.contains(scope_id))
})
}
}
}
}
pub fn enforce_operation_authorization(
router_config: &HiveRouterConfig,
normalized_payload: &Arc<GraphQLNormalizationPayload>,
auth_metadata: &AuthorizationMetadata,
schema_metadata: &SchemaMetadata,
variable_payload: &CoerceVariablesPayload,
jwt_request_details: &JwtRequestDetails,
) -> Result<(Arc<GraphQLNormalizationPayload>, Vec<AuthorizationError>), PipelineError> {
if !router_config.authorization.directives.enabled {
return Ok((normalized_payload.clone(), vec![]));
}
if !router_config.jwt.enabled {
return Ok((normalized_payload.clone(), vec![]));
}
let span = GraphQLAuthorizeSpan::new();
let _guard = span.span.enter();
let reject_mode =
router_config.authorization.directives.unauthorized.mode == UnauthorizedMode::Reject;
let decision = apply_authorization_to_operation(
normalized_payload,
auth_metadata,
schema_metadata,
variable_payload,
jwt_request_details,
reject_mode,
)?;
Ok(match decision {
AuthorizationDecision::NoChange => (normalized_payload.clone(), vec![]),
AuthorizationDecision::Modified {
new_operation_definition,
new_projection_plan,
errors,
} => (
normalized_payload.with_operation(new_operation_definition, new_projection_plan),
errors,
),
AuthorizationDecision::Reject { errors } => {
return Err(PipelineError::AuthorizationFailed(errors));
}
})
}
pub fn apply_authorization_to_operation(
normalized_payload: &GraphQLNormalizationPayload,
auth_metadata: &AuthorizationMetadata,
schema_metadata: &SchemaMetadata,
variable_payload: &CoerceVariablesPayload,
jwt_request_details: &JwtRequestDetails,
reject_mode: bool,
) -> Result<AuthorizationDecision, PipelineError> {
if auth_metadata.is_empty() {
return Ok(AuthorizationDecision::NoChange);
}
let user_context = UserAuthContext::from_jwt(jwt_request_details, auth_metadata);
if user_context.is_authenticated && auth_metadata.scopes.is_empty() {
return Ok(AuthorizationDecision::NoChange);
}
let mut checker = AuthorizationChecker {
auth_metadata,
user_context: &user_context,
cache: HashMap::default(),
};
let operation_filter_output = OperationFilter::new(schema_metadata).filter(
normalized_payload.root_type_name,
&normalized_payload.operation_for_plan.selection_set,
variable_payload,
|selection| match selection {
Selection::Field(field) => {
if !checker.parent_has_auth(field.parent_type_name) {
return selection.keep();
}
let is_authorized = checker.is_type_authorized(field.parent_type_name)
&& checker.is_field_authorized(field.parent_type_name, field.field_name)
&& checker.is_type_authorized(field.output_type_name);
if is_authorized {
selection.keep()
} else {
selection.reject(unauthorized_error())
}
}
Selection::Fragment(fragment) => {
if !checker.parent_has_auth(fragment.parent_type_name) {
return selection.keep();
}
if checker.is_type_authorized(fragment.type_condition) {
selection.keep()
} else {
selection.reject(unauthorized_error())
}
}
},
)?;
if operation_filter_output.errors.is_empty() {
return Ok(AuthorizationDecision::NoChange);
}
let errors: Vec<AuthorizationError> = operation_filter_output
.errors
.iter()
.map(AuthorizationError::from)
.collect();
if reject_mode {
tracing::debug!("Request rejected due to unauthorized fields and reject mode being set");
return Ok(AuthorizationDecision::Reject { errors });
}
let nulled_field_trie = Trie::from_paths(&operation_filter_output.rejected_paths);
let new_operation =
rebuild_nulled_operation(&normalized_payload.operation_for_plan, &nulled_field_trie);
let new_projection_plan =
rebuild_nulled_projection_plan(&normalized_payload.projection_plan, &nulled_field_trie);
Ok(AuthorizationDecision::Modified {
new_operation_definition: new_operation,
new_projection_plan,
errors,
})
}