I have implemented Audit Trail in asp.net core 3.1 application using great library which has a very good documentation also : https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.WebApi/README.md
I have implemented it in a asp.net core 3.1 web api project with the recommended approach : Middleware + Action Filters (Asp.Net Core): Adding the Audit Middleware together with the Global Action Filter (or Local Action Filters).
I have the following sample output:
{
"EventType":"POST Values/Post",
"Environment":{
"UserName":"Federico",
"MachineName":"HP",
"DomainName":"HP",
"CallingMethodName":"WebApiTest.Controllers.ValuesController.Post()",
"AssemblyName":"WebApiTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Culture":"en-US"
},
"StartDate":"2017-03-09T18:03:05.5287603-06:00",
"EndDate":"2017-03-09T18:03:05.5307604-06:00",
"Duration":2,
"Action":{
"TraceId": "0HLFLQP4HGFAF_00000001",
"HttpMethod":"POST",
"ControllerName":"Values",
"ActionName":"Post",
"ActionParameters":{
"value":{
"Id":100,
"Text":"Test"
}
},
"FormVariables":{
},
"RequestUrl":"http://localhost:65080/api/values",
"IpAddress":"127.0.0.1",
"ResponseStatus":"OK",
"ResponseStatusCode":200,
"RequestBody":{
"Type":"application/json",
"Length":27,
"Value":"{ Id: 100, Text: \"Test\" }"
},
"ResponseBody":{
"Type":"SomeObject",
"Value":{
"Id":1795824380,
"Text":"Test"
}
},
"Headers": {
"Connection": "Keep-Alive",
"Accept": "text/html, application/xhtml+xml, image/jxr, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-GB",
"Host": "localhost:37341",
"User-Agent": "Mozilla/5.0, (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0), like, Gecko"
}
}
}
From the above output I want ActionParameters not to be captured as part of the Audit trail data. I have gone through the documentation but did not see any out of the box solution for it.
Can anyone help me here with some code sample which will serve as a reference for my implementation
There are at least three ways to accomplish this.
You can avoid the parameters being captured in the event object by marking those with [AuditIgnore] Attribute in the action method.
[HttpPost]
public IEnumerable<string> PostAccount(string user, [AuditIgnore]string password)
{
// password argument will not be audited
}
Or you can remove the action parameters from the event object before saving the scope, by using a custom action:
// On your start-up code
using Audit.WebApi;
Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
{
scope.GetWebApiAuditAction().ActionParameters = null;
});
Or, as ChatGPT suggested, you could implement your own AuditDataProvider, inheriting from the Data Provider you currently use, and removing the Action Parameters before calling the real provider's InsertEvent/ReplaceEvent methods. But this complicates things unnecessarily.
public class CustomAuditDataProvider : Audit.Core.Providers.FileDataProvider
{
public override object InsertEvent(AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.InsertEvent(auditEvent);
}
public override Task<object> InsertEventAsync(AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.InsertEventAsync(auditEvent);
}
public override void ReplaceEvent(object path, AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
base.ReplaceEvent(path, auditEvent);
}
public override Task ReplaceEventAsync(object path, AuditEvent auditEvent)
{
RemoveActionParams(auditEvent);
return base.ReplaceEventAsync(path, auditEvent);
}
private void RemoveActionParams(AuditEvent auditEvent)
{
auditEvent.GetWebApiAuditAction().ActionParameters = null;
}
}
// In your start-up code:
Audit.Core.Configuration.Setup().UseCustomProvider(new CustomAuditDataProvider());
Related
I'm using OData with ASP.NET Core to work with Oracle database. I have a Startup.cs file where I configure authentication, build the model and load all dependencies. I am also reading a config file to load custom odata controllers identified like this :
CustomControllerList": [
{
"Assembly": "assemblyName",
"PublicToken" : "123456789",
"Controller": "UsersController",
"Name": "Users",
"Entity": "ModelNameSpace.UserEntity",
"AccessRights" : [1, 2, 3]
},
{
"Assembly": "assemblyName",
"PublicToken" : "123456789",
"Controller": "GroupsController",
"Name": "Users",
"Entity": "ModelNameSpace.GroupsEntity",
"AccessRights" : [4, 5, 6]
}]
I would like to store access rights related to a controller in order to check before each request if user is authorized.
Concerning code execution before each request, I saw that creating a new controller's functions attribute like this
public class AccessRightsFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
try
{
String username = Tools.GetUsernameFromToken(context.Controller as ControllerBase);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
My first question is : Is this the only one solution for executing code before each request ? Is there a way to avoid having to put attribute for every OData controller's functions ?
I can't connect to db inside Startup execution and custom OData controllers are not able to access the Startup project.
So my second question is : What is the best practice for reading this data from OData controllers ? I didn't found anything, my only choice is to write inside a file but I would like to know if there is a better way to do it.
Thanks in advance !
For all those stumbling accross logging issues I created a small public playground on Github. Feel free to join.
I am still experimenting with the one hits all logging filter for my requests.
The goal is simple. Instead of writing the logs in each controller method implement them once in a filter (or middleware) and apply them globally to all. This is used when something wents wrong I can see which data was used causing the problem.
so for example a controller action (with the globally applied filter)
[HttpPut("{id:int}")
public IActionResult UpdateModel(int id, [FromBody] MyRequestModel request) {}
would create logs like
[Timestamp] INFO: MyNamespace.Controllers.MyModelController.UpdateModel
{
"Parameters": {
"Id": 1,
"Request": {
"Name": "abc",
"SomeInt": 3
}
}
}
Pretty, no more logging manually in each method.
But wait: it's an API and I used the [ApiController] attribute and the request model has invalid data (let's say Name = "a" but it needs to be at least of length of 3).
This gives me 3 problems
ActionFilter.OnActionExecuting is shortcutted and does not get called (due to the ApiController)
the binding of the arguments seems to be skipped and the bound (invalid) data is not applied to the ActionArguments
only the ResultFilter.OnResultExecuted is called but there seems to be no way for accessing/logging the invalid data
This somehow means logging works only when everything goes well but aren't the most interesting logs those were things go wrong?
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new { Parameters = context.ActionArguments },
Formatting.Indented
)
);
}
Point 1: I of course could remove the ApiController from each controller and go back to return BadRequest results manually. But I like the centralized approach and would like to stick with it.
I liked the model binding approach giving me classes to serialize for the logs (instead of reading the request body manually as one string). With a custom Json contract resolver I am able to mark model properties as sensitive and they are hidden in the logs (for those how care about security).
So my actual question:
Is there a way to get the model binding values in a ResultFilter or are they thrown away totally?
Or is there a way to hook into the model binding/model validation and write the logs there before they get thrown away?
Both especially for the case where the ApiController attribute starts shortcutting the filter pipeline.
public class LogFilter : IActionFilter, IResultFilter, IExceptionFilter
{
private readonly ILoggerFactory _loggerFactory;
public LogFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
Parameter = context.ActionArguments
},
Formatting.Indented
)
);
}
public void OnActionExecuted(ActionExecutedContext context) {}
public void OnResultExecuting(ResultExecutingContext context)
{
if (!context.ModelState.IsValid)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogWarning
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
ModelErrors = context.ModelState
.Where(kvp => kvp.Value.Errors.Count > 0)
.ToDictionary
(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
)
},
Formatting.Indented
)
);
}
}
public void OnResultExecuted(ResultExecutedContext context) {}
public void OnException(ExceptionContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogError
(
context.Exception,
JsonConvert.SerializeObject
(
new
{
//RequestBody = ReadBodyAsString(context.HttpContext.Request) // body is disposed here
},
Formatting.Indented
)
);
}
}
You can create a class which implements OnActionExecuting method of IActionFilter. And register it in global filter so that it applies to all controllers and actions. When exception happens for model binding(coz of length), your OnActionExecuting method still gets called even when [ApiController] is used and you can log it there.
Eg.
public class MyActionFilterAttribute: IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//Your logic to log the request, you can get the details from
//context parameter
//You can check if model state is valid also by using the property
//context.ModelState.IsValid
}
}
In Startup.cs, you need to set SuppressModelStateInvalidFilter to true. This will not return the 400 status automatically. Your controller method still gets called and since you have action filter, the OnActionExecuting gets called.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config =>
{
config.Filters.Add(new MyActionFilterAttribute());
})
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
I think you are right about the ActionFilter only catch successful requests.
For your invalid requests, maybe you can have a look at the InvalidModelStateResponseFactory.
When using options.InvalidModelStateResponseFactory you can read your Model and validationerrors.
I have been trying to recreate an Ajax version of the ValidateAntiForgeryToken - there are many blog posts on how to do this for previous versions of MVC, but with the latest MVC 6, none of the code is relevant. The core principle that I am going after, though, is to have the validation look at the Cookie and the Header for the __RequestVerificationToken, instead of comparing the Cookie to a form value. I am using MVC 6.0.0-rc1-final, dnx451 framework, and all of the Microsoft.Extensions libraries are 1.0.0-rc1-final.
My initial thought was to just inherit ValidateAntiForgeryTokenAttribute, but looking at the source code, I would need to return my own implementation of an an Authorization Filter to get it to look at the header.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAjaxAntiForgeryTokenAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
public int Order { get; set; }
public bool IsReusable => true;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
}
As such, I then made my own version of ValidateAntiforgeryTokenAuthorizationFilter
public class ValidateAjaxAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
private readonly IAntiforgery _antiforgery;
private readonly ILogger _logger;
public ValidateAjaxAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
_antiforgery = antiforgery;
_logger = loggerFactory.CreateLogger<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
public async Task OnAuthorizationAsync(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
{
try
{
await _antiforgery.ValidateRequestAsync(context.HttpContext);
}
catch (AjaxAntiforgeryValidationException exception)
{
_logger.LogInformation(1, string.Concat("Ajax Antiforgery token validation failed. ", exception.Message));
context.Result = new BadRequestResult();
}
}
}
protected virtual bool ShouldValidate(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return true;
}
private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' antiforgery policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IAntiforgeryPolicy)
{
return object.ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
However, I cannot find the proper Nuget package and namespace that contains IAntiforgeryPolicy. While I found the interface on GitHub - what package do I find it in?
My next attempt was to instead go after the IAntiforgery injection, and replace the DefaultAntiforgery with my own AjaxAntiforgery.
public class AjaxAntiforgery : DefaultAntiforgery
{
private readonly AntiforgeryOptions _options;
private readonly IAntiforgeryTokenGenerator _tokenGenerator;
private readonly IAntiforgeryTokenSerializer _tokenSerializer;
private readonly IAntiforgeryTokenStore _tokenStore;
private readonly ILogger<AjaxAntiforgery> _logger;
public AjaxAntiforgery(
IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor,
IAntiforgeryTokenGenerator tokenGenerator,
IAntiforgeryTokenSerializer tokenSerializer,
IAntiforgeryTokenStore tokenStore,
ILoggerFactory loggerFactory)
{
_options = antiforgeryOptionsAccessor.Value;
_tokenGenerator = tokenGenerator;
_tokenSerializer = tokenSerializer;
_tokenStore = tokenStore;
_logger = loggerFactory.CreateLogger<AjaxAntiforgery>();
}
}
I got this far before I stalled out because there is no generic method on ILoggerFactory for CreateLogger<T>(). The source code for DefaultAntiforgery has Microsoft.Extensions.Options, but I cannot find that namespace in any Nuget package. Microsoft.Extensions.OptionsModel exists, but that just brings in the IOptions<out TOptions> interface.
To follow all of this up, once I do get the Authorization Filter to work, or I get a new implementation of IAntiforgery, where or how do I register it with the dependency injection to use it - and only for the actions that I will be accepting Ajax requests?
I had similar issue. I don't know if any changes are coming regarding this in .NET but, at the time, I added the following lines to ConfigureServices method in Startup.cs, before the line services.AddMvc(), in order to validate the AntiForgeryToken sent via Ajax:
services.AddAntiforgery(options =>
{
options.CookieName = "yourChosenCookieName";
options.HeaderName = "RequestVerificationToken";
});
The AJAX call would be something like the following:
var token = $('input[type=hidden][name=__RequestVerificationToken]', document).val();
var request = $.ajax({
data: { 'yourField': 'yourValue' },
...
headers: { 'RequestVerificationToken': token }
});
Then, just use the native attribute [ValidadeAntiForgeryToken] in your Actions.
I've been wrestling with a similar situation, interfacing angular POSTs with MVC6, and came up with the following.
There are two problems that need to be addressed: getting the security token into MVC's antiforgery validation subsystem, and translating angular's JSON-formatted postback data into an MVC model.
I handle the first step via some custom middleware inserted in Startup.Configure(). The middleware class is pretty simple:
public static class UseAngularXSRFExtension
{
public const string XSRFFieldName = "X-XSRF-TOKEN";
public static IApplicationBuilder UseAngularXSRF( this IApplicationBuilder builder )
{
return builder.Use( next => context =>
{
switch( context.Request.Method.ToLower() )
{
case "post":
case "put":
case "delete":
if( context.Request.Headers.ContainsKey( XSRFFieldName ) )
{
var formFields = new Dictionary<string, StringValues>()
{
{ XSRFFieldName, context.Request.Headers[XSRFFieldName] }
};
// this assumes that any POST, PUT or DELETE having a header
// which includes XSRFFieldName is coming from angular, so
// overwriting context.Request.Form is okay (since it's not
// being parsed by MVC's internals anyway)
context.Request.Form = new FormCollection( formFields );
}
break;
}
return next( context );
} );
}
}
You insert this into the pipeline with the following line inside the Startup.Configure() method:
app.UseAngularXSRF();
I did this right before the call to app.UseMVC().
Note that this extension transfers the XSRF header on any POST, PUT or DELETE where it exists, and it does so by overwriting the existing form field collection. That fits my design pattern -- the only time the XSRF header will be in a request is if it's coming from some angular code I've written -- but it may not fit yours.
I also think you need to configure the antiforgery subsystem to use the correct name for the XSRF field name (I'm not sure what the default is). You can do this by inserting the following line into Startup.ConfigureServices():
services.ConfigureAntiforgery( options => options.FormFieldName = UseAngularXSRFExtension.XSRFFieldName );
I inserted this right before the line services.AddAntiforgery().
There are several ways of getting the XSRF token into the request stream. What I do is add the following to the view:
...top of view...
#inject Microsoft.AspNet.Antiforgery.IAntiforgery af
...rest of view...
...inside the angular function...
var postHeaders = {
'X-XSRF-TOKEN': '#(af.GetTokens(this.Context).FormToken)',
'Content-Type': 'application/json; charset=utf-8',
};
$http.post( '/Dataset/DeleteDataset', JSON.stringify({ 'siteID': siteID }),
{
headers: postHeaders,
})
...rest of view...
The second part -- translating the JSON data -- is handled by decorating the model class on your action method with [FromBody]:
// the [FromBody] attribute on the model -- and a class model, rather than a
// single integer model -- are necessary so that MVC can parse the JSON-formatted
// text POSTed by angular
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DeleteDataset( [FromBody] DeleteSiteViewModel model )
{
}
[FromBody] only works on class instances. Even though in my case all I'm interested in is a single integer, I still had to dummy up a class, which only contains a single integer property.
Hope this helps.
Using a anti forgery token in a Ajax call is possible but if you are trying to secure a Api I really would suggest using a Access Token instead.
If you are relying on a identity token stored in a cookie as authentication for your Api, you will need to write code to compensate for when your cookie authentication times out, and your Ajax post is getting redirected to a login screen. This is especially important for SPAs and Angular apps.
Using a Access Token implementation instead, will allow you to refresh you access token (using a refresh token), to have long running sessions and also stop cookie thiefs from accessing your Apis.. and it will also stop XSRF :)
A access token purpose is to secure resources, like Web Apis.
I'm working on an ASP.NET Web Forms application where in the front end I have jquery DataTable and it was a bit difficult at first to operate with the queries from and to the dataTable I saw an example where .ashx file was used for this purposed and it was working fine for me too so I end up using .ashx file to deal with my dataTable however now I need to make some Ajax requests from different places and till now I was writing all the code inside here:
public class TData : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Those parameters are sent by the plugin
var iDisplayLength = int.Parse(context.Request["iDisplayLength"]);
//more code...
}
}
but it's getting harder to keep all this logic in one place so I want to call different methods but at the same time keeping them at the same class Tdata so all the logic for the dataTable is kept in one place.
I tried this:
$.ajax({
url: "/TData.ashx",
type: "GET",
data: { method: 'Test', args: { blabla: 'blabla' } },
});
with this code on the server side:
public object Test(string blabla)
{
return string.Format("Hello {0}!", blabla);
}
but this method actually is never called and it seems that using this syntax the request is going directly to public void ProcessRequest(HttpContext context). I am using .NET 4.5 so is there a way to add additional methods in the same class and call them with jQuery Ajax from the client side?
If you really want to stick with the *.ashx-handler you could redirect the flow based on your method parameter:
public class TData : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
switch (context.Request["method"])
{
case "test":
Test(context);
break;
//other methods
default:
throw new ArgumentException("unknown method");
}
}
public void Test(HttpContext context)
{
// Those parameters are sent by the plugin
var iDisplayLength = int.Parse(context.Request["iDisplayLength"]);
//more code...
context.Response.Write(String.Format("Hello {0}!", blabla));
}
}
I detected a problem in the RequestFilter execution order.
The ValidationFeature in ServiceStack is a Plugin that just registers a Global Request Filter. The Order of Operations points out that Global Request Filters are executed after Filter Attributes with a Priority <0 and before Filter Attributes with a Priority >=0
My BasicAuth filter has -100 priority, and in fact everything goes well if the Service is annotated at class level, but it fails when the annotation is at method level, with the authentication filter being executed after.
I am using 3.9.70
Is there any quick fix for this? Thanks
When you add the annotation at method level then you are creating an Action Request Filter (because you are adding the annotation to an action method) which in the Order of Operations is operation 8, after the other filters have run.
5: Request Filter Attributes with Priority < 0 gets executed
6: Then any Global Request Filters get executed
7: Followed by Request Filter Attributes with Priority >= 0
8: Action Request Filters (New API only)
The best workaround I can suggest is to reconsider your service structure. I imagine you are having these difficulties because you are adding unauthenticated api methods alongside your secure api methods, and thus are using method level attributes to control authentication. So you are presumably doing something like this Your classes and attributes will be different, this is just exemplar:
public class MyService : Service
{
// Unauthenticated API method
public object Get(GetPublicData request)
{
return {};
}
// Secure API method
[MyBasicAuth] // <- Checks user has permission to run this method
public object Get(GetSecureData request)
{
return {};
}
}
I would do this differently, and separate your insecure and secure methods into 2 services. So I use this:
// Wrap in an outer class, then you can still register AppHost with `typeof(MyService).Assembly`
public partial class MyService
{
public class MyPublicService : Service
{
public object Get(GetPublicData request)
{
return {};
}
}
[MyBasicAuth] // <- Check is now class level, can run as expected before Validation
public class MySecureService : Service
{
public object Get(GetSecureData request)
{
return {};
}
}
}
Solution - Deferred Validation:
You can solve your execution order problem by creating your own custom validation feature, which will allow you to defer the validation process. I have created a fully functional self hosted ServiceStack v3 application that demonstrates this.
Full source code here.
Essentially instead of adding the standard ValidationFeature plugin we implement a slightly modified version:
public class MyValidationFeature : IPlugin
{
static readonly ILog Log = LogManager.GetLogger(typeof(MyValidationFeature));
public void Register(IAppHost appHost)
{
// Registers to use your custom validation filter instead of the standard one.
if(!appHost.RequestFilters.Contains(MyValidationFilters.RequestFilter))
appHost.RequestFilters.Add(MyValidationFilters.RequestFilter);
}
}
public static class MyValidationFilters
{
public static void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
// Determine if the Request DTO type has a MyRoleAttribute.
// If it does not, run the validation normally. Otherwise defer doing that, it will happen after MyRoleAttribute.
if(!requestDto.GetType().HasAttribute<MyRoleAttribute>()){
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
return;
}
Console.WriteLine("Deferring Validation until Roles are checked");
}
}
Configure to use our plugin:
// Configure to use our custom Validation Feature (MyValidationFeature)
Plugins.Add(new MyValidationFeature());
Then we need to create our custom attribute. Your attribute will be different of course. The key thing you need to do is call ValidationFilters.RequestFilter(req, res, requestDto); if you are satisfied the user has the required role and meets your conditions.
public class MyRoleAttribute : RequestFilterAttribute
{
readonly string[] _roles;
public MyRoleAttribute(params string[] roles)
{
_roles = roles;
}
#region implemented abstract members of RequestFilterAttribute
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
Console.WriteLine("Checking for required role");
// Replace with your actual role checking code
var role = req.GetParam("role");
if(role == null || !_roles.Contains(role))
throw HttpError.Unauthorized("You don't have the correct role");
Console.WriteLine("Has required role");
// Perform the deferred validation
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
}
#endregion
}
For this to work we need to apply our custom attribute on the DTO route not the action method. So this will be slightly different to how you are doing it now, but should still be flexible.
[Route("/HaveChristmas", "GET")]
[MyRole("Santa","Rudolph","MrsClaus")] // Notice our custom MyRole attribute.
public class HaveChristmasRequest {}
[Route("/EasterEgg", "GET")]
[MyRole("Easterbunny")]
public class GetEasterEggRequest {}
[Route("/EinsteinsBirthday", "GET")]
public class EinsteinsBirthdayRequest {}
Then your service would look something like this:
public class TestController : Service
{
// Roles: Santa, Rudolph, MrsClaus
public object Get(HaveChristmasRequest request)
{
return new { Presents = "Toy Car, Teddy Bear, Xbox" };
}
// Roles: Easterbunny
public object Get(GetEasterEggRequest request)
{
return new { EasterEgg = "Chocolate" };
}
// No roles required
public object Get(EinsteinsBirthdayRequest request)
{
return new { Birthdate = new DateTime(1879, 3, 14) };
}
}
So when we call the route /EinsteinsBirthday which does not have a MyRole attribute the validation will be called normally, as if using the standard ValidationFeature.
If we call the route /HaveChristmas?role=Santa then our validation plugin will determine that the DTO has our attribute and not run. Then our attribute filter triggers and it will trigger the validation to run. Thus the order is correct.