-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2ca84e2
commit 699a1c9
Showing
8 changed files
with
463 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Dolittle.SDK.Analyzers; | ||
|
||
/// <summary> | ||
/// Analyzer for <see cref="DescriptorRules.Events.Redaction.RedactablePersonalDataAttribute"/>. | ||
/// </summary> | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class RedactablePropertyAnalyzer : DiagnosticAnalyzer | ||
{ | ||
static readonly DiagnosticDescriptor _rule = DescriptorRules.NonNullableRedactableField; | ||
|
||
/// <inheritdoc /> | ||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [_rule]; | ||
|
||
/// <inheritdoc /> | ||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | | ||
GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterSyntaxNodeAction(AnalyzeProperty, SyntaxKind.PropertyDeclaration); | ||
context.RegisterSyntaxNodeAction(AnalyzeRecordDeclaration, SyntaxKind.RecordDeclaration); | ||
} | ||
|
||
private static void AnalyzeProperty(SyntaxNodeAnalysisContext context) | ||
{ | ||
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node; | ||
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration); | ||
|
||
if (propertySymbol == null) return; | ||
|
||
var hasAttribute = propertySymbol.GetAttributes() | ||
.Any(attr => attr.AttributeClass?.Name == "RedactablePersonalDataAttribute"); | ||
|
||
if (!hasAttribute) return; | ||
|
||
if (propertySymbol.Type.NullableAnnotation != NullableAnnotation.Annotated) | ||
{ | ||
context.ReportDiagnostic(Diagnostic.Create(_rule, propertyDeclaration.GetLocation(), propertySymbol.Name)); | ||
} | ||
} | ||
|
||
private static void AnalyzeRecordDeclaration(SyntaxNodeAnalysisContext context) | ||
{ | ||
var recordDeclaration = (RecordDeclarationSyntax)context.Node; | ||
|
||
// Check if it's a record with a primary constructor | ||
if (recordDeclaration.ParameterList == null) return; | ||
|
||
foreach (var parameter in recordDeclaration.ParameterList.Parameters) | ||
{ | ||
var isPersonalDataAnnotated = parameter.AttributeLists | ||
.SelectMany(list => list.Attributes) | ||
.Any(attr => attr.Name.ToString().StartsWith("RedactablePersonalData")); | ||
if (!isPersonalDataAnnotated) | ||
{ | ||
continue; | ||
} | ||
|
||
var parameterSymbol = context.SemanticModel.GetDeclaredSymbol(parameter); | ||
if (!IsNullable(parameterSymbol)) | ||
{ | ||
context.ReportDiagnostic(Diagnostic.Create(_rule, parameter.GetLocation(), parameterSymbol.Name)); | ||
} | ||
|
||
} | ||
} | ||
|
||
static bool IsNullable(IParameterSymbol? parameterSymbol) | ||
{ | ||
return parameterSymbol?.Type.NullableAnnotation == NullableAnnotation.Annotated; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Dolittle.SDK.Events.Redaction; | ||
|
||
/// <summary> | ||
/// Event that triggers redaction of the given personal data | ||
/// It will target the given event type and redact the properties specified within the EventSourceId of the event | ||
/// </summary> | ||
[EventType("de1e7e17-bad5-da7a-fad4-fbc6ec3c0ea5")] | ||
public class PersonalDataRedactedForEvent | ||
{ | ||
public string EventId { get; init; } | ||
public string EventAlias { get; init; } | ||
public Dictionary<string, object?> RedactedProperties { get; init; } | ||
public string RedactedBy { get; init; } | ||
public string Reason { get; init; } | ||
|
||
/// <summary> | ||
/// Try to create a PersonalDataRedacted event for a given event type | ||
/// Must include reason and redactedBy | ||
/// </summary> | ||
/// <param name="reason"></param> | ||
/// <param name="redactedBy"></param> | ||
/// <param name="redactionEvent"></param> | ||
/// <param name="error"></param> | ||
/// <typeparam name="TEvent"></typeparam> | ||
/// <returns></returns> | ||
public static bool TryCreate<TEvent>(string reason, string redactedBy, | ||
[NotNullWhen(true)] out PersonalDataRedactedForEvent? redactionEvent, [NotNullWhen(false)] out string? error) | ||
where TEvent : class | ||
{ | ||
redactionEvent = default; | ||
error = default; | ||
if (string.IsNullOrWhiteSpace(reason)) | ||
{ | ||
error = "Reason cannot be empty"; | ||
return false; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(redactedBy)) | ||
{ | ||
error = "RedactedBy cannot be empty"; | ||
return false; | ||
} | ||
|
||
var eventType = EventTypeMetadata<TEvent>.EventType; | ||
if (eventType?.Id is null) | ||
{ | ||
error = $"EventType not defined for type {typeof(TEvent).Name}"; | ||
return false; | ||
} | ||
|
||
var redactedFields = RedactedType<TEvent>.RedactedProperties; | ||
if (redactedFields.Count == 0) | ||
{ | ||
error = $"No redacted fields found for event type {eventType.Alias}"; | ||
return false; | ||
} | ||
|
||
redactionEvent = new PersonalDataRedactedForEvent | ||
{ | ||
EventId = eventType.Id.ToString(), | ||
EventAlias = eventType.Alias ?? typeof(TEvent).Name, | ||
RedactedProperties = new Dictionary<string, object?>(redactedFields), | ||
RedactedBy = redactedBy, | ||
Reason = reason | ||
}; | ||
return true; | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
Source/Events/Redaction/RedactablePersonalDataAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
|
||
namespace Dolittle.SDK.Events.Redaction; | ||
|
||
/// <summary> | ||
/// Attribute to mark a property as redactable. | ||
/// This allows the system to remove personal data from persisted events. | ||
/// Properties that should be redactable must be nullable, and redacted properties will be set to null. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property)] | ||
public class RedactablePersonalDataAttribute : Attribute | ||
{ | ||
/// <summary> | ||
/// Gets the value to replace the redacted property with. | ||
/// </summary> | ||
public object? ReplacementValue { get; protected set; } = null; | ||
} | ||
|
||
/// <summary> | ||
/// Attribute to mark a property as redactable. | ||
/// This allows the system to remove personal data from persisted events. | ||
/// This subclass allows for a specific replacement value to be set, instead of the complete removal of the property. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property)] | ||
public class RedactablePersonalDataAttribute<T> : RedactablePersonalDataAttribute | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="RedactablePersonalDataAttribute{T}"/> class. | ||
/// </summary> | ||
/// <param name="replacementValue">The value to replace the redacted property with.</param> | ||
public RedactablePersonalDataAttribute(T replacementValue) | ||
{ | ||
ReplacementValue = replacementValue; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace Dolittle.SDK.Events.Redaction; | ||
|
||
/// <summary> | ||
/// Tool to get the list of properties that are redacted for a given type | ||
/// </summary> | ||
public static class RedactedType<T> where T : class | ||
{ | ||
/// <summary> | ||
/// Get the properties that are redacted for a given type. Could be empty | ||
/// </summary> | ||
// ReSharper disable once StaticMemberInGenericType | ||
public static ImmutableDictionary<string, object?> RedactedProperties { get; } = GetRedactableProperties(); | ||
|
||
private static ImmutableDictionary<string, object?> GetRedactableProperties() | ||
{ | ||
var redactableRemovedProperties = | ||
GetRedactableReplacedProperties() | ||
.ToImmutableDictionary(pair => pair.Key, pair => pair.Value); | ||
return redactableRemovedProperties; | ||
} | ||
|
||
// private static IEnumerable<KeyValuePair<string, object?>> GetRemovedProperties() | ||
// { | ||
// return typeof(T).GetProperties() | ||
// .Where(prop => Attribute.IsDefined(prop, typeof(RedactablePersonalDataAttribute))) | ||
// .Select(it => new KeyValuePair<string, object?>(it.Name, it)); | ||
// } | ||
|
||
private static IEnumerable<KeyValuePair<string, object?>> GetRedactableReplacedProperties() | ||
{ | ||
return typeof(T).GetProperties() | ||
.Where(prop => Attribute.IsDefined(prop, typeof(RedactablePersonalDataAttribute))) | ||
.Select(ToReplacement) | ||
.OfType<KeyValuePair<string, object?>>(); | ||
} | ||
|
||
static KeyValuePair<string, object?>? ToReplacement(PropertyInfo prop) | ||
{ | ||
var attribute = prop.GetCustomAttribute<RedactablePersonalDataAttribute>(); | ||
if (attribute is null) | ||
{ | ||
return null; | ||
} | ||
return new KeyValuePair<string, object?>(prop.Name, attribute.ReplacementValue); | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
Tests/Analyzers/Diagnostics/RedactablePropertyAnalyzerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) Dolittle. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Threading.Tasks; | ||
|
||
namespace Dolittle.SDK.Analyzers.Diagnostics; | ||
|
||
public class RedactablePropertyAnalyzerTests : AnalyzerTest<RedactablePropertyAnalyzer> | ||
{ | ||
[Fact] | ||
public async Task OnPropertyWithoutRedactableAttribute() | ||
{ | ||
var code = @" | ||
using Dolittle.SDK.Events; | ||
[EventType(""44a755a7-e755-4076-bad4-fbc6ec3c0ea5"")] | ||
class SomeEvent | ||
{ | ||
public string SomeProperty { get; set; } | ||
} | ||
"; | ||
|
||
await VerifyAnalyzerFindsNothingAsync(code); | ||
} | ||
|
||
[Fact] | ||
public async Task OnPropertyWithNullableRedactableAttribute() | ||
{ | ||
var code = @" | ||
using Dolittle.SDK.Events; | ||
using Dolittle.SDK.Events.Redaction; | ||
[EventType(""44a755a7-e755-4076-bad4-fbc6ec3c0ea5"")] | ||
class SomeEvent | ||
{ | ||
[RedactablePersonalData] | ||
public string? SomeProperty { get; set; } | ||
} | ||
"; | ||
|
||
await VerifyAnalyzerFindsNothingAsync(code); | ||
} | ||
|
||
[Fact] | ||
public async Task OnPropertyWithNonNullableRedactableAttribute() | ||
{ | ||
var code = @" | ||
using Dolittle.SDK.Events; | ||
using Dolittle.SDK.Events.Redaction; | ||
[EventType(""44a755a7-e755-4076-bad4-fbc6ec3c0ea5"")] | ||
class SomeEvent | ||
{ | ||
[RedactablePersonalData] | ||
public string SomeProperty { get; set; } | ||
} | ||
"; | ||
await VerifyAnalyzerAsync(code, | ||
Diagnostic(DescriptorRules.NonNullableRedactableField) | ||
.WithSpan(8, 5, 9, 45).WithArguments("SomeProperty")); | ||
} | ||
|
||
[Fact] | ||
public async Task OnRecordPropertyWithNonNullableRedactableAttribute() | ||
{ | ||
var code = @" | ||
using Dolittle.SDK.Events; | ||
using Dolittle.SDK.Events.Redaction; | ||
[EventType(""44a755a7-e755-4076-bad4-fbc6ec3c0ea5"")] | ||
record SomeEvent([property: RedactablePersonalData] string SomeProperty); | ||
"; | ||
await VerifyAnalyzerAsync(code, | ||
Diagnostic(DescriptorRules.NonNullableRedactableField) | ||
.WithSpan(6, 18, 6, 72).WithArguments("SomeProperty")); | ||
} | ||
|
||
[Fact] | ||
public async Task OnRecordPropertyWithNullableRedactableAttribute() | ||
{ | ||
var code = @" | ||
using Dolittle.SDK.Events; | ||
using Dolittle.SDK.Events.Redaction; | ||
[EventType(""44a755a7-e755-4076-bad4-fbc6ec3c0ea5"")] | ||
record SomeEvent([property: RedactablePersonalData] string? SomeProperty); | ||
"; | ||
await VerifyAnalyzerFindsNothingAsync(code); | ||
} | ||
} |
Oops, something went wrong.