btcpayserver/BTCPayServer.Tests/ApiKeysTests.cs

436 lines
22 KiB
C#
Raw Normal View History

using System;
2022-06-20 15:54:36 +02:00
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using BTCPayServer.Client;
2020-03-20 05:22:10 +01:00
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
2022-01-14 05:05:23 +01:00
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Views.Manage;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
2022-06-20 15:43:24 +02:00
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
2020-03-24 16:18:43 +01:00
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Tests
{
2021-11-22 09:16:08 +01:00
public class ApiKeysTests : UnitTestBase
{
public const int TestTimeout = 120_000;
public const string TestApiPath = "api/test/apikey";
2021-11-22 09:16:08 +01:00
public ApiKeysTests(ITestOutputHelper helper) : base(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
2022-01-14 09:50:29 +01:00
using var s = CreateSeleniumTester();
await s.StartAsync();
var tester = s.Server;
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin(false);
s.GoToLogin();
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
2022-01-14 09:50:29 +01:00
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
TestLogs.LogInformation("Checking admin permissions");
2022-01-14 09:50:29 +01:00
//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);
2022-01-14 09:50:29 +01:00
s.GoToProfile(ManageNavPages.APIKeys);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
2022-06-20 15:43:24 +02:00
2022-01-14 09:50:29 +01:00
//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;
TestLogs.LogInformation("Checking super admin key");
2022-01-14 09:50:29 +01:00
//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;
TestLogs.LogInformation("Checking CanModifyServerSettings permissions");
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
Policies.CanModifyServerSettings);
2022-01-14 09:50:29 +01:00
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;
TestLogs.LogInformation("Checking CanModifyStoreSettings permissions");
2022-01-14 09:50:29 +01:00
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 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.WaitForAndClick(By.Id("Generate"));
2022-01-14 09:50:29 +01:00
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation("Checking CanModifyStoreSettings with StoreId permissions");
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
TestLogs.LogInformation("Adding API key for no permissions");
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
TestLogs.LogInformation("Generating API key for no permissions");
s.Driver.WaitForAndClick(By.Id("Generate"));
2022-01-14 09:50:29 +01:00
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation($"Checking no permissions: {noPermissionsApiKey}");
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
});
TestLogs.LogInformation("Checking authorize screen");
2022-01-14 09:50:29 +01:00
//let's test the authorized screen now
//options for authorize are:
//applicationName
//redirect
//permissions
//strict
//selectiveStores
//redirect
//appidentifier
var appidentifier = "testapp";
var callbackUrl = s.ServerUri + "postredirect-callback-test";
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
2022-06-20 15:43:24 +02:00
new[] { Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
// No upfront store selection with only server settings
TestLogs.LogInformation($"Going to auth URL {authUrl}");
s.GoToUrl(authUrl);
Assert.Contains(appidentifier, s.Driver.PageSource);
Assert.False(s.Driver.FindElement(By.Id("SpecificStores")).Displayed);
// Now with store settings
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
2022-01-14 09:50:29 +01:00
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
TestLogs.LogInformation($"Going to auth URL {authUrl}");
s.GoToUrl(authUrl);
2022-01-14 09:50:29 +01:00
Assert.Contains(appidentifier, s.Driver.PageSource);
2022-06-20 15:43:24 +02:00
// Select a store
var select = new SelectElement(s.Driver.FindElement(By.Id("SpecificStores")));
select.SelectByIndex(0);
s.Driver.FindElement(By.Id("continue")).Click();
2022-01-14 09:50:29 +01:00
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);
TestLogs.LogInformation("Going to callback URL");
s.Driver.WaitForAndClick(By.Id("consent-yes"));
2022-01-14 09:50:29 +01:00
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL");
2022-01-14 09:50:29 +01:00
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
var accessToken = GetAccessTokenFromCallbackResult(s.Driver);
TestLogs.LogInformation($"Access token: {accessToken}");
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
TestLogs.LogInformation($"Going to auth URL 2 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 2");
2022-01-14 09:50:29 +01:00
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
2022-06-20 15:43:24 +02:00
// Select a store
select = new SelectElement(s.Driver.FindElement(By.Id("SpecificStores")));
select.SelectByIndex(0);
s.Driver.FindElement(By.Id("continue")).Click();
2022-01-14 09:50:29 +01:00
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);
TestLogs.LogInformation("Going to callback URL 2");
s.Driver.WaitForAndClick(By.Id("consent-yes"));
2022-01-14 09:50:29 +01:00
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL 2");
2022-01-14 09:50:29 +01:00
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
TestLogs.LogInformation($"Access token: {accessToken}");
TestLogs.LogInformation("Checking authorized permissions");
2022-01-14 09:50:29 +01:00
await TestApiAgainstAccessToken(accessToken, tester, user,
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
//let's test the app identifier system
TestLogs.LogInformation("Checking app identifier system");
2022-01-14 09:50:29 +01:00
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString();
//if it's the same, go to the confirm page
TestLogs.LogInformation($"Going to auth URL 3 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 3");
s.Driver.WaitForAndClick(By.Id("continue"));
TestLogs.LogInformation("Going to callback URL 3");
2022-01-14 09:50:29 +01:00
Assert.Equal(callbackUrl, s.Driver.Url);
TestLogs.LogInformation("On callback URL 3");
2022-01-14 09:50:29 +01:00
//same app but different redirect = nono
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString();
TestLogs.LogInformation($"Going to auth URL 4 {authUrl}");
s.GoToUrl(authUrl);
TestLogs.LogInformation("On auth URL 4");
2022-01-14 09:50:29 +01:00
Assert.False(s.Driver.Url.StartsWith("https://international.com/callback"));
// Make sure we can check all permissions when not an admin
TestLogs.LogInformation("Make sure we can check all permissions when not an admin");
2022-01-14 09:50:29 +01:00
await user.MakeAdmin(false);
s.Logout();
s.GoToLogin();
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
TestLogs.LogInformation("Go to API Keys page");
s.GoToUrl("/account/apikeys");
TestLogs.LogInformation("On API Keys page");
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
int checkedPermissionCount = s.Driver.FindElements(By.ClassName("form-check-input")).Count;
TestLogs.LogInformation($"Adding API key: {checkedPermissionCount} permissions");
s.Driver.ExecuteJavaScript("document.querySelectorAll('#Permissions .form-check-input').forEach(i => i.click())");
TestLogs.LogInformation($"Clicked {checkedPermissionCount}");
TestLogs.LogInformation("Generating API key");
s.Driver.WaitForAndClick(By.Id("Generate"));
2022-01-14 09:50:29 +01:00
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
TestLogs.LogInformation($"Checking API key permissions: {allAPIKey}");
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, "api/v1/api-keys/current", tester.PayTester.HttpClient);
2022-01-14 09:50:29 +01:00
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
params string[] expectedPermissionsArr)
{
var expectedPermissions = Permission.ToPermissions(expectedPermissionsArr).ToArray();
2020-03-20 05:22:10 +01:00
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
var permissions = apikeydata.Permissions;
TestLogs.LogInformation($"TestApiAgainstAccessToken: Permissions {permissions.Length}");
2020-03-20 05:22:10 +01:00
Assert.Equal(expectedPermissions.Length, permissions.Length);
foreach (var expectPermission in expectedPermissions)
{
Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}");
}
TestLogs.LogInformation("Testing CanViewProfile");
if (permissions.Contains(Permission.Create(Policies.CanViewProfile)))
2020-03-20 05:22:10 +01:00
{
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 up in our results.
TestLogs.LogInformation("Testing second user");
var secondUser = tester.NewAccount();
await secondUser.GrantAccessAsync();
2022-06-20 15:54:36 +02:00
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings);
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings);
var unrestricted = Permission.Create(Policies.Unrestricted);
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
TestLogs.LogInformation("Testing can edit store for first user");
2022-06-20 15:54:36 +02:00
IEnumerable<Permission> storePermissions = selectiveStorePermissions as Permission[] ?? selectiveStorePermissions.ToArray();
if (permissions.Contains(canModifyAllStores) || storePermissions.Any())
{
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
2022-06-20 15:54:36 +02:00
foreach (var selectiveStorePermission in storePermissions)
{
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));
}
2020-03-19 11:11:15 +01:00
bool shouldBeAuthorized = false;
2022-06-20 15:54:36 +02:00
if (permissions.Contains(canModifyAllStores) || storePermissions.Contains(Permission.Create(Policies.CanViewStoreSettings, testAccount.StoreId)))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
2020-03-19 11:11:15 +01:00
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
shouldBeAuthorized = true;
}
2022-06-20 15:54:36 +02:00
if (permissions.Contains(canModifyAllStores) || storePermissions.Contains(Permission.Create(Policies.CanModifyStoreSettings, testAccount.StoreId)))
2020-03-19 11:11:15 +01:00
{
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));
2020-03-19 11:11:15 +01:00
shouldBeAuthorized = true;
}
2020-03-20 05:22:10 +01:00
2020-03-19 11:11:15 +01:00
if (!shouldBeAuthorized)
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
2020-03-19 11:11:15 +01:00
$"{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);
});
2020-03-19 11:11:15 +01:00
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
}
2020-03-20 05:22:10 +01:00
else if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
2020-03-16 08:13:44 +01:00
else
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient);
}
TestLogs.LogInformation("Testing can edit store for second user");
2020-03-19 11:11:15 +01:00
if (!permissions.Contains(unrestricted))
{
2020-03-13 07:58:09 +01:00
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
2020-03-16 08:13:44 +01:00
else
{
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
}
TestLogs.LogInformation("Testing can edit store for second user expectation met");
TestLogs.LogInformation($"Testing CanModifyServer with {permissions.Contains(canModifyServer)}");
2020-03-19 11:11:15 +01:00
if (permissions.Contains(canModifyServer))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",
tester.PayTester.HttpClient));
}
2020-03-16 08:13:44 +01:00
else
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",
tester.PayTester.HttpClient);
});
}
TestLogs.LogInformation("Testing CanModifyServer expectation met");
}
2022-06-20 15:54:36 +02:00
private async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
{
var uri = new Uri(client.BaseAddress, url);
var httpRequest = new HttpRequestMessage(HttpMethod.Get, uri);
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
TestLogs.LogInformation($"Testing {uri}");
var result = await client.SendAsync(httpRequest);
TestLogs.LogInformation($"Testing {uri} status: {result.StatusCode}");
result.EnsureSuccessStatusCode();
var rawJson = await result.Content.ReadAsStringAsync();
TestLogs.LogInformation($"Testing {uri} result: {rawJson}");
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(rawJson, typeof(T));
}
2020-03-20 10:56:30 +01:00
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>();
}
}
}