Skip to main content

launchdarkly_server_sdk_evaluation/contexts/
context.rs

1use super::attribute_reference::Reference;
2use crate::contexts::context_serde::ContextVariant;
3use crate::{AttributeValue, MultiContextBuilder};
4use itertools::Itertools;
5use log::warn;
6use maplit::hashmap;
7use serde::de::Error;
8use serde::ser::SerializeMap;
9use serde::{ser, Deserialize, Serialize};
10use sha1::{Digest, Sha1};
11use std::borrow::{Cow, ToOwned};
12use std::cmp::Ordering;
13use std::collections::{HashMap, HashSet};
14use std::convert::{TryFrom, TryInto};
15use std::fmt;
16use std::fmt::Formatter;
17use std::string::ToString;
18
19const BUCKET_SCALE_INT: i64 = 0x0FFF_FFFF_FFFF_FFFF;
20const BUCKET_SCALE: f32 = BUCKET_SCALE_INT as f32;
21
22/// Kind describes the type of entity represented by a [Context].
23/// The meaning of a kind is entirely up to the application. To construct a custom kind other than
24/// ["user"](Kind::user), see [Kind::try_from].
25#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26pub struct Kind(Cow<'static, str>);
27
28impl Kind {
29    /// Returns true if the kind is "user". Users are the default context kind created by [crate::ContextBuilder].
30    pub fn is_user(&self) -> bool {
31        self == "user"
32    }
33
34    /// Returns true if the kind is "multi". Multi-contexts are created by [crate::MultiContextBuilder].
35    pub fn is_multi(&self) -> bool {
36        self == "multi"
37    }
38
39    /// Constructs a kind of type "user". See also [Kind::try_from] to create a custom kind, which may
40    /// then be passed to [crate::ContextBuilder::kind].
41    pub fn user() -> Self {
42        Self(Cow::Borrowed("user"))
43    }
44
45    pub(crate) fn multi() -> Self {
46        Self(Cow::Borrowed("multi"))
47    }
48
49    #[cfg(test)]
50    // Constructs a Kind from an arbitrary string, which may result in a Kind that
51    // violates the normal requirements.
52    pub(crate) fn from(s: &str) -> Self {
53        Kind(Cow::Owned(s.to_owned()))
54    }
55}
56
57impl AsRef<str> for Kind {
58    /// Returns a reference to the kind's underlying string.
59    fn as_ref(&self) -> &str {
60        &self.0
61    }
62}
63
64impl Ord for Kind {
65    fn cmp(&self, other: &Self) -> Ordering {
66        self.as_ref().cmp(other.as_ref())
67    }
68}
69
70impl PartialOrd for Kind {
71    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
72        Some(self.cmp(other))
73    }
74}
75
76impl PartialEq<&str> for Kind {
77    fn eq(&self, other: &&str) -> bool {
78        self.as_ref() == *other
79    }
80}
81
82impl PartialEq<str> for Kind {
83    fn eq(&self, other: &str) -> bool {
84        self.as_ref() == other
85    }
86}
87
88impl Default for Kind {
89    /// Kind defaults to "user".
90    fn default() -> Self {
91        Kind::user()
92    }
93}
94
95impl TryFrom<String> for Kind {
96    type Error = String;
97
98    /// Fallibly constructs a kind from an owned string.
99    /// To be a valid kind, the value cannot be empty or equal to "kind".
100    /// Additionally, it must be composed entirely of ASCII alphanumeric characters, in
101    /// addition to `-`, `.` and `_`.
102    fn try_from(value: String) -> Result<Self, Self::Error> {
103        match value.as_str() {
104            "" => Err(String::from("context kind cannot be empty")),
105            "kind" => Err(String::from("context kind cannot be 'kind'")),
106            "multi" => Err(String::from("context kind cannot be 'multi'")),
107            "user" => Ok(Kind::user()),
108            k if !k
109                .chars()
110                .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_')) =>
111            {
112                Err(String::from("context kind contains disallowed characters"))
113            }
114            _ => Ok(Kind(Cow::Owned(value))),
115        }
116    }
117}
118
119impl TryFrom<&str> for Kind {
120    type Error = String;
121
122    /// Fallibly constructs a kind from a string reference.
123    /// See [Kind::try_from].
124    fn try_from(value: &str) -> Result<Self, Self::Error> {
125        match value {
126            "user" => Ok(Kind::user()),
127            _ => Self::try_from(value.to_owned()),
128        }
129    }
130}
131
132impl fmt::Display for Kind {
133    /// Displays the string representation of a kind.
134    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", self.as_ref())
136    }
137}
138
139impl From<Kind> for String {
140    /// Converts a kind into its string representation.
141    fn from(k: Kind) -> Self {
142        k.0.into_owned()
143    }
144}
145
146impl Serialize for Kind {
147    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148    where
149        S: serde::Serializer,
150    {
151        serializer.serialize_str(self.as_ref())
152    }
153}
154
155impl<'de> Deserialize<'de> for Kind {
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: serde::Deserializer<'de>,
159    {
160        let s = String::deserialize(deserializer)?;
161        let kind = s.as_str().try_into().map_err(Error::custom)?;
162        Ok(kind)
163    }
164}
165
166#[cfg(test)]
167pub(crate) mod proptest_generators {
168    use super::Kind;
169    use proptest::prelude::*;
170
171    prop_compose! {
172        pub(crate) fn any_kind_string()(
173            s in "[-._a-zA-Z0-9]+".prop_filter("must not be 'kind' or 'multi'", |s| s != "kind" && s != "multi")
174        ) -> String {
175            s
176        }
177    }
178
179    prop_compose! {
180        pub(crate) fn any_kind()(s in any_kind_string()) -> Kind {
181            Kind::from(s.as_str())
182        }
183    }
184}
185
186/// Context is a collection of attributes that can be referenced in flag evaluations and analytics
187/// events. These attributes are described by one or more [Kind]s.
188///
189/// For example, a context might represent the user of a service, the description of an organization,
190/// IoT device metadata, or any combination of those at once.
191///
192/// To create a context of a single kind, such as a user, you may use [crate::ContextBuilder].
193///
194/// To create a context with multiple kinds, use [crate::MultiContextBuilder].
195#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
196#[serde(try_from = "ContextVariant", into = "ContextVariant")]
197pub struct Context {
198    // Kind is required. For multi-contexts, Kind will always be Kind::Multi.
199    pub(super) kind: Kind,
200    // Contexts is only present for a multi-context.
201    pub(super) contexts: Option<Vec<Context>>,
202    // Name may be optionally set.
203    pub(super) name: Option<String>,
204    // Anonymous may be optionally set, but is false by default.
205    pub(super) anonymous: bool,
206    // Private attributes may be optionally set.
207    pub(super) private_attributes: Option<Vec<Reference>>,
208    // All Contexts have a canonical key, which is a way of uniquely representing all the
209    // (kind, key) pairs in a Context.
210    pub(super) canonical_key: String,
211    // Attributes that aren't builtins may be optionally set.
212    pub(super) attributes: HashMap<String, AttributeValue>,
213    // Secondary serves as an additional key for bucketing purposes.
214    // It has been deprecated by the u2c specification, and can only be set by deserializing
215    // pre-Context data. Its presence is necessary for backwards-compatibility.
216    pub(super) secondary: Option<String>,
217    // Single contexts have keys, which serve as identifiers. For a multi-context,
218    // key is an empty string.
219    pub(super) key: String,
220}
221
222impl Context {
223    /// Returns true if the context is a multi-context.
224    pub fn is_multi(&self) -> bool {
225        self.kind.is_multi()
226    }
227
228    /// For a multi-kind context:
229    ///
230    /// A multi-kind context is made up of two or more single-kind contexts. This method will first
231    /// discard any single-kind contexts which are anonymous. It will then create a new multi-kind
232    /// context from the remaining single-kind contexts. This may result in an invalid context
233    /// (e.g. all single-kind contexts are anonymous).
234    ///
235    /// For a single-kind context:
236    ///
237    /// If the context is not anonymous, this method will return the current context as is and
238    /// unmodified.
239    ///
240    /// If the context is anonymous, this method will return an `Err(String)` result.
241    pub fn without_anonymous_contexts(&self) -> Result<Context, String> {
242        let contexts = match &self.contexts {
243            Some(contexts) => contexts.clone(),
244            None => vec![self.clone()],
245        };
246        let contexts = contexts.into_iter().filter(|c| !c.anonymous).collect_vec();
247
248        MultiContextBuilder::of(contexts).build()
249    }
250
251    /// Looks up the value of any attribute of the context, or a value contained within an
252    /// attribute, based on the given reference.
253    ///
254    /// This lookup includes only attributes that are addressable in evaluations-- not metadata
255    /// such as private attributes.
256    ///
257    /// This method implements the same behavior that the SDK uses to resolve attribute references during a flag
258    /// evaluation. In a single context, the reference can represent a simple attribute name-- either a
259    /// built-in one like "name" or "key", or a custom attribute that was set by methods like
260    /// [crate::ContextBuilder::set_string]-- or, it can be a slash-delimited path.
261    ///
262    /// For a multi-context, the only supported attribute name is "kind". Use
263    /// [Context::as_kind] to inspect a context for a particular [Kind] and then get its
264    /// attributes.
265    pub fn get_value(&self, reference: &Reference) -> Option<AttributeValue> {
266        if !reference.is_valid() {
267            return None;
268        }
269
270        let first_path_component = reference.component(0)?;
271
272        if self.is_multi() {
273            if reference.depth() == 1 && first_path_component == "kind" {
274                return Some(AttributeValue::String(self.kind.to_string()));
275            }
276
277            warn!("Multi-contexts only support retrieving the 'kind' attribute");
278            return None;
279        }
280
281        let mut attribute =
282            self.get_top_level_addressable_attribute_single_kind(first_path_component)?;
283
284        for i in 1..reference.depth() {
285            let name = reference.component(i)?;
286            if let AttributeValue::Object(map) = attribute {
287                attribute = map.get(name).cloned()?;
288            } else {
289                return None;
290            }
291        }
292
293        Some(attribute)
294    }
295
296    /// Returns the "key" attribute.
297    ///
298    /// For a single context, this value is set by the [crate::ContextBuilder::new] or
299    /// [crate::ContextBuilder::key] methods.
300    ///
301    /// For a multi-context, there is no single key, so [Context::key] returns an empty string; use
302    /// [Context::as_kind] to inspect a context for a particular kind and call [Context::key] on it.
303    pub fn key(&self) -> &str {
304        &self.key
305    }
306
307    /// Returns the canonical key.
308    ///
309    /// 1. For a single context of kind "user", the canonical key is equivalent to the key.
310    /// 2. For other kinds of single contexts, the canonical key is "kind:key".
311    /// 3. For a multi-context, the canonical key is the concatenation of its constituent contexts'
312    ///    canonical keys with `:` according to (2) (including kind "user").
313    pub fn canonical_key(&self) -> &str {
314        &self.canonical_key
315    }
316
317    /// Returns the "kind" attribute.
318    pub fn kind(&self) -> &Kind {
319        &self.kind
320    }
321
322    /// If the specified kind exists within the context, returns a reference to it.
323    /// Otherwise, returns None.
324    pub fn as_kind(&self, kind: &Kind) -> Option<&Context> {
325        match &self.contexts {
326            Some(contexts) => contexts.iter().find(|c| c.kind() == kind),
327            None => self.kind.eq(kind).then_some(self),
328        }
329    }
330
331    /// Returns a map of all (kind, key) pairs contained in this context.
332    pub fn context_keys(&self) -> HashMap<&Kind, &str> {
333        match &self.contexts {
334            Some(contexts) => contexts
335                .iter()
336                .map(|context| (context.kind(), context.key()))
337                .collect(),
338            None => hashmap! { self.kind() => self.key() },
339        }
340    }
341
342    /// Returns a list of all kinds represented by this context.
343    pub fn kinds(&self) -> Vec<&Kind> {
344        if !self.is_multi() {
345            return vec![self.kind()];
346        }
347
348        match &self.contexts {
349            Some(contexts) => contexts.iter().map(|context| context.kind()).collect(),
350            None => Vec::new(),
351        }
352    }
353
354    fn get_optional_attribute_names(&self) -> Vec<String> {
355        if self.is_multi() {
356            return Vec::new();
357        }
358
359        let mut names = Vec::with_capacity(self.attributes.len() + 1);
360        names.extend(self.attributes.keys().cloned());
361
362        if self.name.is_some() {
363            names.push(String::from("name"));
364        }
365
366        names
367    }
368
369    pub(crate) fn bucket(
370        &self,
371        by_attr: &Option<Reference>,
372        prefix: BucketPrefix,
373        is_experiment: bool,
374        context_kind: &Kind,
375    ) -> Result<(f32, bool), String> {
376        let reference = match (is_experiment, by_attr) {
377            (true, _) | (false, None) => Reference::new("key"),
378            (false, Some(reference)) => reference.clone(),
379        };
380
381        if !reference.is_valid() {
382            return Err(reference.error());
383        }
384
385        match self.as_kind(context_kind) {
386            Some(context) => {
387                let attr_value = context.get_value(&reference);
388
389                Ok((
390                    self._bucket(attr_value.as_ref(), prefix, is_experiment)
391                        .unwrap_or(0.0),
392                    false,
393                ))
394            }
395            // If the required context wasn't found, we still want the bucket to be 0, but we need
396            // to show that the context was missing. This will affect the inExperiment field
397            // upstream.
398            _ => Ok((0.0, true)),
399        }
400    }
401
402    fn _bucket(
403        &self,
404        value: Option<&AttributeValue>,
405        prefix: BucketPrefix,
406        is_experiment: bool,
407    ) -> Option<f32> {
408        let mut id = value?.as_bucketable()?;
409
410        if cfg!(feature = "secondary_key_bucketing") && !is_experiment {
411            if let Some(secondary) = &self.secondary {
412                id.push('.');
413                id.push_str(secondary);
414            }
415        }
416
417        let mut hash = Sha1::new();
418        prefix.write_hash(&mut hash);
419        hash.update(b".");
420        hash.update(id.as_bytes());
421
422        let digest = hash.finalize();
423        let hexhash = base16ct::lower::encode_string(&digest);
424
425        let hexhash_15 = &hexhash[..15]; // yes, 15 chars, not 16
426        let numhash = i64::from_str_radix(hexhash_15, 16).unwrap();
427
428        Some(numhash as f32 / BUCKET_SCALE)
429    }
430
431    fn get_top_level_addressable_attribute_single_kind(
432        &self,
433        name: &str,
434    ) -> Option<AttributeValue> {
435        match name {
436            "kind" => Some(AttributeValue::String(self.kind.to_string())),
437            "key" => Some(AttributeValue::String(self.key.clone())),
438            "name" => self.name.clone().map(AttributeValue::String),
439            "anonymous" => Some(AttributeValue::Bool(self.anonymous)),
440            _ => self.attributes.get(name).map(|v| v.to_owned()),
441        }
442    }
443}
444
445#[derive(Clone, Copy)]
446pub(crate) enum BucketPrefix<'a> {
447    KeyAndSalt(&'a str, &'a str),
448    Seed(i64),
449}
450
451impl BucketPrefix<'_> {
452    pub(crate) fn write_hash(&self, hash: &mut Sha1) {
453        match self {
454            BucketPrefix::KeyAndSalt(key, salt) => {
455                hash.update(key.as_bytes());
456                hash.update(b".");
457                hash.update(salt.as_bytes());
458            }
459            BucketPrefix::Seed(seed) => {
460                let seed_str = seed.to_string();
461                hash.update(seed_str.as_bytes());
462            }
463        }
464    }
465}
466
467#[derive(Debug)]
468struct PrivateAttributeLookupNode {
469    reference: Option<Reference>,
470    children: HashMap<String, Box<PrivateAttributeLookupNode>>,
471}
472
473/// ContextAttributes is used to handle redaction of select context properties when serializing a
474/// [Context] that will be sent to LaunchDarkly.
475#[derive(Debug)]
476pub struct ContextAttributes {
477    context: Context,
478    all_attributes_private: bool,
479    global_private_attributes: HashMap<String, Box<PrivateAttributeLookupNode>>,
480    redact_anonymous: bool,
481}
482
483impl ContextAttributes {
484    /// Construct from a source context, indicating if all attributes should be private,
485    /// and providing a set of attribute references that should be selectively marked private.
486    pub fn from_context(
487        context: Context,
488        all_attributes_private: bool,
489        private_attributes: HashSet<Reference>,
490    ) -> Self {
491        Self {
492            context,
493            all_attributes_private,
494            global_private_attributes: Self::make_private_attribute_lookup_data(private_attributes),
495            redact_anonymous: false,
496        }
497    }
498
499    /// Construct from a source context, indicating if all attributes should be private,
500    /// and providing a set of attribute references that should be selectively marked private.
501    ///
502    /// If a provided context is anonymous, all attributes will be redacted except for key, kind,
503    /// and anonymous.
504    pub fn from_context_with_anonymous_redaction(
505        context: Context,
506        all_attributes_private: bool,
507        private_attributes: HashSet<Reference>,
508    ) -> Self {
509        Self {
510            context,
511            all_attributes_private,
512            global_private_attributes: Self::make_private_attribute_lookup_data(private_attributes),
513            redact_anonymous: true,
514        }
515    }
516
517    // This function transforms a set of [Reference]s into a data structure that allows for more
518    // efficient check_global_private_attribute_refs.
519    //
520    // For instance, if the original references were "/name", "/address", "/address/street", and
521    // "/address/city", it would produce the following map:
522    //
523    // "name": {
524    //   reference: Reference::new("/name"),
525    // },
526    // "address": {
527    //   reference: Reference::new("/address"),
528    //   children: {
529    //     "street": {
530    //       reference: Reference::new("/address/street"),
531    //     },
532    //     "city": {
533    //       reference: Reference::new("/address/city"),
534    //     },
535    //   },
536    // }
537    //
538    // The "address" node has both a reference and children. A reference must mark its node even
539    // when a longer reference already created that node. Otherwise "/address/street" followed by
540    // "/address" would leave "address" without a reference, and so unredacted.
541    fn make_private_attribute_lookup_data(
542        references: impl IntoIterator<Item = Reference>,
543    ) -> HashMap<String, Box<PrivateAttributeLookupNode>> {
544        let mut return_value = HashMap::new();
545
546        for reference in references.into_iter() {
547            let mut parent_map = &mut return_value;
548            for i in 0..reference.depth() {
549                if let Some(name) = reference.component(i) {
550                    let node = parent_map.entry(name.to_owned()).or_insert_with(|| {
551                        Box::new(PrivateAttributeLookupNode {
552                            reference: None,
553                            children: HashMap::new(),
554                        })
555                    });
556
557                    if i == reference.depth() - 1 {
558                        node.reference = Some(reference.clone());
559                    }
560
561                    parent_map = &mut node.children;
562                }
563            }
564        }
565
566        return_value
567    }
568
569    fn write_multi_context(&self) -> HashMap<String, AttributeValue> {
570        let mut map: HashMap<String, AttributeValue> = HashMap::new();
571        map.insert("kind".to_string(), self.context.kind().to_string().into());
572
573        if let Some(contexts) = &self.context.contexts {
574            for context in contexts.iter() {
575                let context_map = self.write_single_context(context, false);
576                map.insert(
577                    context.kind().to_string(),
578                    AttributeValue::Object(context_map),
579                );
580            }
581        }
582
583        map
584    }
585
586    fn write_single_context(
587        &self,
588        context: &Context,
589        include_kind: bool,
590    ) -> HashMap<String, AttributeValue> {
591        let mut map: HashMap<String, AttributeValue> = HashMap::new();
592
593        if include_kind {
594            map.insert("kind".into(), context.kind().to_string().into());
595        }
596
597        map.insert(
598            "key".to_string(),
599            AttributeValue::String(context.key().to_owned()),
600        );
601
602        let optional_attribute_names = context.get_optional_attribute_names();
603        let mut redacted_attributes = Vec::<String>::with_capacity(20);
604
605        let redact_all =
606            self.all_attributes_private || (self.redact_anonymous && context.anonymous);
607
608        for key in optional_attribute_names.iter() {
609            let reference = Reference::from_literal_name(key);
610            if let Some(value) = context.get_value(&reference) {
611                // If redact_all is true, then there's no complex filtering or
612                // recursing to be done: all of these values are by definition private, so just add
613                // their names to the redacted list.
614                if redact_all {
615                    redacted_attributes.push(String::from(reference));
616                    continue;
617                }
618
619                let path = Vec::with_capacity(10);
620                self.write_filter_attribute(
621                    context,
622                    &mut map,
623                    path,
624                    key,
625                    value,
626                    &mut redacted_attributes,
627                )
628            }
629        }
630
631        if context.anonymous {
632            map.insert("anonymous".to_string(), true.into());
633        }
634
635        if context.secondary.is_some() || !redacted_attributes.is_empty() {
636            let mut meta: HashMap<String, AttributeValue> = HashMap::new();
637            if let Some(secondary) = &context.secondary {
638                meta.insert(
639                    "secondary".to_string(),
640                    AttributeValue::String(secondary.to_string()),
641                );
642            }
643
644            if !redacted_attributes.is_empty() {
645                meta.insert(
646                    "redactedAttributes".to_string(),
647                    AttributeValue::Array(
648                        redacted_attributes
649                            .into_iter()
650                            .map(AttributeValue::String)
651                            .collect(),
652                    ),
653                );
654            }
655
656            map.insert("_meta".to_string(), AttributeValue::Object(meta));
657        }
658
659        map
660    }
661
662    // Checks whether a given value should be considered private, and then either writes the
663    // attribute to the output HashMap if it is *not* private, or adds the corresponding attribute
664    // reference to the redacted_attributes list if it is private.
665    //
666    // The parent_path parameter indicates where we are in the context data structure. If it is
667    // empty, we are at the top level and "key" is an attribute name. If it is not empty, we are
668    // recursing into the properties of an attribute value that is a JSON object: for instance, if
669    // parent_path is ["billing", "address"] and key is "street", then the top-level attribute is
670    // "billing" and has a value in the form {"address": {"street": ...}} and we are now deciding
671    // whether to write the "street" property. See maybe_redact for the logic involved in that
672    // decision.
673    //
674    // If all_attributes_private is true, this method is never called.
675    fn write_filter_attribute(
676        &self,
677        context: &Context,
678        map: &mut HashMap<String, AttributeValue>,
679        parent_path: Vec<String>,
680        key: &str,
681        value: AttributeValue,
682        redacted_attributes: &mut Vec<String>,
683    ) {
684        let mut path = parent_path;
685        path.push(key.to_string());
686
687        let (is_redacted, nested_properties_are_redacted) =
688            self.maybe_redact(context, &path, &value, redacted_attributes);
689
690        // If the value is an object, then there are three possible outcomes:
691        //
692        // 1. this value is completely redacted, so drop it and do not recurse;
693        // 2. the value is not redacted, and and neither are any subproperties within it, so output
694        //    the whole thing as-is;
695        // 3. the value itself is not redacted, but some subproperties within it are, so we'll need
696        //    to recurse through it and filter as we go.
697        match value {
698            AttributeValue::Object(_) if is_redacted => (), // outcome 1
699            AttributeValue::Object(ref object_map) => {
700                // outcome 2
701                if !nested_properties_are_redacted {
702                    map.insert(key.to_string(), value.clone());
703                    return;
704                }
705
706                // outcome 3
707                let mut sub_map = HashMap::new();
708                for (k, v) in object_map.iter() {
709                    self.write_filter_attribute(
710                        context,
711                        &mut sub_map,
712                        path.clone(),
713                        k,
714                        v.clone(),
715                        redacted_attributes,
716                    );
717                }
718                map.insert(key.to_string(), AttributeValue::Object(sub_map));
719            }
720            _ if !is_redacted => {
721                map.insert(key.to_string(), value);
722            }
723            _ => (),
724        };
725    }
726
727    // Called by write_filter_attribute to decide whether or not a given value (or, possibly,
728    // properties within it) should be considered private, based on the private attribute
729    // references.
730    //
731    // If the value should be private, then the first return value is true, and also the attribute
732    // reference is added to redacted_attributes.
733    //
734    // The second return value indicates whether there are any private attribute references
735    // designating properties *within* this value. That is, if parent_path is ["address"], and the
736    // configuration says that "/address/street" is private, then the second return value will be
737    // true, which tells us that we can't just dump the value of the "address" object directly into
738    // the output but will need to filter its properties.
739    //
740    // Note that even though a Reference can contain numeric path components to represent an array
741    // element lookup, for the purposes of flag evaluations (like "/animals/0" which conceptually
742    // represents context.animals[0]), those will not work as private attribute references since
743    // we do not recurse to redact anything within an array value. A reference like "/animals/0"
744    // would only work if context.animals were an object with a property named "0".
745    //
746    // If all_attributes_private is true, this method is never called.
747    fn maybe_redact(
748        &self,
749        context: &Context,
750        parent_path: &[String],
751        value: &AttributeValue,
752        redacted_attributes: &mut Vec<String>,
753    ) -> (bool, bool) {
754        let (redacted_attr_ref, mut nested_properties_are_redacted) =
755            self.check_global_private_attribute_refs(parent_path);
756
757        if let Some(redacted_attr_ref) = redacted_attr_ref {
758            redacted_attributes.push(String::from(redacted_attr_ref));
759            return (true, false);
760        }
761
762        let should_check_for_nested_properties = matches!(value, AttributeValue::Object(..));
763
764        if let Some(private_attributes) = &context.private_attributes {
765            for private_attribute in private_attributes.iter() {
766                let depth = private_attribute.depth();
767                if depth < parent_path.len() {
768                    // If the attribute reference is shorter than the current path, then it can't
769                    // possibly be a match, because if it had matched the first part of our path,
770                    // we wouldn't have recursed this far.
771                    continue;
772                }
773
774                if !should_check_for_nested_properties && depth > parent_path.len() {
775                    continue;
776                }
777
778                let mut has_match = true;
779                for (i, parent_part) in parent_path.iter().enumerate() {
780                    match private_attribute.component(i) {
781                        None => break,
782                        Some(name) if name != parent_part => {
783                            has_match = false;
784                            break;
785                        }
786                        _ => continue,
787                    };
788                }
789
790                if has_match {
791                    if depth == parent_path.len() {
792                        redacted_attributes.push(private_attribute.to_string());
793                        return (true, false);
794                    }
795                    nested_properties_are_redacted = true;
796                }
797            }
798        }
799
800        (false, nested_properties_are_redacted)
801    }
802
803    // Checks whether the given attribute or subproperty matches any Reference that was designated
804    // as private in the SDK options.
805    //
806    // If parent_path has just one element, it is the name of a top-level attribute. If it has
807    // multiple elements, it is a path to a property within a custom object attribute: for
808    // instance, if you represented the overall context as a JSON object, the parent_path
809    // ["billing", "address", "street"] would refer to the street property within something like
810    // {"billing": {"address": {"street": "x"}}}.
811    //
812    // The first return value is None if the attribute does not need to be redacted; otherwise it
813    // is the specific attribute reference that was matched.
814    //
815    // The second return value is true if and only if there's at least one configured private
816    // attribute reference for *children* of parent_path (and there is not one for parent_path
817    // itself, since if there was, we would not bother recursing to write the children). See
818    // comments on write_filter_attribute.
819    fn check_global_private_attribute_refs(
820        &self,
821        parent_path: &[String],
822    ) -> (Option<Reference>, bool) {
823        let mut lookup = &self.global_private_attributes;
824
825        if self.global_private_attributes.is_empty() {
826            return (None, false);
827        }
828
829        for (index, path) in parent_path.iter().enumerate() {
830            let next_node = match lookup.get(path.as_str()) {
831                None => break,
832                Some(v) => v,
833            };
834
835            if index == parent_path.len() - 1 {
836                let var_name = (next_node.reference.clone(), next_node.reference.is_none());
837                return var_name;
838            } else if !next_node.children.is_empty() {
839                lookup = &next_node.children;
840            }
841        }
842
843        (None, false)
844    }
845}
846
847impl ser::Serialize for ContextAttributes {
848    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
849    where
850        S: ser::Serializer,
851    {
852        let mut serialize_map = serializer.serialize_map(None)?;
853
854        let map = match self.context.is_multi() {
855            true => self.write_multi_context(),
856            false => self.write_single_context(&self.context, true),
857        };
858
859        for (k, v) in map.iter().sorted_by_key(|p| p.0) {
860            serialize_map.serialize_entry(k, v)?;
861        }
862
863        serialize_map.end()
864    }
865}
866
867#[cfg(test)]
868mod tests {
869    use super::proptest_generators::*;
870    use crate::{AttributeValue, ContextBuilder, MultiContextBuilder, Reference};
871    use maplit::hashmap;
872    use proptest::proptest;
873    use std::collections::HashSet;
874    use test_case::test_case;
875
876    use super::{ContextAttributes, Kind};
877
878    proptest! {
879        #[test]
880        fn all_generated_kinds_are_valid(kind in any_kind()) {
881            let maybe_kind = Kind::try_from(kind.as_ref());
882            assert!(maybe_kind.is_ok());
883        }
884    }
885
886    #[test_case("kind"; "Cannot set kind as kind")]
887    #[test_case("multi"; "Cannot set kind as multi")]
888    #[test_case("🦀"; "Cannot set kind as invalid character")]
889    #[test_case(" "; "Cannot set kind as only whitespace")]
890    fn invalid_kinds(kind: &str) {
891        assert!(Kind::try_from(kind).is_err());
892    }
893
894    #[test_case(Kind::user(), true)]
895    #[test_case(Kind::from("user"), true)]
896    #[test_case(Kind::multi(), false)]
897    #[test_case(Kind::from("foo"), false)]
898    fn is_user(kind: Kind, is_user: bool) {
899        assert_eq!(kind.is_user(), is_user);
900    }
901
902    #[test_case(Kind::multi(), true)]
903    #[test_case(Kind::from("multi"), true)]
904    #[test_case(Kind::user(), false)]
905    #[test_case(Kind::from("foo"), false)]
906    fn is_multi(kind: Kind, is_multi: bool) {
907        assert_eq!(kind.is_multi(), is_multi);
908    }
909
910    #[test]
911    fn kind_sorts_based_on_string() {
912        let mut kinds = vec![
913            Kind::user(),
914            Kind::multi(),
915            Kind::from("n"),
916            Kind::from("v"),
917            Kind::from("l"),
918        ];
919        kinds.sort();
920        assert_eq!(
921            kinds,
922            vec![
923                Kind::from("l"),
924                Kind::multi(),
925                Kind::from("n"),
926                Kind::user(),
927                Kind::from("v"),
928            ]
929        );
930    }
931
932    proptest! {
933        #[test]
934        fn kind_comparison_identity(kind in any_kind()) {
935            assert_eq!(kind, kind);
936        }
937    }
938
939    proptest! {
940        #[test]
941        fn kind_comparison_identity_str(kind in any_kind()) {
942            assert_eq!(kind, kind.as_ref());
943            assert_eq!(&kind, kind.as_ref());
944        }
945    }
946
947    proptest! {
948        #[test]
949        fn kind_comparison_different(a in any_kind(), b in any_kind()) {
950            if a.0 != b.0 {
951                assert_ne!(a, b);
952            }
953        }
954    }
955
956    proptest! {
957        #[test]
958        fn kind_serialize(kind in any_kind()) {
959            assert_eq!(format!("\"{}\"", kind.0), serde_json::to_string(&kind).unwrap());
960        }
961    }
962
963    proptest! {
964        #[test]
965        fn kind_deserialize(kind_str in any_kind_string()) {
966            let json_str = format!("\"{}\"", &kind_str);
967            let kind: Result<Kind, _> = serde_json::from_str(&json_str);
968            assert!(kind.is_ok());
969        }
970    }
971
972    // Since "multi" is reserved as the signifier for multi-contexts,
973    // it cannot be constructed directly.
974    #[test]
975    fn cannot_deserialize_multi_kind() {
976        let maybe_kind: Result<Kind, _> = serde_json::from_str("\"multi\"");
977        assert!(maybe_kind.is_err());
978    }
979
980    // Basic simple attribute retrievals
981    #[test_case("kind", Some(AttributeValue::String("org".to_string())))]
982    #[test_case("key", Some(AttributeValue::String("my-key".to_string())))]
983    #[test_case("name", Some(AttributeValue::String("my-name".to_string())))]
984    #[test_case("anonymous", Some(AttributeValue::Bool(true)))]
985    #[test_case("attr", Some(AttributeValue::String("my-attr".to_string())))]
986    #[test_case("/starts-with-slash", Some(AttributeValue::String("love that prefix".to_string())))]
987    #[test_case("/crazy~0name", Some(AttributeValue::String("still works".to_string())))]
988    #[test_case("/other", None)]
989    // Invalid reference retrieval
990    #[test_case("/", None; "Single slash")]
991    #[test_case("", None; "Empty reference")]
992    #[test_case("/a//b", None; "Double slash")]
993    // Hidden meta attributes
994    #[test_case("privateAttributes", None)]
995    #[test_case("secondary", None)]
996    // Can index objects
997    #[test_case("/my-map/array", Some(AttributeValue::Array(vec![AttributeValue::String("first".to_string()), AttributeValue::String("second".to_string())])))]
998    #[test_case("/my-map/1", Some(AttributeValue::Bool(true)))]
999    #[test_case("/my-map/missing", None)]
1000    #[test_case("/starts-with-slash/1", None; "handles providing an index to a non-array value")]
1001    fn context_can_get_value(input: &str, value: Option<AttributeValue>) {
1002        let mut builder = ContextBuilder::new("my-key");
1003
1004        let array = vec![
1005            AttributeValue::String("first".to_string()),
1006            AttributeValue::String("second".to_string()),
1007        ];
1008        let map = hashmap! {
1009            "array".to_string() => AttributeValue::Array(array),
1010            "1".to_string() => AttributeValue::Bool(true)
1011        };
1012
1013        let context = builder
1014            .kind("org".to_string())
1015            .name("my-name")
1016            .anonymous(true)
1017            .secondary("my-secondary")
1018            .set_string("attr", "my-attr")
1019            .set_string("starts-with-slash", "love that prefix")
1020            .set_string("crazy~name", "still works")
1021            .set_value("my-map", AttributeValue::Object(map))
1022            .add_private_attribute("attr")
1023            .build()
1024            .expect("Failed to build context");
1025
1026        assert_eq!(context.get_value(&Reference::new(input)), value);
1027    }
1028
1029    #[test_case("kind", Some(AttributeValue::String("multi".to_string())))]
1030    #[test_case("key", None)]
1031    #[test_case("name", None)]
1032    #[test_case("anonymous", None)]
1033    #[test_case("attr", None)]
1034    fn multi_context_get_value(input: &str, value: Option<AttributeValue>) {
1035        let mut multi_builder = MultiContextBuilder::new();
1036        let mut builder = ContextBuilder::new("user");
1037
1038        multi_builder.add_context(builder.build().expect("Failed to create context"));
1039
1040        builder
1041            .key("org")
1042            .kind("org".to_string())
1043            .name("my-name")
1044            .anonymous(true)
1045            .set_string("attr", "my-attr");
1046        multi_builder.add_context(builder.build().expect("Failed to create context"));
1047
1048        let context = multi_builder.build().expect("Failed to create context");
1049
1050        assert_eq!(context.get_value(&Reference::new(input)), value);
1051    }
1052
1053    #[test]
1054    fn can_retrieve_context_from_multi_context() {
1055        let user_context = ContextBuilder::new("user").build().unwrap();
1056        let org_context = ContextBuilder::new("org").kind("org").build().unwrap();
1057
1058        assert!(org_context.as_kind(&Kind::user()).is_none());
1059
1060        let multi_context = MultiContextBuilder::new()
1061            .add_context(user_context)
1062            .add_context(org_context)
1063            .build()
1064            .unwrap();
1065
1066        assert!(multi_context
1067            .as_kind(&Kind::user())
1068            .unwrap()
1069            .kind()
1070            .is_user());
1071
1072        assert_eq!(
1073            "org",
1074            multi_context
1075                .as_kind(&Kind::from("org"))
1076                .unwrap()
1077                .kind()
1078                .as_ref()
1079        );
1080
1081        assert!(multi_context.as_kind(&Kind::from("custom")).is_none());
1082    }
1083
1084    #[test]
1085    fn redacting_anon_from_anon_is_invalid() {
1086        let anon_context = ContextBuilder::new("user")
1087            .anonymous(true)
1088            .build()
1089            .expect("context build failed");
1090        let result = anon_context.without_anonymous_contexts();
1091
1092        assert!(result.is_err());
1093    }
1094
1095    #[test]
1096    fn redacting_anon_from_nonanon_results_in_no_change() {
1097        let context = ContextBuilder::new("user")
1098            .build()
1099            .expect("context build failed");
1100        let result = context.without_anonymous_contexts();
1101
1102        assert_eq!(context, result.unwrap());
1103    }
1104
1105    #[test]
1106    fn can_redact_anon_from_multi() {
1107        let user_context = ContextBuilder::new("user")
1108            .anonymous(true)
1109            .build()
1110            .expect("Failed to create context");
1111        let org_context = ContextBuilder::new("org")
1112            .kind("org")
1113            .build()
1114            .expect("Failed to create context");
1115
1116        let multi_context = MultiContextBuilder::new()
1117            .add_context(user_context)
1118            .add_context(org_context)
1119            .build()
1120            .expect("Failed to create context");
1121
1122        let context = multi_context
1123            .without_anonymous_contexts()
1124            .expect("Failed to redact anon");
1125
1126        assert_eq!("org", context.kind().as_ref());
1127    }
1128
1129    #[test]
1130    fn redact_anon_from_all_anon_multi_is_invalid() {
1131        let user_context = ContextBuilder::new("user")
1132            .anonymous(true)
1133            .build()
1134            .expect("Failed to create context");
1135        let org_context = ContextBuilder::new("org")
1136            .kind("org")
1137            .anonymous(true)
1138            .build()
1139            .expect("Failed to create context");
1140
1141        let multi_context = MultiContextBuilder::new()
1142            .add_context(user_context)
1143            .add_context(org_context)
1144            .build()
1145            .expect("Failed to create context");
1146
1147        let context = multi_context.without_anonymous_contexts();
1148
1149        assert!(context.is_err());
1150    }
1151
1152    // Collects the strings in an event context's redactedAttributes as a set, since their order is
1153    // not significant.
1154    fn redacted_attributes(json: &serde_json::Value) -> HashSet<String> {
1155        json["_meta"]["redactedAttributes"]
1156            .as_array()
1157            .expect("redactedAttributes should be an array")
1158            .iter()
1159            .map(|v| v.as_str().unwrap().to_string())
1160            .collect()
1161    }
1162
1163    #[test_case("/ssn", "/~1ssn"; "slash prefix is escaped")]
1164    #[test_case("/a~b", "/~1a~0b"; "slash prefix with tilde escapes both")]
1165    fn all_attributes_private_reports_escaped_references(name: &str, expected_ref: &str) {
1166        let context = ContextBuilder::new("my-key")
1167            .set_string(name, "secret")
1168            .build()
1169            .expect("Failed to build context");
1170
1171        let attributes = ContextAttributes::from_context(context, true, HashSet::new());
1172        let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1173
1174        assert_eq!(
1175            redacted_attributes(&json),
1176            HashSet::from([expected_ref.to_string()])
1177        );
1178        // The attribute value must not be leaked into the serialized output.
1179        assert!(json.get(name).is_none());
1180    }
1181
1182    #[test]
1183    fn anonymous_redaction_reports_slash_prefixed_names_as_escaped_references() {
1184        let context = ContextBuilder::new("my-key")
1185            .anonymous(true)
1186            .set_string("/ssn", "123-45-6789")
1187            .build()
1188            .expect("Failed to build context");
1189
1190        let attributes = ContextAttributes::from_context_with_anonymous_redaction(
1191            context,
1192            false,
1193            HashSet::new(),
1194        );
1195        let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1196
1197        assert_eq!(
1198            redacted_attributes(&json),
1199            HashSet::from(["/~1ssn".to_string()])
1200        );
1201        assert!(json.get("/ssn").is_none());
1202    }
1203
1204    // A set of references is iterated in no particular order, so each order is tested
1205    // explicitly.
1206    #[test_case(&["/address", "/address/street"]; "parent first")]
1207    #[test_case(&["/address/street", "/address"]; "child first")]
1208    fn global_private_parent_is_redacted_when_a_child_is_also_private(references: &[&str]) {
1209        let address = hashmap! {
1210            "street".to_string() => AttributeValue::String("123 Main St".to_string()),
1211            "city".to_string() => AttributeValue::String("Springfield".to_string()),
1212        };
1213        let context = ContextBuilder::new("my-key")
1214            .set_value("address", AttributeValue::Object(address))
1215            .build()
1216            .expect("Failed to build context");
1217
1218        let attributes = ContextAttributes {
1219            context,
1220            all_attributes_private: false,
1221            global_private_attributes: ContextAttributes::make_private_attribute_lookup_data(
1222                references.iter().map(Reference::new),
1223            ),
1224            redact_anonymous: false,
1225        };
1226        let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1227
1228        assert_eq!(
1229            redacted_attributes(&json),
1230            HashSet::from(["/address".to_string()])
1231        );
1232        assert!(json.get("address").is_none());
1233    }
1234
1235    #[test]
1236    fn slash_prefixed_attribute_is_serialized_under_its_literal_name_when_not_private() {
1237        let context = ContextBuilder::new("my-key")
1238            .set_string("/ssn", "123-45-6789")
1239            .build()
1240            .expect("Failed to build context");
1241
1242        let attributes = ContextAttributes::from_context(context, false, HashSet::new());
1243        let json = serde_json::to_value(&attributes).expect("Failed to serialize");
1244
1245        assert_eq!(json["/ssn"], serde_json::json!("123-45-6789"));
1246        assert!(json.get("_meta").is_none());
1247    }
1248}