diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..765b22f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +indent_style = tab \ No newline at end of file diff --git a/README.md b/README.md index c29288d..804990d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ public IHttpActionResult Patch( } ``` +## Deny access to properties +If you need to stop JsonPatch from reading or writing to some properties, +then you can decorate them with `[DenyPatch]`, if a patch occurs that happens to access the property then a `JsonPatchAccessDeniedException` is thrown. + + ## Migration from v1 JsonPatchDocumentConverterFactory no longer needs to be set to JsonSerializerOptions. diff --git a/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs b/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs new file mode 100644 index 0000000..6a3dcb5 --- /dev/null +++ b/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text.Json; +using SystemTextJsonPatch.Exceptions; +using Xunit; + +namespace SystemTextJsonPatch.IntegrationTests; + +public class DenyIntegrationTest +{ + [Fact] + public void TestInList() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.SimpleObject.IntegerList, 3, 2); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny.SimpleObject), exception.Property); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny), exception.Type); + } + + [Fact] + public void AddToComplextTypeListSpecifyIndex() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny() + { + SimpleObjectList = new List() + { + new SimpleObject + { + StringProperty = "String1" + }, + new SimpleObject + { + StringProperty = "String2" + } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => o.SimpleObjectList[0].StringProperty, "ChangedString1"); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny.SimpleObjectList), exception.Property); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny), exception.Type); + } + + [Fact] + public void RemoveFromList() + { + // Arrange + var targetObject = new SimpleObjectWithDeny() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("IntegerList/2"); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithDeny.IntegerList), exception.Property); + Assert.Equal(nameof(SimpleObjectWithDeny), exception.Type); + } + + + [Fact] + public void InnerObjectDeny() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny(); + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("Allow/IntegerValue", 1); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithDeny.IntegerValue), exception.Property); + Assert.Equal(nameof(SimpleObjectWithDeny), exception.Type); + } + +} diff --git a/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs new file mode 100644 index 0000000..1194d72 --- /dev/null +++ b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace SystemTextJsonPatch; + +public class SimpleObjectWithDeny +{ + [DenyPatch] + public List IntegerList { get; set; } + [DenyPatch] + public IList IntegerIList { get; set; } + [DenyPatch] + public int IntegerValue { get; set; } + [DenyPatch] + public int AnotherIntegerValue { get; set; } + [DenyPatch] + public string StringProperty { get; set; } + [DenyPatch] + public string AnotherStringProperty { get; set; } + [DenyPatch] + public decimal DecimalValue { get; set; } + [DenyPatch] + public double DoubleValue { get; set; } + [DenyPatch] + public float FloatValue { get; set; } + [DenyPatch] + public Guid GuidValue { get; set; } +} diff --git a/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs new file mode 100644 index 0000000..91c1416 --- /dev/null +++ b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SystemTextJsonPatch; + +public class SimpleObjectWithNestedObjectAndDeny +{ + public int IntegerValue { get; set; } + [DenyPatch] + public NestedObject NestedObject { get; set; } + [DenyPatch] + public SimpleObject SimpleObject { get; set; } + [DenyPatch] + public InheritedObject InheritedObject { get; set; } + [DenyPatch] + public List SimpleObjectList { get; set; } + [DenyPatch] + public IList SimpleObjectIList { get; set; } + + public SimpleObjectWithDeny Allow { get; set; } + public SimpleObjectWithNestedObjectAndDeny() + { + NestedObject = new NestedObject(); + SimpleObject = new SimpleObject(); + InheritedObject = new InheritedObject(); + SimpleObjectList = new List(); + Allow = new SimpleObjectWithDeny(); + } +} diff --git a/SystemTextJsonPatch.sln b/SystemTextJsonPatch.sln index dbb305f..9439aef 100644 --- a/SystemTextJsonPatch.sln +++ b/SystemTextJsonPatch.sln @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJsonPatch.Tests", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{24A69526-2027-4376-B6DE-E66612C73D16}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig CHANGELOG.md = CHANGELOG.md README.md = README.md EndProjectSection diff --git a/SystemTextJsonPatch/DenyPatchAttribute.cs b/SystemTextJsonPatch/DenyPatchAttribute.cs new file mode 100644 index 0000000..2864f99 --- /dev/null +++ b/SystemTextJsonPatch/DenyPatchAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace SystemTextJsonPatch +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class DenyPatchAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs new file mode 100644 index 0000000..2895885 --- /dev/null +++ b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace SystemTextJsonPatch.Exceptions +{ + +#pragma warning disable CA1032 // Implement standard exception constructors + [Serializable] + public class JsonPatchAccessDeniedException : JsonPatchException +#pragma warning restore CA1032 // Implement standard exception constructors + { + public string Type { get; } = "N/A"; + public string Property { get; } = "N/A"; + public JsonPatchAccessDeniedException(string message, Exception? innerException) : base(message, innerException) + { + } + + public JsonPatchAccessDeniedException(string type, string property) : this($"Patch is not allowed to access the property {property} on the type {type}", (Exception?)null) + { + this.Type = type; + this.Property = property; + } + + public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this(propertyInfo?.DeclaringType?.Name ?? "N/A", propertyInfo?.Name ?? "N/A") + { + + } + } +} + diff --git a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs index 9049624..61dbb11 100644 --- a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs +++ b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Reflection; using System.Text.Json.Serialization; +using SystemTextJsonPatch.Exceptions; using SystemTextJsonPatch.Internal.Proxies; namespace SystemTextJsonPatch.Internal @@ -40,6 +41,7 @@ internal static class PropertyProxyCache var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute(); if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.OrdinalIgnoreCase)) { + EnsureAccessToProperty(propertyInfo); return new PropertyProxy(propertyInfo); } } @@ -49,11 +51,20 @@ internal static class PropertyProxyCache { if (string.Equals(propertyInfo.Name, propName, StringComparison.OrdinalIgnoreCase)) { + EnsureAccessToProperty(propertyInfo); return new PropertyProxy(propertyInfo); } } return null; } + + private static void EnsureAccessToProperty(PropertyInfo propertyInfo) + { + if (propertyInfo.GetCustomAttribute(typeof(DenyPatchAttribute), true) != null) + { + throw new JsonPatchAccessDeniedException(propertyInfo); + } + } } }