Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deny access functionality #34

Merged
merged 6 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.cs]
indent_style = tab
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs
Original file line number Diff line number Diff line change
@@ -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<int>() { 1, 2, 3 }
}
};

var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObjectAndDeny>();
patchDocument.Test(o => o.SimpleObject.IntegerList, 3, 2);

// Act & Assert
var exception = Assert.Throws<JsonPatchAccessDeniedException>(() => 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<SimpleObject>()
{
new SimpleObject
{
StringProperty = "String1"
},
new SimpleObject
{
StringProperty = "String2"
}
}
};

var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObjectAndDeny>();
patchDocument.Add(o => o.SimpleObjectList[0].StringProperty, "ChangedString1");

// Act & Assert
var exception = Assert.Throws<JsonPatchAccessDeniedException>(() => 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<int>() { 1, 2, 3 }
};

var patchDocument = new JsonPatchDocument();
patchDocument.Remove("IntegerList/2");

// Act & Assert
var exception = Assert.Throws<JsonPatchAccessDeniedException>(() => 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<JsonPatchAccessDeniedException>(() => patchDocument.ApplyTo(targetObject));
Assert.Equal(nameof(SimpleObjectWithDeny.IntegerValue), exception.Property);
Assert.Equal(nameof(SimpleObjectWithDeny), exception.Type);
}

}
28 changes: 28 additions & 0 deletions SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;

namespace SystemTextJsonPatch;

public class SimpleObjectWithDeny
{
[DenyPatch]
public List<int> IntegerList { get; set; }
[DenyPatch]
public IList<int> 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; }
}
Original file line number Diff line number Diff line change
@@ -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<SimpleObject> SimpleObjectList { get; set; }
[DenyPatch]
public IList<SimpleObject> SimpleObjectIList { get; set; }

public SimpleObjectWithDeny Allow { get; set; }
public SimpleObjectWithNestedObjectAndDeny()
{
NestedObject = new NestedObject();
SimpleObject = new SimpleObject();
InheritedObject = new InheritedObject();
SimpleObjectList = new List<SimpleObject>();
Allow = new SimpleObjectWithDeny();
}
}
1 change: 1 addition & 0 deletions SystemTextJsonPatch.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions SystemTextJsonPatch/DenyPatchAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace SystemTextJsonPatch
{
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class DenyPatchAttribute : Attribute
{
}
}
32 changes: 32 additions & 0 deletions SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs
Original file line number Diff line number Diff line change
@@ -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")
{

}
}
}

11 changes: 11 additions & 0 deletions SystemTextJsonPatch/Internal/PropertyProxyCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +41,7 @@ internal static class PropertyProxyCache
var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.OrdinalIgnoreCase))
{
EnsureAccessToProperty(propertyInfo);
return new PropertyProxy(propertyInfo);
}
}
Expand All @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't there also generic overload propertyInfo.GetCustomAttribute<DenyPatchAttribute>(...) `?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not a big deal anyway

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there is, but i don't think it exists in netstandard2.0.

{
throw new JsonPatchAccessDeniedException(propertyInfo);
}
}
}
}
Loading