From 233fa8a4a1ff3a6987ac6db28573573d5d373dd6 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 2 Mar 2020 16:50:28 +0100 Subject: [PATCH] BTCPayServer.Client library + Revoke API Key --- .../BTCPayServer.Client.csproj | 7 ++ .../BTCPayServerClient.APIKeys.cs | 22 +++++ .../BTCPayServerClient.Authorization.cs | 24 +++++ BTCPayServer.Client/BTCPayServerClient.cs | 96 +++++++++++++++++++ BTCPayServer.Client/Models/ApiKeyData.cs | 10 ++ BTCPayServer.Client/Permissions.cs | 18 ++++ BTCPayServer.Tests/ApiKeysTests.cs | 71 +++----------- BTCPayServer.Tests/GreenfieldAPITests.cs | 23 +++-- BTCPayServer/BTCPayServer.csproj | 1 + .../Controllers/ManageController.APIKeys.cs | 15 +-- .../Controllers/RestApi/ApiKeys/ApiKeyData.cs | 23 ----- .../RestApi/ApiKeys/ApiKeysController.cs | 32 ++++++- .../APIKeys/APIKeyAuthorizationHandler.cs | 11 ++- .../Security/APIKeys/APIKeyConstants.cs | 17 +--- .../Security/APIKeys/APIKeyExtensions.cs | 5 +- BTCPayServer/Views/Manage/AddApiKey.cshtml | 15 +-- .../Views/Manage/AuthorizeAPIKey.cshtml | 17 ++-- btcpayserver.sln | 14 +++ 18 files changed, 285 insertions(+), 136 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServer.Client.csproj create mode 100644 BTCPayServer.Client/BTCPayServerClient.APIKeys.cs create mode 100644 BTCPayServer.Client/BTCPayServerClient.Authorization.cs create mode 100644 BTCPayServer.Client/BTCPayServerClient.cs create mode 100644 BTCPayServer.Client/Models/ApiKeyData.cs create mode 100644 BTCPayServer.Client/Permissions.cs delete mode 100644 BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj new file mode 100644 index 000000000..8642d92f3 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs new file mode 100644 index 000000000..dcced301b --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs @@ -0,0 +1,22 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task GetCurrentAPIKeyInfo(CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current"), token); + return await HandleResponse(response); + } + + public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default) + { + var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token); + HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.Authorization.cs b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs new file mode 100644 index 000000000..bfe3c891a --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Authorization.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + + public static Uri GenerateAuthorizeUri(Uri btcpayHost, string[] permissions, bool strict = true, + bool selectiveStores = false) + { + var result = new UriBuilder(btcpayHost); + result.Path = "api-keys/authorize"; + + AppendPayloadToQuery(result, + new Dictionary() + { + {"strict", strict}, {"selectiveStores", selectiveStores}, {"permissions", permissions} + }); + + return result.Uri; + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs new file mode 100644 index 000000000..53ad88d0c --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + private readonly string _apiKey; + private readonly Uri _btcpayHost; + private readonly HttpClient _httpClient; + + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public BTCPayServerClient(Uri btcpayHost, string APIKey, HttpClient httpClient = null) + { + _apiKey = APIKey; + _btcpayHost = btcpayHost; + _httpClient = httpClient ?? new HttpClient(); + } + + protected void HandleResponse(HttpResponseMessage message) + { + message.EnsureSuccessStatusCode(); + } + + protected async Task HandleResponse(HttpResponseMessage message) + { + HandleResponse(message); + return JsonSerializer.Deserialize(await message.Content.ReadAsStringAsync(), _serializerOptions); + } + + protected virtual HttpRequestMessage CreateHttpRequest(string path, + Dictionary queryPayload = null, + HttpMethod method = null) + { + UriBuilder uriBuilder = new UriBuilder(_btcpayHost) {Path = path}; + if (queryPayload != null && queryPayload.Any()) + { + AppendPayloadToQuery(uriBuilder, queryPayload); + } + + var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey); + + + return httpRequest; + } + + protected virtual HttpRequestMessage CreateHttpRequest(string path, + Dictionary queryPayload = null, + T bodyPayload = default, HttpMethod method = null) + { + var request = CreateHttpRequest(path, queryPayload, method); + if (typeof(T).IsPrimitive || !EqualityComparer.Default.Equals(bodyPayload, default(T))) + { + request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions)); + } + + return request; + } + + private static void AppendPayloadToQuery(UriBuilder uri, Dictionary payload) + { + if (uri.Query.Length > 1) + uri.Query += "&"; + foreach (KeyValuePair keyValuePair in payload) + { + UriBuilder uriBuilder = uri; + if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable)))) + { + foreach (var item in (IEnumerable)keyValuePair.Value) + { + uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" + + Uri.EscapeDataString(item.ToString()) + "&"; + } + } + else + { + uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" + + Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&"; + } + } + + uri.Query = uri.Query.Trim('&'); + } + } +} diff --git a/BTCPayServer.Client/Models/ApiKeyData.cs b/BTCPayServer.Client/Models/ApiKeyData.cs new file mode 100644 index 000000000..58872cb11 --- /dev/null +++ b/BTCPayServer.Client/Models/ApiKeyData.cs @@ -0,0 +1,10 @@ +namespace BTCPayServer.Client.Models +{ + public class ApiKeyData + { + public string ApiKey { get; set; } + public string Label { get; set; } + public string UserId { get; set; } + public string[] Permissions { get; set; } + } +} diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs new file mode 100644 index 000000000..adb9ccfa5 --- /dev/null +++ b/BTCPayServer.Client/Permissions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace BTCPayServer.Client +{ + public static class Permissions + { + public const string ServerManagement = nameof(ServerManagement); + public const string StoreManagement = nameof(StoreManagement); + public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; + + public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions + .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) + .Select(s => s.Split(":")[1]); + } +} diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index f27760243..0669dbcae 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; @@ -67,8 +68,8 @@ namespace BTCPayServer.Tests var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything - await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement, - APIKeyConstants.Permissions.StoreManagement); + await TestApiAgainstAccessToken(superApiKey, tester, user, Permissions.ServerManagement, + Permissions.StoreManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -76,7 +77,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, - APIKeyConstants.Permissions.ServerManagement); + Permissions.ServerManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); @@ -84,7 +85,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, - APIKeyConstants.Permissions.StoreManagement); + Permissions.StoreManagement); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); @@ -96,7 +97,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Generate")).Click(); var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, - APIKeyConstants.Permissions.GetStorePermission(storeId)); + Permissions.GetStorePermission(storeId)); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("Generate")).Click(); @@ -117,31 +118,8 @@ namespace BTCPayServer.Tests //permissions //strict //selectiveStores - UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri); - authorize.Path = "api-keys/authorize"; - - authorize.AppendPayloadToQuery(new Dictionary() - { - {"redirect", "https://local.local/callback"}, - {"applicationName", "kukksappname"}, - {"strict", true}, - {"selectiveStores", false}, - { - "permissions", - new[] - { - APIKeyConstants.Permissions.StoreManagement, - APIKeyConstants.Permissions.ServerManagement - } - }, - }); - var authUrl = authorize.ToString(); - var perms = new[] - { - APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement - }; - authUrl = authUrl.Replace("permissions=System.String%5B%5D", - string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, + new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); s.Driver.Navigate().GoToUrl(authUrl); s.Driver.PageSource.Contains("kukksappname"); Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); @@ -159,28 +137,9 @@ namespace BTCPayServer.Tests await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); - authorize = new UriBuilder(tester.PayTester.ServerUri); - authorize.Path = "api-keys/authorize"; - authorize.AppendPayloadToQuery(new Dictionary() - { - {"strict", false}, - {"selectiveStores", true}, - { - "permissions", - new[] - { - APIKeyConstants.Permissions.StoreManagement, - APIKeyConstants.Permissions.ServerManagement - } - } - }); - authUrl = authorize.ToString(); - perms = new[] - { - APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement - }; - authUrl = authUrl.Replace("permissions=System.String%5B%5D", - string.Join("&", perms.Select(s1 => $"permissions={s1}"))); + authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri, + new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString(); + s.Driver.Navigate().GoToUrl(authUrl); Assert.DoesNotContain("kukksappname", s.Driver.PageSource); @@ -214,8 +173,8 @@ namespace BTCPayServer.Tests var secondUser = tester.NewAccount(); secondUser.GrantAccess(); - var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any()) + var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(permissions); + if (permissions.Contains(Permissions.StoreManagement) || selectiveStorePermissions.Any()) { var resultStores = await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores", @@ -231,7 +190,7 @@ namespace BTCPayServer.Tests data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase)); } - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (permissions.Contains(Permissions.StoreManagement)) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/stores/actions", @@ -272,7 +231,7 @@ namespace BTCPayServer.Tests tester.PayTester.HttpClient); }); - if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement)) + if (permissions.Contains(Permissions.ServerManagement)) { Assert.True(await TestApiAgainstAccessToken(accessToken, $"{TestApiPath}/me/is-admin", diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index d2ba99676..497dc69b2 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1,14 +1,10 @@ -using System; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Controllers; -using BTCPayServer.Controllers.RestApi.ApiKeys; -using BTCPayServer.Data; -using BTCPayServer.Security.APIKeys; using BTCPayServer.Tests.Logging; +using Microsoft.AspNet.SignalR.Client; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; @@ -37,17 +33,20 @@ namespace BTCPayServer.Tests user.GrantAccess(); await user.MakeAdmin(); string apiKey = await GenerateAPIKey(tester, user); - + var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey); //Get current api key - var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current"); - request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey); - var result = await tester.PayTester.HttpClient.SendAsync(request); - Assert.True(result.IsSuccessStatusCode); - var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject(); + var apiKeyData = await client.GetCurrentAPIKeyInfo(); Assert.NotNull(apiKeyData); Assert.Equal(apiKey, apiKeyData.ApiKey); Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(2, apiKeyData.Permissions.Length); + + //revoke current api key + await client.RevokeCurrentAPIKeyInfo(); + await Assert.ThrowsAsync(async () => + { + await client.GetCurrentAPIKeyInfo(); + }); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 68d803f81..9d69f7fff 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -124,6 +124,7 @@ + diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index b5abfbb0e..8dd638788 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; @@ -109,8 +110,8 @@ namespace BTCPayServer.Controllers var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() { Label = applicationName, - ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), - StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), + ServerManagementPermission = permissions.Contains(Permissions.ServerManagement), + StoreManagementPermission = permissions.Contains(Permissions.StoreManagement), PermissionsFormatted = permissions, ApplicationName = applicationName, SelectiveStores = selectiveStores, @@ -133,7 +134,7 @@ namespace BTCPayServer.Controllers } - if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement)) + if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement)) { if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) { @@ -147,7 +148,7 @@ namespace BTCPayServer.Controllers } } - if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (viewModel.PermissionsFormatted.Contains(Permissions.StoreManagement)) { if (!viewModel.SelectiveStores && viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) @@ -265,16 +266,16 @@ namespace BTCPayServer.Controllers if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) { - permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission)); + permissions.AddRange(viewModel.SpecificStores.Select(Permissions.GetStorePermission)); } else if (viewModel.StoreManagementPermission) { - permissions.Add(APIKeyConstants.Permissions.StoreManagement); + permissions.Add(Permissions.StoreManagement); } if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission) { - permissions.Add(APIKeyConstants.Permissions.ServerManagement); + permissions.Add(Permissions.ServerManagement); } return permissions; diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs deleted file mode 100644 index b308976c3..000000000 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeyData.cs +++ /dev/null @@ -1,23 +0,0 @@ -using BTCPayServer.Data; - -namespace BTCPayServer.Controllers.RestApi.ApiKeys -{ - public class ApiKeyData - { - public string ApiKey { get; set; } - public string Label { get; set; } - public string UserId { get; set; } - public string[] Permissions { get; set; } - - public static ApiKeyData FromModel(APIKeyData data) - { - return new ApiKeyData() - { - Permissions = data.GetPermissions(), - ApiKey = data.Id, - UserId = data.UserId, - Label = data.Label - }; - } - } -} diff --git a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs index ed45d77d6..486c048ff 100644 --- a/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs +++ b/BTCPayServer/Controllers/RestApi/ApiKeys/ApiKeysController.cs @@ -1,9 +1,12 @@ using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Data; using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; @@ -16,10 +19,12 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys public class ApiKeysController : ControllerBase { private readonly APIKeyRepository _apiKeyRepository; + private readonly UserManager _userManager; - public ApiKeysController(APIKeyRepository apiKeyRepository) + public ApiKeysController(APIKeyRepository apiKeyRepository, UserManager userManager) { _apiKeyRepository = apiKeyRepository; + _userManager = userManager; } [OpenApiOperation("Get current API Key information", "View information about the current API key")] @@ -31,7 +36,30 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys { ControllerContext.HttpContext.GetAPIKey(out var apiKey); var data = await _apiKeyRepository.GetKey(apiKey); - return Ok(ApiKeyData.FromModel(data)); + return Ok(FromModel(data)); + } + + [OpenApiOperation("Revoke the current API Key", "Revoke the current API key so that it cannot be used anymore")] + [SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData), + Description = "The key was revoked and is no longer usable")] + [HttpDelete("~/api/v1/api-keys/current")] + [HttpDelete("~/api/v1/users/me/api-keys/current")] + public async Task> RevokeKey() + { + ControllerContext.HttpContext.GetAPIKey(out var apiKey); + await _apiKeyRepository.Remove(apiKey, _userManager.GetUserId(User)); + return Ok(); + } + + private static ApiKeyData FromModel(APIKeyData data) + { + return new ApiKeyData() + { + Permissions = data.GetPermissions(), + ApiKey = data.Id, + UserId = data.UserId, + Label = data.Label + }; } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs index 0ed687374..f38d05ce2 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Services.Stores; using Microsoft.AspNetCore.Authorization; @@ -35,14 +36,14 @@ namespace BTCPayServer.Security.APIKeys { case Policies.CanListStoreSettings.Key: var selectiveStorePermissions = - APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions()); - success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) || + Permissions.ExtractStorePermissionsIds(context.GetPermissions()); + success = context.HasPermissions(Permissions.StoreManagement) || selectiveStorePermissions.Any(); break; case Policies.CanModifyStoreSettings.Key: string storeId = _HttpContext.GetImplicitStoreId(); - if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) && - !context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId))) + if (!context.HasPermissions(Permissions.StoreManagement) && + !context.HasPermissions(Permissions.GetStorePermission(storeId))) break; if (storeId == null) @@ -63,7 +64,7 @@ namespace BTCPayServer.Security.APIKeys break; case Policies.CanModifyServerSettings.Key: - if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement)) + if (!context.HasPermissions(Permissions.ServerManagement)) break; // For this authorization, we stil check in database because it is super sensitive. var user = await _userManager.GetUserAsync(context.User); diff --git a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs index 427de68e5..0d1893f49 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyConstants.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyConstants.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; namespace BTCPayServer.Security.APIKeys { @@ -16,21 +13,13 @@ namespace BTCPayServer.Security.APIKeys public static class Permissions { - public const string ServerManagement = nameof(ServerManagement); - public const string StoreManagement = nameof(StoreManagement); - public static readonly Dictionary PermissionDescriptions = new Dictionary() { - {StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, - {$"{nameof(StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, - {ServerManagement, ("Manage your server", "The app will have total control on your server")}, + {Client.Permissions.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")}, + {$"{nameof(Client.Permissions.StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")}, + {Client.Permissions.ServerManagement, ("Manage your server", "The app will have total control on your server")}, }; - public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}"; - - public static IEnumerable ExtractStorePermissionsIds(IEnumerable permissions) => permissions - .Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) - .Select(s => s.Split(":")[1]); } } } diff --git a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs index 17f696f0d..6f5429011 100644 --- a/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs +++ b/BTCPayServer/Security/APIKeys/APIKeyExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Security.Bitpay; using BTCPayServer.Services.Stores; @@ -35,12 +36,12 @@ namespace BTCPayServer.Security.APIKeys claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions) .Select(claim => claim.Value).ToList(); - if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) + if (permissions.Contains(Permissions.StoreManagement)) { return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal)); } - var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); + var storeIds = Permissions.ExtractStorePermissionsIds(permissions); return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds); } diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index f5316c545..dd1fabada 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -1,6 +1,7 @@ +@using BTCPayServer.Client @using BTCPayServer.Controllers @using BTCPayServer.Security.APIKeys -@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel +@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel @{ ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); @@ -39,9 +40,9 @@ {
- + -

