CarterCommunity / Carter

Carter is framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing code to be more explicit and most importantly more enjoyable.
MIT License
2.15k stars 177 forks source link

How to register a global ASP.NET Core response masking filter? #363

Open Milad-Rashidi-Git opened 1 month ago

Milad-Rashidi-Git commented 1 month ago

I am using Carter (version 8.1.0) and have developed some minimal APIs with CQRS and vertical slice architecture in my .Net Core 8 web application. I need to mask some sensitive data in the response, for which I’ve created a ResultFilterAttribute as shown below:

public class MaskSensitiveDataAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        // Check if the result is an ObjectResult (which holds the JSON response)  
        if (context.Result is ObjectResult objectResult)
        {
            var responseObject = objectResult.Value;
            if (responseObject != null)
            {
                var responseType = responseObject.GetType();
                var jsonResponse = JsonConvert.SerializeObject(responseObject);
                var maskedJsonResponse = MaskResponse(jsonResponse, responseType);

                // Create a new ObjectResult with masked data  
                context.Result = new ObjectResult(JsonConvert.DeserializeObject(maskedJsonResponse))
                {
                    StatusCode = objectResult.StatusCode,
                    ContentTypes = { "application/json" }
                };
            }
        }

        base.OnResultExecuting(context);
    }

    private string MaskResponse(string jsonResponse, Type modelType)
    {
        var jsonObject = JsonConvert.DeserializeObject<JObject>(jsonResponse);
        if (jsonObject != null)
        {
            MaskSensitiveProperties(jsonObject, modelType);
        }

        return jsonObject.ToString(Formatting.None);
    }

    private void MaskSensitiveProperties(JObject jsonObject, Type modelType)
    {
        var properties = modelType.GetProperties();

        foreach (var property in properties)
        {
            if (Attribute.IsDefined(property, typeof(MaskedAttribute)))
            {
                if (jsonObject.ContainsKey(property.Name))
                {
                    jsonObject[property.Name] = new string('*', jsonObject[property.Name].ToString().Length);
                }
            }
        }
    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class MaskedAttribute : Attribute { }

This is the Program.cs file:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
var assembly = typeof(Program).Assembly;

builder.Services
    .AddCarter()
    .AddSwaggerAndUI()
    .AddApplicationServices()
    .ConfigureCrossOriginPolicy()
    .RegisterMappingsAndTypeConfigs()
    .AddMediatorConfiguration(assembly)
    .AddValidatorsFromAssembly(assembly)
    .AddAuthConfig(builder.Configuration)
    .AddApplicationSettings(builder.Configuration)
    .AddEfConfig(builder.Configuration, assembly.FullName, true)
    .AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

WebApplication app = builder.Build();
IdentityModelEventSource.ShowPII = true;

await app
    .UseMiddleware<ExceptionHandlingMiddleware>()
    .UseSwaggerAndUI(app)
    .UseCors("AllowAllOrigins")
    .UseAuthentication()
    .UseAuthorization()
    .UseAntiforgery()
    .ApplyDbMigrationsAsync(app);

app.MapCarter();

await app.RunAsync();

To add the MaskSensitiveDataAttribute filter to the pipeline, I researched extensively and expected to find something like the following code:

services.AddCarter(null, config =>  
{
    config.Filters.Add<MaskSensitiveDataAttribute>(); // Here I wanted to register global response masking filter
    config.AddFilter<MaskSensitiveDataAttribute>(); // Or register it like this
});

However, the AddCarter extension method is defined as follows and takes an Action<CarterConfigurator>, which doesn’t provide an option to add filters:

public static IServiceCollection AddCarter(this IServiceCollection services,
    DependencyContextAssemblyCatalog assemblyCatalog = null,
    Action<CarterConfigurator> configurator = null)

My question is: How can I register this global response masking filter in carter?

If there were a solution to not using reflection, it would be much appreciated as the reflection has a performance overhead.

Please Note: I considered creating a response masking middleware, but it has some issues:

  1. I don't know the type of the response model to find all properties decorated with the Masked attribute. (While it’s possible to set the type of the response DTO using HttpContext.Items["ResponseModelType"] and read it in the middleware, I prefer not to do this as it requires passing this data in all endpoints.)
  2. It necessitates maintaining a predefined list of property names that must be masked.
  3. It requires looking up in the response body and masking all properties with similar names, such as Email, but there may be cases where we do not want to mask Email.
jchannon commented 1 month ago

I think middleware is your best option tbh

jchannon commented 1 month ago

Or you use some sort of service class that you inject into the routes you know the types you want to mask

app.MapGet("/foo/{id}", string id, IDb db, IMaskService maskService) =>
    {
        var entity = db.GetById(id);

       var maskedEntity = maskService.Mask(entity);
        return maskedEntity;
    });
Milad-Rashidi-Git commented 1 month ago

I think middleware is your best option tbh

I don't think so because of those reasons I mentioned in the question.

JoeStead commented 1 month ago

Which specific reason? I don't see why this can't be achieved by middleware with the information presented

Milad-Rashidi-Git commented 1 month ago

Or you use some sort of service class that you inject into the routes you know the types you want to mask

app.MapGet("/foo/{id}", string id, IDb db, IMaskService maskService) =>
    {
        var entity = db.GetById(id);

       var maskedEntity = maskService.Mask(entity);
        return maskedEntity;
    });

As I mentioned in my question, I need to automate the Data Masking process.

Milad-Rashidi-Git commented 1 month ago

