diff --git a/README.md b/README.md index 91eed03..8d28287 100644 --- a/README.md +++ b/README.md @@ -17,20 +17,27 @@ After installing the Nuget package in your project, you need to take the followi - **SubstituteText**: If set, the entire property value will be override with this text. Note that using this setting will ignore all other settings. - **Mask**: Set to a character to use it when masking the property's value. By default, the character `*` is used. + > PS.: These customization fields only work for fields with a `string` base type. + 2. Call the `JsonMask.MaskSensitiveData()` function, passing in your object instance as a parameter. ## Support -This library supports masking of `string` fields only, although it also supports `List`/`IEnumerable` and `Dictionary`. Nested class properties are also masked, independently of depth. - -| Property Type | Support | -|:---: |:---: | -| string | ✅ | -| List\, where T is a class or string | ✅ | -| IEnumerable\, where T is a class or string | ✅ | -| Dictionary | ✅ | -| Any other collection type, such as Array, ArrayList\, etc | ❌ | -| Any other base type different from string | ❌ | +### Base Types +This library supports masking of the following base types: +- `string` fields, which are masked following the rules detailed in the [Usage](#usage) section. +- Other types such as `bool`, `(s)byte`, `(u)short`, `(u)int`, `(u)long`, `float`, `double`, `decimal`, `char`, `Datetime`, `DatetimeOffset`, and `Guid`, which are set to their respective default values when having the `[SensitiveData]` attribute. +- Any other base types are currently NOT supported. + +### Collections +The library also supports the masking of some collections, such as: +- `List`, where T is a class or `string`. +- `IEnumerable`, where T is a class or `string`. +- `Dictionary`. +- Any other collection type, such as `Array`, `ArrayList`, etc., is NOT supported. + +### Nested class fields +Nested class properties are also masked, independently of depth. ## Examples diff --git a/src/JsonDataMasking.Test/JsonMaskTests.cs b/src/JsonDataMasking.Test/JsonMaskTests.cs index 7a47269..f8d75ea 100644 --- a/src/JsonDataMasking.Test/JsonMaskTests.cs +++ b/src/JsonDataMasking.Test/JsonMaskTests.cs @@ -108,16 +108,6 @@ public void MaskSensitiveData_UsesCustomMask_WhenHasAttributeWithCustomMaskChara Assert.Equal(expectedMask, maskedCustomerData.CreditCardSecurityCode); } - [Fact] - public void MaskSensitiveData_ThrowsNotSupportedException_WhenPropertyWithNotSupportedTypeHasAttribute() - { - // Arrange - var creditCard = new CreditCardMock { SecurityCode = 999 }; - - // Act and Assert - Assert.Throws(() => JsonMask.MaskSensitiveData(creditCard)); - } - [Fact] public void MaskSensitiveData_MasksUsingDefaultSize_WhenHasAttributeWithInvalidShowFirstAndLastRange() { @@ -373,5 +363,61 @@ public void MaskSensitiveData_ThrowsNotSupportedException_WhenPropertyIsArray() // Act and assert Assert.Throws(() => JsonMask.MaskSensitiveData(passcodes)); } + + [Fact] + public void MaskSensitiveData_MasksOtherBaseTypes_WhenTheyHaveAttribute() + { + // Arrange + var baseTypes = new BasicTypesSensitiveMock(); + + // Act + var maskedBaseTypes = JsonMask.MaskSensitiveData(baseTypes); + + // Assert + Assert.False(maskedBaseTypes.Bool); + Assert.Equal(0, maskedBaseTypes.Byte); + Assert.Equal(0, maskedBaseTypes.Sbyte); + Assert.Equal(0, maskedBaseTypes.Short); + Assert.Equal(0, maskedBaseTypes.Ushort); + Assert.Equal(0, maskedBaseTypes.Int); + Assert.Equal(0u, maskedBaseTypes.Uint); + Assert.Equal(0, maskedBaseTypes.Long); + Assert.Equal(0u, maskedBaseTypes.Ulong); + Assert.Equal(0, maskedBaseTypes.Float); + Assert.Equal(0, maskedBaseTypes.Double); + Assert.Equal(0, maskedBaseTypes.Decimal); + Assert.Equal('\0', maskedBaseTypes.Char); + Assert.Equal(new DateTime(), maskedBaseTypes.DateTime); + Assert.Equal(new DateTimeOffset(), maskedBaseTypes.DateTimeOffset); + Assert.Equal(Guid.Empty, maskedBaseTypes.Guid); + } + + [Fact] + public void MaskSensitiveData_DoesNotMaskOtherBaseTypes_WhenWithoutSensitiveAttribute() + { + // Arrange + var nonSensitiveBaseTypes = new BasicTypesMock(); + + // Act + var maskedBaseTypes = JsonMask.MaskSensitiveData(nonSensitiveBaseTypes); + + // Assert + Assert.True(maskedBaseTypes.Bool); + Assert.Equal(1, maskedBaseTypes.Byte); + Assert.Equal(1, maskedBaseTypes.Sbyte); + Assert.Equal(1, maskedBaseTypes.Short); + Assert.Equal(1, maskedBaseTypes.Ushort); + Assert.Equal(1, maskedBaseTypes.Int); + Assert.Equal(1u, maskedBaseTypes.Uint); + Assert.Equal(1, maskedBaseTypes.Long); + Assert.Equal(1u, maskedBaseTypes.Ulong); + Assert.Equal(1, maskedBaseTypes.Float); + Assert.Equal(1, maskedBaseTypes.Double); + Assert.Equal(1, maskedBaseTypes.Decimal); + Assert.Equal('1', maskedBaseTypes.Char); + Assert.Equal(new DateTime().AddYears(1), maskedBaseTypes.DateTime); + Assert.Equal(new DateTimeOffset().AddYears(1), maskedBaseTypes.DateTimeOffset); + Assert.Equal(Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), maskedBaseTypes.Guid); + } } } \ No newline at end of file diff --git a/src/JsonDataMasking.Test/MockData/BasicTypesMock.cs b/src/JsonDataMasking.Test/MockData/BasicTypesMock.cs new file mode 100644 index 0000000..b0d0da3 --- /dev/null +++ b/src/JsonDataMasking.Test/MockData/BasicTypesMock.cs @@ -0,0 +1,24 @@ +using System; + +namespace JsonDataMasking.Test.MockData +{ + public class BasicTypesMock + { + public bool Bool { get; set; } = true; + public byte Byte { get; set; } = 1; + public sbyte Sbyte { get; set; } = 1; + public short Short { get; set; } = 1; + public ushort Ushort { get; set; } = 1; + public int Int { get; set; } = 1; + public uint Uint { get; set; } = 1; + public long Long { get; set; } = 1; + public ulong Ulong { get; set; } = 1; + public float Float { get; set; } = 1; + public double Double { get; set; } = 1; + public decimal Decimal { get; set; } = 1; + public char Char { get; set; } = '1'; + public DateTime DateTime { get; set; } = new DateTime().AddYears(1); + public DateTimeOffset DateTimeOffset { get; set; } = new DateTimeOffset().AddYears(1); + public Guid Guid { get; set; } = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"); + } +} diff --git a/src/JsonDataMasking.Test/MockData/BasicTypesSensitiveMock.cs b/src/JsonDataMasking.Test/MockData/BasicTypesSensitiveMock.cs new file mode 100644 index 0000000..e4abe7c --- /dev/null +++ b/src/JsonDataMasking.Test/MockData/BasicTypesSensitiveMock.cs @@ -0,0 +1,41 @@ +using JsonDataMasking.Attributes; +using System; + +namespace JsonDataMasking.Test.MockData +{ + public class BasicTypesSensitiveMock + { + [SensitiveData] + public bool Bool { get; set; } = true; + [SensitiveData] + public byte Byte { get; set; } = 1; + [SensitiveData] + public sbyte Sbyte { get; set; } = 1; + [SensitiveData] + public short Short { get; set; } = 1; + [SensitiveData] + public ushort Ushort { get; set; } = 1; + [SensitiveData] + public int Int { get; set; } = 1; + [SensitiveData] + public uint Uint { get; set; } = 1; + [SensitiveData] + public long Long { get; set; } = 1; + [SensitiveData] + public ulong Ulong { get; set; } = 1; + [SensitiveData] + public float Float { get; set; } = 1; + [SensitiveData] + public double Double { get; set; } = 1; + [SensitiveData] + public decimal Decimal { get; set; } = 1; + [SensitiveData] + public char Char { get; set; } = '1'; + [SensitiveData] + public DateTime DateTime { get; set; } = new DateTime().AddYears(1); + [SensitiveData] + public DateTimeOffset DateTimeOffset { get; set; } = new DateTimeOffset().AddYears(1); + [SensitiveData] + public Guid Guid { get; set; } = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"); + } +} diff --git a/src/JsonDataMasking/Masks/JsonMask.cs b/src/JsonDataMasking/Masks/JsonMask.cs index 9edd4a4..718c27b 100644 --- a/src/JsonDataMasking/Masks/JsonMask.cs +++ b/src/JsonDataMasking/Masks/JsonMask.cs @@ -18,7 +18,7 @@ public static class JsonMask #region Masking /// - /// Mask values of string and some string collections type class properties that have the [SensitiveData] attribute. + /// Mask values of primitive types and some string collections type class properties that have the [SensitiveData] attribute. /// Properties with null values or that don't have the attribute remain unchanged. /// /// @@ -61,16 +61,44 @@ private static T MaskPropertiesWithSensitiveDataAttribute(T data) { MaskDictionaryProperty(data, property); } - else if (propertyAttribute != null && IsSupportedBaseType(property.PropertyType)) + else if (propertyAttribute != null) { - var maskedPropertyValue = GetMaskedPropertyValue(propertyValue?.ToString(), propertyAttribute); + var maskedPropertyValue = GetMaskedPropertyValue(property.PropertyType, propertyValue, propertyAttribute); property.SetValue(data, maskedPropertyValue); } } return data; } - private static string? GetMaskedPropertyValue(string? currentPropertyValue, SensitiveDataAttribute attribute) + private static readonly Dictionary MaskDefaults = new Dictionary() + { + [typeof(bool)] = default(bool), + [typeof(byte)] = default(byte), + [typeof(sbyte)] = default(sbyte), + [typeof(short)] = default(short), + [typeof(ushort)] = default(ushort), + [typeof(int)] = default(int), + [typeof(uint)] = default(uint), + [typeof(long)] = default(long), + [typeof(ulong)] = default(ulong), + [typeof(float)] = default(float), + [typeof(double)] = default(double), + [typeof(decimal)] = default(decimal), + [typeof(char)] = default(char), + [typeof(DateTime)] = default(DateTime), + [typeof(DateTimeOffset)] = default(DateTimeOffset), + [typeof(Guid)] = default(Guid) + }; + + private static object? GetMaskedPropertyValue(Type type, object? currentPropertyValue, SensitiveDataAttribute attribute) + => type switch + { + _ when type == typeof(string) => GetMaskedPropertyValueString(currentPropertyValue?.ToString(), attribute), + _ when MaskDefaults.TryGetValue(type, out var defaultValue) => defaultValue, + _ => throw new NotSupportedException($"Masking of type {type.Name} is not supported") + }; + + private static string? GetMaskedPropertyValueString(string? currentPropertyValue, SensitiveDataAttribute attribute) { if (string.IsNullOrWhiteSpace(currentPropertyValue)) return currentPropertyValue; @@ -114,8 +142,8 @@ private static void MaskIEnumerableProperty(T data, PropertyInfo property) object? maskedCollectionValue = null; if (IsClassReferenceType(collectionType)) maskedCollectionValue = MaskPropertiesWithSensitiveDataAttribute(value); - else if (propertyAttribute != null && IsSupportedBaseType(collectionType)) - maskedCollectionValue = GetMaskedPropertyValue(value?.ToString(), propertyAttribute); + else if (propertyAttribute != null) + maskedCollectionValue = GetMaskedPropertyValue(collectionType, value, propertyAttribute); if (maskedCollectionValue != null) maskedCollection.Add(maskedCollectionValue); @@ -138,7 +166,7 @@ private static void MaskDictionaryProperty(T data, PropertyInfo property) foreach (var pair in collection) { - var maskedCollectionValue = GetMaskedPropertyValue(pair.Value, propertyAttribute); + var maskedCollectionValue = GetMaskedPropertyValueString(pair.Value, propertyAttribute); maskedCollection.Add(pair.Key, maskedCollectionValue); } property.SetValue(data, maskedCollection); @@ -160,12 +188,6 @@ private static IEnumerable ConvertCollectionType(IEnumerable collection, Type ty private static bool IsPropertyTypeEqualsToAnonymousType(PropertyInfo property) => property.ReflectedType.AssemblyQualifiedName.Contains("AnonymousType"); - private static bool IsSupportedBaseType(Type type) => type switch - { - Type _ when type == typeof(string) => true, - _ => throw new NotSupportedException("Masking of non-string base types is not supported") - }; - private static bool AreFirstAndLastParametersInValidRange(int propertySize, SensitiveDataAttribute attribute) => attribute.ShowFirst <= propertySize && attribute.ShowLast <= propertySize && (attribute.ShowFirst + attribute.ShowLast) <= propertySize;