Add secret attribute for arguments, they will not appear in the log. #463

This commit is contained in:
Sunli 2021-04-07 16:23:28 +08:00
parent 709bb49e07
commit a9ac598e2e
15 changed files with 437 additions and 96 deletions

View File

@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- Add `secret` attribute for arguments, they will not appear in the log.
```rust
#[Object]
impl Query {
async fn login(&self, username:String, #[graphql(secret)] password: String) -> i32 {
todo!()
}
}
```
## [2.8.0] 2021-04-05
### Changed

View File

@ -185,6 +185,7 @@ pub struct Argument {
pub validator: Option<Meta>,
pub key: bool, // for entity
pub visible: Option<Visible>,
pub secret: bool,
}
#[derive(FromMeta, Default)]
@ -322,6 +323,8 @@ pub struct InputObjectField {
pub skip: bool,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub secret: bool,
}
#[derive(FromDeriveInput)]
@ -357,6 +360,8 @@ pub struct InterfaceFieldArgument {
pub default_with: Option<LitStr>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub secret: bool,
}
#[derive(FromMeta)]
@ -441,6 +446,7 @@ pub struct SubscriptionFieldArgument {
pub default_with: Option<LitStr>,
pub validator: Option<Meta>,
pub visible: Option<Visible>,
pub secret: bool,
}
#[derive(FromMeta, Default)]

View File

@ -122,6 +122,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
}
})
.unwrap_or_else(|| quote!(::std::option::Option::None));
let secret = field.secret;
if let Some(default) = default {
get_fields.push(quote! {
@ -161,6 +162,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
default_value: #schema_default,
validator: #validator,
visible: #visible,
is_secret: #secret,
});
})
}

View File

@ -184,6 +184,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
default,
default_with,
visible,
secret,
} in args
{
let ident = Ident::new(name, Span::call_site());
@ -229,6 +230,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
default_value: #schema_default,
validator: ::std::option::Option::None,
visible: #visible,
is_secret: #secret,
});
});
}

View File

@ -350,6 +350,7 @@ pub fn generate(
default_with,
validator,
visible,
secret,
..
},
) in &args
@ -392,6 +393,7 @@ pub fn generate(
default_value: #schema_default,
validator: #validator,
visible: #visible,
is_secret: #secret,
});
});

View File

@ -152,6 +152,7 @@ pub fn generate(
default_with,
validator,
visible: arg_visible,
secret,
},
) in &args
{
@ -194,6 +195,7 @@ pub fn generate(
default_value: #schema_default,
validator: #validator,
visible: #visible,
is_secret: #secret,
});
});

View File