@JoeStead I considered creating a response masking middleware, but it has some issues:

  1. I don't know the type of the response model to find all properties decorated with the Masked attribute. (While it’s possible to set the type of the response DTO using HttpContext.Items["ResponseModelType"] and read it in the middleware, I prefer not to do this as it requires passing this data in all endpoints.)
  2. It necessitates maintaining a predefined list of property names that must be masked.
  3. It requires looking up in the response body and masking all properties with similar names, such as Email, but there may be cases where we do not want to mask Email.
JoeStead commented 1 month ago

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

Milad-Rashidi-Git commented 1 month ago

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

Are you saying that there is no way to add an ASP.NET Core result filter to Carter?

JoeStead commented 1 month ago

That is an ASP.NET Core MVC feature, not asn ASP.NET Core feature afaik

jchannon commented 1 month ago

You can use endpoint filter. Not sure about result filter

On Mon, 23 Sep 2024 at 17:07, Milad Rashidi @.***> wrote:

Carter operates at the level you're having issues with. The issues you're facing, carter will have to deal with internally too.

If you want to mask responses, surely the correct solution would be to not send fields, and do any masking on the UI?

Are you saying that there is no way to add an ASP.NET Core result filter to Carter?

— Reply to this email directly, view it on GitHub https://github.com/CarterCommunity/Carter/issues/363#issuecomment-2368740143, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJXL5JNFHVD22W2KV4TZYA4E7AVCNFSM6AAAAABOU3O6ESVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRYG42DAMJUGM . You are receiving this because you commented.Message ID: @.***>

Milad-Rashidi-Git commented 1 month ago

@JoeStead Since ASP.NET Core integrates MVC into its structure, even in an API project, you can still leverage filters, including result filters.

dotnet new webapi -n SampleApiWithFilter  
cd SampleApiWithFilter
// Filters/MyResultFilter.cs  
using Microsoft.AspNetCore.Mvc.Filters;  
using Microsoft.AspNetCore.Mvc;  
using System.Threading.Tasks;  

public class MyResultFilter : IAsyncResultFilter  
{  
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)  
    {  
        // Code to execute before the result is processed  
        // For example, adding a custom header  
        context.HttpContext.Response.Headers.Add("X-My-Custom-Header", "HeaderValue");  

        // Continue executing the next result filter or the action result  
        await next(); // Call the next delegate/middleware in the pipeline  

        // Code to execute after the result is processed  
        // You can log results or modify the response if needed  
    }  
}
// Controllers/WeatherForecastController.cs  
using Microsoft.AspNetCore.Mvc;  
using System.Collections.Generic;  

[ApiController]  
[Route("[controller]")]  
[ServiceFilter(typeof(MyResultFilter))] // Apply filter at the controller level  
public class WeatherForecastController : ControllerBase  
{  
    private static readonly string[] Summaries = new[]  
    {  
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"  
    };  

    [HttpGet]  
    public IEnumerable<WeatherForecast> Get()  
    {  
        var rng = new Random();  
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast  
        {  
            Date = DateTime.Now.AddDays(index),  
            TemperatureC = rng.Next(-20, 55),  
            Summary = Summaries[rng.Next(Summaries.Length)]  
        })  
        .ToArray();  
    }  
}  

public class WeatherForecast  
{  
    public DateTime Date { get; set; }  
    public int TemperatureC { get; set; }  
    public string Summary { get; set; }  
}
public class Startup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        services.AddControllers(options =>  
        {  
            // You can also apply globally with options.Filters.Add(new MyResultFilter());  
        });  
        services.AddScoped<MyResultFilter>(); // Registering the filter  
    }  

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
    {  
        if (env.IsDevelopment())  
        {  
            app.UseDeveloperExceptionPage();  
        }  

        app.UseRouting();  

        app.UseAuthorization();  

        app.UseEndpoints(endpoints =>  
        {  
            endpoints.MapControllers();  
        });  
    }  
}
dotnet run
JoeStead commented 1 month ago

That template generates an MVC project

Milad-Rashidi-Git commented 1 month ago

You can use endpoint filter. Not sure about result filter

I'm okay with using an Endpoint Filter instead of a Result Filter. How to add it to the carter?

jchannon commented 1 month ago

https://github.com/CarterCommunity/Carter/issues/339

Milad-Rashidi-Git commented 1 month ago

@jchannon

I believe that the issues I mentioned regarding response middleware also apply to Endpoint Filters. How can I determine the type of the response DTO model within an IEndpointFilter? For instance, how can I ascertain whether this response is of type UserDto?

jchannon commented 1 month ago

No idea I’m afraid

You could look to use Carters IResponseNegotiator will give you object you can do a Gettype on but not sure this is the right thing to do either

On Mon, 23 Sep 2024 at 18:10, Milad Rashidi @.***> wrote:

@jchannon https://github.com/jchannon

I believe that the issues I mentioned regarding response middleware also apply to Endpoint Filters. How can I determine the type of the response DTO model within an IEndpointFilter? For instance, how can I ascertain whether this response is of type UserDto ?

— Reply to this email directly, view it on GitHub https://github.com/CarterCommunity/Carter/issues/363#issuecomment-2368880644, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJRA37X2GGL26P7KTSTZYBDOTAVCNFSM6AAAAABOU3O6ESVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRYHA4DANRUGQ . You are receiving this because you were mentioned.Message ID: @.***>

Milad-Rashidi-Git commented 1 month ago

No idea I’m afraid You could look to use Carters IResponseNegotiator will give you object you can do a Gettype on but not sure this is the right thing to do either

Ok, thanks.

MrDave1999 commented 1 month ago

@Milad-Rashidi-Git The solution is simple: Don't use minimal APIs. Use controllers.