mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
04726b3ee4
* Refactor and decouple Payout logic So that we can support lightning + external payout payments Fixes & refactoring almost there final Remove uneeded payment method checks Refactor payouts to handle custom payment method specific actions External onchain payments to approved payouts will now require "confirmation" from the merchant that it was sent by them. add pill tabs for payout status * Improve some UX around feature * add test and some fixes * Only listen to address tracked source and determine based on wallet get tx call from nbx * Simplify isInternal for Payout detection * fix test * Fix Noreferrer test * Make EnsureNewLightningInvoiceOnPartialPayment more resilient * Make notifications section test more resilient in CanUsePullPaymentsViaUI
365 lines
19 KiB
C#
365 lines
19 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Threading.Tasks;
|
|
using BTCPayServer.Client;
|
|
using BTCPayServer.Client.Models;
|
|
using BTCPayServer.Data;
|
|
using BTCPayServer.Security.GreenField;
|
|
using BTCPayServer.Tests.Logging;
|
|
using BTCPayServer.Views.Manage;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using OpenQA.Selenium;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
using StoreData = BTCPayServer.Data.StoreData;
|
|
|
|
namespace BTCPayServer.Tests
|
|
{
|
|
public class ApiKeysTests
|
|
{
|
|
public const int TestTimeout = TestUtils.TestTimeout;
|
|
|
|
public const string TestApiPath = "api/test/apikey";
|
|
public ApiKeysTests(ITestOutputHelper helper)
|
|
{
|
|
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
|
Logs.LogProvider = new XUnitLogProvider(helper);
|
|
}
|
|
|
|
[Fact(Timeout = TestTimeout)]
|
|
[Trait("Selenium", "Selenium")]
|
|
public async Task CanCreateApiKeys()
|
|
{
|
|
//there are 2 ways to create api keys:
|
|
//as a user through your profile
|
|
//as an external application requesting an api key from a user
|
|
|
|
using (var s = SeleniumTester.Create())
|
|
{
|
|
await s.StartAsync();
|
|
var tester = s.Server;
|
|
|
|
var user = tester.NewAccount();
|
|
user.GrantAccess();
|
|
await user.MakeAdmin(false);
|
|
s.GoToLogin();
|
|
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
|
s.GoToProfile(ManageNavPages.APIKeys);
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
|
|
//not an admin, so this permission should not show
|
|
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
|
await user.MakeAdmin();
|
|
s.Logout();
|
|
s.GoToLogin();
|
|
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
|
s.GoToProfile(ManageNavPages.APIKeys);
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
|
|
|
//server management should show now
|
|
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
|
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
|
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
|
|
//this api key has access to everything
|
|
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
|
|
|
|
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
|
|
Policies.CanModifyServerSettings);
|
|
|
|
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
|
|
Policies.CanModifyStoreSettings);
|
|
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
|
|
//there should be a store already by default in the dropdown
|
|
var src = s.Driver.PageSource;
|
|
var getPermissionValueIndex =
|
|
s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']"))
|
|
.GetAttribute("name")
|
|
.Replace(".Permission", ".SpecificStores[0]");
|
|
var dropdown = s.Driver.FindElement(By.Name(getPermissionValueIndex));
|
|
var option = dropdown.FindElement(By.TagName("option"));
|
|
var storeId = option.GetAttribute("value");
|
|
option.Click();
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
|
|
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
|
|
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
|
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
|
|
//let's test the authorized screen now
|
|
//options for authorize are:
|
|
//applicationName
|
|
//redirect
|
|
//permissions
|
|
//strict
|
|
//selectiveStores
|
|
//redirect
|
|
//appidentifier
|
|
var appidentifier = "testapp";
|
|
var callbackUrl = tester.PayTester.ServerUri + "postredirect-callback-test";
|
|
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
|
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
|
|
s.Driver.Navigate().GoToUrl(authUrl);
|
|
Assert.Contains(appidentifier, s.Driver.PageSource);
|
|
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
|
|
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
|
|
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
|
|
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
|
|
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
|
|
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
|
Assert.Equal(callbackUrl, s.Driver.Url);
|
|
|
|
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
|
|
var accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
|
|
|
await TestApiAgainstAccessToken(accessToken, tester, user,
|
|
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
|
|
|
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
|
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
|
|
|
|
s.Driver.Navigate().GoToUrl(authUrl);
|
|
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
|
|
|
|
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
|
|
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
|
|
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
|
|
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
|
|
|
|
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false);
|
|
Assert.Contains("change-store-mode", s.Driver.PageSource);
|
|
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
|
Assert.Equal(callbackUrl, s.Driver.Url);
|
|
|
|
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
|
await TestApiAgainstAccessToken(accessToken, tester, user,
|
|
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
|
|
|
//let's test the app identifier system
|
|
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
|
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString();
|
|
|
|
//if it's the same, go to the confirm page
|
|
s.Driver.Navigate().GoToUrl(authUrl);
|
|
s.Driver.FindElement(By.Id("continue")).Click();
|
|
Assert.Equal(callbackUrl, s.Driver.Url);
|
|
|
|
//same app but different redirect = nono
|
|
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
|
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString();
|
|
|
|
s.Driver.Navigate().GoToUrl(authUrl);
|
|
Assert.False(s.Driver.Url.StartsWith("https://international.com/callback"));
|
|
|
|
// Make sure we can check all permissions when not an admin
|
|
await user.MakeAdmin(false);
|
|
s.Logout();
|
|
s.GoToLogin();
|
|
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
|
s.GoToProfile(ManageNavPages.APIKeys);
|
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
|
int checkedPermissionCount = 0;
|
|
foreach (var checkbox in s.Driver.FindElements(By.ClassName("form-check-input")))
|
|
{
|
|
checkedPermissionCount++;
|
|
checkbox.Click();
|
|
}
|
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
|
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
|
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
|
|
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
|
|
}
|
|
}
|
|
|
|
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
|
|
params string[] expectedPermissionsArr)
|
|
{
|
|
var expectedPermissions = Permission.ToPermissions(expectedPermissionsArr).ToArray();
|
|
expectedPermissions ??= new Permission[0];
|
|
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
|
|
var permissions = apikeydata.Permissions;
|
|
Assert.Equal(expectedPermissions.Length, permissions.Length);
|
|
foreach (var expectPermission in expectedPermissions)
|
|
{
|
|
Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}");
|
|
}
|
|
|
|
if (permissions.Contains(Permission.Create(Policies.CanViewProfile)))
|
|
{
|
|
var resultUser = await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
|
|
Assert.Equal(testAccount.UserId, resultUser);
|
|
}
|
|
else
|
|
{
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
|
|
});
|
|
}
|
|
//create a second user to see if any of its data gets messed upin our results.
|
|
var secondUser = tester.NewAccount();
|
|
secondUser.GrantAccess();
|
|
|
|
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
|
|
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
|
|
var unrestricted = Permission.Create(Policies.Unrestricted, null);
|
|
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
|
|
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
|
|
{
|
|
var resultStores =
|
|
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
|
tester.PayTester.HttpClient);
|
|
|
|
foreach (var selectiveStorePermission in selectiveStorePermissions)
|
|
{
|
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{selectiveStorePermission.Scope}/can-edit",
|
|
tester.PayTester.HttpClient));
|
|
|
|
Assert.Contains(resultStores,
|
|
data => data.Id.Equals(selectiveStorePermission.Scope, StringComparison.InvariantCultureIgnoreCase));
|
|
}
|
|
|
|
bool shouldBeAuthorized = false;
|
|
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanViewStoreSettings, testAccount.StoreId)))
|
|
{
|
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
|
|
tester.PayTester.HttpClient));
|
|
Assert.Contains(resultStores,
|
|
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
|
shouldBeAuthorized = true;
|
|
}
|
|
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Policies.CanModifyStoreSettings, testAccount.StoreId)))
|
|
{
|
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
|
|
tester.PayTester.HttpClient));
|
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient));
|
|
Assert.Contains(resultStores,
|
|
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
|
shouldBeAuthorized = true;
|
|
}
|
|
|
|
if (!shouldBeAuthorized)
|
|
{
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
Assert.DoesNotContain(resultStores,
|
|
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
|
}
|
|
}
|
|
else if (!permissions.Contains(unrestricted))
|
|
{
|
|
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient);
|
|
}
|
|
|
|
if (!permissions.Contains(unrestricted))
|
|
{
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
|
tester.PayTester.HttpClient);
|
|
}
|
|
|
|
if (permissions.Contains(canModifyServer))
|
|
{
|
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/is-admin",
|
|
tester.PayTester.HttpClient));
|
|
}
|
|
else
|
|
{
|
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
|
{
|
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
|
$"{TestApiPath}/me/is-admin",
|
|
tester.PayTester.HttpClient);
|
|
});
|
|
}
|
|
}
|
|
|
|
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
|
|
{
|
|
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
|
|
new Uri(client.BaseAddress, url));
|
|
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
|
|
var result = await client.SendAsync(httpRequest);
|
|
result.EnsureSuccessStatusCode();
|
|
|
|
var rawJson = await result.Content.ReadAsStringAsync();
|
|
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
|
|
{
|
|
return (T)Convert.ChangeType(rawJson, typeof(T));
|
|
}
|
|
|
|
return JsonConvert.DeserializeObject<T>(rawJson);
|
|
}
|
|
|
|
private string GetAccessTokenFromCallbackResult(IWebDriver driver)
|
|
{
|
|
var source = driver.FindElement(By.TagName("body")).Text;
|
|
var json = JObject.Parse(source);
|
|
return json.GetValue("apiKey")!.Value<string>();
|
|
}
|
|
}
|
|
}
|