diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 2acaf0d0d..8f4ce19cc 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -37,6 +37,11 @@ namespace BTCPayServer.Client { return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase); } + + public static bool IsServerPolicy(string policy) + { + return policy.StartsWith("btcpay.server", StringComparison.OrdinalIgnoreCase); + } } public class Permission { diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 1b42a93de..342952332 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -86,9 +86,9 @@ namespace BTCPayServer.Tests Policies.CanModifyStoreSettings); s.Driver.FindElement(By.Id("AddApiKey")).Click(); - s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).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 dropdown = s.Driver.FindElement(By.Name("SpecificStores[0]")); + var dropdown = s.Driver.FindElement(By.Name("PermissionValues[2].SpecificStores[0]")); var option = dropdown.FindElement(By.TagName("option")); var storeId = option.GetAttribute("value"); option.Click(); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 969855297..498fdb022 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -70,13 +70,11 @@ namespace BTCPayServer.Tests var x = Assert.IsType(await manageController.AddApiKey( new ManageController.AddApiKeyViewModel() { - PermissionValues = - permissions.Select(s => - new ManageController.AddApiKeyViewModel.PermissionValueItem() - { - Permission = s, Value = true - }).ToList(), - StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores + PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem() + { + Permission = s, + Value = true + }).ToList() })); var statusMessage = manageController.TempData.GetStatusMessageModel(); Assert.NotNull(statusMessage); diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 3b2ad3d65..6bdf4a19b 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -93,65 +93,93 @@ namespace BTCPayServer.Controllers }); return RedirectToAction("APIKeys"); } - + permissions ??= Array.Empty(); - var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel(Permission.ToPermissions(permissions)) + var parsedPermissions = Permission.ToPermissions(permissions).GroupBy(permission => permission.Policy); + var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() { Label = applicationName, ApplicationName = applicationName, SelectiveStores = selectiveStores, Strict = strict, + Permissions = string.Join(';', parsedPermissions.SelectMany(grouping => grouping.Select(permission => permission.ToString()))) }); + AdjustVMForAuthorization(vm); + return View(vm); } + private void AdjustVMForAuthorization(AuthorizeApiKeysViewModel vm) + { + var parsedPermissions = Permission.ToPermissions(vm.Permissions.Split(';')).GroupBy(permission => permission.Policy); + + for (var index = vm.PermissionValues.Count - 1; index >= 0; index--) + { + var permissionValue = vm.PermissionValues[index]; + var wanted = parsedPermissions?.SingleOrDefault(permission => + permission.Key.Equals(permissionValue.Permission, + StringComparison.InvariantCultureIgnoreCase)); + if (vm.Strict && !(wanted?.Any()??false)) + { + vm.PermissionValues.RemoveAt(index); + continue; + } + else if (wanted?.Any()??false) + { + if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) && + wanted.Any(permission => !string.IsNullOrEmpty(permission.StoreId))) + { + permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.Specific; + permissionValue.SpecificStores = wanted.Select(permission => permission.StoreId).ToList(); + } + else + { + permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores; + permissionValue.SpecificStores = new List(); + permissionValue.Value = true; + } + } + } + } + [HttpPost("~/api-keys/authorize")] public async Task AuthorizeAPIKey([FromForm] AuthorizeApiKeysViewModel viewModel) { await SetViewModelValues(viewModel); + + AdjustVMForAuthorization(viewModel); var ar = HandleCommands(viewModel); - + if (ar != null) { return ar; } - - if (viewModel.Strict) + + for (int i = 0; i < viewModel.PermissionValues.Count; i++) { - for (int i = 0; i < viewModel.PermissionValues.Count; i++) + if (viewModel.PermissionValues[i].Forbidden && viewModel.Strict) { - if (viewModel.PermissionValues[i].Forbidden) - { - ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value", - $"The permission '{viewModel.PermissionValues[i].Title}' is required for this application."); - } + viewModel.PermissionValues[i].Value = false; + ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value", + $"The permission '{viewModel.PermissionValues[i].Title}' is required for this application."); + } + + if (viewModel.PermissionValues[i].StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific && + !viewModel.SelectiveStores) + { + viewModel.PermissionValues[i].StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores; + ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value", + $"The permission '{viewModel.PermissionValues[i].Title}' cannot be store specific for this application."); } } - - var permissions = Permission.ToPermissions(viewModel.Permissions.Split(';')).ToHashSet(); - if (permissions.Contains(Permission.Create(Policies.CanModifyStoreSettings))) - { - if (!viewModel.SelectiveStores && - viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) - { - viewModel.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores; - ModelState.AddModelError(nameof(viewModel.StoreManagementPermission), - "This application does not allow selective store permissions."); - } - - if (!viewModel.StoreManagementPermission.Value && !viewModel.SpecificStores.Any() && viewModel.Strict) - { - ModelState.AddModelError(nameof(viewModel.StoreManagementPermission), - $"This permission '{viewModel.StoreManagementPermission.Title}' is required for this application."); - } - } - + + if (!ModelState.IsValid) { return View(viewModel); } - + switch (viewModel.Command.ToLowerInvariant()) { case "no": @@ -197,35 +225,48 @@ namespace BTCPayServer.Controllers } private IActionResult HandleCommands(AddApiKeyViewModel viewModel) { - switch (viewModel.Command) + if (string.IsNullOrEmpty(viewModel.Command)) + { + return null; + } + var parts = viewModel.Command.Split(':', StringSplitOptions.RemoveEmptyEntries); + var permission = parts[0]; + if (!Policies.IsStorePolicy(permission)) + { + return null; + } + var permissionValueItem = viewModel.PermissionValues.Single(item => item.Permission == permission); + var command = parts[1]; + var storeIndex = parts.Length == 3 ? parts[2] : null; + + ModelState.Clear(); + switch (command) { case "change-store-mode": - viewModel.StoreMode = viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific + + permissionValueItem.StoreMode = permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific ? AddApiKeyViewModel.ApiKeyStoreMode.AllStores : AddApiKeyViewModel.ApiKeyStoreMode.Specific; - if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific && - !viewModel.SpecificStores.Any() && viewModel.Stores.Any()) + if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific && + !permissionValueItem.SpecificStores.Any() && viewModel.Stores.Any()) { - viewModel.SpecificStores.Add(null); + permissionValueItem.SpecificStores.Add(null); } return View(viewModel); case "add-store": - viewModel.SpecificStores.Add(null); + permissionValueItem.SpecificStores.Add(null); return View(viewModel); - case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase): - { - ModelState.Clear(); - var index = int.Parse( - viewModel.Command.Substring( - viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), - CultureInfo.InvariantCulture); - viewModel.SpecificStores.RemoveAt(index); - return View(viewModel); - } + case "remove-store": + { + if (storeIndex != null) + permissionValueItem.SpecificStores.RemoveAt(int.Parse(storeIndex, + CultureInfo.InvariantCulture)); + return View(viewModel); + } } - + return null; } @@ -249,95 +290,56 @@ namespace BTCPayServer.Controllers private IEnumerable GetPermissionsFromViewModel(AddApiKeyViewModel viewModel) { List permissions = new List(); - foreach (var p in viewModel.PermissionValues.Where(tuple => tuple.Value && !tuple.Forbidden)) + foreach (var p in viewModel.PermissionValues.Where(tuple => !tuple.Forbidden)) { - if (Permission.TryCreatePermission(p.Permission, null, out var pp)) + if (Policies.IsStorePolicy(p.Permission)) + { + if (p.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && p.Value) + { + permissions.Add(Permission.Create(p.Permission)); + } + else if (p.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) + { + permissions.AddRange(p.SpecificStores.Select(s => Permission.Create(p.Permission, s))); + } + } + else if (p.Value && Permission.TryCreatePermission(p.Permission, null, out var pp)) permissions.Add(pp); } - if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && viewModel.StoreManagementPermission.Value) - { - permissions.Add(Permission.Create(Policies.CanModifyStoreSettings)); - } - else if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) - { - permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Policies.CanModifyStoreSettings, s))); - } + + return permissions.Distinct(); } private async Task SetViewModelValues(T viewModel) where T : AddApiKeyViewModel { viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); - var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded; - viewModel.PermissionValues ??= Policies.AllPolicies.Where(p => p != Policies.CanModifyStoreSettings) - .Select(s => new AddApiKeyViewModel.PermissionValueItem() { Permission = s, Value = false }).ToList(); + var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)) + .Succeeded; + viewModel.PermissionValues ??= Policies.AllPolicies + .Select(s => new AddApiKeyViewModel.PermissionValueItem() + { + Permission = s, + Value = false, + Forbidden = Policies.IsServerPolicy(s) && !isAdmin + }).ToList(); + + if (!isAdmin) { - foreach (var p in viewModel.PermissionValues) + foreach (var p in viewModel.PermissionValues.Where(item => Policies.IsServerPolicy(item.Permission))) { - if (p.Permission == Policies.CanCreateUser || - p.Permission == Policies.CanModifyServerSettings) - { - p.Forbidden = true; - } + p.Forbidden = true; } } + return viewModel; } public class AddApiKeyViewModel { - public AddApiKeyViewModel() - { - StoreManagementPermission = new PermissionValueItem() - { - Permission = Policies.CanModifyStoreSettings, - Value = false - }; - StoreManagementSelectivePermission = new PermissionValueItem() - { - Permission = $"{Policies.CanModifyStoreSettings}:", - Value = true - }; - } - public AddApiKeyViewModel(IEnumerable permissions):this() - { - StoreManagementPermission.Value = permissions.Any(p => p.Policy == Policies.CanModifyStoreSettings && p.StoreId == null); - PermissionValues = permissions.Where(p => p.Policy != Policies.CanModifyStoreSettings) - .Select(p => new PermissionValueItem() { Permission = p.ToString(), Value = true }) - .ToList(); - } - - public IEnumerable GetPermissions() - { - if (!(PermissionValues is null)) - { - foreach (var p in PermissionValues.Where(o => o.Value)) - { - if (Permission.TryCreatePermission(p.Permission, null, out var pp)) - yield return pp; - } - } - if (this.StoreMode == ApiKeyStoreMode.AllStores) - { - if (StoreManagementPermission.Value) - yield return Permission.Create(Policies.CanModifyStoreSettings); - } - else if (this.StoreMode == ApiKeyStoreMode.Specific && SpecificStores is List) - { - foreach (var p in SpecificStores) - { - if (Permission.TryCreatePermission(Policies.CanModifyStoreSettings, p, out var pp)) - yield return pp; - } - } - } public string Label { get; set; } public StoreData[] Stores { get; set; } - public ApiKeyStoreMode StoreMode { get; set; } - public List SpecificStores { get; set; } = new List(); - public PermissionValueItem StoreManagementPermission { get; set; } - public PermissionValueItem StoreManagementSelectivePermission { get; set; } public string Command { get; set; } public List PermissionValues { get; set; } @@ -354,43 +356,40 @@ namespace BTCPayServer.Controllers {BTCPayServer.Client.Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")}, {BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")}, {BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")}, - {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")}, {$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")}, + {BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")}, + {$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")}, {BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")}, {BTCPayServer.Client.Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")}, {BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")}, - {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")}, + {BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")}, + {$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")}, }; public string Title { get { - return PermissionDescriptions[Permission].Title; + return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific? ":": "")}"].Title; } } public string Description { get { - return PermissionDescriptions[Permission].Description; + return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific? ":": "")}"].Description; } } public string Permission { get; set; } public bool Value { get; set; } public bool Forbidden { get; set; } + + public ApiKeyStoreMode StoreMode { get; set; } = ApiKeyStoreMode.AllStores; + public List SpecificStores { get; set; } = new List(); } } public class AuthorizeApiKeysViewModel : AddApiKeyViewModel { - public AuthorizeApiKeysViewModel() - { - - } - public AuthorizeApiKeysViewModel(IEnumerable permissions) : base(permissions) - { - Permissions = string.Join(';', permissions.Select(p => p.ToString()).ToArray()); - } public string ApplicationName { get; set; } public bool Strict { get; set; } public bool SelectiveStores { get; set; } diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index c647ac805..863111e46 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -8,102 +8,108 @@ }

