mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
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 <nicolas.dorier@gmail.com> * Fix tests * [GreenField]: Make sure we are sending fully typed errors Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
89a52703f6
commit
6de4f6a3ac
13 changed files with 194 additions and 90 deletions
|
@ -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<Models.GreenfieldValidationError[]>(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<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
|
||||
if (err.Code != null)
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
|
||||
throw new GreenFieldValidationException(err);
|
||||
}
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldPermissionAPIError>(await message.Content.ReadAsStringAsync());
|
||||
throw new GreenFieldAPIException((int)message.StatusCode, err);
|
||||
}
|
||||
else
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
|
||||
if (err.Code != null)
|
||||
throw new GreenFieldAPIException((int)message.StatusCode, err);
|
||||
}
|
||||
}
|
||||
message.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
|
17
BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs
Normal file
17
BTCPayServer.Client/Models/GreenfieldPermissionAPIError.cs
Normal file
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
|
@ -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<GreenfieldPermissionAPIError>(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<SettingsRepository>();
|
||||
|
@ -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<HttpRequestException>(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<HttpRequestException>(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<Task> act)
|
||||
private async Task<GreenFieldAPIException> AssertAPIError(string expectedError, Func<Task> act)
|
||||
{
|
||||
var err = await Assert.ThrowsAsync<GreenFieldAPIException>(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<Task> act)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Eventually all exception should be GreenFieldAPIException
|
||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
|
||||
Assert.Contains(code.ToString(), ex.Message);
|
||||
}
|
||||
catch (ThrowsException e) when (e.InnerException is GreenFieldAPIException ex && ex.HttpCode == code)
|
||||
{
|
||||
}
|
||||
var ex = await Assert.ThrowsAsync<GreenFieldAPIException>(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<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
|
||||
await AssertHttpError(403, async () => await clientInsufficient.GetCurrentUser());
|
||||
await clientServer.GetCurrentUser();
|
||||
await clientProfile.GetCurrentUser();
|
||||
await clientBasic.GetCurrentUser();
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(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<GreenFieldAPIException>(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<HttpRequestException>(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<HttpRequestException>(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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,12 +77,11 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
[Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> 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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -80,30 +80,29 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public ActionResult<LightningNetworkPaymentMethodData> 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<IActionResult> 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<BTCPayNetwork>(cryptoCode);
|
||||
network = network?.SupportLightning is true ? network : null;
|
||||
return network != null;
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(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<bool> CanUseInternalLightning()
|
||||
|
|
|
@ -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<BTCPayNetwork>(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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -472,6 +472,7 @@ namespace BTCPayServer.Hosting
|
|||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseMiddleware<GreenfieldMiddleware>();
|
||||
app.UseMiddleware<BTCPayMiddleware>();
|
||||
return app;
|
||||
}
|
||||
|
|
69
BTCPayServer/Hosting/GreenfieldMiddleware.cs
Normal file
69
BTCPayServer/Hosting/GreenfieldMiddleware.cs
Normal file
|
@ -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<MvcNewtonsoftJsonOptions> _mvcOptions;
|
||||
|
||||
public GreenfieldMiddleware(RequestDelegate next, IOptions<MvcNewtonsoftJsonOptions> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ else
|
|||
ItemCode = item.Id
|
||||
}, Context.Request.Scheme, Context.Request.Host.ToString()));
|
||||
var lnUrl = LNURL.EncodeUri(lnurlEndpoint, "payRequest", supported.UseBech32Scheme);
|
||||
<a href="@lnUrl"><vc:qr-code data="@lnUrl.ToString().ToUpperInvariant()" /></a>
|
||||
<a href="@lnUrl" rel="noreferrer noopener"><vc:qr-code data="@lnUrl.ToString().ToUpperInvariant()" /></a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue