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:
Wouter Samaey 2021-12-16 15:04:06 +01:00 committed by GitHub
parent 89a52703f6
commit 6de4f6a3ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 90 deletions

View file

@ -47,19 +47,26 @@ namespace BTCPayServer.Client
} }
protected async Task HandleResponse(HttpResponseMessage message) protected async Task HandleResponse(HttpResponseMessage message)
{
if (!message.IsSuccessStatusCode && message.Content?.Headers?.ContentType?.MediaType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true)
{ {
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
{ {
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
;
throw new GreenFieldValidationException(err); throw new GreenFieldValidationException(err);
} }
else if (!message.IsSuccessStatusCode && message.Content?.Headers?.ContentType?.MediaType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) is true) 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()); var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
if (err.Code != null) if (err.Code != null)
throw new GreenFieldAPIException((int)message.StatusCode, err); throw new GreenFieldAPIException((int)message.StatusCode, err);
} }
}
message.EnsureSuccessStatusCode(); message.EnsureSuccessStatusCode();
} }

View 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; }
}
}

View file

@ -56,6 +56,23 @@ namespace BTCPayServer.Tests
var s = await client.GetStores(); 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)] [Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
@ -161,7 +178,7 @@ namespace BTCPayServer.Tests
})); }));
await unrestricted.RevokeAPIKey(apiKey.ApiKey); 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) // 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 // 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( async () => await unauthClient.CreateUser(
new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" })); 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 // But should be ok with subscriptions unlocked
var settings = tester.PayTester.GetService<SettingsRepository>(); var settings = tester.PayTester.GetService<SettingsRepository>();
@ -263,7 +280,7 @@ namespace BTCPayServer.Tests
new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }); new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" });
// But it should be forbidden to create an admin without being authenticated // But it should be forbidden to create an admin without being authenticated
await AssertHttpError(403, await AssertHttpError(401,
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()
{ {
Email = "admin2@gmail.com", Email = "admin2@gmail.com",
@ -281,7 +298,7 @@ namespace BTCPayServer.Tests
await AssertHttpError(403, await AssertHttpError(403,
async () => await adminClient.CreateUser( async () => await adminClient.CreateUser(
new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" })); new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403, await AssertAPIError("missing-permission",
async () => await adminClient.CreateUser(new CreateApplicationUserRequest() async () => await adminClient.CreateUser(new CreateApplicationUserRequest()
{ {
Email = "test4@gmail.com", Email = "test4@gmail.com",
@ -394,9 +411,10 @@ namespace BTCPayServer.Tests
}); });
TestLogs.LogInformation("Can't archive without knowing the walletId"); 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"); 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); await client.ArchivePullPayment(storeId, result.Id);
result = await unauthenticated.GetPullPayment(result.Id); result = await unauthenticated.GetPullPayment(result.Id);
Assert.True(result.Archived); 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); 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()); var err = await Assert.ThrowsAsync<GreenFieldAPIException>(async () => await act());
Assert.Equal(expectedError, err.APIError.Code); Assert.Equal(expectedError, err.APIError.Code);
return err;
} }
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
@ -662,15 +681,8 @@ namespace BTCPayServer.Tests
private async Task AssertHttpError(int code, Func<Task> act) private async Task AssertHttpError(int code, Func<Task> act)
{ {
try var ex = await Assert.ThrowsAsync<GreenFieldAPIException>(act);
{ Assert.Equal(code, ex.HttpCode);
// 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)
{
}
} }
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
@ -696,12 +708,12 @@ namespace BTCPayServer.Tests
Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email); Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email);
Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles); Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser()); await AssertHttpError(403, async () => await clientInsufficient.GetCurrentUser());
await clientServer.GetCurrentUser(); await clientServer.GetCurrentUser();
await clientProfile.GetCurrentUser(); await clientProfile.GetCurrentUser();
await clientBasic.GetCurrentUser(); await clientBasic.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await AssertHttpError(403, async () =>
await clientInsufficient.CreateUser(new CreateApplicationUserRequest() await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{ {
Email = $"{Guid.NewGuid()}@g.com", Email = $"{Guid.NewGuid()}@g.com",
@ -1434,11 +1446,9 @@ namespace BTCPayServer.Tests
Assert.Single(info.NodeURIs); Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight); Assert.NotEqual(0, info.BlockHeight);
var err = await Assert.ThrowsAsync<GreenFieldAPIException>(async () => await client.GetLightningNodeChannels("BTC")); await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
Assert.Equal(503, err.HttpCode);
// Not permission for the store! // Not permission for the store!
var err2 = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
Assert.Contains("403", err2.Message);
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{ {
Amount = LightMoney.Satoshis(1000), Amount = LightMoney.Satoshis(1000),
@ -1451,8 +1461,7 @@ namespace BTCPayServer.Tests
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// Not permission for the server // Not permission for the server
err2 = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC")); await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
Assert.Contains("403", err2.Message);
var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); var data = await client.GetLightningNodeChannels(user.StoreId, "BTC");
Assert.Equal(2, data.Count()); Assert.Equal(2, data.Count());
@ -1502,7 +1511,7 @@ namespace BTCPayServer.Tests
await client.GetLightningNodeInfo(user.StoreId, "BTC"); await client.GetLightningNodeInfo(user.StoreId, "BTC");
// But if not admin anymore, nope // But if not admin anymore, nope
await user.MakeAdmin(false); await user.MakeAdmin(false);
await AssertAPIError("insufficient-api-permissions", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); await AssertAPIError("missing-permission", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
} }
} }

View file

@ -77,12 +77,11 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.Unrestricted, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> RevokeKey(string apikey) public async Task<IActionResult> RevokeKey(string apikey)
{ {
if (string.IsNullOrEmpty(apikey)) if (!string.IsNullOrEmpty(apikey) &&
return NotFound(); await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
return Ok(); return Ok();
else else
return NotFound(); return this.CreateAPIError("apikey-not-found", "This apikey does not exists");
} }
private static ApiKeyData FromModel(APIKeyData data) private static ApiKeyData FromModel(APIKeyData data)

View file

@ -34,5 +34,9 @@ namespace BTCPayServer.Controllers.GreenField
{ {
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage)); 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));
}
} }
} }

