diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 745331f9c1..70a6651e47 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -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") diff --git a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs index a7454eb5a6..aace99648d 100644 --- a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs +++ b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs @@ -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>(); 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(); - 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(); + 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); } } } diff --git a/src/Api/Utilities/ModelStateValidationFilterAttribute.cs b/src/Api/Utilities/ModelStateValidationFilterAttribute.cs index b60d21999b..f8f602d2b3 100644 --- a/src/Api/Utilities/ModelStateValidationFilterAttribute.cs +++ b/src/Api/Utilities/ModelStateValidationFilterAttribute.cs @@ -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)); + } } } } diff --git a/src/Api/Utilities/PublicApiControllersModelConvention.cs b/src/Api/Utilities/PublicApiControllersModelConvention.cs index 3ffc42bd78..08f417ad5d 100644 --- a/src/Api/Utilities/PublicApiControllersModelConvention.cs +++ b/src/Api/Utilities/PublicApiControllersModelConvention.cs @@ -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)); } } } diff --git a/src/Core/Exceptions/BadRequestException.cs b/src/Core/Exceptions/BadRequestException.cs index 5a1ee2d040..13285c1647 100644 --- a/src/Core/Exceptions/BadRequestException.cs +++ b/src/Core/Exceptions/BadRequestException.cs @@ -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.") diff --git a/src/Core/Models/Api/Public/Response/ErrorResponseModel.cs b/src/Core/Models/Api/Public/Response/ErrorResponseModel.cs new file mode 100644 index 0000000000..5f6234068d --- /dev/null +++ b/src/Core/Models/Api/Public/Response/ErrorResponseModel.cs @@ -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>(); + + 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> errors) + : this("Errors have occurred.", errors) + { } + + public ErrorResponseModel(string errorKey, string errorValue) + : this(errorKey, new string[] { errorValue }) + { } + + public ErrorResponseModel(string errorKey, IEnumerable errorValues) + : this(new Dictionary> { { errorKey, errorValues } }) + { } + + public ErrorResponseModel(string message, Dictionary> errors) + { + Message = message; + Errors = errors; + } + + /// + /// String representing the object's type. Objects of the same type share the same properties. + /// + /// error + [Required] + public string Object => "error"; + /// + /// A human-readable message providing details about the error. + /// + /// The request model is invalid. + [Required] + public string Message { get; set; } + /// + /// If multiple errors occurred, they are listed in dictionary. Errors related to a specific + /// request parameter will include a dictionary key describing that parameter. + /// + public Dictionary> Errors { get; set; } + } +}