Skip to main content

graphql_strip_sensitive_literals/
lib.rs

1//! Redact sensitive literals from a GraphQL document before logging or usage
2//! reporting.
3//!
4//! GraphQL operations often inline user data as literal values. A query like
5//! `user(email: "a@b.com")` carries the email in the operation text. If that
6//! text is logged or sent to a usage-reporting service, the data leaks. This
7//! crate rewrites those literals to fixed placeholders so the operation shape
8//! stays useful while the data is gone.
9//!
10//! [`strip_sensitive_literals`] returns a redacted copy of a parsed document.
11//! Scalar literals are replaced:
12//!
13//! - integers and floats become `0`
14//! - strings become `""`
15//!
16//! With [`StripOptions::hide_list_and_object_literals`] set, list literals
17//! become `[]` and object literals become `{}`.
18//!
19//! Everything else is left as it is. Field names, aliases, variable names and
20//! types, fragment names, directives, and the `Boolean`, `Null`, `Enum`, and
21//! variable values stay untouched. Booleans, nulls, and enums are not treated
22//! as sensitive.
23//!
24//! The walk covers the whole document. Operations and fragments are redacted.
25//! Type-system definitions are too, so literals in field-argument defaults,
26//! input-field defaults, and directive arguments collapse the same way.
27//!
28//! The best defense is to avoid inline literals. Pass user data through GraphQL
29//! variables so it never appears in the operation text. This crate is the
30//! backstop for operations that still inline data.
31//!
32//! # apollo-compiler in the public API
33//!
34//! The document type is [`apollo_compiler::ast::Document`], re-exported here as
35//! [`Document`]. It appears in the signature of [`strip_sensitive_literals`], so
36//! it is part of this crate's public API. A caller builds input with
37//! `Document::parse` from `apollo-compiler`, which means the caller depends on
38//! `apollo-compiler` directly. The re-export names the boundary type so call
39//! sites can reach it through one path.
40//!
41//! This couples the crate to the `apollo-compiler` major version. The dependency
42//! is pinned to `1.x`. A consumer that passes a `Document` across this boundary
43//! must resolve to the same `apollo-compiler` major. An `apollo-compiler` `2.0`
44//! would change this crate's public type and require a matching major bump here.
45//!
46//! # Example
47//!
48//! ```
49//! use apollo_compiler::ast::Document;
50//! use graphql_strip_sensitive_literals::{strip_sensitive_literals, StripOptions};
51//!
52//! let doc = Document::parse(
53//!     r#"{ user(name: "Ada", age: 36) { id } }"#,
54//!     "query.graphql",
55//! )
56//! .unwrap();
57//!
58//! let redacted = strip_sensitive_literals(&doc, StripOptions::default());
59//! assert_eq!(
60//!     redacted.to_string().trim(),
61//!     "{\n  user(name: \"\", age: 0) {\n    id\n  }\n}",
62//! );
63//! ```
64
65#![forbid(unsafe_code)]
66#![warn(missing_docs)]
67
68mod options;
69mod strip;
70
71pub use apollo_compiler::ast::Document;
72pub use options::{StripOptions, StripOptionsBuilder};
73
74/// Return a redacted copy of `ast`.
75///
76/// Scalar literals are replaced with fixed placeholders. Integers and floats
77/// become `0`, strings become `""`. When `options.hide_list_and_object_literals`
78/// is set, list literals become `[]` and object literals become `{}`. In the
79/// default mode lists and objects keep their shape and their nested scalars are
80/// redacted instead.
81///
82/// Non-literal nodes are preserved. So are `Boolean`, `Null`, `Enum`, and
83/// variable values, which are not treated as sensitive.
84///
85/// The input is not modified. The returned [`Document`] is a fresh owned tree.
86/// Applying the function twice gives the same result as applying it once.
87///
88/// # Example
89///
90/// ```
91/// use apollo_compiler::ast::Document;
92/// use graphql_strip_sensitive_literals::{strip_sensitive_literals, StripOptions};
93///
94/// let doc = Document::parse(
95///     r#"{ f(list: [1, 2], obj: { k: "v" }) }"#,
96///     "query.graphql",
97/// )
98/// .unwrap();
99///
100/// // Default: keep list and object shape, redact the scalars inside.
101/// let kept = strip_sensitive_literals(&doc, StripOptions::default());
102/// assert!(kept.to_string().contains(r#"f(list: [0, 0], obj: {k: ""})"#));
103///
104/// // Opt in: empty the list and the object.
105/// let opts = StripOptions::builder()
106///     .hide_list_and_object_literals(true)
107///     .build();
108/// let hidden = strip_sensitive_literals(&doc, opts);
109/// assert!(hidden.to_string().contains("f(list: [], obj: {})"));
110/// ```
111#[must_use = "redaction returns a new document and does not modify `ast` in place"]
112pub fn strip_sensitive_literals(ast: &Document, options: StripOptions) -> Document {
113    strip::strip(ast, options)
114}