View file

@ -269,7 +269,7 @@ namespace BTCPayServer.Controllers.GreenField
} }
protected JsonHttpException ErrorShouldBeAdminForInternalNode() 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) private LightningInvoiceData ToModel(LightningInvoice invoice)

View file

@ -80,30 +80,29 @@ namespace BTCPayServer.Controllers.GreenField
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public ActionResult<LightningNetworkPaymentMethodData> GetLightningNetworkPaymentMethod(string storeId, string cryptoCode) public ActionResult<LightningNetworkPaymentMethodData> GetLightningNetworkPaymentMethod(string storeId, string cryptoCode)
{ {
if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) AssertSupportLightning(cryptoCode);
{
return NotFound();
}
var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store); var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store);
if (method is null) if (method is null)
{ {
return NotFound(); throw ErrorPaymentMethodNotConfigured();
} }
return Ok(method); 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)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] [HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod( public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
string storeId, string storeId,
string cryptoCode) string cryptoCode)
{ {
if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) AssertSupportLightning(cryptoCode);
{
return NotFound();
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var store = Store; var store = Store;
@ -118,11 +117,7 @@ namespace BTCPayServer.Controllers.GreenField
[FromBody] UpdateLightningNetworkPaymentMethodRequest request) [FromBody] UpdateLightningNetworkPaymentMethodRequest request)
{ {
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
AssertSupportLightning(cryptoCode);
if (!GetNetwork(cryptoCode, out var network))
{
return NotFound();
}
if (string.IsNullOrEmpty(request.ConnectionString)) if (string.IsNullOrEmpty(request.ConnectionString))
{ {
@ -210,11 +205,14 @@ namespace BTCPayServer.Controllers.GreenField
paymentMethod.PaymentId.ToStringNormalized(), paymentMethod.DisableBOLT11PaymentOption); 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); var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
network = network?.SupportLightning is true ? network : null; if (network is null)
return network != 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() private async Task<bool> CanUseInternalLightning()

View file

@ -79,20 +79,21 @@ namespace BTCPayServer.Controllers.GreenField
string storeId, string storeId,
string cryptoCode) string cryptoCode)
{ {
if (!GetCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _)) AssertCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _);
{
return NotFound();
}
var method = GetExistingBtcLikePaymentMethod(cryptoCode); var method = GetExistingBtcLikePaymentMethod(cryptoCode);
if (method is null) if (method is null)
{ {
return NotFound(); throw ErrorPaymentMethodNotConfigured();
} }
return Ok(method); 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)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview")]
public IActionResult GetOnChainPaymentMethodPreview( public IActionResult GetOnChainPaymentMethodPreview(
@ -100,15 +101,12 @@ namespace BTCPayServer.Controllers.GreenField
string cryptoCode, string cryptoCode,
int offset = 0, int amount = 10) int offset = 0, int amount = 10)
{ {
if (!GetCryptoCodeWallet(cryptoCode, out var network, out BTCPayWallet _)) AssertCryptoCodeWallet(cryptoCode, out var network, out _);
{
return NotFound();
}
var paymentMethod = GetExistingBtcLikePaymentMethod(cryptoCode); var paymentMethod = GetExistingBtcLikePaymentMethod(cryptoCode);
if (string.IsNullOrEmpty(paymentMethod?.DerivationScheme)) if (string.IsNullOrEmpty(paymentMethod?.DerivationScheme))
{ {
return NotFound(); throw ErrorPaymentMethodNotConfigured();
} }
try try
@ -149,10 +147,7 @@ namespace BTCPayServer.Controllers.GreenField
[FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData, [FromBody] UpdateOnChainPaymentMethodRequest paymentMethodData,
int offset = 0, int amount = 10) int offset = 0, int amount = 10)
{ {
if (!GetCryptoCodeWallet(cryptoCode, out var network, out BTCPayWallet _)) AssertCryptoCodeWallet(cryptoCode, out var network, out _);
{
return NotFound();
}
if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme)) if (string.IsNullOrEmpty(paymentMethodData?.DerivationScheme))
{ {
@ -202,10 +197,7 @@ namespace BTCPayServer.Controllers.GreenField
string cryptoCode, string cryptoCode,
int offset = 0, int amount = 10) int offset = 0, int amount = 10)
{ {
if (!GetCryptoCodeWallet(cryptoCode, out BTCPayNetwork _, out BTCPayWallet _)) AssertCryptoCodeWallet(cryptoCode, out _, out _);
{
return NotFound();
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var store = Store; var store = Store;
@ -222,11 +214,7 @@ namespace BTCPayServer.Controllers.GreenField
[FromBody] UpdateOnChainPaymentMethodRequest request) [FromBody] UpdateOnChainPaymentMethodRequest request)
{ {
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike); var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
AssertCryptoCodeWallet(cryptoCode, out var network, out var wallet);
if (!GetCryptoCodeWallet(cryptoCode, out var network, out var wallet))
{
return NotFound();
}
if (string.IsNullOrEmpty(request?.DerivationScheme)) 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); network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
wallet = network != null ? _walletProvider.GetWallet(network) : null; if (network is null)
return wallet != 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) private OnChainPaymentMethodData GetExistingBtcLikePaymentMethod(string cryptoCode, StoreData store = null)

View file

@ -102,11 +102,11 @@ namespace BTCPayServer.Controllers.GreenField
// If registration are locked and that an admin exists, don't accept unauthenticated connection // If registration are locked and that an admin exists, don't accept unauthenticated connection
if (anyAdmin && policies.LockSubscription && !isAuth) 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 // Even if subscription are unlocked, it is forbidden to create admin unauthenticated
if (anyAdmin && request.IsAdministrator is true && !isAuth) 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 // 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 bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded
&& (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded && (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.Unrestricted))).Succeeded
@ -114,14 +114,14 @@ namespace BTCPayServer.Controllers.GreenField
: true; : true;
// You need to be admin to create an admin // You need to be admin to create an admin
if (request.IsAdministrator is true && !isAdmin) 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 (!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 // 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; var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded;
if (!isAuth || !canCreateUser) if (!isAuth || !canCreateUser)
return Forbid(AuthenticationSchemes.GreenfieldBasic); return this.CreateAPIPermissionError(Policies.CanCreateUser);
} }
var user = new ApplicationUser var user = new ApplicationUser

View file

@ -472,6 +472,7 @@ namespace BTCPayServer.Hosting
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{ {
app.UseMiddleware<GreenfieldMiddleware>();
app.UseMiddleware<BTCPayMiddleware>(); app.UseMiddleware<BTCPayMiddleware>();
return app; return app;
} }

View 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;
}
}
}

View file

@ -1,11 +1,17 @@
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Security.GreenField namespace BTCPayServer.Security.GreenField
{ {
@ -117,6 +123,8 @@ namespace BTCPayServer.Security.GreenField
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
_HttpContext.Items[RequestedPermissionKey] = policy;
} }
public const string RequestedPermissionKey = nameof(RequestedPermissionKey);
} }
} }

View file

@ -81,7 +81,7 @@ else
ItemCode = item.Id ItemCode = item.Id
}, Context.Request.Scheme, Context.Request.Host.ToString())); }, Context.Request.Scheme, Context.Request.Host.ToString()));
var lnUrl = LNURL.EncodeUri(lnurlEndpoint, "payRequest", supported.UseBech32Scheme); 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> </div>