mirror of
https://github.com/bitwarden/server.git
synced 2025-04-14 01:28:14 -05:00
modelstate and exception handling for public apis
This commit is contained in:
parent
e0d7da892e
commit
8d51700120
@ -112,8 +112,6 @@ namespace Bit.Api
|
||||
{
|
||||
config.Conventions.Add(new ApiExplorerGroupConvention());
|
||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||
config.Filters.Add(new ExceptionHandlerFilterAttribute());
|
||||
config.Filters.Add(new ModelStateValidationFilterAttribute());
|
||||
}).AddJsonOptions(options =>
|
||||
{
|
||||
if(Configuration["swaggerGen"] != "true")
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Api;
|
||||
using InternalApi = Bit.Core.Models.Api;
|
||||
using PublicApi = Bit.Core.Models.Api.Public;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -13,9 +14,16 @@ namespace Bit.Api.Utilities
|
||||
{
|
||||
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
private readonly bool _publicApi;
|
||||
|
||||
public ExceptionHandlerFilterAttribute(bool publicApi)
|
||||
{
|
||||
_publicApi = publicApi;
|
||||
}
|
||||
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
var errorModel = new ErrorResponseModel("An error has occurred.");
|
||||
var errorMessage = "An error has occurred.";
|
||||
|
||||
var exception = context.Exception;
|
||||
if(exception == null)
|
||||
@ -24,34 +32,50 @@ namespace Bit.Api.Utilities
|
||||
return;
|
||||
}
|
||||
|
||||
var badRequestException = exception as BadRequestException;
|
||||
var stripeException = exception as StripeException;
|
||||
if(badRequestException != null)
|
||||
PublicApi.ErrorResponseModel publicErrorModel = null;
|
||||
InternalApi.ErrorResponseModel internalErrorModel = null;
|
||||
if(exception is BadRequestException badRequestException)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
|
||||
if(badRequestException.ModelState != null)
|
||||
{
|
||||
errorModel = new ErrorResponseModel(badRequestException.ModelState);
|
||||
if(_publicApi)
|
||||
{
|
||||
publicErrorModel = new PublicApi.ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
else
|
||||
{
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorModel.Message = badRequestException.Message;
|
||||
errorMessage = badRequestException.Message;
|
||||
}
|
||||
}
|
||||
else if(stripeException != null && stripeException?.StripeError?.ErrorType == "card_error")
|
||||
else if(exception is StripeException stripeException &&
|
||||
stripeException?.StripeError?.ErrorType == "card_error")
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
errorModel = new ErrorResponseModel(stripeException.StripeError.Parameter, stripeException.Message);
|
||||
if(_publicApi)
|
||||
{
|
||||
publicErrorModel = new PublicApi.ErrorResponseModel(stripeException.StripeError.Parameter,
|
||||
stripeException.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(stripeException.StripeError.Parameter,
|
||||
stripeException.Message);
|
||||
}
|
||||
}
|
||||
else if(exception is GatewayException)
|
||||
{
|
||||
errorModel.Message = exception.Message;
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if(exception is NotSupportedException && !string.IsNullOrWhiteSpace(exception.Message))
|
||||
{
|
||||
errorModel.Message = exception.Message;
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if(exception is ApplicationException)
|
||||
@ -60,37 +84,44 @@ namespace Bit.Api.Utilities
|
||||
}
|
||||
else if(exception is NotFoundException)
|
||||
{
|
||||
errorModel.Message = "Resource not found.";
|
||||
errorMessage = "Resource not found.";
|
||||
context.HttpContext.Response.StatusCode = 404;
|
||||
}
|
||||
else if(exception is SecurityTokenValidationException)
|
||||
{
|
||||
errorModel.Message = "Invalid token.";
|
||||
errorMessage = "Invalid token.";
|
||||
context.HttpContext.Response.StatusCode = 403;
|
||||
}
|
||||
else if(exception is UnauthorizedAccessException)
|
||||
{
|
||||
errorModel.Message = "Unauthorized.";
|
||||
errorMessage = "Unauthorized.";
|
||||
context.HttpContext.Response.StatusCode = 401;
|
||||
}
|
||||
else
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||
logger.LogError(0, exception, exception.Message);
|
||||
|
||||
errorModel.Message = "An unhandled server error has occurred.";
|
||||
errorMessage = "An unhandled server error has occurred.";
|
||||
context.HttpContext.Response.StatusCode = 500;
|
||||
}
|
||||
|
||||
var env = context.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
|
||||
if(env.IsDevelopment())
|
||||
if(_publicApi)
|
||||
{
|
||||
errorModel.ExceptionMessage = exception.Message;
|
||||
errorModel.ExceptionStackTrace = exception.StackTrace;
|
||||
errorModel.InnerExceptionMessage = exception?.InnerException?.Message;
|
||||
var errorModel = publicErrorModel ?? new PublicApi.ErrorResponseModel(errorMessage);
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorModel = internalErrorModel ?? new InternalApi.ErrorResponseModel(errorMessage);
|
||||
var env = context.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
|
||||
if(env.IsDevelopment())
|
||||
{
|
||||
errorModel.ExceptionMessage = exception.Message;
|
||||
errorModel.ExceptionStackTrace = exception.StackTrace;
|
||||
errorModel.InnerExceptionMessage = exception?.InnerException?.Message;
|
||||
}
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
}
|
||||
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Bit.Core.Models.Api;
|
||||
using InternalApi = Bit.Core.Models.Api;
|
||||
using PublicApi = Bit.Core.Models.Api.Public;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
{
|
||||
public class ModelStateValidationFilterAttribute : ActionFilterAttribute
|
||||
{
|
||||
private readonly bool _publicApi;
|
||||
|
||||
public ModelStateValidationFilterAttribute(bool publicApi)
|
||||
{
|
||||
_publicApi = publicApi;
|
||||
}
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var model = context.ActionArguments.FirstOrDefault(a => a.Key == "model");
|
||||
@ -17,7 +25,14 @@ namespace Bit.Api.Utilities
|
||||
|
||||
if(!context.ModelState.IsValid)
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));
|
||||
if(_publicApi)
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(new PublicApi.ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(new InternalApi.ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,13 @@ namespace Bit.Api.Utilities
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
var controllerNamespace = controller.ControllerType.Namespace;
|
||||
if(controllerNamespace.Contains(".Public."))
|
||||
var publicApi = controllerNamespace.Contains(".Public.");
|
||||
if(publicApi)
|
||||
{
|
||||
controller.Filters.Add(new CamelCaseJsonResultFilterAttribute());
|
||||
}
|
||||
controller.Filters.Add(new ExceptionHandlerFilterAttribute(publicApi));
|
||||
controller.Filters.Add(new ModelStateValidationFilterAttribute(publicApi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ namespace Bit.Core.Exceptions
|
||||
{
|
||||
public class BadRequestException : Exception
|
||||
{
|
||||
public BadRequestException(string message) : this(string.Empty, message) { }
|
||||
public BadRequestException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public BadRequestException(string key, string errorMessage)
|
||||
: base("The model state is invalid.")
|
||||
|
79
src/Core/Models/Api/Public/Response/ErrorResponseModel.cs
Normal file
79
src/Core/Models/Api/Public/Response/ErrorResponseModel.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Models.Api.Public
|
||||
{
|
||||
public class ErrorResponseModel : IResponseModel
|
||||
{
|
||||
public ErrorResponseModel(string message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public ErrorResponseModel(ModelStateDictionary modelState)
|
||||
{
|
||||
Message = "The request's model state is invalid.";
|
||||
Errors = new Dictionary<string, IEnumerable<string>>();
|
||||
|
||||
var keys = modelState.Keys.ToList();
|
||||
var values = modelState.Values.ToList();
|
||||
|
||||
for(var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
if(keys.Count <= i)
|
||||
{
|
||||
// Keys not available for some reason.
|
||||
break;
|
||||
}
|
||||
|
||||
var key = keys[i];
|
||||
if(value.ValidationState != ModelValidationState.Invalid || value.Errors.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var errors = value.Errors.Select(e => e.ErrorMessage);
|
||||
Errors.Add(key, errors);
|
||||
}
|
||||
}
|
||||
|
||||
public ErrorResponseModel(Dictionary<string, IEnumerable<string>> errors)
|
||||
: this("Errors have occurred.", errors)
|
||||
{ }
|
||||
|
||||
public ErrorResponseModel(string errorKey, string errorValue)
|
||||
: this(errorKey, new string[] { errorValue })
|
||||
{ }
|
||||
|
||||
public ErrorResponseModel(string errorKey, IEnumerable<string> errorValues)
|
||||
: this(new Dictionary<string, IEnumerable<string>> { { errorKey, errorValues } })
|
||||
{ }
|
||||
|
||||
public ErrorResponseModel(string message, Dictionary<string, IEnumerable<string>> errors)
|
||||
{
|
||||
Message = message;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// String representing the object's type. Objects of the same type share the same properties.
|
||||
/// </summary>
|
||||
/// <example>error</example>
|
||||
[Required]
|
||||
public string Object => "error";
|
||||
/// <summary>
|
||||
/// A human-readable message providing details about the error.
|
||||
/// </summary>
|
||||
/// <example>The request model is invalid.</example>
|
||||
[Required]
|
||||
public string Message { get; set; }
|
||||
/// <summary>
|
||||
/// If multiple errors occurred, they are listed in dictionary. Errors related to a specific
|
||||
/// request parameter will include a dictionary key describing that parameter.
|
||||
/// </summary>
|
||||
public Dictionary<string, IEnumerable<string>> Errors { get; set; }
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user