@ViewData["Title"]

- +

Generate a new api key to use BTCPay through its API.

- -
- +
@for (int i = 0; i < Model.PermissionValues.Count; i++) { - @if (!Model.PermissionValues[i].Forbidden) + @if (!Model.PermissionValues[i].Forbidden) { -
-
- - - - - @Model.PermissionValues[i].Description -
-
- } - } - @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) - { -
-
- - - - - @Model.StoreManagementPermission.Description -
-
- } - else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) - { -
-
@Model.StoreManagementSelectivePermission.Title
- @Model.StoreManagementSelectivePermission.Description - -
- @if (!Model.Stores.Any()) + + @if (Policies.IsStorePolicy(Model.PermissionValues[i].Permission)) { -
- You currently have no stores configured. -
- } - @for (var index = 0; index < Model.SpecificStores.Count; index++) - { -
-
-
-
- @if (Model.SpecificStores[index] == null) - { - - } - else - { - var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); - - } + + @if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + { +
+
+ + - -
-
-
- - -
-
-
+ + + @Model.PermissionValues[i].Description +
+
+ } + else + { +
+
@Model.PermissionValues[i].Title
+ @Model.PermissionValues[i].Description + +
+ @if (!Model.Stores.Any()) + { +
+ You currently have no stores configured. +
+ } + @for (var index = 0; index < Model.PermissionValues[i].SpecificStores.Count; index++) + { +
+
+
+
+ @if (Model.PermissionValues[i].SpecificStores[index] == null) + { + + } + else + { + var store = Model.Stores.SingleOrDefault(data => data.Id == Model.PermissionValues[i].SpecificStores[index]); + + } + + +
+
+
+ + +
+
+
+ } + @if (Model.PermissionValues[i].SpecificStores.Count < Model.Stores.Length) + { +
+ +
+ } + } } - @if (Model.SpecificStores.Count < Model.Stores.Length) + else { -
- -
+
+
+ + + + @Model.PermissionValues[i].Description +
+
} + } }
diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 5217d7482..b835b8f76 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -6,17 +6,15 @@ @{ Layout = "_Layout"; ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}"; - var permissions = Permission.ToPermissions(Model.Permissions.Split(';')); - var hasStorePermission = permissions.Any(p => p.Policy == Policies.CanModifyStoreSettings); + var permissions = Permission.ToPermissions(Model.Permissions.Split(';')).GroupBy(permission => permission.Policy); } - +
- - - - - + + + +
@@ -32,119 +30,138 @@
- +
@if (!permissions.Any()) { -
-

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.

-
+
+

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

+
} + @for (int i = 0; i < Model.PermissionValues.Count; i++) { -
-
- - @if (Model.Strict) - { - - - } - else - { - - } - - @if (Model.PermissionValues[i].Forbidden) - { -
- - This permission is not available for your account. - - } - @Model.PermissionValues[i].Description -
-
- } - @if (hasStorePermission) - { - @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + + + + @if (Model.PermissionValues[i].Forbidden && !Model.Strict) { -
-
- @if (Model.Strict) - { - - - } - else - { - - } - - @if (Model.SelectiveStores) - { - - } -
- - @Model.StoreManagementPermission.Description -
-
- } - else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) + continue; + } + @if (Policies.IsStorePolicy(Model.PermissionValues[i].Permission)) { -
-
@Model.StoreManagementSelectivePermission.Title
- @Model.StoreManagementSelectivePermission.Description - -
- @if (!Model.Stores.Any()) + @if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) + { +
+
+ @if (Model.Strict || Model.PermissionValues[i].Forbidden) + { + + + } + else + { + + } + + @if (Model.SelectiveStores) + { + + } + + @if (Model.PermissionValues[i].Forbidden) + { +
+ + This permission is not available for your account. + + } + + + @Model.PermissionValues[i].Description +
+
+ } + else if (Model.SelectiveStores) + { +
+
@Model.PermissionValues[i].Title
+ @Model.PermissionValues[i].Description + +
+ @if (!Model.Stores.Any()) { -
- You currently have no stores configured. -
+
+ You currently have no stores configured. +
} - @for (var index = 0; index < Model.SpecificStores.Count; index++) + @for (var index = 0; index < Model.PermissionValues[i].SpecificStores.Count; index++) { -
-
-
-
- @if (Model.SpecificStores[index] == null) +
+
+
+
+ @if (Model.PermissionValues[i].SpecificStores[index] == null) { - + } else { - var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]); - + var store = Model.Stores.SingleOrDefault(data => data.Id == Model.PermissionValues[i].SpecificStores[index]); + } - + +
+
+
+ + +
+
+
+ } + @if (Model.PermissionValues[i].SpecificStores.Count < Model.Stores.Length) + { +
+ +
+ } + } + } + else + { +
+
+ @if (Model.Strict || Model.PermissionValues[i].Forbidden) + { + + + } + else + { + + } + + @if (Model.PermissionValues[i].Forbidden) + { +
+ + This permission is not available for your account. + + } + + @Model.PermissionValues[i].Description
-
- - -
-
-
- } - @if (Model.SpecificStores.Count < Model.Stores.Length) - { -
- -
- } } }