@ -1,13 +1,11 @@
use std::fmt::{self, Display, Formatter};
use std::fmt::Write;
use std::sync::Arc;
use futures_util::lock::Mutex;
use crate::extensions::{
Extension, ExtensionContext, ExtensionFactory, NextParseQuery, NextResolve, ResolveInfo,
Extension, ExtensionContext, ExtensionFactory, NextExecute, NextParseQuery,
};
use crate::parser::types::{ExecutableDocument, OperationType, Selection};
use crate::{PathSegment, ServerError, ServerResult, Value, Variables};
use crate::{PathSegment, Response, ServerResult, Variables};
/// Logger extension
#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
@ -15,25 +13,11 @@ pub struct Logger;
impl ExtensionFactory for Logger {
fn create(&self) -> Arc<dyn Extension> {
Arc::new(LoggerExtension {
inner: Mutex::new(Inner {
enabled: true,
query: String::new(),
variables: Default::default(),
}),
})
Arc::new(LoggerExtension)
}
}
struct Inner {
enabled: bool,
query: String,
variables: Variables,
}
struct LoggerExtension {
inner: Mutex<Inner>,
}
struct LoggerExtension;
#[async_trait::async_trait]
impl Extension for LoggerExtension {
@ -44,82 +28,53 @@ impl Extension for LoggerExtension {
variables: &Variables,
next: NextParseQuery<'_>,
) -> ServerResult<ExecutableDocument> {
let mut inner = self.inner.lock().await;
inner.query = query.replace(char::is_whitespace, "");
inner.variables = variables.clone();
let document = next.run(ctx, query, variables).await?;
let is_schema = document
.operations
.iter()
.filter(|(_, operation)| operation.node.ty == OperationType::Query)
.any(|(_, operation)| operation.node.selection_set.node.items.iter().any(|selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema")));
inner.enabled = !is_schema;
log::info!(target: "async-graphql", "[Query] query: \"{}\", variables: {}", inner.query, inner.variables);
if !is_schema {
log::info!(
target: "async-graphql",
"[Execute] {}", ctx.stringify_execute_doc(&document, variables)
);
}
Ok(document)
}
async fn resolve(
&self,
ctx: &ExtensionContext<'_>,
info: ResolveInfo<'_>,
next: NextResolve<'_>,
) -> ServerResult<Option<Value>> {
let enabled = self.inner.lock().await.enabled;
if enabled {
let path = info.path_node.to_string();
log::trace!(target: "async-graphql", "[ResolveStart] path: \"{}\"", path);
let res = next.run(ctx, info).await;
if let Err(err) = &res {
let inner = self.inner.lock().await;
log::error!(
target: "async-graphql",
"{}",
DisplayError { query:&inner.query,variables:&inner.variables, e: &err }
);
async fn execute(&self, ctx: &ExtensionContext<'_>, next: NextExecute<'_>) -> Response {
let resp = next.run(ctx).await;
if resp.is_err() {
for err in &resp.errors {
if !err.path.is_empty() {
let mut path = String::new();
for (idx, s) in err.path.iter().enumerate() {
if idx > 0 {
path.push('.');
}
match s {
PathSegment::Index(idx) => {
let _ = write!(&mut path, "{}", idx);
}
PathSegment::Field(name) => {
let _ = write!(&mut path, "{}", name);
}
}
}
log::error!(
target: "async-graphql",
"[Error] path={} message={}", path, err.message,
);
} else {
log::error!(
target: "async-graphql",
"[Error] message={}", err.message,
);
}
}
log::trace!(target: "async-graphql", "[ResolveEnd] path: \"{}\"", path);
res
} else {
next.run(ctx, info).await
}
}
}
struct DisplayError<'a> {
query: &'a str,
variables: &'a Variables,
e: &'a ServerError,
}
impl<'a> Display for DisplayError<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "[Error] ")?;
if !self.e.path.is_empty() {
write!(f, "path: ")?;
for (i, segment) in self.e.path.iter().enumerate() {
if i != 0 {
write!(f, ".")?;
}
match segment {
PathSegment::Field(field) => write!(f, "{}", field),
PathSegment::Index(i) => write!(f, "{}", i),
}?;
}
write!(f, ", ")?;
}
if !self.e.locations.is_empty() {
write!(f, "pos: [")?;
for (i, location) in self.e.locations.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{}:{}", location.line, location.column)?;
}
write!(f, "], ")?;
}
write!(f, r#"query: "{}", "#, self.query)?;
write!(f, "variables: {}", self.variables)?;
write!(f, "{}", self.e.message)
resp
}
}

View File

@ -37,7 +37,7 @@ use crate::{
/// Context for extension
pub struct ExtensionContext<'a> {
#[doc(hidden)]
pub schema_data: &'a Data,
pub schema_env: &'a SchemaEnv,
#[doc(hidden)]
pub session_data: &'a Data,
@ -47,6 +47,16 @@ pub struct ExtensionContext<'a> {
}
impl<'a> ExtensionContext<'a> {
/// Convert the specified [ExecutableDocument] into a query string.
///
/// Usually used for log extension, it can hide secret arguments.
pub fn stringify_execute_doc(&self, doc: &ExecutableDocument, variables: &Variables) -> String {
self.schema_env
.registry
.stringify_exec_doc(variables, doc)
.unwrap_or_default()
}
/// Gets the global data defined in the `Context` or `Schema`.
///
/// If both `Schema` and `Query` have the same data type, the data in the `Query` is obtained.
@ -78,7 +88,7 @@ impl<'a> ExtensionContext<'a> {
self.query_data
.and_then(|query_data| query_data.get(&TypeId::of::<D>()))
.or_else(|| self.session_data.get(&TypeId::of::<D>()))
.or_else(|| self.schema_data.get(&TypeId::of::<D>()))
.or_else(|| self.schema_env.data.get(&TypeId::of::<D>()))
.and_then(|d| d.downcast_ref::<D>())
}
}
@ -393,7 +403,7 @@ impl Extensions {
#[inline]
fn create_context(&self) -> ExtensionContext {
ExtensionContext {
schema_data: &self.schema_env.data,
schema_env: &self.schema_env,
session_data: &self.session_data,
query_data: self.query_data.as_deref(),
}

View File

@ -98,9 +98,18 @@ impl<T: Tracer + Send + Sync> Extension for OpenTelemetryExtension<T> {
.with_kind(SpanKind::Server)
.with_attributes(attributes)
.start(&*self.tracer);
next.run(ctx, query, variables)
.with_context(OpenTelemetryContext::current_with_span(span))
.await
async move {
let res = next.run(ctx, query, variables).await;
if let Ok(doc) = &res {
OpenTelemetryContext::current()
.span()
.set_attribute(KEY_SOURCE.string(ctx.stringify_execute_doc(doc, variables)));
}
res
}
.with_context(OpenTelemetryContext::current_with_span(span))
.await
}
async fn validation(

View File

@ -85,10 +85,19 @@ impl Extension for TracingExtension {
target: "async_graphql::graphql",
Level::INFO,
"parse",
source = query,
variables = %serde_json::to_string(&variables).unwrap(),
);
next.run(ctx, query, variables).instrument(span).await
async move {
let res = next.run(ctx, query, variables).await;
if let Ok(doc) = &res {
tracinglib::Span::current().record(
"source",
&ctx.stringify_execute_doc(doc, variables).as_str(),
);
}
res
}
.instrument(span)
.await
}
async fn validation(

View File

@ -283,6 +283,7 @@ pub type FieldResult<T> = Result<T>;
/// | complexity | Custom field complexity. | string | Y |
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
/// | key | Is entity key(for Federation) | bool | Y |
///
/// # Valid field return types
@ -630,6 +631,7 @@ pub use async_graphql_derive::Enum;
/// | skip | Skip this field, use `Default::default` to get a default value for this field. | bool | Y |
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Examples
///
@ -710,6 +712,7 @@ pub use async_graphql_derive::InputObject;
/// | default_with | Expression to generate default value | code string | Y |
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Define an interface
///
@ -908,6 +911,7 @@ pub use async_graphql_derive::Union;
/// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Field argument parameters
///

View File

@ -1,5 +1,6 @@
mod cache_control;
mod export_sdl;
mod stringify_exec_doc;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
@ -108,6 +109,7 @@ pub struct MetaInputValue {
pub default_value: Option<String>,
pub validator: Option<Arc<dyn InputValueValidator>>,
pub visible: Option<MetaVisibleFn>,
pub is_secret: bool,
}
type ComputeComplexityFn = fn(
@ -552,6 +554,7 @@ impl Registry {
default_value: None,
validator: None,
visible: None,
is_secret: false,
},
);
args

View File

@ -0,0 +1,315 @@
use std::fmt::{Error, Result as FmtResult, Write};
use async_graphql_value::ConstValue;
use crate::parser::types::{
ExecutableDocument, FragmentDefinition, OperationType, Selection, SelectionSet,
};
use crate::registry::{MetaInputValue, MetaType, MetaTypeName, Registry};
use crate::Variables;
impl Registry {
pub(crate) fn stringify_exec_doc(
&self,
variables: &Variables,
doc: &ExecutableDocument,
) -> Result<String, Error> {
let mut output = String::new();
for (name, fragment) in &doc.fragments {
self.stringify_fragment_definition(
&mut output,
variables,
&*name,
self.types
.get(fragment.node.type_condition.node.on.node.as_str()),
&fragment.node,
)?;
}
for (name, operation_definition) in doc.operations.iter() {
write!(&mut output, "{} ", operation_definition.node.ty)?;
if let Some(name) = name {
write!(&mut output, "{}", name)?;
if !operation_definition.node.variable_definitions.is_empty() {
output.push('(');
for (idx, variable_definition) in operation_definition
.node
.variable_definitions
.iter()
.enumerate()
{
if idx > 0 {
output.push_str(", ");
}
write!(
output,
"{}: {}",
variable_definition.node.name.node,
variable_definition.node.var_type.node
);
if let Some(default_value) = &variable_definition.node.default_value {
write!(output, " = {}", default_value.node);
}
}
output.push(')');
}
output.push(' ');
}
let root_type = match operation_definition.node.ty {
OperationType::Query => self.types.get(&self.query_type),
OperationType::Mutation => self
.mutation_type
.as_ref()
.and_then(|name| self.types.get(name)),
OperationType::Subscription => self
.subscription_type
.as_ref()
.and_then(|name| self.types.get(name)),
};
self.stringify_selection_set(
&mut output,
variables,
&operation_definition.node.selection_set.node,
root_type,
)?;
}
Ok(output)
}
fn stringify_fragment_definition(
&self,
output: &mut String,
variables: &Variables,
name: &str,
parent_type: Option<&MetaType>,
fragment_definition: &FragmentDefinition,
) -> FmtResult {
write!(
output,
"fragment {} on {}",
name, fragment_definition.type_condition.node.on.node
)?;
self.stringify_selection_set(
output,
variables,
&fragment_definition.selection_set.node,
parent_type,
)?;
output.push_str("}\n\n");
Ok(())
}
fn stringify_input_value(
&self,
output: &mut String,
meta_input_value: Option<&MetaInputValue>,
value: &ConstValue,
) -> FmtResult {
if meta_input_value.map(|v| v.is_secret).unwrap_or_default() {
output.push_str("\"<secret>\"");
return Ok(());
}
match value {
ConstValue::Object(obj) => {
let parent_type = meta_input_value.and_then(|input_value| {
self.types
.get(MetaTypeName::concrete_typename(&input_value.ty))
});
if let Some(MetaType::InputObject { input_fields, .. }) = parent_type {
output.push('{');
for (idx, (key, value)) in obj.iter().enumerate() {
if idx > 0 {
output.push_str(", ");
}
write!(output, "{}: ", key)?;
self.stringify_input_value(output, input_fields.get(key.as_str()), value)?;
}
output.push('}');
} else {
write!(output, "{}", value)?;
}
}
_ => write!(output, "{}", value)?,
}
Ok(())
}
fn stringify_selection_set(
&self,
output: &mut String,
variables: &Variables,
selection_set: &SelectionSet,
parent_type: Option<&MetaType>,
) -> FmtResult {
output.push_str("{ ");
for (idx, selection) in selection_set.items.iter().map(|s| &s.node).enumerate() {
if idx > 0 {
output.push(' ');
}
match selection {
Selection::Field(field) => {
if let Some(alias) = &field.node.alias {
write!(output, "{}:", alias.node)?;
}
write!(output, "{}", field.node.name.node)?;
if !field.node.arguments.is_empty() {
output.push('(');
for (idx, (name, argument)) in field.node.arguments.iter().enumerate() {
let meta_input_value = parent_type
.and_then(|parent_type| {
parent_type.field_by_name(field.node.name.node.as_str())
})
.and_then(|field| field.args.get(name.node.as_str()));
if idx > 0 {
output.push_str(", ");
}
write!(output, "{}: ", name)?;
let value = argument
.node
.clone()
.into_const_with(|name| variables.get(&name).cloned().ok_or(()))
.unwrap_or_default();
self.stringify_input_value(output, meta_input_value, &value)?;
}
output.push(')');
}
if !field.node.selection_set.node.items.is_empty() {
output.push(' ');
let parent_type = parent_type
.and_then(|ty| ty.field_by_name(field.node.name.node.as_str()))
.and_then(|field| {
self.types.get(MetaTypeName::concrete_typename(&field.ty))
});
self.stringify_selection_set(
output,
variables,
&field.node.selection_set.node,
parent_type,
)?;
}
}
Selection::FragmentSpread(fragment_spread) => {
write!(output, "... {}", fragment_spread.node.fragment_name.node)?;
}
Selection::InlineFragment(inline_fragment) => {
output.push_str("... ");
let parent_type = if let Some(name) = &inline_fragment.node.type_condition {
write!(output, "on {} ", name.node.on.node)?;
self.types.get(name.node.on.node.as_str())
} else {
None
};
self.stringify_selection_set(
output,
variables,
&inline_fragment.node.selection_set.node,
parent_type,
)?;
}
}
}
output.push_str(" }");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_query;
use crate::*;
#[test]
fn test_stringify() {
let registry = Registry::default();
let doc = parse_query(
r#"
query Abc {
a b c(a:1,b:2) {
d e f
}
}
"#,
)
.unwrap();
assert_eq!(
registry
.stringify_exec_doc(&Default::default(), &doc)
.unwrap(),
r#"query Abc { a b c(a: 1, b: 2) { d e f } }"#
);
let doc = parse_query(
r#"
query Abc($a:Int) {
value(input:$a)
}
"#,
)
.unwrap();
assert_eq!(
registry
.stringify_exec_doc(
&Variables::from_value(value! ({
"a": 10,
})),
&doc
)
.unwrap(),
r#"query Abc { value(input: 10) }"#
);
}
#[test]
fn test_stringify_secret() {
#[derive(InputObject)]
#[graphql(internal)]
struct MyInput {
v1: i32,
#[graphql(secret)]
v2: i32,
v3: MyInput2,
}
#[derive(InputObject)]
#[graphql(internal)]
struct MyInput2 {
v4: i32,
#[graphql(secret)]
v5: i32,
}
struct Query;
#[Object(internal)]
#[allow(unreachable_code, unused_variables)]
impl Query {
async fn value(&self, a: i32, #[graphql(secret)] b: i32, c: MyInput) -> i32 {
todo!()
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let registry = schema.registry();
let s = registry
.stringify_exec_doc(
&Default::default(),
&parse_query(
r#"
{
value(a: 10, b: 20, c: { v1: 1, v2: 2, v3: { v4: 4, v5: 5}})
}
"#,
)
.unwrap(),
)
.unwrap();
assert_eq!(
s,
r#"query { value(a: 10, b: "<secret>", c: {v1: 1, v2: "<secret>", v3: {v4: 4, v5: "<secret>"}}) }"#
);
}
}

View File

@ -271,6 +271,7 @@ where
default_value: None,
validator: None,
visible: None,
is_secret: false,
});
args
}
@ -293,6 +294,7 @@ where
default_value: None,
validator: None,
visible: None,
is_secret: false,
});
args
}
@ -332,6 +334,12 @@ where
Self::build(query, mutation, subscription).finish()
}
#[inline]
#[allow(unused)]
pub(crate) fn registry(&self) -> &Registry {
&self.env.registry
}
/// Returns SDL(Schema Definition Language) of this schema.
pub fn sdl(&self) -> String {
self.0.env.registry.export_sdl(false)

View File

@ -67,6 +67,7 @@ impl<T: Type> Type for QueryRoot<T> {
default_value: None,
validator: None,
visible: None,
is_secret: false,
},
);
args