@GetDescription(APIKeyConstants.Permissions.ServerManagement).

+

@GetDescription(Permissions.ServerManagement).

} @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) @@ -49,9 +50,9 @@
@Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary() {{"class", "form-check-inline"}}) - + -

@GetDescription(APIKeyConstants.Permissions.StoreManagement).

+

@GetDescription(Permissions.StoreManagement).

} @@ -59,8 +60,8 @@ {
  • -
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    +
    @GetTitle(Permissions.StoreManagement + ":")
    +

    @GetDescription(Permissions.StoreManagement + ":").

  • @if (!Model.Stores.Any()) diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 303e171aa..71310d7c4 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -1,3 +1,4 @@ +@using BTCPayServer.Client @using BTCPayServer.Controllers @using BTCPayServer.Security.APIKeys @model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel @@ -48,11 +49,11 @@

    There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

    } - @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) + @if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
    - + @if (!Model.IsServerAdmin) { @@ -61,19 +62,19 @@ } -

    @GetDescription(APIKeyConstants.Permissions.ServerManagement).

    +

    @GetDescription(Permissions.ServerManagement).

    } - @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) + @if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement)) { @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) {
    - + -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement).

    +

    @GetDescription(Permissions.StoreManagement).

    @if (Model.SelectiveStores) { @@ -84,8 +85,8 @@ {
  • -
    @GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")
    -

    @GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").

    +
    @GetTitle(Permissions.StoreManagement + ":")
    +

    @GetDescription(Permissions.StoreManagement + ":").

  • @if (!Model.Stores.Any()) diff --git a/btcpayserver.sln b/btcpayserver.sln index 72fc1a167..29f7e4822 100644 --- a/btcpayserver.sln +++ b/btcpayserver.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Common", "BTCP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Data", "BTCPayServer.Data\BTCPayServer.Data.csproj", "{4D7A865D-3945-4C70-9CC8-B09A274A697E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "BTCPayServer.Client\BTCPayServer.Client.csproj", "{21A13304-7168-49A0-86C2-0A1A9453E9C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x64.Build.0 = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.ActiveCfg = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.Build.0 = Debug|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.Build.0 = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.ActiveCfg = Release|Any CPU + {21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE