From 6de4f6a3acc8645b2957b7afa6790edf2f9a89f7 Mon Sep 17 00:00:00 2001 From: Wouter Samaey Date: Thu, 16 Dec 2021 15:04:06 +0100 Subject: [PATCH] Mention the missing API permission in the response of a Greenfield request (#3195) * Mention the missing API permission in the response header or body * Fixes + Added a unit test. 1 TODO remains. * Added MissingPermissionDescription to the error * Update BTCPayServer.Tests/GreenfieldAPITests.cs Co-authored-by: Nicolas Dorier * Fix tests * [GreenField]: Make sure we are sending fully typed errors Co-authored-by: Nicolas Dorier --- BTCPayServer.Client/BTCPayServerClient.cs | 25 ++++--- .../Models/GreenfieldPermissionAPIError.cs | 17 +++++ BTCPayServer.Tests/GreenfieldAPITests.cs | 61 +++++++++------- .../GreenField/ApiKeysController.cs | 7 +- .../Controllers/GreenField/GreenFieldUtils.cs | 4 ++ .../GreenField/LightningNodeApiController.cs | 2 +- ...ightningNetworkPaymentMethodsController.cs | 34 +++++---- .../StoreOnChainPaymentMethodsController.cs | 46 +++++-------- .../Controllers/GreenField/UsersController.cs | 8 +-- BTCPayServer/Hosting/BTCPayServerServices.cs | 1 + BTCPayServer/Hosting/GreenfieldMiddleware.cs | 69 +++++++++++++++++++ .../GreenFieldAuthorizationHandler.cs | 8 +++ .../Views/AppsPublic/PointOfSale/Print.cshtml | 2 +- 13 files changed, 194 insertions(+), 90 deletions(-) create mode 100644 BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs create mode 100644 BTCPayServer/Hosting/GreenfieldMiddleware.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 353135fdf..0fa8a0658 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -48,17 +48,24 @@ namespace BTCPayServer.Client protected async Task HandleResponse(HttpResponseMessage message) { - if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) + if (!message.IsSuccessStatusCode && message.Content?.Headers?.ContentType?.MediaType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true) { - var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); - ; - throw new GreenFieldValidationException(err); - } - else if (!message.IsSuccessStatusCode && message.Content?.Headers?.ContentType?.MediaType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true) - { - var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); - if (err.Code != null) + if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) + { + var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); + throw new GreenFieldValidationException(err); + } + if (message.StatusCode == System.Net.HttpStatusCode.Forbidden) + { + var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); throw new GreenFieldAPIException((int)message.StatusCode, err); + } + else + { + var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); + if (err.Code != null) + throw new GreenFieldAPIException((int)message.StatusCode, err); + } } message.EnsureSuccessStatusCode(); } diff --git a/BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs b/BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs new file mode 100644 index 000000000..482a4992a --- /dev/null +++ b/BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs @@ -0,0 +1,17 @@ +using System; + +namespace BTCPayServer.Client.Models +{ + public class GreenfieldPermissionAPIError : GreenfieldAPIError + { + public GreenfieldPermissionAPIError(string missingPermission, string message = null) : base() + { + MissingPermission = missingPermission; + Code = "missing-permission"; + Message = message ?? $"Insufficient API Permissions. Please use an API key with permission \"{MissingPermission}\". You can create an API key in your account's settings / Api Keys."; + } + + public string MissingPermission { get; } + + } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index de5ecec3a..d010ce765 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -56,6 +56,23 @@ namespace BTCPayServer.Tests var s = await client.GetStores(); } + [Fact(Timeout = TestTimeout)] + [Trait("Integration", "Integration")] + public async Task MissingPermissionTest() + { + using (var tester = CreateServerTester()) + { + await tester.StartAsync(); + var user = tester.NewAccount(); + user.GrantAccess(); + var clientWithWrongPermissions = await user.CreateClient(Policies.CanViewProfile); + var e = await AssertAPIError("missing-permission", () => clientWithWrongPermissions.CreateStore(new CreateStoreRequest() { Name = "mystore" })); + Assert.Equal("missing-permission", e.APIError.Code); + Assert.NotNull(e.APIError.Message); + GreenfieldPermissionAPIError permissionError = Assert.IsType(e.APIError); + Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings); + } + } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] @@ -161,7 +178,7 @@ namespace BTCPayServer.Tests })); await unrestricted.RevokeAPIKey(apiKey.ApiKey); - await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey)); + await AssertAPIError("apikey-not-found", () => unrestricted.RevokeAPIKey(apiKey.ApiKey)); } } @@ -251,10 +268,10 @@ namespace BTCPayServer.Tests // Creating a new user without proper creds is now impossible (unauthorized) // Because if registration are locked and that an admin exists, we don't accept unauthenticated connection - await AssertHttpError(401, + var ex = await AssertAPIError("unauthenticated", async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); - + Assert.Equal("New user creation isn't authorized to users who are not admin", ex.APIError.Message); // But should be ok with subscriptions unlocked var settings = tester.PayTester.GetService(); @@ -263,7 +280,7 @@ namespace BTCPayServer.Tests new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); // But it should be forbidden to create an admin without being authenticated - await AssertHttpError(403, + await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", @@ -281,7 +298,7 @@ namespace BTCPayServer.Tests await AssertHttpError(403, async () => await adminClient.CreateUser( new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); - await AssertHttpError(403, + await AssertAPIError("missing-permission", async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", @@ -394,9 +411,10 @@ namespace BTCPayServer.Tests }); TestLogs.LogInformation("Can't archive without knowing the walletId"); - await Assert.ThrowsAsync(async () => await client.ArchivePullPayment("lol", result.Id)); + var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id)); + Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission); TestLogs.LogInformation("Can't archive without permission"); - await Assert.ThrowsAsync(async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); + await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id)); await client.ArchivePullPayment(storeId, result.Id); result = await unauthenticated.GetPullPayment(result.Id); Assert.True(result.Archived); @@ -584,10 +602,11 @@ namespace BTCPayServer.Tests return new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Offset); } - private async Task AssertAPIError(string expectedError, Func act) + private async Task AssertAPIError(string expectedError, Func act) { var err = await Assert.ThrowsAsync(async () => await act()); Assert.Equal(expectedError, err.APIError.Code); + return err; } [Fact(Timeout = TestTimeout)] @@ -662,15 +681,8 @@ namespace BTCPayServer.Tests private async Task AssertHttpError(int code, Func act) { - try - { - // Eventually all exception should be GreenFieldAPIException - var ex = await Assert.ThrowsAsync(act); - Assert.Contains(code.ToString(), ex.Message); - } - catch (ThrowsException e) when (e.InnerException is GreenFieldAPIException ex && ex.HttpCode == code) - { - } + var ex = await Assert.ThrowsAsync(act); + Assert.Equal(code, ex.HttpCode); } [Fact(Timeout = TestTimeout)] @@ -696,12 +708,12 @@ namespace BTCPayServer.Tests Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles); - await Assert.ThrowsAsync(async () => await clientInsufficient.GetCurrentUser()); + await AssertHttpError(403, async () => await clientInsufficient.GetCurrentUser()); await clientServer.GetCurrentUser(); await clientProfile.GetCurrentUser(); await clientBasic.GetCurrentUser(); - await Assert.ThrowsAsync(async () => + await AssertHttpError(403, async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}@g.com", @@ -1434,11 +1446,9 @@ namespace BTCPayServer.Tests Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); - var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); - Assert.Equal(503, err.HttpCode); + await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC")); // Not permission for the store! - var err2 = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); - Assert.Contains("403", err2.Message); + await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC")); var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() { Amount = LightMoney.Satoshis(1000), @@ -1451,8 +1461,7 @@ namespace BTCPayServer.Tests client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); // Not permission for the server - err2 = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); - Assert.Contains("403", err2.Message); + await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC")); var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); Assert.Equal(2, data.Count()); @@ -1502,7 +1511,7 @@ namespace BTCPayServer.Tests await client.GetLightningNodeInfo(user.StoreId, "BTC"); // But if not admin anymore, nope await user.MakeAdmin(false); - await AssertAPIError("insufficient-api-permissions", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); + await AssertAPIError("missing-permission", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); } } diff --git a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs index ca6549212..b7db9759b 100644 --- a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs +++ b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs @@ -77,12 +77,11 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] public async Task RevokeKey(string apikey) { - if (string.IsNullOrEmpty(apikey)) - return NotFound(); - if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User))) + if (!string.IsNullOrEmpty(apikey) && + await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User))) return Ok(); else - return NotFound(); + return this.CreateAPIError("apikey-not-found", "This apikey does not exists"); } private static ApiKeyData FromModel(APIKeyData data) diff --git a/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs b/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs index 4ea8bf905..9500c8fee 100644 --- a/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs +++ b/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs @@ -34,5 +34,9 @@ namespace BTCPayServer.Controllers.GreenField { return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage)); } + public static IActionResult CreateAPIPermissionError(this ControllerBase controller, string missingPermission, string message = null) + { + return controller.StatusCode(403, new GreenfieldPermissionAPIError(missingPermission, message)); + } } } diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs index b9db10a53..325f13886 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs @@ -269,7 +269,7 @@ namespace BTCPayServer.Controllers.GreenField } protected JsonHttpException ErrorShouldBeAdminForInternalNode() { - return new JsonHttpException(this.CreateAPIError(403, "insufficient-api-permissions", "The user should be admin to use the internal lightning node")); + return new JsonHttpException(this.CreateAPIError(403, "missing-permission", "The user should be admin to use the internal lightning node")); } private LightningInvoiceData ToModel(LightningInvoice invoice) diff --git a/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs index 3357bf6f8..657b40da6 100644 --- a/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs @@ -80,30 +80,29 @@ namespace BTCPayServer.Controllers.GreenField [HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] public ActionResult GetLightningNetworkPaymentMethod(string storeId, string cryptoCode) { - if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) - { - return NotFound(); - } + AssertSupportLightning(cryptoCode); var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store); if (method is null) { - return NotFound(); + throw ErrorPaymentMethodNotConfigured(); } return Ok(method); } + protected JsonHttpException ErrorPaymentMethodNotConfigured() + { + return new JsonHttpException(this.CreateAPIError(404, "paymentmethod-not-configured", "The lightning payment method is not set up")); + } + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] public async Task RemoveLightningNetworkPaymentMethod( string storeId, string cryptoCode) { - if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) - { - return NotFound(); - } + AssertSupportLightning(cryptoCode); var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var store = Store; @@ -118,11 +117,7 @@ namespace BTCPayServer.Controllers.GreenField [FromBody] UpdateLightningNetworkPaymentMethodRequest request) { var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); - - if (!GetNetwork(cryptoCode, out var network)) - { - return NotFound(); - } + AssertSupportLightning(cryptoCode); if (string.IsNullOrEmpty(request.ConnectionString)) { @@ -210,11 +205,14 @@ namespace BTCPayServer.Controllers.GreenField paymentMethod.PaymentId.ToStringNormalized(), paymentMethod.DisableBOLT11PaymentOption); } - private bool GetNetwork(string cryptoCode, [MaybeNullWhen(false)] out BTCPayNetwork network) + private BTCPayNetwork AssertSupportLightning(string cryptoCode) { - network = _btcPayNetworkProvider.GetNetwork(cryptoCode); - network = network?.SupportLightning is true ? network : null; - return network != null; + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + if (network is null) + throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance")); + if (!(network.SupportLightning is true)) + throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code doesn't support lightning")); + return network; } private async Task CanUseInternalLightning() diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs index 347dd7050..fc77c0bcc 100644 --- a/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainPaymentMethodsController.cs @@ -79,20 +79,21 @@ namespace BTCPayServer.Controllers.GreenField string storeId, string cryptoCode) { - if (!GetCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _)) - { - return NotFound(); - } - + AssertCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _); var method = GetExistingBtcLikePaymentMethod(cryptoCode); if (method is null) { - return NotFound(); + throw ErrorPaymentMethodNotConfigured(); } return Ok(method); } + protected JsonHttpException ErrorPaymentMethodNotConfigured() + { + return new JsonHttpException(this.CreateAPIError(404, "paymentmethod-not-configured", "The lightning node is not set up")); + } + [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")] public IActionResult GetOnChainPaymentMethodPreview( @@ -100,15 +101,12 @@ namespace BTCPayServer.Controllers.GreenField string cryptoCode, int offset = 0, int amount = 10) { - if (!GetCryptoCodeWallet(cryptoCode, out var network, out BTCPayWallet _)) - { - return NotFound(); - } + AssertCryptoCodeWallet(cryptoCode, out var network, out _); var paymentMethod = GetExistingBtcLikePaymentMethod(cryptoCode); if (string.IsNullOrEmpty(paymentMethod?.DerivationScheme)) { - return NotFound(); + throw ErrorPaymentMethodNotConfigured(); } try @@ -149,10 +147,7 @@ namespace BTCPayServer.Controllers.GreenField [FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData, int offset = 0, int amount = 10) { - if (!GetCryptoCodeWallet(cryptoCode, out var network, out BTCPayWallet _)) - { - return NotFound(); - } + AssertCryptoCodeWallet(cryptoCode, out var network, out _); if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme)) { @@ -202,10 +197,7 @@ namespace BTCPayServer.Controllers.GreenField string cryptoCode, int offset = 0, int amount = 10) { - if (!GetCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _)) - { - return NotFound(); - } + AssertCryptoCodeWallet(cryptoCode, out _, out _); var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); var store = Store; @@ -222,11 +214,7 @@ namespace BTCPayServer.Controllers.GreenField [FromBody] UpdateOnChainPaymentMethodRequest request) { var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); - - if (!GetCryptoCodeWallet(cryptoCode, out var network, out var wallet)) - { - return NotFound(); - } + AssertCryptoCodeWallet(cryptoCode, out var network, out var wallet); if (string.IsNullOrEmpty(request?.DerivationScheme)) { @@ -271,11 +259,15 @@ namespace BTCPayServer.Controllers.GreenField } } - private bool GetCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network, out BTCPayWallet wallet) + private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network, out BTCPayWallet wallet) { network = _btcPayNetworkProvider.GetNetwork(cryptoCode); - wallet = network != null ? _walletProvider.GetWallet(network) : null; - return wallet != null; + if (network is null) + throw new JsonHttpException(this.CreateAPIError(404, "unknown-cryptocode", "This crypto code isn't set up in this BTCPay Server instance")); + + wallet = _walletProvider.GetWallet(network); + if (wallet is null) + throw ErrorPaymentMethodNotConfigured(); } private OnChainPaymentMethodData GetExistingBtcLikePaymentMethod(string cryptoCode, StoreData store = null) diff --git a/BTCPayServer/Controllers/GreenField/UsersController.cs b/BTCPayServer/Controllers/GreenField/UsersController.cs index 22e2d1860..de7c1beba 100644 --- a/BTCPayServer/Controllers/GreenField/UsersController.cs +++ b/BTCPayServer/Controllers/GreenField/UsersController.cs @@ -102,11 +102,11 @@ namespace BTCPayServer.Controllers.GreenField // If registration are locked and that an admin exists, don't accept unauthenticated connection if (anyAdmin && policies.LockSubscription && !isAuth) - return Unauthorized(); + return this.CreateAPIError(401, "unauthenticated", "New user creation isn't authorized to users who are not admin"); // Even if subscription are unlocked, it is forbidden to create admin unauthenticated if (anyAdmin && request.IsAdministrator is true && !isAuth) - return Forbid(AuthenticationSchemes.GreenfieldBasic); + return this.CreateAPIError(401, "unauthenticated", "New admin creation isn't authorized to users who are not admin"); // You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded @@ -114,14 +114,14 @@ namespace BTCPayServer.Controllers.GreenField : true; // You need to be admin to create an admin if (request.IsAdministrator is true && !isAdmin) - return Forbid(AuthenticationSchemes.GreenfieldBasic); + return this.CreateAPIPermissionError(Policies.Unrestricted, $"Insufficient API Permissions. Please use an API key with permission: {Policies.Unrestricted} and be an admin."); if (!isAdmin && (policies.LockSubscription || (await _settingsRepository.GetPolicies()).DisableNonAdminCreateUserApi)) { // If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded; if (!isAuth || !canCreateUser) - return Forbid(AuthenticationSchemes.GreenfieldBasic); + return this.CreateAPIPermissionError(Policies.CanCreateUser); } var user = new ApplicationUser diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index ab825a359..6d4a6b8ba 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -472,6 +472,7 @@ namespace BTCPayServer.Hosting public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { + app.UseMiddleware(); app.UseMiddleware(); return app; } diff --git a/BTCPayServer/Hosting/GreenfieldMiddleware.cs b/BTCPayServer/Hosting/GreenfieldMiddleware.cs new file mode 100644 index 000000000..45f5dde72 --- /dev/null +++ b/BTCPayServer/Hosting/GreenfieldMiddleware.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Security.GreenField; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace BTCPayServer.Hosting +{ + public class GreenfieldMiddleware + { + private readonly RequestDelegate _next; + private readonly IOptions _mvcOptions; + + public GreenfieldMiddleware(RequestDelegate next, IOptions mvcOptions) + { + _next = next; + _mvcOptions = mvcOptions; + } + + public async Task Invoke(HttpContext httpContext) + { + await _next(httpContext); + if (!httpContext.Response.HasStarted && + !IsJson(httpContext.Response.ContentType) && + !IsHtml(httpContext.Response.ContentType) && + !httpContext.GetIsBitpayAPI() && + (httpContext.Response.StatusCode == 401 || httpContext.Response.StatusCode == 403)) + { + if (httpContext.Response.StatusCode == 403 && + httpContext.Items.TryGetValue(GreenFieldAuthorizationHandler.RequestedPermissionKey, out var p) && + p is string policy) + { + var outputObj = new GreenfieldPermissionAPIError(policy); + await WriteError(httpContext, outputObj); + } + if (httpContext.Response.StatusCode == 401) + { + var outputObj = new GreenfieldAPIError("unauthenticated", "Authentication is required for accessing this endpoint"); + await WriteError(httpContext, outputObj); + } + } + } + + private async Task WriteError(HttpContext httpContext, object outputObj) + { + string output = JsonConvert.SerializeObject(outputObj, _mvcOptions.Value.SerializerSettings); + var outputBytes = new UTF8Encoding(false).GetBytes(output); + httpContext.Response.Headers.Add("Content-Type", "application/json"); + httpContext.Response.Headers.Add("Content-Length", outputBytes.Length.ToString(CultureInfo.InvariantCulture)); + await httpContext.Response.Body.WriteAsync(outputBytes, 0, outputBytes.Length); + } + private bool IsHtml(string contentType) + { + return contentType?.StartsWith("text/html", StringComparison.OrdinalIgnoreCase) is true; + } + private bool IsJson(string contentType) + { + return contentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true; + } + } +} diff --git a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs index 4e6ae14bd..80b2bf74f 100644 --- a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs +++ b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs @@ -1,11 +1,17 @@ +using System.Buffers; using System.Collections.Generic; +using System.Globalization; +using System.Text; using System.Threading.Tasks; using BTCPayServer.Client; +using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Newtonsoft.Json; +using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Security.GreenField { @@ -117,6 +123,8 @@ namespace BTCPayServer.Security.GreenField { context.Succeed(requirement); } + _HttpContext.Items[RequestedPermissionKey] = policy; } + public const string RequestedPermissionKey = nameof(RequestedPermissionKey); } } diff --git a/BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml b/BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml index 74cdd88bf..f89631b9d 100644 --- a/BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml +++ b/BTCPayServer/Views/AppsPublic/PointOfSale/Print.cshtml @@ -81,7 +81,7 @@ else ItemCode = item.Id }, Context.Request.Scheme, Context.Request.Host.ToString())); var lnUrl = LNURL.EncodeUri(lnurlEndpoint, "payRequest", supported.UseBech32Scheme); - + } }