#[cfg(test)]
mod tests;
mod collector;
pub mod metadata;
mod rebuilder;
mod tree;
use std::sync::Arc;
use crate::pipeline::authorization::collector::{
collect_authorization_statuses, propagate_null_bubbling,
};
use crate::pipeline::authorization::metadata::AuthorizationMetadataExt;
use crate::pipeline::authorization::rebuilder::{
rebuild_authorized_operation, rebuild_authorized_projection_plan,
};
use crate::pipeline::authorization::tree::UnauthorizedPathTrie;
use crate::pipeline::coerce_variables::CoerceVariablesPayload;
use crate::pipeline::error::PipelineError;
use crate::pipeline::normalize::GraphQLNormalizationPayload;
use hive_router_config::authorization::UnauthorizedMode;
use hive_router_config::HiveRouterConfig;
use hive_router_internal::authorization::metadata::AuthorizationMetadata;
use hive_router_plan_executor::execution::client_request_details::JwtRequestDetails;
use hive_router_plan_executor::introspection::schema::SchemaMetadata;
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)
}
}
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,
} => {
(
Arc::new(GraphQLNormalizationPayload {
operation_for_plan: Arc::new(new_operation_definition),
operation_for_introspection: normalized_payload
.operation_for_introspection
.clone(),
root_type_name: normalized_payload.root_type_name,
projection_plan: Arc::new(new_projection_plan),
operation_indentity: normalized_payload.operation_indentity.clone(),
}),
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,
) -> AuthorizationDecision {
if auth_metadata.is_empty() {
return AuthorizationDecision::NoChange;
}
let user_context = create_user_auth_context(jwt_request_details, auth_metadata);
if user_context.is_authenticated && auth_metadata.scopes.is_empty() {
return AuthorizationDecision::NoChange;
}
let collection_result = collect_authorization_statuses(
&normalized_payload.operation_for_plan.selection_set,
normalized_payload.root_type_name,
schema_metadata,
variable_payload,
auth_metadata,
&user_context,
);
if collection_result.errors.is_empty() {
return AuthorizationDecision::NoChange;
}
if reject_mode {
tracing::debug!("Request rejected due to unauthorized fields and reject mode being set");
return AuthorizationDecision::Reject {
errors: collection_result.errors,
};
}
let removal_flags = if collection_result.has_non_null_unauthorized {
propagate_null_bubbling(&collection_result.checks)
} else {
vec![false; collection_result.checks.len()]
};
let unauthorized_path_trie =
UnauthorizedPathTrie::from_checks(&collection_result.checks, &removal_flags);
let new_operation = rebuild_authorized_operation(
&normalized_payload.operation_for_plan,
&unauthorized_path_trie,
);
let new_projection_plan = rebuild_authorized_projection_plan(
&normalized_payload.projection_plan,
&unauthorized_path_trie,
);
AuthorizationDecision::Modified {
new_operation_definition: new_operation,
new_projection_plan,
errors: collection_result.errors,
}
}
fn create_user_auth_context(
jwt_request_details: &JwtRequestDetails,
auth_metadata: &AuthorizationMetadata,
) -> UserAuthContext {
match jwt_request_details {
JwtRequestDetails::Authenticated { scopes, .. } => {
UserAuthContext::new(true, scopes.as_deref().unwrap_or(&[]), auth_metadata)
}
JwtRequestDetails::Unauthenticated => UserAuthContext::new(false, &[], auth_metadata),
}
}