What changed, and why it matters
This is a large refactoring commit that reworks how permissions are defined and enforced in BTCPay Server so that plugins can register their own permissions. It moves the permission hierarchy out of a hard-coded static class into a runtime service, changes how store context is tracked during requests, and updates authorization handlers. The change is architectural rather than a targeted security fix, but any mistake in the new permission logic could allow users to access stores or functions they should not.
Treat this as a high-risk refactor requiring focused review of authorization behavior. Verify that every endpoint previously protected by static policy checks still receives equivalent enforcement through PermissionService, that plugin-registered permissions cannot impersonate built-in policies, that store scoping still isolates cross-store access, and that the new navigation/current-store split does not leak store-scoped UI actions when the page context is null or different. Run the updated integration tests and add negative tests for plugin policy collisions and unscoped requirement bypasses.
Security signals we found
Large-scale authorization system refactor
Removal of static permission whitelist and policy map
Introduction of dynamic plugin-provided permissions
Change in store context propagation (GetNavStoreData vs GetStoreData, SetPreferredStoreId)
Authorization attribute change on ListStores from CanModifyStoreSettingsUnscoped to CanViewStoreSettings
New PolicyRequirement.RequireUnscoped flag added
Permission validation now uses regex rather than hard-coded list
Evidence from the diff
The commit ‘Pluginize permissions’ replaces the static Policies.AllPolicies/PolicyMap permission system with a dynamic PermissionService that loads PolicyDefinition singletons. It introduces IPermissionHandler/IPermissionScopeProvider, a new PermissionAuthorizationHandler, and a CookieAuthenticationClaimTransformer. Store context handling is split into ‘current page’ store data (BTCPAY.STOREDATA) and ‘navigation’ store data (BTCPAY.NAVSTOREDATA), with SetPreferredStoreId now controlling the cookie. Several controllers switch from static Contains() checks to PermissionService.Contains(). Tests are updated to reflect the new API, and some authorization attributes change (e.g., UIUserStoresController.ListStores now uses CanViewStoreSettings instead of CanModifyStoreSettingsUnscoped).
Changed components
BTCPayServer.Client.Permissions / PermissionBTCPayServer.Security authorization handlersBTCPayServer.Services.PermissionServiceBTCPayServer.Controllers.UIUserStoresControllerBTCPayServer.Controllers.UIStoresControllerBTCPayServer.Controllers.UIManageController.APIKeysBTCPayServer.Extensions (store context helpers)BTCPayServer.Hosting.BTCPayServerServices DI registrationBTCPayServer.Plugins.Bitpay authorizationInspect captured patch +1336 / −1081
diff --git a/BTCPayServer.Abstractions/Security/AuthorizationFilterHandle.cs b/BTCPayServer.Abstractions/Security/AuthorizationFilterHandle.cs
deleted file mode 100644
index ef7d297..0000000
--- a/BTCPayServer.Abstractions/Security/AuthorizationFilterHandle.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace BTCPayServer.Security;
-
-public class AuthorizationFilterHandle
-{
- public AuthorizationHandlerContext Context { get; }
- public PolicyRequirement Requirement { get; }
- public HttpContext HttpContext { get; }
- public bool Success { get; private set; }
-
- public AuthorizationFilterHandle(
- AuthorizationHandlerContext context,
- PolicyRequirement requirement,
- HttpContext httpContext)
- {
- Context = context;
- Requirement = requirement;
- HttpContext = httpContext;
- }
-
- public void MarkSuccessful()
- {
- Success = true;
- }
-}
diff --git a/BTCPayServer.Abstractions/Security/PolicyRequirement.cs b/BTCPayServer.Abstractions/Security/PolicyRequirement.cs
index a2bdbc7..81e7a0a 100644
--- a/BTCPayServer.Abstractions/Security/PolicyRequirement.cs
+++ b/BTCPayServer.Abstractions/Security/PolicyRequirement.cs
@@ -1,15 +1,12 @@
+#nullable enable
using System;
+using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
-namespace BTCPayServer.Security
+namespace BTCPayServer.Security;
+
+public class PolicyRequirement(string policy, bool requireUnscoped = false) : IAuthorizationRequirement
{
- public class PolicyRequirement : IAuthorizationRequirement
- {
- public PolicyRequirement(string policy)
- {
- ArgumentNullException.ThrowIfNull(policy);
- Policy = policy;
- }
- public string Policy { get; }
- }
+ public bool RequireUnscoped { get; } = requireUnscoped;
+ public string Policy { get; } = Permission.IsValidPolicy(policy) ? policy : throw new ArgumentException("Invalid policy (it should be 'btcpay.some.permission.name')", nameof(policy));
}
diff --git a/BTCPayServer.Client/Models/PermissionMetadata.cs b/BTCPayServer.Client/Models/PermissionMetadata.cs
index b4156c7..12ee0a4 100644
--- a/BTCPayServer.Client/Models/PermissionMetadata.cs
+++ b/BTCPayServer.Client/Models/PermissionMetadata.cs
@@ -1,36 +1,10 @@
-using System;
using System.Collections.Generic;
-using System.Linq;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PermissionMetadata
{
- static PermissionMetadata()
- {
- Dictionary<string, PermissionMetadata> nodes = new Dictionary<string, PermissionMetadata>();
- foreach (var policy in Policies.AllPolicies)
- {
- nodes.Add(policy, new PermissionMetadata() { PermissionName = policy });
- }
- foreach (var n in nodes)
- {
- foreach (var policy in Policies.AllPolicies)
- {
- if (policy.Equals(n.Key, StringComparison.OrdinalIgnoreCase))
- continue;
- if (Permission.Create(n.Key).Contains(Permission.Create(policy)))
- n.Value.SubPermissions.Add(policy);
- }
- }
- foreach (var n in nodes)
- {
- n.Value.SubPermissions.Sort();
- }
- PermissionNodes = nodes.Values.OrderBy(v => v.PermissionName).ToArray();
- }
- public readonly static PermissionMetadata[] PermissionNodes;
[JsonProperty("name")]
public string PermissionName { get; set; }
[JsonProperty("included")]
diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs
index 640900a..3114f5d 100644
--- a/BTCPayServer.Client/Permissions.cs
+++ b/BTCPayServer.Client/Permissions.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
@@ -40,89 +39,8 @@ namespace BTCPayServer.Client
public const string CanViewPayouts = "btcpay.store.canviewpayouts";
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanViewPullPayments = "btcpay.store.canviewpullpayments";
- public const string CanViewOfferings = "btcpay.store.canviewofferings";
- public const string CanModifyOfferings = "btcpay.store.canmodifyofferings";
- public const string CanManageSubscribers = "btcpay.store.canmanagesubscribers";
- public const string CanCreditSubscribers = "btcpay.store.cancreditsubscribers";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string Unrestricted = "unrestricted";
- public static IEnumerable<string> AllPolicies
- {
- get
- {
- yield return CanViewInvoices;
- yield return CanCreateInvoice;
- yield return CanModifyInvoices;
- yield return CanModifyWebhooks;
- yield return CanModifyServerSettings;
- yield return CanModifyStoreSettings;
- yield return CanViewStoreSettings;
- yield return CanViewReports;
- yield return CanViewPaymentRequests;
- yield return CanModifyPaymentRequests;
- yield return CanModifyProfile;
- yield return CanViewProfile;
- yield return CanViewUsers;
- yield return CanCreateUser;
- yield return CanDeleteUser;
- yield return CanManageNotificationsForUser;
- yield return CanViewNotificationsForUser;
- yield return Unrestricted;
- yield return CanUseInternalLightningNode;
- yield return CanViewLightningInvoiceInternalNode;
- yield return CanCreateLightningInvoiceInternalNode;
- yield return CanUseLightningNodeInStore;
- yield return CanViewLightningInvoiceInStore;
- yield return CanCreateLightningInvoiceInStore;
- yield return CanManagePullPayments;
- yield return CanArchivePullPayments;
- yield return CanCreatePullPayments;
- yield return CanViewPullPayments;
- yield return CanViewOfferings;
- yield return CanModifyOfferings;
- yield return CanManageSubscribers;
- yield return CanCreditSubscribers;
- yield return CanCreateNonApprovedPullPayments;
- yield return CanManageUsers;
- yield return CanManagePayouts;
- yield return CanViewPayouts;
- }
- }
- public static bool IsValidPolicy(string policy)
- {
- return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase));
- }
-
- public static bool IsStorePolicy(string policy)
- {
- return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase);
- }
- public static bool IsStoreModifyPolicy(string policy)
- {
- return policy.StartsWith("btcpay.store.canmodify", StringComparison.OrdinalIgnoreCase);
- }
- public static bool IsServerPolicy(string policy)
- {
- return policy.StartsWith("btcpay.server", StringComparison.OrdinalIgnoreCase);
- }
- public static bool IsPluginPolicy(string policy)
- {
- return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase);
- }
- public static bool IsUserPolicy(string policy)
- {
- return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
- }
-
- private static readonly CultureInfo _culture = new (CultureInfo.InvariantCulture.Name);
- public static string DisplayName(string policy)
- {
- var p = policy.Split(".");
- if (p.Length < 3 || p[0] != "btcpay") return policy;
- var constName = typeof(Policies).GetFields().Select(f => f.Name).FirstOrDefault(f => f.Equals(p[^1], StringComparison.OrdinalIgnoreCase));
- var perm = string.IsNullOrEmpty(constName) ? string.Join(' ', p[2..]) : Regex.Replace(constName, "([A-Z])", " $1", RegexOptions.Compiled).Trim();
- return $"{_culture.TextInfo.ToTitleCase(p[1])}: {_culture.TextInfo.ToTitleCase(perm)}";
- }
}
public class PermissionSet
@@ -137,27 +55,19 @@ namespace BTCPayServer.Client
}
public Permission[] Permissions { get; }
+ }
- public bool Contains(Permission requestedPermission)
- {
- return Permissions.Any(p => p.Contains(requestedPermission));
- }
- public bool Contains(string permission, string store)
- {
- if (permission is null)
- throw new ArgumentNullException(nameof(permission));
- if (store is null)
- throw new ArgumentNullException(nameof(store));
- return Contains(Permission.Create(permission, store));
- }
+ public enum PolicyType
+ {
+ Server,
+ User,
+ Store
}
+
public class Permission
{
- static Permission()
- {
- PolicyMap = Init();
- }
-
+ private static readonly Regex _isPolicy = new Regex(@"^(?:btcpay\.([a-z0-9]+\.)*[a-z0-9]+|unrestricted)$", RegexOptions.Compiled);
+ public PolicyType? Type { get; }
public static Permission Create(string policy, string scope = null)
{
if (TryCreatePermission(policy, scope, out var r))
@@ -171,14 +81,27 @@ namespace BTCPayServer.Client
if (policy == null)
throw new ArgumentNullException(nameof(policy));
policy = policy.Trim().ToLowerInvariant();
- if (!Policies.IsValidPolicy(policy))
- return false;
- if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
+ if (!IsValidPolicy(policy))
return false;
permission = new Permission(policy, scope);
return true;
}
+
+ public static Permission Parse(string str)
+ {
+ if (!TryParse(str, out var p))
+ throw new FormatException("Invalid format for permission (Regex is ^(?:btcpay\\.([a-z0-9]+\\.)*[a-z0-9]+|unrestricted)$)");
+ return p;
+ }
+
+ public static PolicyType? TryGetPolicyType(string policy)
+ => Permission.TryParse(policy, out var permission) &&
+ permission is
+ {
+ Scope: null
+ } ? permission.Type : null;
+
public static bool TryParse(string str, out Permission permission)
{
permission = null;
@@ -189,7 +112,7 @@ namespace BTCPayServer.Client
if (separator == -1)
{
str = str.ToLowerInvariant();
- if (!Policies.IsValidPolicy(str))
+ if (!IsValidPolicy(str))
return false;
permission = new Permission(str, null);
return true;
@@ -197,14 +120,12 @@ namespace BTCPayServer.Client
else
{
var policy = str.Substring(0, separator).ToLowerInvariant();
- if (!Policies.IsValidPolicy(policy))
- return false;
- if (!Policies.IsStorePolicy(policy))
+ var scope = str.Substring(separator + 1);
+ if (scope.Length == 0)
return false;
- var storeId = str.Substring(separator + 1);
- if (storeId.Length == 0)
+ if (!IsValidPolicy(policy))
return false;
- permission = new Permission(policy, storeId);
+ permission = new Permission(policy, scope);
return true;
}
}
@@ -213,20 +134,21 @@ namespace BTCPayServer.Client
{
Policy = policy;
Scope = scope;
- }
-
- public bool Contains(Permission subpermission)
- {
- if (subpermission is null)
- throw new ArgumentNullException(nameof(subpermission));
-
- if (!ContainsPolicy(subpermission.Policy))
+ if (policy.StartsWith("btcpay.store.", StringComparison.OrdinalIgnoreCase) ||
+ policy.StartsWith("btcpay.plugin.store.", StringComparison.OrdinalIgnoreCase))
{
- return false;
+ Type = PolicyType.Store;
+ }
+ else if (policy.StartsWith("btcpay.user.", StringComparison.OrdinalIgnoreCase) ||
+ policy.StartsWith("btcpay.plugin.user.", StringComparison.OrdinalIgnoreCase))
+ {
+ Type = PolicyType.User;
+ }
+ else if (policy.StartsWith("btcpay.server.", StringComparison.OrdinalIgnoreCase) ||
+ policy.StartsWith("btcpay.plugin.server.", StringComparison.OrdinalIgnoreCase))
+ {
+ Type = PolicyType.Server;
}
- if (!Policies.IsStorePolicy(subpermission.Policy))
- return true;
- return Scope == null || subpermission.Scope == Scope;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
@@ -240,88 +162,6 @@ namespace BTCPayServer.Client
}
}
- private bool ContainsPolicy(string subpolicy)
- {
- return ContainsPolicy(Policy, subpolicy);
- }
-
- private static bool ContainsPolicy(string policy, string subpolicy)
- {
- if (policy == Policies.Unrestricted)
- return true;
- if (policy == subpolicy)
- return true;
- if (!PolicyMap.TryGetValue(policy, out var subPolicies))
- return false;
- return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
- }
-
- public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
-
-
- private static ReadOnlyDictionary<string, HashSet<string>> Init()
- {
- var policyMap = new Dictionary<string, HashSet<string>>();
- PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
- Policies.CanManagePullPayments,
- Policies.CanModifyInvoices,
- Policies.CanViewStoreSettings,
- Policies.CanModifyWebhooks,
- Policies.CanModifyPaymentRequests,
- Policies.CanManagePayouts,
- Policies.CanModifyOfferings,
- Policies.CanUseLightningNodeInStore);
-
- PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
- PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
- PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
- PolicyHasChild(policyMap, Policies.CanCreateNonApprovedPullPayments, Policies.CanViewPullPayments);
- PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
- PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
- PolicyHasChild(policyMap,Policies.CanModifyOfferings, Policies.CanViewOfferings, Policies.CanManageSubscribers, Policies.CanCreditSubscribers);
- PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
- PolicyHasChild(policyMap,Policies.CanCreateLightningInvoiceInStore, Policies.CanViewLightningInvoiceInStore);
- PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
- PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
- Policies.CanUseInternalLightningNode,
- Policies.CanManageUsers);
- PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
- PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
- PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts);
- PolicyHasChild(policyMap, Policies.CanManagePayouts, Policies.CanViewPayouts);
-
- var missingPolicies = Policies.AllPolicies.ToHashSet();
- //recurse through the tree to see which policies are not included in the tree
- foreach (var policy in policyMap)
- {
- missingPolicies.Remove(policy.Key);
- foreach (var subPolicy in policy.Value)
- {
- missingPolicies.Remove(subPolicy);
- }
- }
-
- foreach (var missingPolicy in missingPolicies)
- {
- policyMap.Add(missingPolicy, new HashSet<string>());
- }
- return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
- }
-
- private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
- {
- if (policyMap.TryGetValue(policy, out var existingSubPolicies))
- {
- foreach (string subPolicy in subPolicies)
- {
- existingSubPolicies.Add(subPolicy);
- }
- }
- else
- {
- policyMap.Add(policy, subPolicies.ToHashSet());
- }
- }
public string Scope { get; }
public string Policy { get; }
@@ -354,5 +194,15 @@ namespace BTCPayServer.Client
{
return ToString().GetHashCode();
}
+
+ public Permission WithScope(string scope)
+ => new Permission(Policy, scope);
+
+ public static bool IsValidPolicy(string policy)
+ {
+ if (policy == null)
+ throw new ArgumentNullException(nameof(policy));
+ return _isPolicy.IsMatch(policy);
+ }
}
}
diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs
index b71e592..57aa74a 100644
--- a/BTCPayServer.Tests/ApiKeysTests.cs
+++ b/BTCPayServer.Tests/ApiKeysTests.cs
@@ -17,16 +17,11 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
- public class ApiKeysTests : UnitTestBase
+ public class ApiKeysTests(ITestOutputHelper helper) : UnitTestBase(helper)
{
- public const int TestTimeout = 120_000;
-
public const string TestApiPath = "api/test/apikey";
- public ApiKeysTests(ITestOutputHelper helper) : base(helper)
- {
- }
- [Fact(Timeout = TestTimeout)]
+ [Fact]
[Trait("Playwright", "Playwright-2")]
public async Task CanCreateApiKeys()
{
diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs
index f6e1791..b7fc40f 100644
--- a/BTCPayServer.Tests/BTCPayServerTester.cs
+++ b/BTCPayServer.Tests/BTCPayServerTester.cs
@@ -9,11 +9,13 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
+using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
+using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@@ -381,6 +383,8 @@ namespace BTCPayServer.Tests
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (isAdmin)
claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin));
+ claims.Add(new Claim(GreenfieldConstants.ClaimTypes.Permission,
+ Permission.Create(Policies.Unrestricted).ToString()));
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie));
}
if (storeId != null)
diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs
index b3dc858..90fc02d 100644
--- a/BTCPayServer.Tests/CrowdfundTests.cs
+++ b/BTCPayServer.Tests/CrowdfundTests.cs
@@ -11,6 +11,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models;
+using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http.HttpResults;
@@ -62,7 +63,8 @@ namespace BTCPayServer.Tests
Assert.Empty(appList2.Apps);
Assert.Equal("test", app.AppName);
Assert.Equal(apps.CreatedAppId, app.Id);
- Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
+ var permissionService = tester.PayTester.GetService<PermissionService>();
+ Assert.True(app.Role.ToPermissionSet(app.StoreId).HasPermission(Permission.Create(Policies.CanModifyStoreSettings, app.StoreId), permissionService));
Assert.Equal(user.StoreId, app.StoreId);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs
index 3d1b184..8afe74c 100644
--- a/BTCPayServer.Tests/FastTests.cs
+++ b/BTCPayServer.Tests/FastTests.cs
@@ -1430,30 +1430,6 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
}
}
- [Fact]
- public void CanUsePermission()
- {
- Assert.True(Permission.Create(Policies.CanModifyServerSettings)
- .Contains(Permission.Create(Policies.CanModifyServerSettings)));
- Assert.True(Permission.Create(Policies.CanModifyProfile)
- .Contains(Permission.Create(Policies.CanViewProfile)));
- Assert.True(Permission.Create(Policies.CanModifyStoreSettings)
- .Contains(Permission.Create(Policies.CanViewStoreSettings)));
- Assert.False(Permission.Create(Policies.CanViewStoreSettings)
- .Contains(Permission.Create(Policies.CanModifyStoreSettings)));
- Assert.False(Permission.Create(Policies.CanModifyServerSettings)
- .Contains(Permission.Create(Policies.CanModifyStoreSettings)));
- Assert.True(Permission.Create(Policies.Unrestricted)
- .Contains(Permission.Create(Policies.CanModifyStoreSettings)));
- Assert.True(Permission.Create(Policies.Unrestricted)
- .Contains(Permission.Create(Policies.CanModifyStoreSettings, "abc")));
-
- Assert.True(Permission.Create(Policies.CanViewStoreSettings)
- .Contains(Permission.Create(Policies.CanViewStoreSettings, "abcd")));
- Assert.False(Permission.Create(Policies.CanModifyStoreSettings, "abcd")
- .Contains(Permission.Create(Policies.CanModifyStoreSettings)));
- }
-
[Fact]
public void CanParseFilter()
{
@@ -2343,20 +2319,6 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Equal(0.0m, accounting.DueUncapped);
}
- [Fact]
- public void AllPoliciesShowInUI()
- {
- new BitpayRateProvider(new System.Net.Http.HttpClient()).GetRatesAsync(default).GetAwaiter().GetResult();
- foreach (var policy in Policies.AllPolicies)
- {
- Assert.True(UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(policy));
- if (Policies.IsStorePolicy(policy))
- {
- Assert.True(UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey($"{policy}:"));
- }
- }
- }
-
[Fact]
public void CanParseMetadata()
{
diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs
index f18f32d..fb0f6ae 100644
--- a/BTCPayServer.Tests/GreenfieldAPITests.cs
+++ b/BTCPayServer.Tests/GreenfieldAPITests.cs
@@ -11,6 +11,7 @@ using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
+using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@@ -987,6 +988,8 @@ namespace BTCPayServer.Tests
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
adminAcc.IsAdmin = true;
+ adminAcc.RegisterDetails = new RegisterViewModel() { Email = "admin@gmail.com",
+ Password = "abceudhqw" };
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
// We should be forbidden to create a new user without proper admin permissions
@@ -1023,6 +1026,7 @@ namespace BTCPayServer.Tests
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
+ user1Acc.RegisterDetails = new RegisterViewModel() { Email = "test@gmail.com", Password = "abceudhqw" };
var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings);
// User1 trying to get server management would still fail to create user
@@ -1080,6 +1084,7 @@ namespace BTCPayServer.Tests
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
adminAcc.IsAdmin = true;
+ adminAcc.RegisterDetails = new RegisterViewModel() { Email = "admin@gmail.com", Password = "abceudhqw"};
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
// Invalid email
@@ -1311,6 +1316,9 @@ namespace BTCPayServer.Tests
//remove store
await client.RemoveStore(newStore.Id);
+ // Can still access, because the user is admin!
+ await client.GetStore(newStore.Id);
+ await user.MakeAdmin(false);
await AssertHttpError(403, async () =>
{
await client.GetStore(newStore.Id);
@@ -3178,6 +3186,7 @@ namespace BTCPayServer.Tests
await employeeClient.RemoveStoreUser(user.StoreId, user.UserId);
//test no access to api when unrelated to store at all
+ await user.MakeAdmin(false);
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStore(user.StoreId));
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.AddStoreUser(user.StoreId, new StoreUserData()));
@@ -3249,11 +3258,14 @@ namespace BTCPayServer.Tests
{
await unapprovedUserBasicAuthClient.GetCurrentUser();
});
+ await adminClient.ApproveUser(unapprovedUser.UserId, true, CancellationToken.None);
var unapprovedUserApiKeyClient = await unapprovedUser.CreateClient(Policies.Unrestricted);
+ await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None);
await AssertAPIError("unauthenticated", async () =>
{
await unapprovedUserApiKeyClient.GetCurrentUser();
});
+
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).RequiresApproval);
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
Assert.Single(await adminClient.GetNotifications(false));
diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs
index 2b7320d..a0cfb54 100644
--- a/BTCPayServer.Tests/POSTests.cs
+++ b/BTCPayServer.Tests/POSTests.cs
@@ -15,6 +15,7 @@ using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
+using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Tests.PMO;
using BTCPayServer.Views.Server;
@@ -361,7 +362,8 @@ goodies:
pos.HttpContext.SetAppData(appData);
Assert.Single(appList.Apps);
Assert.Equal("test", app.AppName);
- Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
+ var permissionService = tester.PayTester.GetService<PermissionService>();
+ Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).HasPermission(Permission.Create(Policies.CanModifyStoreSettings, app.StoreId), permissionService));
Assert.Equal(user.StoreId, app.StoreId);
Assert.False(app.Archived);
// Archive
@@ -386,6 +388,36 @@ goodies:
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
+
+ // Quick tests on permissions
+ Assert.True(permissionService.Contains(Permission.Create(Policies.CanModifyServerSettings),
+ Permission.Create(Policies.CanModifyServerSettings)));
+ Assert.True(permissionService.Contains(Permission.Create(Policies.CanModifyProfile),
+ Permission.Create(Policies.CanViewProfile)));
+ Assert.True(permissionService.Contains(Permission.Create(Policies.CanModifyStoreSettings),
+ Permission.Create(Policies.CanViewStoreSettings)));
+ Assert.False(permissionService.Contains(Permission.Create(Policies.CanViewStoreSettings),
+ Permission.Create(Policies.CanModifyStoreSettings)));
+ Assert.False(permissionService.Contains(Permission.Create(Policies.CanModifyServerSettings),
+ Permission.Create(Policies.CanModifyStoreSettings)));
+ Assert.True(permissionService.Contains(Permission.Create(Policies.Unrestricted),
+ Permission.Create(Policies.CanModifyStoreSettings)));
+ Assert.True(permissionService.Contains(Permission.Create(Policies.Unrestricted),
+ Permission.Create(Policies.CanModifyStoreSettings, "abc")));
+
+ Assert.True(permissionService.Contains(Permission.Create(Policies.CanViewStoreSettings),
+ Permission.Create(Policies.CanViewStoreSettings, "abcd")));
+ Assert.False(permissionService.Contains(Permission.Create(Policies.CanModifyStoreSettings, "abcd"),
+ Permission.Create(Policies.CanModifyStoreSettings)));
+
+ foreach (var def in permissionService.Definitions.Values)
+ {
+ Assert.NotNull(def?.Display);
+ if (def.Type is PolicyType.Store)
+ {
+ Assert.NotNull(def?.ScopeDisplay);
+ }
+ }
}
[Fact]
diff --git a/BTCPayServer.Tests/RolesTests.cs b/BTCPayServer.Tests/RolesTests.cs
index 6b8e80e..0d0cef7 100644
--- a/BTCPayServer.Tests/RolesTests.cs
+++ b/BTCPayServer.Tests/RolesTests.cs
@@ -266,7 +266,7 @@ public class RolesTests(ITestOutputHelper testOutputHelper) : UnitTestBase(testO
await s.Page.Locator("#Role").FillAsync("Malice");
await s.Page.EvaluateAsync(
- $"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
+ $"document.getElementById('Permissions')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
await s.ClickPagePrimary();
await s.FindAlertMessage();
diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs
index b920388..749fdeb 100644
--- a/BTCPayServer.Tests/TestAccount.cs
+++ b/BTCPayServer.Tests/TestAccount.cs
@@ -77,34 +77,13 @@ namespace BTCPayServer.Tests
public async Task<BTCPayServerClient> CreateClient(params string[] permissions)
{
- var manageController = parent.PayTester.GetController<UIManageController>(UserId, StoreId, IsAdmin);
- Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
- new UIManageController.AddApiKeyViewModel()
- {
- PermissionValues = permissions.Select(s =>
- {
- Permission.TryParse(s, out var p);
- return p;
- }).GroupBy(permission => permission.Policy).Select(p =>
- {
- var stores = p.Where(permission => !string.IsNullOrEmpty(permission.Scope))
- .Select(permission => permission.Scope).ToList();
- return new UIManageController.AddApiKeyViewModel.PermissionValueItem()
- {
- Permission = p.Key,
- Forbidden = false,
- StoreMode = stores.Any() ? UIManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific : UIManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores,
- SpecificStores = stores,
- Value = true
- };
- }).ToList()
- }));
- var statusMessage = manageController.TempData.GetStatusMessageModel();
- Assert.NotNull(statusMessage);
- var str = "<code class='alert-link'>";
- var apiKey = statusMessage.Html.Substring(statusMessage.Html.IndexOf(str) + str.Length);
- apiKey = apiKey.Substring(0, apiKey.IndexOf("</code>"));
- return new BTCPayServerClient(parent.PayTester.ServerUri, apiKey);
+ var client = await CreateClient();
+ var k = await client.CreateAPIKey(new()
+ {
+ Label = "API Key Test",
+ Permissions = permissions.Select(Permission.Parse).ToArray()
+ });
+ return new BTCPayServerClient(parent.PayTester.ServerUri, k.ApiKey);
}
public void Register(bool isAdmin = false)
@@ -287,32 +266,44 @@ namespace BTCPayServer.Tests
{
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
- public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null)
+ public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true)
{
- return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId);
+ return RegisterLightningNodeAsync(cryptoCode, null, isMerchant);
}
- public async Task RegisterLightningNodeAsync(string cryptoCode, string connectionType, bool isMerchant = true, string storeId = null)
+ public async Task RegisterLightningNodeAsync(string cryptoCode, string connectionType, bool isMerchant = true)
{
- var storeController = GetController<UIStoresController>();
-
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
- var nodeType = connectionString == LightningPaymentMethodConfig.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
+ var client = await this.CreateClient();
+ await RegisterLightnignNodeCore(client, cryptoCode, connectionString);
+ }
- var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
- await storeController.SetupLightningNode(storeId ?? StoreId,
- vm, "save", cryptoCode);
- if (storeController.ModelState.ErrorCount != 0)
- Assert.Fail(storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
+ private async Task RegisterLightnignNodeCore(BTCPayServerClient client, string cryptoCode, string connectionString)
+ {
+ await client.UpdateStorePaymentMethod(this.StoreId, $"{cryptoCode}-LN", new UpdatePaymentMethodRequest()
+ {
+ Enabled = true,
+ Config = connectionString == LightningPaymentMethodConfig.InternalNode ?
+ JValue.CreateString("Internal Node") :
+ new JObject()
+ {
+ ["connectionString"] = connectionString
+ }
+ });
+ await client.UpdateStorePaymentMethod(this.StoreId, $"{cryptoCode}-LNURL", new UpdatePaymentMethodRequest()
+ {
+ Enabled = true,
+ Config = new JObject()
+ {
+ ["useBech32Scheme"] = true,
+ ["lud12Enabled"] = false
+ }
+ });
}
public async Task RegisterInternalLightningNodeAsync(string cryptoCode, string storeId = null)
{
- var storeController = GetController<UIStoresController>();
- var vm = new LightningNodeViewModel { ConnectionString = "", LightningNodeType = LightningNodeType.Internal, SkipPortTest = true };
- await storeController.SetupLightningNode(storeId ?? StoreId,
- vm, "save", cryptoCode);
- if (storeController.ModelState.ErrorCount != 0)
- Assert.Fail(storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
+ var client = await this.CreateClient();
+ await RegisterLightnignNodeCore(client, cryptoCode, "Internal Node");
}
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network = null)
diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs
index b8c7c2e..6dbf77e 100644
--- a/BTCPayServer.Tests/UnitTest1.cs
+++ b/BTCPayServer.Tests/UnitTest1.cs
@@ -170,7 +170,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
var acc = tester.NewAccount();
- var description = UtilitiesTests.GetSecuritySchemeDescription();
+ var description = UtilitiesTests.GetSecuritySchemeDescription(tester);
TestLogs.LogInformation(description);
var sresp = Assert
@@ -1508,7 +1508,8 @@ namespace BTCPayServer.Tests
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
- Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
+ var permissionService = tester.PayTester.GetService<PermissionService>();
+ Assert.True(app.Role.ToPermissionSet(app.StoreId).HasPermission(Permission.Create(Policies.CanModifyStoreSettings, app.StoreId), permissionService));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs
index 5cb2f30..d7adf63 100644
--- a/BTCPayServer.Tests/UtilitiesTests.cs
+++ b/BTCPayServer.Tests/UtilitiesTests.cs
@@ -39,27 +39,23 @@ namespace BTCPayServer.Tests
{
Logs = logs;
}
- internal static string GetSecuritySchemeDescription()
+ internal static string GetSecuritySchemeDescription(ServerTester tester)
{
var description =
"BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n#OTHERPERMISSIONS#\n\nThe following permissions are available if the user is an administrator:\n\n#SERVERPERMISSIONS#\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n#STOREPERMISSIONS#\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n";
- var storePolicies =
- UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
- Policies.IsStorePolicy(pair.Key) && !pair.Key.EndsWith(":", StringComparison.InvariantCulture));
- var serverPolicies =
- UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
- Policies.IsServerPolicy(pair.Key));
- var otherPolicies =
- UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.Where(pair =>
- !Policies.IsStorePolicy(pair.Key) && !Policies.IsServerPolicy(pair.Key));
+ var permissionService = tester.PayTester.GetService<PermissionService>();
+ var orderedDefinitions = permissionService.Definitions.Values.OrderBy(v => v.Policy).ToArray();
+ var storePolicies = orderedDefinitions.Where(definition => definition.Type is PolicyType.Store);
+ var serverPolicies = orderedDefinitions.Where(definition => definition.Type is PolicyType.Server);
+ var otherPolicies = orderedDefinitions.Where(definition => definition.Type is not (PolicyType.Store or PolicyType.Server));
description = description.Replace("#OTHERPERMISSIONS#",
- string.Join("\n", otherPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")))
+ string.Join("\n", otherPolicies.Select(definition => $"* `{definition.Policy}`: {definition.Display.Title}")))
.Replace("#SERVERPERMISSIONS#",
- string.Join("\n", serverPolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")))
+ string.Join("\n", serverPolicies.Select(definition => $"* `{definition.Policy}`: {definition.Display.Title}")))
.Replace("#STOREPERMISSIONS#",
- string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")));
+ string.Join("\n", storePolicies.Select(definition => $"* `{definition.Policy}`: {definition.Display.Title}")));
return description;
}
@@ -355,6 +351,7 @@ namespace BTCPayServer.Tests
/// We disabled that because doesn't work on CI because of the fact we disable runtime razor
// [Trait("PreReleaseCheck", "PreReleaseCheck")]
// [Fact]
+ // ReSharper disable once UnusedMember.Global
public async Task CheckDefaultTranslationsUpToDate()
{
var soldir = TestUtils.TryGetSolutionDirectoryInfo();
@@ -382,11 +379,13 @@ namespace BTCPayServer.Tests
/// </summary>
[Trait("Utilities", "Utilities")]
[Fact]
- public void UpdateSwagger()
+ public async Task UpdateSwagger()
{
+ using var tester = CreateServerTester(newDb: true);
+ await tester.StartAsync();
var filePath = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "swagger", "v1", "swagger.template.json");
var o = JObject.Parse(File.ReadAllText(filePath));
- o["components"]["securitySchemes"]["API_Key"]["description"] = GetSecuritySchemeDescription();
+ o["components"]["securitySchemes"]["API_Key"]["description"] = GetSecuritySchemeDescription(tester);
File.WriteAllText(filePath, o.ToString(Newtonsoft.Json.Formatting.Indented));
}
diff --git a/BTCPayServer/Components/MainNav/Default.cshtml b/BTCPayServer/Components/MainNav/Default.cshtml
index c2e5356..2516fd5 100644
--- a/BTCPayServer/Components/MainNav/Default.cshtml
+++ b/BTCPayServer/Components/MainNav/Default.cshtml
@@ -20,6 +20,15 @@
@model BTCPayServer.Components.MainNav.MainNavViewModel
+@{
+ // The store's context in navigation might be different
+ // from the rest of the page.
+ // For example, if on the Server settings page, GetStoreData should be
+ // null, but the navigation menu should show the last selected store.
+ var oldStore = Context.GetStoreData();
+ Context.SetStoreData(Context.GetNavStoreData());
+}
+
<nav id="mainNav" class="d-flex flex-column justify-content-between">
<div class="accordion px-3 px-lg-4">
@if (SignInManager.IsSignedIn(User))
@@ -432,3 +441,7 @@
if (activeEl) activeEl.scrollIntoView({ block: 'center', inline: 'center' })
})()
</script>
+
+@{
+ Context.SetStoreData(oldStore);
+}
diff --git a/BTCPayServer/Components/MainNav/MainNav.cs b/BTCPayServer/Components/MainNav/MainNav.cs
index 2c33e18..fc2e1c5 100644
--- a/BTCPayServer/Components/MainNav/MainNav.cs
+++ b/BTCPayServer/Components/MainNav/MainNav.cs
@@ -1,6 +1,5 @@
using System;
using System.Linq;
-using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
@@ -11,7 +10,6 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
-using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
@@ -19,48 +17,23 @@ using NBitcoin;
namespace BTCPayServer.Components.MainNav
{
- public class MainNav : ViewComponent
+ public class MainNav(
+ AppService appService,
+ UIStoresController storesController,
+ UserManager<ApplicationUser> userManager,
+ PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
+ SettingsRepository settingsRepository,
+ IMemoryCache cache,
+ UriResolver uriResolver,
+ PoliciesSettings policiesSettings)
+ : ViewComponent
{
- private readonly AppService _appService;
- private readonly StoreRepository _storeRepo;
- private readonly UIStoresController _storesController;
- private readonly BTCPayNetworkProvider _networkProvider;
- private readonly UserManager<ApplicationUser> _userManager;
- private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
- private readonly SettingsRepository _settingsRepository;
- private readonly UriResolver _uriResolver;
- private readonly IMemoryCache _cache;
-
- public PoliciesSettings PoliciesSettings { get; }
-
- public MainNav(
- AppService appService,
- StoreRepository storeRepo,
- UIStoresController storesController,
- BTCPayNetworkProvider networkProvider,
- UserManager<ApplicationUser> userManager,
- PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
- SettingsRepository settingsRepository,
- IMemoryCache cache,
- UriResolver uriResolver,
- PoliciesSettings policiesSettings)
- {
- _storeRepo = storeRepo;
- _appService = appService;
- _userManager = userManager;
- _networkProvider = networkProvider;
- _storesController = storesController;
- _paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
- _settingsRepository = settingsRepository;
- _uriResolver = uriResolver;
- _cache = cache;
- PoliciesSettings = policiesSettings;
- }
+ public PoliciesSettings PoliciesSettings { get; } = policiesSettings;
public async Task<IViewComponentResult> InvokeAsync()
{
- var store = ViewContext.HttpContext.GetStoreData();
- var serverSettings = await _settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
+ var store = ViewContext.HttpContext.GetNavStoreData();
+ var serverSettings = await settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
var vm = new MainNavViewModel
{
Store = store,
@@ -71,13 +44,13 @@ namespace BTCPayServer.Components.MainNav
var storeBlob = store.GetStoreBlob();
// Wallets
- _storesController.AddPaymentMethods(store, storeBlob,
+ storesController.AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
foreach (var lnNode in lightningNodes)
{
var pmi = PaymentTypes.LN.GetPaymentMethodId(lnNode.CryptoCode);
- if (_paymentMethodHandlerDictionary.TryGet(pmi) is not LightningLikePaymentHandler handler)
+ if (paymentMethodHandlerDictionary.TryGet(pmi) is not LightningLikePaymentHandler handler)
continue;
if (lnNode.CacheKey is not null)
@@ -85,13 +58,13 @@ namespace BTCPayServer.Components.MainNav
using var cts = new CancellationTokenSource(5000);
try
{
- lnNode.Available = await _cache.GetOrCreateAsync(lnNode.CacheKey, async entry =>
+ lnNode.Available = await cache.GetOrCreateAsync(lnNode.CacheKey, async entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
try
{
- var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _paymentMethodHandlerDictionary);
- await handler.GetNodeInfo(paymentMethodDetails, null, throws: true);
+ var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, paymentMethodHandlerDictionary);
+ await handler.GetNodeInfo(paymentMethodDetails!, null, throws: true);
// if we came here without exception, this means the node is available
return true;
}
@@ -109,7 +82,7 @@ namespace BTCPayServer.Components.MainNav
vm.LightningNodes = lightningNodes;
// Apps
- var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
+ var apps = await appService.GetAllApps(UserId, false, store.Id, true);
vm.Apps = apps
.Where(a => !a.Archived)
.Select(a => new StoreApp
@@ -123,19 +96,19 @@ namespace BTCPayServer.Components.MainNav
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
}
- var user = await _userManager.GetUserAsync(HttpContext.User);
+ var user = await userManager.GetUserAsync(HttpContext.User);
if (user != null)
{
var blob = user.GetBlob();
vm.UserName = blob?.Name;
vm.UserImageUrl = string.IsNullOrEmpty(blob?.ImageUrl)
? null
- : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob?.ImageUrl));
+ : await uriResolver.Resolve(Request.GetAbsoluteRootUri(), UnresolvedUri.Create(blob.ImageUrl));
}
return View(vm);
}
- private string UserId => _userManager.GetUserId(HttpContext.User);
+ private string UserId => userManager.GetUserId(HttpContext.User);
}
}
diff --git a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
index bf2b520..3ddf99f 100644
--- a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
+++ b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
@@ -12,24 +12,15 @@ namespace BTCPayServer.Components.StoreRecentInvoices;
public class StoreRecentInvoices : ViewComponent
{
- private readonly StoreRepository _storeRepo;
private readonly InvoiceRepository _invoiceRepo;
- private readonly CurrencyNameTable _currencyNameTable;
private readonly UserManager<ApplicationUser> _userManager;
- private readonly ApplicationDbContextFactory _dbContextFactory;
public StoreRecentInvoices(
- StoreRepository storeRepo,
InvoiceRepository invoiceRepo,
- CurrencyNameTable currencyNameTable,
- UserManager<ApplicationUser> userManager,
- ApplicationDbContextFactory dbContextFactory)
+ UserManager<ApplicationUser> userManager)
{
- _storeRepo = storeRepo;
_invoiceRepo = invoiceRepo;
_userManager = userManager;
- _currencyNameTable = currencyNameTable;
- _dbContextFactory = dbContextFactory;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store, bool initialRendering)
diff --git a/BTCPayServer/Components/StoreSelector/StoreSelector.cs b/BTCPayServer/Components/StoreSelector/StoreSelector.cs
index 9827ce7..20338ff 100644
--- a/BTCPayServer/Components/StoreSelector/StoreSelector.cs
+++ b/BTCPayServer/Components/StoreSelector/StoreSelector.cs
@@ -9,27 +9,17 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.StoreSelector
{
- public class StoreSelector : ViewComponent
+ public class StoreSelector(
+ StoreRepository storeRepo,
+ UriResolver uriResolver,
+ UserManager<ApplicationUser> userManager)
+ : ViewComponent
{
- private readonly StoreRepository _storeRepo;
- private readonly UriResolver _uriResolver;
- private readonly UserManager<ApplicationUser> _userManager;
-
- public StoreSelector(
- StoreRepository storeRepo,
- UriResolver uriResolver,
- UserManager<ApplicationUser> userManager)
- {
- _storeRepo = storeRepo;
- _uriResolver = uriResolver;
- _userManager = userManager;
- }
-
public async Task<IViewComponentResult> InvokeAsync()
{
- var userId = _userManager.GetUserId(UserClaimsPrincipal);
- var stores = await _storeRepo.GetStoresByUserId(userId);
- var currentStore = ViewContext.HttpContext.GetStoreData();
+ var userId = userManager.GetUserId(UserClaimsPrincipal);
+ var stores = await storeRepo.GetStoresByUserId(userId ?? "");
+ var currentStore = ViewContext.HttpContext.GetNavStoreData();
var archivedCount = stores.Count(s => s.Archived);
var options = stores
.Where(store => !store.Archived)
@@ -49,7 +39,7 @@ namespace BTCPayServer.Components.StoreSelector
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
- CurrentStoreLogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), blob?.LogoUrl),
+ CurrentStoreLogoUrl = await uriResolver.Resolve(Request.GetAbsoluteRootUri(), blob?.LogoUrl),
ArchivedCount = archivedCount
};
diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs
index 4ddeb17..42b3631 100644
--- a/BTCPayServer/Controllers/UIAccountController.cs
+++ b/BTCPayServer/Controllers/UIAccountController.cs
@@ -18,6 +18,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Plugins.Emails.Services;
+using BTCPayServer.Security;
using Fido2NetLib;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -68,14 +69,15 @@ namespace BTCPayServer.Controllers
[HttpGet("/cheat/permissions")]
[HttpGet("/cheat/permissions/stores/{storeId}")]
[CheatModeRoute]
- public async Task<IActionResult> CheatPermissions([FromServices] IAuthorizationService authorizationService, string storeId = null)
+ public async Task<IActionResult> CheatPermissions([FromServices] IAuthorizationService authorizationService, [FromServices] PermissionService permissionService, string storeId = null)
{
var vm = new CheatPermissionsViewModel();
vm.StoreId = storeId;
var results = new System.Collections.Generic.List<(string, Task<AuthorizationResult>)>();
- foreach (var p in Policies.AllPolicies.Concat(new[] { Policies.CanModifyStoreSettingsUnscoped }))
+ foreach (var p in permissionService.Definitions.Values)
{
- results.Add((p, authorizationService.AuthorizeAsync(User, storeId, p)));
+ results.Add((p.Policy, authorizationService.AuthorizeAsync(User, storeId, new PolicyRequirement(p.Policy))));
+ results.Add((p.Policy + ":", authorizationService.AuthorizeAsync(User, storeId, new PolicyRequirement(p.Policy, requireUnscoped: true))));
}
await Task.WhenAll(results.Select(r => r.Item2));
results = results.OrderBy(r => r.Item1).ToList();
diff --git a/BTCPayServer/Controllers/UIHomeController.cs b/BTCPayServer/Controllers/UIHomeController.cs
index 766d928..6d1b20b 100644
--- a/BTCPayServer/Controllers/UIHomeController.cs
+++ b/BTCPayServer/Controllers/UIHomeController.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -7,6 +8,7 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
+using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
@@ -29,7 +31,8 @@ namespace BTCPayServer.Controllers
LanguageService languageService,
StoreRepository storeRepository,
IWebHostEnvironment environment,
- SignInManager<ApplicationUser> signInManager)
+ SignInManager<ApplicationUser> signInManager,
+ PermissionService permissionService)
: Controller
{
private SignInManager<ApplicationUser> SignInManager { get; } = signInManager;
@@ -89,7 +92,18 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
public IActionResult Permissions()
{
- return Json(Client.Models.PermissionMetadata.PermissionNodes, new JsonSerializerSettings { Formatting = Formatting.Indented });
+ var nodes = new Dictionary<string, PermissionMetadata>();
+ foreach (var def in permissionService.Definitions.Values)
+ {
+ var m = new PermissionMetadata { PermissionName = def.Policy };
+ m.SubPermissions = permissionService.PermissionNodesByPolicy[m.PermissionName]
+ .EnumerateDescendants(false).Select(e => e.Definition.Policy)
+ .OrderBy(e => e, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ nodes.Add(def.Policy, m);
+ }
+ var metadata = nodes.Values.OrderBy(v => v.PermissionName, StringComparer.OrdinalIgnoreCase).ToArray();
+ return Json(metadata, new JsonSerializerSettings { Formatting = Formatting.Indented });
}
[Route("misc/translations/{resource}/{lang}")]
[AllowAnonymous]
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index 61b625d..962895f 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -654,7 +654,7 @@ namespace BTCPayServer.Controllers
var explorer = network is null ? null : _ExplorerClients.GetExplorerClient(network);
if (explorer is null || network is null)
return NotSupported(StringLocalizer["This feature is only available to BTC wallets"]);
- if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
+ if (!GetCurrentStore().HasPolicy(GetUserId(), Policies.CanModifyStoreSettings, _permissionService))
return Forbid();
var derivationScheme = GetCurrentStore().GetDerivationSchemeSettings(_handlers, network.CryptoCode)?.AccountDerivation;
diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs
index e2c6b01..4047dfa 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.cs
@@ -62,6 +62,7 @@ namespace BTCPayServer.Controllers
private readonly AppService _appService;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;
+ private readonly PermissionService _permissionService;
public WebhookSender WebhookNotificationManager { get; }
public IEnumerable<IGlobalCheckoutModelExtension> GlobalCheckoutModelExtensions { get; }
@@ -97,7 +98,8 @@ namespace BTCPayServer.Controllers
IEnumerable<IGlobalCheckoutModelExtension> globalCheckoutModelExtensions,
IStringLocalizer stringLocalizer,
ViewLocalizer viewLocalizer,
- PrettyNameProvider prettyName)
+ PrettyNameProvider prettyName,
+ PermissionService permissionService)
{
_displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
@@ -128,6 +130,7 @@ namespace BTCPayServer.Controllers
_appService = appService;
StringLocalizer = stringLocalizer;
ViewLocalizer = viewLocalizer;
+ _permissionService = permissionService;
}
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(Data.PaymentRequestData prData, decimal? amount, decimal amountDue, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
diff --git a/BTCPayServer/Controllers/UIManageController.APIKeys.cs b/BTCPayServer/Controllers/UIManageController.APIKeys.cs
index 42d4908..e590c88 100644
--- a/BTCPayServer/Controllers/UIManageController.APIKeys.cs
+++ b/BTCPayServer/Controllers/UIManageController.APIKeys.cs
@@ -2,17 +2,16 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
-using BTCPayServer.Abstractions.Services;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Security.Greenfield;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using NBitcoin.DataEncoders;
@@ -98,7 +97,9 @@ namespace BTCPayServer.Controllers
permissions ??= Array.Empty<string>();
- var requestPermissions = Permission.ToPermissions(permissions).ToList();
+ var requestPermissions = Permission.ToPermissions(permissions)
+ .Where(permission => _permissionService.IsValidPolicy(permission.Policy))
+ .ToList();
if (redirect?.IsAbsoluteUri is false)
{
@@ -208,7 +209,9 @@ namespace BTCPayServer.Controllers
var perms = viewModel.Permissions?.Split(';').ToArray() ?? Array.Empty<string>();
if (perms.Any())
{
- var requestPermissions = Permission.ToPermissions(perms).ToList();
+ var requestPermissions = Permission.ToPermissions(perms)
+ .Where(permission => _permissionService.IsValidPolicy(permission.Policy))
+ .ToList();
var existingApiKey = await CheckForMatchingApiKey(requestPermissions, viewModel);
if (existingApiKey != null)
{
@@ -281,7 +284,7 @@ namespace BTCPayServer.Controllers
break;
}
- if (Policies.IsStorePolicy(requested.Key))
+ if (IsStorePolicy(requested.Key))
{
if ((vm.SelectiveStores && !existing.Any(p => p.Scope == vm.StoreId)) ||
(!vm.SelectiveStores && existing.Any(p => !string.IsNullOrEmpty(p.Scope))))
@@ -309,13 +312,13 @@ namespace BTCPayServer.Controllers
var permissions = vm.Permissions?.Split(';') ?? Array.Empty<string>();
var permissionsWithStoreIDs = new List<string>();
- vm.NeedsStorePermission = vm.SelectiveStores && (permissions.Any(Policies.IsStorePolicy) || !vm.Strict);
+ vm.NeedsStorePermission = vm.SelectiveStores && (permissions.Any(IsStorePolicy) || !vm.Strict);
// Go over each permission and associated store IDs and join them
// so that permission for a specific store is parsed correctly
foreach (var permission in permissions)
{
- if (!Policies.IsStorePolicy(permission) || string.IsNullOrEmpty(vm.StoreId))
+ if (!IsStorePolicy(permission) || string.IsNullOrEmpty(vm.StoreId))
{
permissionsWithStoreIDs.Add(permission);
}
@@ -325,7 +328,9 @@ namespace BTCPayServer.Controllers
}
}
- var parsedPermissions = Permission.ToPermissions(permissionsWithStoreIDs.ToArray()).GroupBy(permission => permission.Policy);
+ var parsedPermissions = Permission.ToPermissions(permissionsWithStoreIDs.ToArray())
+ .Where(permission => _permissionService.IsValidPolicy(permission.Policy))
+ .GroupBy(permission => permission.Policy);
for (var index = vm.PermissionValues.Count - 1; index >= 0; index--)
{
@@ -353,7 +358,7 @@ namespace BTCPayServer.Controllers
// Set the value to true and adjust the other fields based on the policy type
permissionValue.Value = true;
- if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) &&
+ if (vm.SelectiveStores && permissionValue.IsStorePolicy &&
wanted.Any(permission => !string.IsNullOrEmpty(permission.Scope)))
{
permissionValue.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.Specific;
@@ -368,6 +373,9 @@ namespace BTCPayServer.Controllers
}
}
+ private bool IsStorePolicy(string policy)
+ => Permission.TryGetPolicyType(policy) is PolicyType.Store;
+
private IActionResult HandleCommands(AddApiKeyViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Command))
@@ -376,7 +384,7 @@ namespace BTCPayServer.Controllers
}
var parts = viewModel.Command.Split(':', StringSplitOptions.RemoveEmptyEntries);
var permission = parts[0];
- if (!Policies.IsStorePolicy(permission))
+ if (!IsStorePolicy(permission))
{
return null;
}
@@ -445,7 +453,7 @@ namespace BTCPayServer.Controllers
var permissions = new List<Permission>();
foreach (var p in viewModel.PermissionValues.Where(tuple => !tuple.Forbidden))
{
- if (Policies.IsStorePolicy(p.Permission))
+ if (p.IsStorePolicy)
{
if (p.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && p.Value)
{
@@ -466,23 +474,34 @@ namespace BTCPayServer.Controllers
private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel
{
- var stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User));
+ var stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User) ?? "");
viewModel.Stores = stores.OrderBy(store => store.StoreName, StringComparer.InvariantCultureIgnoreCase).ToArray();
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
- viewModel.PermissionValues ??= Policies.AllPolicies
- .Where(p => AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(p))
- .Select(s => new AddApiKeyViewModel.PermissionValueItem()
+ viewModel.PermissionValues ??= _permissionService.Definitions.Values.OrderBy(d => d.Policy)
+ .Select(definition => new AddApiKeyViewModel.PermissionValueItem()
{
- Permission = s,
+ Permission = definition.Policy,
Value = false,
- Forbidden = Policies.IsServerPolicy(s) && !isAdmin
+ Forbidden = definition.Type is PolicyType.Server && !isAdmin
}).ToList();
+ foreach (var permissionValue in viewModel.PermissionValues)
+ {
+ if (permissionValue.Permission == null)
+ continue;
+ if (!_permissionService.TryGetDefinition(permissionValue.Permission, out var definition))
+ continue;
+ permissionValue.AllStoresTitle = definition.Display?.Title;
+ permissionValue.AllStoresDescription = definition.Display?.Description;
+ permissionValue.StoreSpecificTitle = definition.ScopeDisplay?.Title;
+ permissionValue.StoreSpecificDescription = definition.ScopeDisplay?.Description;
+ }
+
if (!isAdmin)
{
- foreach (var p in viewModel.PermissionValues.Where(item => item.Permission is null || Policies.IsServerPolicy(item.Permission)))
+ foreach (var p in viewModel.PermissionValues.Where(item => item.Permission is null || Permission.TryGetPolicyType(item.Permission) is PolicyType.Server))
{
p.Forbidden = true;
}
@@ -506,83 +525,42 @@ namespace BTCPayServer.Controllers
public class PermissionValueItem
{
- public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
- {
- {Policies.Unrestricted, ("Unrestricted access", "Grants unrestricted access to your account.")},
- {Policies.CanViewUsers, ("View users", "Allows seeing all users on this server.")},
- {Policies.CanCreateUser, ("Create new users", "Allows creating new users on this server.")},
- {Policies.CanManageUsers, ("Manage users", "Allows creating/deleting API keys for users.")},
- {Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")},
- {Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")},
- {$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")},
- {Policies.CanModifyWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
- {$"{Policies.CanModifyWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
- {Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")},
- {$"{Policies.CanViewStoreSettings}:", ("View your stores", "Allows viewing the selected stores' settings.")},
- {Policies.CanViewReports, ("View your reports", "Allows viewing reports.")},
- {$"{Policies.CanViewReports}:", ("View your selected stores' reports", "Allows viewing the selected stores' reports.")},
- {Policies.CanModifyServerSettings, ("Manage your server", "Grants total control on the server settings of your server.")},
- {Policies.CanViewProfile, ("View your profile", "Allows viewing your user profile.")},
- {Policies.CanModifyProfile, ("Manage your profile", "Allows viewing and modifying your user profile.")},
- {Policies.CanManageNotificationsForUser, ("Manage your notifications", "Allows viewing and modifying your user notifications.")},
- {Policies.CanViewNotificationsForUser, ("View your notifications", "Allows viewing your user notifications.")},
- {Policies.CanCreateInvoice, ("Create an invoice", "Allows creating new invoices.")},
- {$"{Policies.CanCreateInvoice}:", ("Create an invoice", "Allows creating new invoices on the selected stores.")},
- {Policies.CanViewInvoices, ("View invoices", "Allows viewing invoices.")},
- {Policies.CanModifyInvoices, ("Modify invoices", "Allows viewing and modifying invoices.")},
- {$"{Policies.CanViewInvoices}:", ("View invoices", "Allows viewing invoices on the selected stores.")},
- {$"{Policies.CanModifyInvoices}:", ("Modify invoices", "Allows viewing and modifying invoices on the selected stores.")},
- {Policies.CanModifyPaymentRequests, ("Modify your payment requests", "Allows viewing, modifying, deleting and creating new payment requests on all your stores.")},
- {$"{Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "Allows viewing, modifying, deleting and creating new payment requests on the selected stores.")},
- {Policies.CanViewPaymentRequests, ("View your payment requests", "Allows viewing payment requests.")},
- {$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "Allows viewing the selected stores' payment requests.")},
- {Policies.CanViewPullPayments, ("View your pull payments", "Allows viewing pull payments on all your stores.")},
- {$"{Policies.CanViewPullPayments}:", ("View selected stores' pull payments", "Allows viewing pull payments on the selected stores.")},
- {Policies.CanViewOfferings, ("View your offerings", "Allows viewing offerings on all your stores.")},
- {$"{Policies.CanViewOfferings}:", ("View your offerings", "Allows viewing offerings on the selected stores.")},
- {Policies.CanModifyOfferings, ("Modify your offerings", "Allows modifying offerings on all your stores.")},
- {$"{Policies.CanModifyOfferings}:", ("Modify your offerings", "Allows modifying offerings on the selected stores.")},
- {Policies.CanManageSubscribers, ("Manage your subscribers", "Allows managing subscribers on all your stores.")},
- {$"{Policies.CanManageSubscribers}:", ("Manage your subscribers", "Allows managing subscribers on the selected stores.")},
- {Policies.CanCreditSubscribers, ("Credit your subscribers", "Allows crediting subscribers on all your stores.")},
- {$"{Policies.CanCreditSubscribers}:", ("Credit your subscribers", "Allows crediting subscribers on the selected stores.")},
- {Policies.CanManagePullPayments, ("Manage your pull payments", "Allows viewing, modifying, deleting and creating pull payments on all your stores.")},
- {$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "Allows viewing, modifying, deleting and creating pull payments on the selected stores.")},
- {Policies.CanArchivePullPayments, ("Archive your pull payments", "Allows deleting pull payments on all your stores.")},
- {$"{Policies.CanArchivePullPayments}:", ("Archive selected stores' pull payments", "Allows deleting pull payments on the selected stores.")},
- {Policies.CanCreatePullPayments, ("Create pull payments", "Allows creating pull payments on all your stores.")},
- {$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "Allows creating pull payments on the selected stores.")},
- {Policies.CanManagePayouts, ("Manage payouts", "Allows managing payouts on all your stores.")},
- {$"{Policies.CanManagePayouts}:", ("Manage payouts in selected stores", "Allows managing payouts on the selected stores.")},
- {Policies.CanViewPayouts, ("View payouts", "Allows viewing payouts on all your stores.")},
- {$"{Policies.CanViewPayouts}:", ("View payouts in selected stores", "Allows viewing payouts on the selected stores.")},
- {Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "Allows creating pull payments without automatic approval on all your stores.")},
- {$"{Policies.CanCreateNonApprovedPullPayments}:", ("Create non-approved pull payments in selected stores", "Allows viewing, modifying, deleting and creating pull payments without automatic approval on the selected stores.")},
- {Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
- {Policies.CanViewLightningInvoiceInternalNode, ("View invoices from internal lightning node", "Allows using the internal BTCPay Server lightning node to view BOLT11 invoices.")},
- {Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices.")},
- {Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
- {Policies.CanViewLightningInvoiceInStore, ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to all your stores.")},
- {Policies.CanCreateLightningInvoiceInStore, ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices.")},
- {$"{Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
- {$"{Policies.CanViewLightningInvoiceInStore}:", ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to the selected stores.")},
- {$"{Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
- };
public string Title
{
get
{
- return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific ? ":" : "")}"].Title;
+ if (StoreMode == ApiKeyStoreMode.Specific && !string.IsNullOrEmpty(StoreSpecificTitle))
+ return StoreSpecificTitle;
+ return AllStoresTitle;
}
}
public string Description
{
get
{
- return PermissionDescriptions[$"{Permission}{(StoreMode == ApiKeyStoreMode.Specific ? ":" : "")}"].Description;
+ if (StoreMode == ApiKeyStoreMode.Specific && !string.IsNullOrEmpty(StoreSpecificDescription))
+ return StoreSpecificDescription;
+ return AllStoresDescription;
+ }
+ }
+ public string AllStoresTitle { get; set; }
+ public string AllStoresDescription { get; set; }
+ public string StoreSpecificTitle { get; set; }
+ public string StoreSpecificDescription { get; set; }
+
+ private string _permission;
+ public string Permission
+ {
+ get => _permission;
+ set
+ {
+ BTCPayServer.Client.Permission.TryParse(value, out var permission);
+ _permission = permission?.ToString();
+ IsStorePolicy = permission?.Type == PolicyType.Store;
}
}
- public string Permission { get; set; }
+ [BindNever]
+ public bool IsStorePolicy { get; private set; }
public bool Value { get; set; }
public bool Forbidden { get; set; }
diff --git a/BTCPayServer/Controllers/UIManageController.cs b/BTCPayServer/Controllers/UIManageController.cs
index b18d6c0..e10c754 100644
--- a/BTCPayServer/Controllers/UIManageController.cs
+++ b/BTCPayServer/Controllers/UIManageController.cs
@@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
-using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
@@ -29,7 +28,6 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
- private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
@@ -42,13 +40,13 @@ namespace BTCPayServer.Controllers
private readonly UriResolver _uriResolver;
private readonly IFileService _fileService;
private readonly EventAggregator _eventAggregator;
+ private readonly PermissionService _permissionService;
readonly StoreRepository _StoreRepository;
public IStringLocalizer StringLocalizer { get; }
public UIManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
- EmailSenderFactory emailSenderFactory,
ILogger<UIManageController> logger,
UrlEncoder urlEncoder,
StoreRepository storeRepository,
@@ -62,11 +60,11 @@ namespace BTCPayServer.Controllers
IFileService fileService,
IStringLocalizer stringLocalizer,
IHtmlHelper htmlHelper,
- EventAggregator eventAggregator)
+ EventAggregator eventAggregator,
+ PermissionService permissionService)
{
_userManager = userManager;
_signInManager = signInManager;
- _EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_btcPayServerEnvironment = btcPayServerEnvironment;
@@ -81,6 +79,7 @@ namespace BTCPayServer.Controllers
_fileService = fileService;
_StoreRepository = storeRepository;
StringLocalizer = stringLocalizer;
+ _permissionService = permissionService;
}
[HttpGet]
diff --git a/BTCPayServer/Controllers/UIServerController.Roles.cs b/BTCPayServer/Controllers/UIServerController.Roles.cs
index 42d42c6..f108c20 100644
--- a/BTCPayServer/Controllers/UIServerController.Roles.cs
+++ b/BTCPayServer/Controllers/UIServerController.Roles.cs
@@ -56,10 +56,10 @@ namespace BTCPayServer.Controllers
return View(new UpdateRoleViewModel
{
- Policies = roleData.Permissions,
+ Permissions = roleData.Permissions.ToHashSet(),
Role = roleData.Role
});
- }
+ }
[HttpPost("server/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole([FromRoute] string role, UpdateRoleViewModel viewModel)
@@ -83,7 +83,7 @@ namespace BTCPayServer.Controllers
return View(viewModel);
}
- var r = await _StoreRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
+ var r = await _StoreRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Permissions);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -99,10 +99,10 @@ namespace BTCPayServer.Controllers
Severity = StatusMessageModel.StatusSeverity.Success,
Message = successMessage
});
-
+
return RedirectToAction(nameof(ListRoles));
}
-
+
[HttpGet("server/roles/{role}/delete")]
@@ -136,7 +136,7 @@ namespace BTCPayServer.Controllers
var errorMessage = await _StoreRepository.RemoveStoreRole(roleId);
if (errorMessage is null)
{
-
+
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Role deleted"].Value;
}
else
@@ -160,7 +160,7 @@ namespace BTCPayServer.Controllers
await _StoreRepository.SetDefaultRole(role);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Role set default"].Value;
}
-
+
return RedirectToAction(nameof(ListRoles));
}
}
@@ -171,5 +171,5 @@ public class UpdateRoleViewModel
[Display(Name = "Role")]
public string Role { get; set; }
- [Display(Name = "Policies")] public List<string> Policies { get; set; } = new();
+ [Display(Name = "Permissions")] public HashSet<string> Permissions { get; set; } = new();
}
diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs
index e090fbd..e0a28d4 100644
--- a/BTCPayServer/Controllers/UIServerController.cs
+++ b/BTCPayServer/Controllers/UIServerController.cs
@@ -368,7 +368,7 @@ namespace BTCPayServer.Controllers
if (command == "SetTemplate")
{
ModelState.Clear();
- var storeId = this.HttpContext.GetStoreData()?.Id;
+ var storeId = this.HttpContext.GetNavStoreData()?.Id;
if (storeId is null)
{
this.TempData.SetStatusMessageModel(new()
@@ -380,7 +380,7 @@ namespace BTCPayServer.Controllers
else
{
await _StoreRepository.SetDefaultStoreTemplate(storeId, GetUserId());
- this.TempData.SetStatusSuccess(StringLocalizer["Store template created from store '{0}'. New stores will inherit these settings.", HttpContext.GetStoreData().StoreName]);
+ this.TempData.SetStatusSuccess(StringLocalizer["Store template created from store '{0}'. New stores will inherit these settings.", HttpContext.GetNavStoreData().StoreName]);
}
return RedirectToAction(nameof(Policies));
}
diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
index 064d910..c8e7e0f 100644
--- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
+++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
@@ -26,7 +26,7 @@ public partial class UIStoresController
var store = CurrentStore;
if (store is null)
return NotFound();
-
+ HttpContext.SetPreferredStoreId(store.Id);
var storeBlob = store.GetStoreBlob();
AddPaymentMethods(store, storeBlob,
diff --git a/BTCPayServer/Controllers/UIStoresController.Roles.cs b/BTCPayServer/Controllers/UIStoresController.Roles.cs
index 01f73d7..7415e03 100644
--- a/BTCPayServer/Controllers/UIStoresController.Roles.cs
+++ b/BTCPayServer/Controllers/UIStoresController.Roles.cs
@@ -61,7 +61,7 @@ public partial class UIStoresController
return NotFound();
return View(new UpdateRoleViewModel
{
- Policies = roleData.Permissions,
+ Permissions = roleData.Permissions.ToHashSet(),
Role = roleData.Role
});
}
@@ -95,7 +95,7 @@ public partial class UIStoresController
return View(viewModel);
}
- var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
+ var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Permissions);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -125,7 +125,7 @@ public partial class UIStoresController
var roleId = await storeRepository.ResolveStoreRoleId(storeId, role);
if (roleId == null)
return NotFound();
-
+
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();
@@ -149,7 +149,7 @@ public partial class UIStoresController
var roleId = await storeRepository.ResolveStoreRoleId(storeId, role);
if (roleId == null)
return NotFound();
-
+
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();
diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs
index a4e7bf3..af2ffd2 100644
--- a/BTCPayServer/Controllers/UIStoresController.cs
+++ b/BTCPayServer/Controllers/UIStoresController.cs
@@ -135,16 +135,18 @@ public partial class UIStoresController : Controller
if (string.IsNullOrEmpty(userId))
return Forbid();
- var store = await _storeRepo.FindStore(storeId);
+ var store = await _storeRepo.FindStore(storeId, userId);
if (store is null)
return NotFound();
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings)).Succeeded)
{
+ HttpContext.SetPreferredStoreId(storeId);
return RedirectToAction("Dashboard", new { storeId });
}
if ((await _authorizationService.AuthorizeAsync(User, Policies.CanViewInvoices)).Succeeded)
{
+ HttpContext.SetPreferredStoreId(storeId);
return RedirectToAction("ListInvoices", "UIInvoice", new { storeId });
}
return Forbid();
diff --git a/BTCPayServer/Controllers/UIUserStoresController.cs b/BTCPayServer/Controllers/UIUserStoresController.cs
index 9b90225..d9675e3 100644
--- a/BTCPayServer/Controllers/UIUserStoresController.cs
+++ b/BTCPayServer/Controllers/UIUserStoresController.cs
@@ -43,10 +43,10 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
- public async Task<IActionResult> ListStores(bool archived = false)
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewStoreSettings)]
+ public IActionResult ListStores(bool archived = false)
{
- var stores = await _repo.GetStoresByUserId(GetUserId());
+ var stores = HttpContext.GetStoresData();
var vm = new ListStoresViewModel
{
Stores = stores
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index c3c1a2e..ce044bb 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -466,6 +466,23 @@ namespace BTCPayServer
return services;
}
+ public static IServiceCollection AddPolicyDefinitions(this IServiceCollection services, params PolicyDefinition[] definitions)
+ {
+ if (definitions == null)
+ return services;
+ foreach (var definition in definitions)
+ {
+ if (definition != null)
+ services.AddSingleton(definition);
+ }
+ var strings = definitions
+ .SelectMany(d => new[] {d.Display?.Title, d.Display?.Description, d.ScopeDisplay?.Title, d.ScopeDisplay?.Description})
+ .Where(d => d is not null)
+ .ToArray();
+ services.AddDefaultTranslations(strings);
+ return services;
+ }
+
public static async Task CloseSocket(this WebSocket webSocket)
{
try
@@ -714,7 +731,7 @@ namespace BTCPayServer
ctx.Response.Cookies.Delete(nameof(UserPrefsCookie));
}
- private static void SetCurrentStoreId(this HttpContext ctx, string storeId)
+ public static void SetPreferredStoreId(this HttpContext ctx, string storeId)
{
var prefCookie = ctx.GetUserPrefsCookie();
if (prefCookie.CurrentStoreId != storeId)
@@ -724,32 +741,23 @@ namespace BTCPayServer
}
}
- public static string GetCurrentStoreId(this HttpContext ctx)
- {
- return ctx.GetImplicitStoreId() ?? ctx.GetUserPrefsCookie()?.CurrentStoreId;
- }
+ public static StoreData GetNavStoreData(this HttpContext ctx)
+ => ctx.Items.TryGet("BTCPAY.NAVSTOREDATA") as StoreData;
+ public static void SetNavStoreData(this HttpContext ctx, StoreData storeData)
+ => ctx.Items["BTCPAY.NAVSTOREDATA"] = storeData;
- public static StoreData GetStoreData(this HttpContext ctx)
- {
- return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
- }
+ public static StoreData GetStoreData(this HttpContext ctx)
+ => ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
- {
- ctx.Items["BTCPAY.STOREDATA"] = storeData;
-
- SetCurrentStoreId(ctx, storeData.Id);
- }
+ => ctx.Items["BTCPAY.STOREDATA"] = storeData;
+ public static string GetCurrentStoreId(this HttpContext ctx)
+ => GetStoreData(ctx)?.Id;
public static StoreData[] GetStoresData(this HttpContext ctx)
- {
- return ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[];
- }
-
+ => ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[];
public static void SetStoresData(this HttpContext ctx, StoreData[] storeData)
- {
- ctx.Items["BTCPAY.STORESDATA"] = storeData;
- }
+ => ctx.Items["BTCPAY.STORESDATA"] = storeData;
public static InvoiceEntity GetInvoiceData(this HttpContext ctx)
{
diff --git a/BTCPayServer/Extensions/StoreExtensions.cs b/BTCPayServer/Extensions/StoreExtensions.cs
index 4ece388..56a5942 100644
--- a/BTCPayServer/Extensions/StoreExtensions.cs
+++ b/BTCPayServer/Extensions/StoreExtensions.cs
@@ -2,6 +2,7 @@
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Data;
+using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer
@@ -12,7 +13,7 @@ namespace BTCPayServer
{
return store.UserStores?.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
}
-
+
public static PermissionSet GetPermissionSet(this StoreRole storeRole, string storeId)
{
return new PermissionSet(storeRole.Permissions
@@ -26,16 +27,23 @@ namespace BTCPayServer
return store.GetStoreRoleOfUser(userId)?.GetPermissionSet(store.Id)?? new PermissionSet();
}
- public static bool HasPermission(this StoreData store, string userId, string permission)
+ public static bool HasPolicy(this StoreData store, string userId, string policy, PermissionService permissionService)
{
- return GetPermissionSet(store, userId).HasPermission(permission, store.Id);
+ if (!Permission.TryCreatePermission(policy, store.Id, out var requiredPermission))
+ return false;
+ return GetPermissionSet(store, userId).HasPermission(requiredPermission, permissionService);
}
- public static bool HasPermission(this PermissionSet permissionSet, string permission, string storeId)
+ public static bool HasPermission(this PermissionSet permissionSet, Permission permission, PermissionService permissionService)
{
- return permissionSet.Contains(permission, storeId);
+ foreach (var existing in permissionSet.Permissions)
+ {
+ if (permissionService.Contains(existing, permission))
+ return true;
+ }
+ return false;
}
-
+
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, PaymentMethodHandlerDictionary handlers, string cryptoCode, bool onlyEnabled = false)
{
var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs
index 9c54d01..7e5a072 100644
--- a/BTCPayServer/Hosting/BTCPayServerServices.cs
+++ b/BTCPayServer/Hosting/BTCPayServerServices.cs
@@ -61,15 +61,16 @@ using NBitcoin;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
-using Serilog;
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;
using BTCPayServer.Payments.LNURLPay;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Client;
using BTCPayServer.Payouts;
using ExchangeSharp;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
@@ -163,6 +164,7 @@ namespace BTCPayServer.Hosting
services.AddSettingsAccessor<ThemeSettings>();
services.AddSettingsAccessor<ServerSettings>();
//
+ services.AddSingleton<PermissionService>();
AddOnchainWalletParsers(services);
@@ -457,7 +459,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<PaymentRequestStreamer>();
services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>());
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
- services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
services.AddSingleton<INotificationHandler, NewUserRequiresApprovalNotification.Handler>();
@@ -492,10 +493,26 @@ namespace BTCPayServer.Hosting
services.AddPayoutProcesors();
services.AddForms();
- services.AddAPIKeyAuthentication();
+ services.AddSingleton<APIKeyRepository>();
+ services.AddSingleton<IPermissionHandler, BuiltInPermissionHandler>();
+ services.AddSingleton<IPermissionScopeProvider, BuiltInPermissionScopeProvider>();
+ services.AddSingleton<BuiltInPermissionScopeProvider.IStoreScopeProvider, BuiltInPermissionScopeProvider.SqlStoreScopeProvider>();
+
+ foreach (var routeDataToStoreId in new BuiltInPermissionScopeProvider.RouteValueToStoreIdQuery[]
+ {
+ new("appId", "SELECT \"StoreDataId\" FROM \"Apps\" WHERE \"Id\" = @id"),
+ new("payReqId", "SELECT \"StoreDataId\" FROM \"PaymentRequests\" WHERE \"Id\" = @id"),
+ new("invoiceId", "SELECT \"StoreDataId\" FROM \"Invoices\" WHERE \"Id\" = @id"),
+ })
+ services.AddSingleton(routeDataToStoreId);
+
+ services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
+ services.AddTransient<IClaimsTransformation, CookieAuthenticationClaimTransformer>();
+
services.AddBtcPayServerAuthenticationSchemes();
- services.AddAuthorization(o => o.AddBTCPayPolicies());
+ services.AddAuthorization();
+ services.AddSingleton<IConfigureOptions<AuthorizationOptions>, PermissionAuthorizationOptionsSetup>();
services.AddCors(options =>
{
@@ -513,6 +530,149 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, Cheater>(o => o.GetRequiredService<Cheater>());
}
+ services.AddPolicyDefinitions(new[]
+ {
+ new PolicyDefinition(
+ Policies.CanViewInvoices,
+ new PermissionDisplay("View invoices", "Allows viewing invoices."),
+ new PermissionDisplay("View invoices", "Allows viewing invoices on the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanCreateInvoice,
+ new PermissionDisplay("Create an invoice", "Allows creating new invoices."),
+ new PermissionDisplay("Create an invoice", "Allows creating new invoices on the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanModifyInvoices,
+ new PermissionDisplay("Modify invoices", "Allows viewing and modifying invoices."),
+ new PermissionDisplay("Modify invoices", "Allows viewing and modifying invoices on the selected stores."),
+ new[] { Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore }),
+ new PolicyDefinition(
+ Policies.CanModifyWebhooks,
+ new PermissionDisplay("Modify stores webhooks", "Allows modifying the webhooks of all your stores."),
+ new PermissionDisplay("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanModifyServerSettings,
+ new PermissionDisplay("Manage your server", "Grants total control on the server settings of your server."),
+ includedPermissions: new[] { Policies.CanUseInternalLightningNode, Policies.CanManageUsers }),
+ new PolicyDefinition(
+ Policies.CanModifyStoreSettings,
+ new PermissionDisplay("Modify your stores", "Allows managing invoices on all your stores and modify their settings."),
+ new PermissionDisplay("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings."),
+ new[]
+ {
+ Policies.CanManagePullPayments,
+ Policies.CanModifyInvoices,
+ Policies.CanViewStoreSettings,
+ Policies.CanModifyWebhooks,
+ Policies.CanModifyPaymentRequests,
+ Policies.CanManagePayouts,
+ Policies.CanUseLightningNodeInStore
+ }),
+ new PolicyDefinition(
+ Policies.CanViewStoreSettings,
+ new PermissionDisplay("View your stores", "Allows viewing stores settings."),
+ new PermissionDisplay("View your stores", "Allows viewing the selected stores' settings."),
+ new[] { Policies.CanViewInvoices, Policies.CanViewPaymentRequests, Policies.CanViewReports, Policies.CanViewPullPayments, Policies.CanViewPayouts }),
+ new PolicyDefinition(
+ Policies.CanViewReports,
+ new PermissionDisplay("View your reports", "Allows viewing reports."),
+ new PermissionDisplay("View your selected stores' reports", "Allows viewing the selected stores' reports.")),
+ new PolicyDefinition(
+ Policies.CanViewPaymentRequests,
+ new PermissionDisplay("View your payment requests", "Allows viewing payment requests."),
+ new PermissionDisplay("View your payment requests", "Allows viewing the selected stores' payment requests.")),
+ new PolicyDefinition(
+ Policies.CanModifyPaymentRequests,
+ new PermissionDisplay("Modify your payment requests", "Allows viewing, modifying, deleting and creating new payment requests on all your stores."),
+ new PermissionDisplay("Manage selected stores' payment requests", "Allows viewing, modifying, deleting and creating new payment requests on the selected stores."),
+ new[] { Policies.CanViewPaymentRequests }),
+ new PolicyDefinition(
+ Policies.CanModifyProfile,
+ new PermissionDisplay("Manage your profile", "Allows viewing and modifying your user profile."),
+ includedPermissions: new[] { Policies.CanViewProfile }),
+ new PolicyDefinition(
+ Policies.CanViewProfile,
+ new PermissionDisplay("View your profile", "Allows viewing your user profile.")),
+ new PolicyDefinition(
+ Policies.CanViewUsers,
+ new PermissionDisplay("View users", "Allows seeing all users on this server.")),
+ new PolicyDefinition(
+ Policies.CanCreateUser,
+ new PermissionDisplay("Create new users", "Allows creating new users on this server.")),
+ new PolicyDefinition(
+ Policies.CanDeleteUser,
+ new PermissionDisplay("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")),
+ new PolicyDefinition(
+ Policies.CanManageNotificationsForUser,
+ new PermissionDisplay("Manage your notifications", "Allows viewing and modifying your user notifications."),
+ includedPermissions: new[] { Policies.CanViewNotificationsForUser }),
+ new PolicyDefinition(
+ Policies.CanViewNotificationsForUser,
+ new PermissionDisplay("View your notifications", "Allows viewing your user notifications.")),
+ new PolicyDefinition(
+ Policies.Unrestricted,
+ new PermissionDisplay("Unrestricted access", "Grants unrestricted access to your account.")),
+ new PolicyDefinition(
+ Policies.CanUseInternalLightningNode,
+ new PermissionDisplay("Use the internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices."),
+ includedPermissions: new[] { Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode }),
+ new PolicyDefinition(
+ Policies.CanViewLightningInvoiceInternalNode,
+ new PermissionDisplay("View invoices from internal lightning node", "Allows using the internal BTCPay Server lightning node to view BOLT11 invoices.")),
+ new PolicyDefinition(
+ Policies.CanCreateLightningInvoiceInternalNode,
+ new PermissionDisplay("Create invoices with internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices.")),
+ new PolicyDefinition(
+ Policies.CanUseLightningNodeInStore,
+ new PermissionDisplay("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices."),
+ new PermissionDisplay("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices."),
+ new[] { Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore }),
+ new PolicyDefinition(
+ Policies.CanViewLightningInvoiceInStore,
+ new PermissionDisplay("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to all your stores."),
+ new PermissionDisplay("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanCreateLightningInvoiceInStore,
+ new PermissionDisplay("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices."),
+ new PermissionDisplay("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices."),
+ new[] { Policies.CanViewLightningInvoiceInStore }),
+ new PolicyDefinition(
+ Policies.CanManagePullPayments,
+ new PermissionDisplay("Manage your pull payments", "Allows viewing, modifying, deleting and creating pull payments on all your stores."),
+ new PermissionDisplay("Manage selected stores' pull payments", "Allows viewing, modifying, deleting and creating pull payments on the selected stores."),
+ new[] { Policies.CanCreatePullPayments, Policies.CanArchivePullPayments }),
+ new PolicyDefinition(
+ Policies.CanArchivePullPayments,
+ new PermissionDisplay("Archive your pull payments", "Allows deleting pull payments on all your stores."),
+ new PermissionDisplay("Archive selected stores' pull payments", "Allows deleting pull payments on the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanCreatePullPayments,
+ new PermissionDisplay("Create pull payments", "Allows creating pull payments on all your stores."),
+ new PermissionDisplay("Create pull payments in selected stores", "Allows creating pull payments on the selected stores."),
+ new[] { Policies.CanCreateNonApprovedPullPayments }),
+ new PolicyDefinition(
+ Policies.CanViewPullPayments,
+ new PermissionDisplay("View your pull payments", "Allows viewing pull payments on all your stores."),
+ new PermissionDisplay("View selected stores' pull payments", "Allows viewing pull payments on the selected stores.")),
+ new PolicyDefinition(
+ Policies.CanCreateNonApprovedPullPayments,
+ new PermissionDisplay("Create non-approved pull payments", "Allows creating pull payments without automatic approval on all your stores."),
+ new PermissionDisplay("Create non-approved pull payments in selected stores", "Allows viewing, modifying, deleting and creating pull payments without automatic approval on the selected stores."),
+ new[] { Policies.CanViewPullPayments }),
+ new PolicyDefinition(
+ Policies.CanManageUsers,
+ new PermissionDisplay("Manage users", "Allows creating/deleting API keys for users."),
+ includedPermissions: new[] { Policies.CanCreateUser }),
+ new PolicyDefinition(
+ Policies.CanManagePayouts,
+ new PermissionDisplay("Manage payouts", "Allows managing payouts on all your stores."),
+ new PermissionDisplay("Manage payouts in selected stores", "Allows managing payouts on the selected stores."),
+ new[] { Policies.CanViewPayouts }),
+ new PolicyDefinition(
+ Policies.CanViewPayouts,
+ new PermissionDisplay("View payouts", "Allows viewing payouts on all your stores."),
+ new PermissionDisplay("View payouts in selected stores", "Allows viewing payouts on the selected stores.")),
+ });
+
return services;
}
diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs
index 1cf6a78..26fd340 100644
--- a/BTCPayServer/Hosting/Startup.cs
+++ b/BTCPayServer/Hosting/Startup.cs
@@ -166,6 +166,7 @@ namespace BTCPayServer.Hosting
if (!Configuration.GetOrDefault<bool>("nocsp", false))
o.Filters.Add(new ContentSecurityPolicyAttribute(CSPTemplate.AntiXSS));
o.Filters.Add(new JsonHttpExceptionFilter());
+ o.Filters.Add<Security.SetContextFilter>();
o.Filters.Add(new JsonObjectExceptionFilter());
o.Filters.Add(new UIControllerAntiforgeryTokenAttribute());
})
diff --git a/BTCPayServer/Plugins/Bitpay/Controllers/UIStoresTokenController.cs b/BTCPayServer/Plugins/Bitpay/Controllers/UIStoresTokenController.cs
index 4c66227..a2a4528 100644
--- a/BTCPayServer/Plugins/Bitpay/Controllers/UIStoresTokenController.cs
+++ b/BTCPayServer/Plugins/Bitpay/Controllers/UIStoresTokenController.cs
@@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Bitpay.Security;
using BTCPayServer.Plugins.Bitpay.Views;
+using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@@ -33,7 +34,8 @@ public class UIStoresTokenController(
StoreRepository storeRepository,
UserManager<ApplicationUser> userManager,
IHtmlHelper html,
- PaymentMethodHandlerDictionary handlers) : Controller
+ PaymentMethodHandlerDictionary handlers,
+ PermissionService permissionService) : Controller
{
public IStringLocalizer StringLocalizer { get; } = stringLocalizer;
public StoreData CurrentStore => HttpContext.GetStoreData() ?? throw new InvalidOperationException("Store not found");
@@ -166,7 +168,7 @@ public class UIStoresTokenController(
var model = new CreateTokenViewModel();
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;
- var stores = (await storeRepository.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
+ var stores = (await storeRepository.GetStoresByUserId(userId)).Where(data => data.HasPolicy(userId, Policies.CanModifyStoreSettings, permissionService)).ToArray();
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
if (!model.Stores.Any())
@@ -231,7 +233,7 @@ public class UIStoresTokenController(
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
- var stores = (await storeRepository.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
+ var stores = (await storeRepository.GetStoresByUserId(userId)).Where(data => data.HasPolicy(userId, Policies.CanModifyStoreSettings, permissionService)).ToArray();
return View(new PairingModel
{
Id = pairing.Id,
diff --git a/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs b/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
index 1a4994b..70d1fbf 100644
--- a/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
+++ b/BTCPayServer/Plugins/Bitpay/Security/BitpayAuthorizationHandler.cs
@@ -7,62 +7,53 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
-namespace BTCPayServer.Plugins.Bitpay.Security
+namespace BTCPayServer.Plugins.Bitpay.Security;
+
+public class BitpayAuthorizationHandler(
+ IHttpContextAccessor httpContextAccessor,
+ StoreRepository storeRepository,
+ TokenRepository tokenRepository)
+ : AuthorizationHandler<PolicyRequirement>
{
- public class BitpayAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
+ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
{
- private readonly HttpContext _HttpContext;
- private readonly StoreRepository _storeRepository;
- private readonly TokenRepository _tokenRepository;
-
- public BitpayAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
- StoreRepository storeRepository,
- TokenRepository tokenRepository)
+ string storeId = null;
+ if (context.User.Identity is { AuthenticationType: BitpayAuthenticationTypes.ApiKeyAuthentication })
+ {
+ storeId = context.User.Claims.Where(c => c.Type == BitpayClaims.ApiKeyStoreId).Select(c => c.Value).First();
+ }
+ else if (context.User.Identity is { AuthenticationType: BitpayAuthenticationTypes.SinAuthentication })
{
- _HttpContext = httpContextAccessor.HttpContext;
- _storeRepository = storeRepository;
- _tokenRepository = tokenRepository;
+ var sin = context.User.Claims.Where(c => c.Type == BitpayClaims.SIN).Select(c => c.Value).First();
+ var bitToken = (await tokenRepository.GetTokens(sin)).FirstOrDefault();
+ storeId = bitToken?.StoreId;
+ }
+ else if (context.User.Identity is { AuthenticationType: BitpayAuthenticationTypes.Anonymous })
+ {
+ storeId = httpContextAccessor.HttpContext.GetImplicitStoreId();
}
- protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
+ if (storeId == null)
+ return;
+ var store = await storeRepository.FindStore(storeId);
+ if (store == null)
+ return;
+ var isAnonymous = context.User.Identity.AuthenticationType == BitpayAuthenticationTypes.Anonymous;
+ var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice;
+ switch (requirement.Policy)
{
- string storeId = null;
- if (context.User.Identity.AuthenticationType == BitpayAuthenticationTypes.ApiKeyAuthentication)
- {
- storeId = context.User.Claims.Where(c => c.Type == BitpayClaims.ApiKeyStoreId).Select(c => c.Value).First();
- }
- else if (context.User.Identity.AuthenticationType == BitpayAuthenticationTypes.SinAuthentication)
- {
- var sin = context.User.Claims.Where(c => c.Type == BitpayClaims.SIN).Select(c => c.Value).First();
- var bitToken = (await _tokenRepository.GetTokens(sin)).FirstOrDefault();
- storeId = bitToken?.StoreId;
- }
- else if (context.User.Identity.AuthenticationType == BitpayAuthenticationTypes.Anonymous)
- {
- storeId = _HttpContext.GetImplicitStoreId();
- }
- if (storeId == null)
- return;
- var store = await _storeRepository.FindStore(storeId);
- if (store == null)
- return;
- var isAnonymous = context.User.Identity.AuthenticationType == BitpayAuthenticationTypes.Anonymous;
- var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice;
- switch (requirement.Policy)
- {
- case Policies.CanCreateInvoice:
- if (!isAnonymous || (isAnonymous && anyoneCanInvoice))
- {
- context.Succeed(requirement);
- _HttpContext.SetStoreData(store);
- return;
- }
- break;
- case ServerPolicies.CanGetRates.Key:
+ case Policies.CanCreateInvoice:
+ if (!isAnonymous || anyoneCanInvoice)
+ {
context.Succeed(requirement);
- _HttpContext.SetStoreData(store);
- return;
- }
+ httpContextAccessor.HttpContext.SetStoreData(store);
+ }
+
+ break;
+ case ServerPolicies.CanGetRates.Key:
+ context.Succeed(requirement);
+ httpContextAccessor.HttpContext.SetStoreData(store);
+ break;
}
}
}
diff --git a/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml b/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml
index f7362bc..60a58bb 100644
--- a/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml
+++ b/BTCPayServer/Plugins/Crowdfund/Views/NavExtension.cshtml
@@ -1,12 +1,10 @@
@using BTCPayServer.Client
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using BTCPayServer.Views.Apps
@using BTCPayServer.Plugins.Crowdfund
@using BTCPayServer.Services.Apps
@inject AppService AppService
@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
- var store = Context.GetStoreData();
+ var store = Model.Store;
}
@if (store != null)
diff --git a/BTCPayServer/Plugins/PayButton/PayButtonPlugin.cs b/BTCPayServer/Plugins/PayButton/PayButtonPlugin.cs
index 36b6500..d0a1b97 100644
--- a/BTCPayServer/Plugins/PayButton/PayButtonPlugin.cs
+++ b/BTCPayServer/Plugins/PayButton/PayButtonPlugin.cs
@@ -1,6 +1,4 @@
-using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
-using BTCPayServer.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Plugins.PayButton
diff --git a/BTCPayServer/Plugins/PayButton/Views/NavExtension.cshtml b/BTCPayServer/Plugins/PayButton/Views/NavExtension.cshtml
index 67febf4..f032955 100644
--- a/BTCPayServer/Plugins/PayButton/Views/NavExtension.cshtml
+++ b/BTCPayServer/Plugins/PayButton/Views/NavExtension.cshtml
@@ -1,12 +1,11 @@
@using BTCPayServer.Client
@using BTCPayServer.Views.Stores
+@model BTCPayServer.Components.MainNav.MainNavViewModel
-@{ var store = Context.GetStoreData(); }
-
-@if (store != null)
+@if (Model.Store != null)
{
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
- <a layout-menu-item="PayButton" asp-area="@PayButtonPlugin.Area" asp-controller="UIPayButton" asp-action="PayButton" asp-route-storeId="@store.Id">
+ <a layout-menu-item="PayButton" asp-area="@PayButtonPlugin.Area" asp-controller="UIPayButton" asp-action="PayButton" asp-route-storeId="@Model.Store.Id">
<vc:icon symbol="nav-pay-button"/>
<span text-translate="true">Pay Button</span>
</a>
diff --git a/BTCPayServer/Plugins/PointOfSale/Views/NavExtension.cshtml b/BTCPayServer/Plugins/PointOfSale/Views/NavExtension.cshtml
index 9ba841c..153f663 100644
--- a/BTCPayServer/Plugins/PointOfSale/Views/NavExtension.cshtml
+++ b/BTCPayServer/Plugins/PointOfSale/Views/NavExtension.cshtml
@@ -1,12 +1,10 @@
@using BTCPayServer.Client
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using BTCPayServer.Views.Apps
@using BTCPayServer.Plugins.PointOfSale
@using BTCPayServer.Services.Apps
@inject AppService AppService
@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
- var store = Context.GetStoreData();
+ var store = Model.Store;
}
@if (store != null)
diff --git a/BTCPayServer/Plugins/Shopify/Views/NavExtension.cshtml b/BTCPayServer/Plugins/Shopify/Views/NavExtension.cshtml
index da55666..0fa0a8c 100644
--- a/BTCPayServer/Plugins/Shopify/Views/NavExtension.cshtml
+++ b/BTCPayServer/Plugins/Shopify/Views/NavExtension.cshtml
@@ -3,7 +3,7 @@
@using BTCPayServer.Views.Stores
@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
- var store = Context.GetStoreData();
+ var store = Model.Store;
}
@if (store != null)
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/GreenfieldOfferingController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/GreenfieldOfferingController.cs
index 34932e1..3afaeed 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/GreenfieldOfferingController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/GreenfieldOfferingController.cs
@@ -23,7 +23,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield,
- Policy = Policies.CanViewOfferings)]
+ Policy = SubscriptionsPolicies.CanViewOfferings)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldOfferingController(
ApplicationDbContext ctx,
@@ -53,7 +53,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
return Ok(offerings.Select(Mapper.MapOffering).ToArray());
}
[HttpPost("~/api/v1/stores/{storeId}/offerings")]
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanModifyOfferings)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanModifyOfferings)]
public async Task<IActionResult> CreateOffering(string storeId, [FromBody] OfferingModel request)
{
if (request?.AppName is null)
@@ -81,7 +81,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
}
[HttpPost("~/api/v1/stores/{storeId}/offerings/{offeringId}/plans")]
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanModifyOfferings)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanModifyOfferings)]
public async Task<IActionResult> CreateOfferingPlan(string storeId, string offeringId, [FromBody] CreatePlanRequest request)
{
var offering = await ctx.Offerings.GetOfferingData(offeringId, storeId);
@@ -170,7 +170,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
return Ok(Mapper.MapToSubscriberModel(subscriber));
}
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanManageSubscribers)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanManageSubscribers)]
[HttpGet("~/api/v1/stores/{storeId}/offerings/{offeringId}/subscribers/{customerSelector}/credits/{currency}")]
public async Task<IActionResult> GetCredit(string storeId, string offeringId,
[ModelBinder<CustomerSelectorModelBinder>]
@@ -187,7 +187,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
Value = subscriber.GetCredit(currency)
});
}
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanCreditSubscribers)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanCreditSubscribers)]
[HttpPost("~/api/v1/stores/{storeId}/offerings/{offeringId}/subscribers/{customerSelector}/credits/{currency}")]
public async Task<IActionResult> UpdateCredit(string storeId, string offeringId,
[ModelBinder<CustomerSelectorModelBinder>]
@@ -214,7 +214,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
return await GetCredit(storeId, offeringId, customerSelector, currency);
}
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanManageSubscribers)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanManageSubscribers)]
[HttpPost("~/api/v1/stores/{storeId}/offerings/{offeringId}/subscribers/{customerSelector}/suspend")]
public async Task<IActionResult> SuspendSubscriber(string storeId, string offeringId,
[ModelBinder<CustomerSelectorModelBinder>]
@@ -228,7 +228,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
ctx.ChangeTracker.Clear();
return await GetSubscriber(storeId, offeringId, customerSelector);
}
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanManageSubscribers)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanManageSubscribers)]
[HttpPost("~/api/v1/stores/{storeId}/offerings/{offeringId}/subscribers/{customerSelector}/unsuspend")]
public async Task<IActionResult> UnsuspendSubscriber(string storeId, string offeringId,
[ModelBinder<CustomerSelectorModelBinder>]
@@ -293,7 +293,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
return Ok(Mapper.MapPlanCheckout(checkout));
}
- [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = Policies.CanManageSubscribers)]
+ [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield, Policy = SubscriptionsPolicies.CanManageSubscribers)]
[HttpPost("~/api/v1/plan-checkout")]
public async Task<IActionResult> CreatePlanCheckout([FromBody]CreatePlanCheckoutRequest model)
{
@@ -307,7 +307,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
if (model.NewSubscriberEmail is not null && model.CustomerSelector is not null)
ModelState.AddModelError(nameof(model.NewSubscriberEmail), "If customerSelector is specified, newSubscriberEmail cannot be specified");
if (!await CanManageSubscribers(model.StoreId))
- return this.CreateAPIPermissionError(Policies.CanManageSubscribers);
+ return this.CreateAPIPermissionError(SubscriptionsPolicies.CanManageSubscribers);
var plan = await ctx.Plans.GetPlanFromId(model.PlanId ?? "", model.OfferingId ?? "", model.StoreId ?? "");
if (plan is null)
return PlanNotFound();
@@ -355,7 +355,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
=> Math.Round(amount, currencyNameTable.GetNumberFormatInfo(currency)?.CurrencyDecimalDigits ?? 2);
private async Task<bool> CanManageSubscribers(string? storeId)
- => (await authorizationService.AuthorizeAsync(User, storeId ?? "???", new PolicyRequirement(Policies.CanManageSubscribers))).Succeeded;
+ => (await authorizationService.AuthorizeAsync(User, storeId ?? "???", new PolicyRequirement(SubscriptionsPolicies.CanManageSubscribers))).Succeeded;
[AllowAnonymous]
[HttpGet("~/api/v1/subscriber-portal/{portalSessionId}")]
@@ -379,7 +379,7 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers
if (selector is null || model.OfferingId is null || !ModelState.IsValid || model.StoreId is null)
return this.CreateValidationError(ModelState);
if (!await CanManageSubscribers(model.StoreId))
- return this.CreateAPIPermissionError(Policies.CanManageSubscribers);
+ return this.CreateAPIPermissionError(SubscriptionsPolicies.CanManageSubscribers);
var sub = await ctx.Subscribers.GetBySelector(model.OfferingId, selector, model.StoreId);
if (sub is null)
return SubscriberNotFound();
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
index 782d7bc..73c47ca 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
@@ -27,7 +27,7 @@ using DisplayFormatter = BTCPayServer.Services.DisplayFormatter;
namespace BTCPayServer.Plugins.Subscriptions.Controllers;
-[Authorize(Policy = Policies.CanViewOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+[Authorize(Policy = SubscriptionsPolicies.CanViewOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Area(SubscriptionsPlugin.Area)]
public partial class UIOfferingController(
ApplicationDbContextFactory dbContextFactory,
@@ -43,7 +43,7 @@ public partial class UIOfferingController(
) : UISubscriptionControllerBase(dbContextFactory, linkGenerator, stringLocalizer, subsService)
{
[HttpPost("stores/{storeId}/offerings/{offeringId}/new-subscriber")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewSubscriber(
string storeId, string offeringId,
string planId,
@@ -99,7 +99,7 @@ public partial class UIOfferingController(
=> displayFormatter.Currency(req?.Amount ?? 0m, req?.Currency ?? "USD", DisplayFormatter.CurrencyFormat.CodeAndSymbol);
[HttpPost("stores/{storeId}/offerings/{offeringId}/Subscribers")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> SubscriberSuspend(string storeId, string offeringId, string customerId, string? command = null,
string? suspensionReason = null, decimal? amount = null, string? description = null)
{
@@ -356,7 +356,7 @@ public partial class UIOfferingController(
}
[HttpGet("stores/{storeId}/offerings/{offeringId}/configure")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> ConfigureOffering(string storeId, string offeringId)
{
await using var ctx = DbContextFactory.CreateContext();
@@ -445,7 +445,7 @@ public partial class UIOfferingController(
}
[HttpPost("stores/{storeId}/offerings/{offeringId}/plans/{planId}/delete-plan")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> DeletePlan(string storeId, string offeringId, string planId)
{
await using var ctx = DbContextFactory.CreateContext();
@@ -473,7 +473,7 @@ public partial class UIOfferingController(
[HttpGet("stores/{storeId}/offerings/{offeringId}/add-plan")]
[HttpGet("stores/{storeId}/offerings/{offeringId}/plans/{planId}/edit")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> AddPlan(string storeId, string offeringId, string? planId = null)
{
await using var ctx = DbContextFactory.CreateContext();
@@ -523,7 +523,7 @@ public partial class UIOfferingController(
[HttpPost("stores/{storeId}/offerings/{offeringId}/add-plan")]
[HttpPost("stores/{storeId}/offerings/{offeringId}/plans/{planId}/edit")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> AddPlan(string storeId, string offeringId, AddEditPlanViewModel vm, string? planId = null, string? command = null,
int? removeIndex = null)
{
@@ -614,7 +614,7 @@ public partial class UIOfferingController(
}
[HttpGet("stores/{storeId}/offerings/{offeringId}/subscribers/{customerId}/create-portal")]
- [Authorize(Policy = Policies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+ [Authorize(Policy = SubscriptionsPolicies.CanModifyOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreatePortalSession(string storeId, string offeringId, string customerId)
{
await using var ctx = DbContextFactory.CreateContext();
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
index e0b01db..7832e2f 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
@@ -3,11 +3,13 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
+using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Emails.Views;
using BTCPayServer.Plugins.Subscriptions.Controllers;
+using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Views.UIStoreMembership;
using Microsoft.AspNetCore.Routing;
@@ -64,10 +66,35 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
AddSubscriptionsWebhooks(services);
-
+ AddPolicies(services);
base.Execute(services);
}
+ private void AddPolicies(IServiceCollection services)
+ {
+ services.AddPolicyDefinitions(new[]
+ {
+ new PolicyDefinition(
+ SubscriptionsPolicies.CanViewOfferings,
+ new PermissionDisplay("View your offerings", "Allows viewing offerings on all your stores."),
+ new PermissionDisplay("View your offerings", "Allows viewing offerings on the selected stores.")),
+ new PolicyDefinition(
+ SubscriptionsPolicies.CanModifyOfferings,
+ new PermissionDisplay("Modify your offerings", "Allows modifying offerings on all your stores."),
+ new PermissionDisplay("Modify your offerings", "Allows modifying offerings on the selected stores."),
+ new[] { SubscriptionsPolicies.CanViewOfferings, SubscriptionsPolicies.CanManageSubscribers, SubscriptionsPolicies.CanCreditSubscribers },
+ includedByPermissions: [Policies.CanModifyStoreSettings]),
+ new PolicyDefinition(
+ SubscriptionsPolicies.CanManageSubscribers,
+ new PermissionDisplay("Manage your subscribers", "Allows managing subscribers on all your stores."),
+ new PermissionDisplay("Manage your subscribers", "Allows managing subscribers on the selected stores.")),
+ new PolicyDefinition(
+ SubscriptionsPolicies.CanCreditSubscribers,
+ new PermissionDisplay("Credit your subscribers", "Allows crediting subscribers on all your stores."),
+ new PermissionDisplay("Credit your subscribers", "Allows crediting subscribers on the selected stores.")),
+ });
+ }
+
private void AddSubscriptionsWebhooks(IServiceCollection services)
{
services.AddWebhookTriggerProvider<SubscriberWebhookProvider>();
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPolicies.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPolicies.cs
new file mode 100644
index 0000000..7b9ea1a
--- /dev/null
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPolicies.cs
@@ -0,0 +1,9 @@
+namespace BTCPayServer.Plugins.Subscriptions;
+
+public class SubscriptionsPolicies
+{
+ public const string CanViewOfferings = "btcpay.store.canviewofferings";
+ public const string CanModifyOfferings = "btcpay.store.canmodifyofferings";
+ public const string CanManageSubscribers = "btcpay.store.canmanagesubscribers";
+ public const string CanCreditSubscribers = "btcpay.store.cancreditsubscribers";
+}
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/NavExtension.cshtml b/BTCPayServer/Plugins/Subscriptions/Views/NavExtension.cshtml
index d71a151..b176aa0 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/NavExtension.cshtml
+++ b/BTCPayServer/Plugins/Subscriptions/Views/NavExtension.cshtml
@@ -8,22 +8,22 @@
@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
- var store = Context.GetStoreData();
+ var store = Model.Store;
}
@if (store != null)
{
var appType = SubscriptionsAppType.AppType;
var apps = Model.Apps.Where(app => app.AppType == appType).ToList();
- <li class="nav-item" permission="@Policies.CanModifyOfferings">
- <a layout-menu-item="@nameof(SubscriptionsPlugin)" asp-area="Subscriptions" asp- asp-controller="UIOffering" asp-action="CreateOffering" asp-route-storeId="@store.Id">
+ <li class="nav-item" permission="@SubscriptionsPolicies.CanModifyOfferings">
+ <a layout-menu-item="@nameof(SubscriptionsPlugin)" asp-area="Subscriptions" asp-controller="UIOffering" asp-action="CreateOffering" asp-route-storeId="@store.Id">
<vc:icon symbol="nav-reporting" />
<span text-translate="true">Subscriptions</span>
</a>
</li>
@if (apps.Any())
{
- <li layout-menu-item="@nameof(SubscriptionsPlugin)" not-permission="@Policies.CanModifyOfferings" permission="@Policies.CanViewStoreSettings">
+ <li layout-menu-item="@nameof(SubscriptionsPlugin)" not-permission="@SubscriptionsPolicies.CanModifyOfferings" permission="@Policies.CanViewStoreSettings">
<span class="nav-link">
<vc:icon symbol="nav-reporting" />
<span text-translate="true">Subscriptions</span>
@@ -34,12 +34,12 @@
{
var offeringId = app.Data.GetSettings<SubscriptionsAppType.AppConfig>().OfferingId ?? "";
- <li class="nav-item nav-item-sub" permission="@Policies.CanViewOfferings">
+ <li class="nav-item nav-item-sub" permission="@SubscriptionsPolicies.CanViewOfferings">
<a layout-menu-item="@nameof(SubscriptionsPlugin)-@offeringId" asp-area="Subscriptions" asp-controller="UIOffering" asp-action="Offering" asp-route-storeId="@Model.Store.Id" asp-route-offeringId="@offeringId" asp-route-section="Plans">
<span>@app.AppName</span>
</a>
</li>
- <li class="nav-item nav-item-sub" not-permission="@Policies.CanViewOfferings">
+ <li class="nav-item nav-item-sub" not-permission="@SubscriptionsPolicies.CanViewOfferings">
<a layout-menu-item="@nameof(SubscriptionsPlugin)-@offeringId" asp-area="Subscriptions" asp-controller="UIOffering" asp-action="Offering" asp-route-storeId="@Model.Store.Id" asp-route-offeringId="@offeringId" asp-route-section="Plans" class="nav-link">
<span>@app.AppName</span>
</a>
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/Offering.cshtml b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/Offering.cshtml
index 1be0990..e507090 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/Offering.cshtml
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/Offering.cshtml
@@ -75,7 +75,7 @@
{
<div class="d-flex justify-content-between align-items-center">
<h4>Plans</h4>
- <a id="page-primary" permission="@Policies.CanModifyOfferings" asp-route-storeId="@storeId" asp-route-offeringId="@offeringId" asp-action="AddPlan"
+ <a id="page-primary" permission="@SubscriptionsPolicies.CanModifyOfferings" asp-route-storeId="@storeId" asp-route-offeringId="@offeringId" asp-action="AddPlan"
class="btn btn-primary"
role="button"
text-translate="true">Add Plan</a>
@@ -180,7 +180,7 @@
{
<a
href="#"
- permission="@Policies.CanModifyOfferings"
+ permission="@SubscriptionsPolicies.CanModifyOfferings"
text-translate="true"
role="button"
id="page-primary"
diff --git a/BTCPayServer/Security/BuiltInPermissionHandler.cs b/BTCPayServer/Security/BuiltInPermissionHandler.cs
new file mode 100644
index 0000000..5802422
--- /dev/null
+++ b/BTCPayServer/Security/BuiltInPermissionHandler.cs
@@ -0,0 +1,98 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using BTCPayServer.Client;
+using BTCPayServer.Data;
+using BTCPayServer.Security.Greenfield;
+using BTCPayServer.Services;
+using BTCPayServer.Services.Stores;
+using Microsoft.AspNetCore.Authorization;
+
+namespace BTCPayServer.Security;
+
+public class BuiltInPermissionHandler(
+ StoreRepository storeRepository,
+ PermissionService permissionService) : IPermissionHandler
+{
+ public const string StoreKey = "BuiltInPermissionHandler-Store";
+ public const string StoresKey = "BuiltInPermissionHandler-Stores";
+
+ //TODO: In the future, we will add these store permissions to actual aspnet roles, and remove this class.
+ private static readonly PermissionSet ServerAdminRolePermissions =
+ new PermissionSet(new[] { Permission.Create(Policies.CanViewStoreSettings) });
+
+ public async Task HandleAsync(AuthorizationHandlerContext authContext, PermissionAuthorizationContext permContext)
+ {
+ var isAdmin = authContext.User.IsInRole(Roles.ServerAdmin);
+ bool? success = null;
+ StoreData? permissionedStore = null;
+ List<StoreData>? permissionedStores = null;
+ switch (permContext.Permission)
+ {
+ case { Type: PolicyType.Store }:
+ if (permContext.Permission.Scope is { } storeId)
+ {
+ var store = await GetStoreData(permContext, storeId, isAdmin);
+ if (store is null)
+ {
+ success = false;
+ break;
+ }
+ success =
+ (isAdmin && ServerAdminRolePermissions.HasPermission(permContext.Permission, permissionService))
+ ||
+ store.HasPolicy(permContext.UserId, permContext.Permission.Policy, permissionService);
+ if (success is true)
+ permissionedStore = store;
+ }
+ else
+ {
+ var stores = await storeRepository.GetStoresByUserId(permContext.UserId);
+ permissionedStores = new List<StoreData>();
+ foreach (var store in stores)
+ {
+ if (authContext.HasPermission(permContext.Permission.WithScope(store.Id), permissionService) &&
+ store.HasPolicy(permContext.UserId, permContext.Permission.Policy, permissionService))
+ permissionedStores.Add(store);
+ }
+ success = true;
+ }
+ break;
+ case { Type: PolicyType.Server }:
+ success = isAdmin;
+ break;
+ case { Type: PolicyType.User }:
+ case { Policy: Policies.Unrestricted }:
+ success = true;
+ break;
+ }
+
+ if (success is true)
+ {
+ authContext.Succeed(permContext.Requirement);
+ if (permissionedStore is not null)
+ permContext.HttpContext.Items[StoreKey] = permissionedStore;
+ if (permissionedStores is not null)
+ permContext.HttpContext.Items[StoresKey] = permissionedStores.ToArray();
+ }
+ else if (success is false)
+ authContext.Fail();
+ }
+
+ private async Task<StoreData?> GetStoreData(PermissionAuthorizationContext permContext, string storeId, bool isAdmin)
+ {
+ var store = permContext.HttpContext.GetStoreData();
+ if (store is not null)
+ {
+ if (isAdmin ||
+ store.UserStores.Any(u => u.ApplicationUserId == permContext.UserId))
+ return store;
+ store = null;
+ }
+ store = await storeRepository.FindStore(storeId, permContext.UserId);
+ if (store is null && isAdmin)
+ store = await storeRepository.FindStore(storeId);
+ return store;
+ }
+}
diff --git a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
new file mode 100644
index 0000000..9d0c6d6
--- /dev/null
+++ b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
@@ -0,0 +1,96 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BTCPayServer.Client;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Routing;
+using BTCPayServer.Data;
+using Dapper;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace BTCPayServer.Security;
+
+public class BuiltInPermissionScopeProvider(
+ IEnumerable<BuiltInPermissionScopeProvider.IStoreScopeProvider> storeScopeProviders) : IPermissionScopeProvider
+{
+ /// <summary>
+ /// Resolves the store scope for a request, typically using route data or request context.
+ /// Implementations are discovered by DI so plugins can add store-scoped authorization logic.
+ /// </summary>
+ public interface IStoreScopeProvider
+ {
+ Task<string?> GetStoreId(AuthorizationHandlerContext authContext, ScopeProviderAuthorizationContext providerContext, RouteData routeData);
+ }
+
+ /// <summary>
+ /// Defines a route value name and the SQL used to look up its associated store id.
+ /// Example: route value "invoiceId" -> query returning the owning store id.
+ /// </summary>
+ public record RouteValueToStoreIdQuery(string RouteValue, string Sql);
+
+ internal class SqlStoreScopeProvider(
+ IEnumerable<RouteValueToStoreIdQuery> routeDataToStoreIds,
+ ApplicationDbContextFactory dbContextFactory,
+ IMemoryCache memoryCache) : IStoreScopeProvider
+ {
+ public async Task<string?> GetStoreId(AuthorizationHandlerContext authContext, ScopeProviderAuthorizationContext providerContext, RouteData routeData)
+ {
+ await using var ctx = dbContextFactory.CreateContext();
+ var storeId = providerContext.HttpContext.GetImplicitStoreId();
+ List<AdditionalScope> additionalScopes = new();
+ foreach (var i in routeDataToStoreIds)
+ {
+ if (routeData.Values.TryGetValue(i.RouteValue, out var ido) && ido is string id)
+ {
+ var cacheKey = $"SqlStoreScopeProvider-{i.RouteValue}-{id}";
+ memoryCache.TryGetValue(cacheKey, out var storeIdFromCache);
+ var storeId2 = storeIdFromCache as string;
+ if (storeId2 is null)
+ {
+ storeId2 = await ctx.Database.GetDbConnection().ExecuteScalarAsync<string>(i.Sql, new { id });
+ if (storeId2 is not null)
+ {
+ var id2 = storeId2;
+ memoryCache.GetOrCreate(cacheKey, cacheEntry =>
+ {
+ cacheEntry.SlidingExpiration = TimeSpan.FromMinutes(10);
+ return id2;
+ });
+ }
+ }
+ storeId ??= storeId2;
+
+ // Consider the route /stores/{storeId}/apps/{appId}
+ // This check is making sure that the `storeId` is matching the scope resolved from `appId`.
+ if (storeId2 != storeId)
+ storeId2 = null;
+ if (storeId2 is not null)
+ additionalScopes.Add(new AdditionalScope(i.RouteValue, id));
+ }
+ }
+ providerContext.HttpContext.Items[AdditionalScopeKey] = additionalScopes;
+ return storeId;
+ }
+ }
+
+ internal record AdditionalScope(string ScopeName, string Scope);
+
+ public const string AdditionalScopeKey = "BuiltInPermissionScopeProvider-AdditionalScope";
+ public async Task<string?> GetScope(AuthorizationHandlerContext authContext, ScopeProviderAuthorizationContext providerContext)
+ {
+ var type = Permission.TryGetPolicyType(providerContext.Requirement.Policy);
+ if (type is PolicyType.Store)
+ {
+ string? storeId = null;
+ foreach (var provider in storeScopeProviders)
+ {
+ storeId = await provider.GetStoreId(authContext, providerContext, providerContext.HttpContext.GetRouteData());
+ }
+ return storeId;
+ }
+
+ return null;
+ }
+}
diff --git a/BTCPayServer/Security/CookieAuthenticationClaimTransformer.cs b/BTCPayServer/Security/CookieAuthenticationClaimTransformer.cs
new file mode 100644
index 0000000..d171e28
--- /dev/null
+++ b/BTCPayServer/Security/CookieAuthenticationClaimTransformer.cs
@@ -0,0 +1,21 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Client;
+using BTCPayServer.Security.Greenfield;
+
+namespace BTCPayServer.Security;
+
+public class CookieAuthenticationClaimTransformer : IClaimsTransformation
+{
+ public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
+ {
+ if (principal.Identity is { AuthenticationType : AuthenticationSchemes.Cookie } and ClaimsIdentity claimsIdentity)
+ {
+ claimsIdentity.AddClaim(new Claim(GreenfieldConstants.ClaimTypes.Permission,
+ Permission.Create(Policies.Unrestricted).ToString()));
+ }
+ return Task.FromResult(principal);
+ }
+}
diff --git a/BTCPayServer/Security/CookieAuthorizationHandler.cs b/BTCPayServer/Security/CookieAuthorizationHandler.cs
deleted file mode 100644
index 0771058..0000000
--- a/BTCPayServer/Security/CookieAuthorizationHandler.cs
+++ /dev/null
@@ -1,195 +0,0 @@
-using System.Threading.Tasks;
-using BTCPayServer.Abstractions.Constants;
-using BTCPayServer.Abstractions.Contracts;
-using BTCPayServer.Client;
-using BTCPayServer.Data;
-using BTCPayServer.Services.Apps;
-using BTCPayServer.Services.Invoices;
-using BTCPayServer.Services.PaymentRequests;
-using BTCPayServer.Services.Stores;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Routing;
-
-namespace BTCPayServer.Security
-{
- public class CookieAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
- {
- private readonly HttpContext _httpContext;
- private readonly UserManager<ApplicationUser> _userManager;
- private readonly StoreRepository _storeRepository;
- private readonly AppService _appService;
- private readonly PaymentRequestRepository _paymentRequestRepository;
- private readonly InvoiceRepository _invoiceRepository;
- private readonly IPluginHookService _pluginHookService;
-
- public CookieAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
- UserManager<ApplicationUser> userManager,
- StoreRepository storeRepository,
- AppService appService,
- InvoiceRepository invoiceRepository,
- PaymentRequestRepository paymentRequestRepository,
- IPluginHookService pluginHookService)
- {
- _httpContext = httpContextAccessor.HttpContext;
- _userManager = userManager;
- _appService = appService;
- _storeRepository = storeRepository;
- _invoiceRepository = invoiceRepository;
- _pluginHookService = pluginHookService;
- _paymentRequestRepository = paymentRequestRepository;
- }
- //TODO: In the future, we will add these store permissions to actual aspnet roles, and remove this class.
- private static readonly PermissionSet ServerAdminRolePermissions =
- new PermissionSet(new[] {Permission.Create(Policies.CanViewStoreSettings, null)});
-
- protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
- {
- if (context.User.Identity.AuthenticationType != AuthenticationSchemes.Cookie)
- return;
-
- var userId = _userManager.GetUserId(context.User);
- if (string.IsNullOrEmpty(userId))
- return;
-
- bool success = false;
- var isAdmin = context.User.IsInRole(Roles.ServerAdmin);
-
- AppData app = null;
- StoreData store = null;
- InvoiceEntity invoice = null;
- PaymentRequestData paymentRequest = null;
- string storeId;
- var explicitResource = false;
- if (context.Resource is string s)
- {
- explicitResource = true;
- storeId = s;
- }
- else
- {
- storeId = _httpContext.GetImplicitStoreId();
- store = _httpContext.GetStoreData();
- }
- var routeData = _httpContext.GetRouteData();
- if (routeData != null)
- {
- // resolve from app
- if (routeData.Values.TryGetValue("appId", out var vAppId) && vAppId is string appId)
- {
- app = await _appService.GetAppData(userId, appId);
- if (storeId == null)
- {
- storeId = app?.StoreDataId ?? string.Empty;
- }
- else if (app?.StoreDataId != storeId)
- {
- app = null;
- }
- }
- // resolve from payment request
- if (routeData.Values.TryGetValue("payReqId", out var vPayReqId) && vPayReqId is string payReqId)
- {
- paymentRequest = await _paymentRequestRepository.FindPaymentRequest(payReqId, userId);
- if (storeId == null)
- {
- storeId = paymentRequest?.StoreDataId ?? string.Empty;
- }
- else if (paymentRequest?.StoreDataId != storeId)
- {
- paymentRequest = null;
- }
- }
- // resolve from invoice
- if (routeData.Values.TryGetValue("invoiceId", out var vInvoiceId) && vInvoiceId is string invoiceId)
- {
- invoice = await _invoiceRepository.GetInvoice(invoiceId);
- if (storeId == null)
- {
- storeId = invoice?.StoreId ?? string.Empty;
- }
- else if (invoice?.StoreId != storeId)
- {
- invoice = null;
- }
- }
- }
-
- // Fall back to user prefs cookie
- storeId ??= _httpContext.GetUserPrefsCookie()?.CurrentStoreId;
-
- var policy = requirement.Policy;
- bool requiredUnscoped = false;
- if (policy.EndsWith(':'))
- {
- policy = policy.Substring(0, policy.Length - 1);
- requiredUnscoped = true;
- storeId = null;
- }
-
- if (!string.IsNullOrEmpty(storeId) && store is null)
- {
- store = await _storeRepository.FindStore(storeId, userId);
- }
-
- if (Policies.IsServerPolicy(policy) && isAdmin)
- {
- success = true;
- }
- else if (Policies.IsUserPolicy(policy) && userId is not null)
- {
- success = true;
- }
- else if (Policies.IsStorePolicy(policy))
- {
- if (isAdmin && storeId is not null)
- {
- success = ServerAdminRolePermissions.HasPermission(policy, storeId);
-
- }
-
- if (!success && store?.HasPermission(userId, policy) is true)
- {
- success = true;
- }
-
- if (!success && store is null && requiredUnscoped)
- {
- success = true;
- }
- }
- else if (Policies.IsPluginPolicy(requirement.Policy))
- {
- var handle = (AuthorizationFilterHandle)await _pluginHookService.ApplyFilter("handle-authorization-requirement",
- new AuthorizationFilterHandle(context, requirement, _httpContext));
- success = handle.Success;
- }
-
- if (success)
- {
- context.Succeed(requirement);
- if (!explicitResource)
- {
- if (storeId is not null && store is null)
- {
- store = await _storeRepository.FindStore(storeId);
- }
- if (store != null)
- {
- if (_httpContext.GetStoreData()?.Id != store.Id)
- _httpContext.SetStoreData(store);
-
- // cache associated entities if present
- if (app != null && _httpContext.GetAppData()?.Id != app.Id)
- _httpContext.SetAppData(app);
- if (invoice != null && _httpContext.GetInvoiceData()?.Id != invoice.Id)
- _httpContext.SetInvoiceData(invoice);
- if (paymentRequest != null && _httpContext.GetPaymentRequestData()?.Id != paymentRequest.Id)
- _httpContext.SetPaymentRequestData(paymentRequest);
- }
- }
- }
- }
- }
-}
diff --git a/BTCPayServer/Security/GreenField/APIKeyExtensions.cs b/BTCPayServer/Security/GreenField/APIKeyExtensions.cs
index f85e7de..c29e031 100644
--- a/BTCPayServer/Security/GreenField/APIKeyExtensions.cs
+++ b/BTCPayServer/Security/GreenField/APIKeyExtensions.cs
@@ -3,10 +3,10 @@ using System.Linq;
using System.Text.RegularExpressions;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
+using BTCPayServer.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
namespace BTCPayServer.Security.Greenfield
@@ -29,33 +29,27 @@ namespace BTCPayServer.Security.Greenfield
public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
{
builder.AddScheme<GreenfieldAuthenticationOptions, APIKeysAuthenticationHandler>(AuthenticationSchemes.GreenfieldAPIKeys,
- o => { });
+ _ => { });
builder.AddScheme<GreenfieldAuthenticationOptions, BasicAuthenticationHandler>(AuthenticationSchemes.GreenfieldBasic,
- o => { });
+ _ => { });
return builder;
}
- public static IServiceCollection AddAPIKeyAuthentication(this IServiceCollection serviceCollection)
- {
- serviceCollection.AddSingleton<APIKeyRepository>();
- serviceCollection.AddScoped<IAuthorizationHandler, GreenfieldAuthorizationHandler>();
- return serviceCollection;
- }
-
public static string[] GetPermissions(this AuthorizationHandlerContext context)
{
return context.User.Claims.Where(c =>
c.Type.Equals(GreenfieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))
.Select(claim => claim.Value).ToArray();
}
- public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission)
+
+ public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission, PermissionService permissionService, bool anyScope = false)
{
foreach (var claim in context.User.Claims.Where(c =>
c.Type.Equals(GreenfieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
{
if (Permission.TryParse(claim.Value, out var claimPermission))
{
- if (claimPermission.Contains(permission))
+ if (permissionService.Contains(claimPermission, permission, anyScope))
{
return true;
}
diff --git a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs
index 7ab98ad..ac4ed6b 100644
--- a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs
+++ b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs
@@ -60,7 +60,7 @@ namespace BTCPayServer.Security.Greenfield
applicationUser.NormalizedUserName == userManager.NormalizeName(username));
// We disable throttling for new accounts to give time to create API keys via greenfield API.
- if (user.Created is not {} created ||
+ if (user?.Created is not {} created ||
(DateTimeOffset.UtcNow - created) > TimeSpan.FromMinutes(5))
{
if (Context.Connection.RemoteIpAddress?.ToString() is string ip)
@@ -73,6 +73,8 @@ namespace BTCPayServer.Security.Greenfield
{
return Fail($"Basic authentication failed: {loggingContext.Failures[0].Text.Value}");
}
+ if (user is null)
+ return Fail($"Basic authentication failed");
if (user.Fido2Credentials.Any())
{
return Fail("Cannot use Basic authentication when multi-factor is enabled.");
diff --git a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs
deleted file mode 100644
index 240282a..0000000
--- a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using System.Collections.Generic;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using BTCPayServer.Abstractions.Contracts;
-using BTCPayServer.Client;
-using BTCPayServer.Data;
-using BTCPayServer.Services.Stores;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
-using StoreData = BTCPayServer.Data.StoreData;
-
-namespace BTCPayServer.Security.Greenfield
-{
- public class GreenfieldAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
- {
- private readonly HttpContext _httpContext;
- private readonly UserManager<ApplicationUser> _userManager;
- private readonly StoreRepository _storeRepository;
- private readonly IPluginHookService _pluginHookService;
-
- public GreenfieldAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
- UserManager<ApplicationUser> userManager,
- StoreRepository storeRepository,
- IPluginHookService pluginHookService)
- {
- _httpContext = httpContextAccessor.HttpContext;
- _userManager = userManager;
- _storeRepository = storeRepository;
- _pluginHookService = pluginHookService;
- }
-
- protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
- PolicyRequirement requirement)
- {
- if (context.User.Identity?.AuthenticationType != GreenfieldConstants.AuthenticationType)
- return;
- var userid = _userManager.GetUserId(context.User);
- bool success = false;
- var policy = requirement.Policy;
- var requiredUnscoped = false;
- if (policy.EndsWith(':'))
- {
- policy = policy.Substring(0, policy.Length - 1);
- requiredUnscoped = true;
- }
-
- switch (policy)
- {
- case { } when Policies.IsStorePolicy(policy):
- var storeId = requiredUnscoped ? null : (context.Resource as string ?? _httpContext.GetImplicitStoreId());
- // Specific store action
- if (storeId != null)
- {
- if (context.HasPermission(Permission.Create(policy, storeId)))
- {
- if (string.IsNullOrEmpty(userid))
- break;
- var store = await _storeRepository.FindStore(storeId, userid);
- if (store == null)
- break;
- if (!store.HasPermission(userid, policy))
- break;
- success = true;
- _httpContext.SetStoreData(store);
- }
- }
- else
- {
- if (requiredUnscoped && !context.HasPermission(Permission.Create(policy)))
- break;
- var stores = await _storeRepository.GetStoresByUserId(userid);
- List<StoreData> permissionedStores = new List<StoreData>();
- foreach (var store in stores)
- {
- if (context.HasPermission(Permission.Create(policy, store.Id)))
- permissionedStores.Add(store);
- }
- _httpContext.SetStoresData(permissionedStores.ToArray());
- success = true;
- }
- break;
- case { } when Policies.IsServerPolicy(policy):
- if (context.HasPermission(Permission.Create(policy)))
- {
- var user = await _userManager.GetUserAsync(context.User);
- if (user == null)
- break;
- if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
- break;
- success = true;
- }
- break;
- case { } when Policies.IsPluginPolicy(requirement.Policy):
- var handle = (AuthorizationFilterHandle)await _pluginHookService.ApplyFilter("handle-authorization-requirement",
- new AuthorizationFilterHandle(context, requirement, _httpContext));
- success = handle.Success;
- break;
- case Policies.CanManageNotificationsForUser:
- case Policies.CanViewNotificationsForUser:
- case Policies.CanModifyProfile:
- case Policies.CanViewProfile:
- case Policies.CanDeleteUser:
- case Policies.Unrestricted:
- success = context.HasPermission(Permission.Create(policy));
- break;
- }
-
- if (success)
- {
- context.Succeed(requirement);
- }
- _httpContext.Items[RequestedPermissionKey] = policy;
- }
- public const string RequestedPermissionKey = nameof(RequestedPermissionKey);
- }
-}
diff --git a/BTCPayServer/Security/GreenField/GreenfieldAuthenticationHandler.cs b/BTCPayServer/Security/GreenField/GreenfieldAuthenticationHandler.cs
index a3e0694..2a0b1f4 100644
--- a/BTCPayServer/Security/GreenField/GreenfieldAuthenticationHandler.cs
+++ b/BTCPayServer/Security/GreenField/GreenfieldAuthenticationHandler.cs
@@ -30,10 +30,10 @@ public abstract class GreenfieldAuthenticationHandler(
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
- if (Context.Items.TryGetValue(GreenfieldAuthorizationHandler.RequestedPermissionKey, out var p) &&
- p is string policy)
+ if (Context.Items.TryGetValue(PermissionAuthorizationHandler.PolicyRequirementKey, out var p) &&
+ p is PolicyRequirement policy)
{
- await WriteError(new GreenfieldPermissionAPIError(policy), 403);
+ await WriteError(new GreenfieldPermissionAPIError(policy.Policy), 403);
}
else
{
diff --git a/BTCPayServer/Security/IPermissionHandler.cs b/BTCPayServer/Security/IPermissionHandler.cs
new file mode 100644
index 0000000..c31e23b
--- /dev/null
+++ b/BTCPayServer/Security/IPermissionHandler.cs
@@ -0,0 +1,20 @@
+#nullable enable
+using System.Threading.Tasks;
+using BTCPayServer.Client;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace BTCPayServer.Security;
+
+public class PermissionAuthorizationContext(PolicyRequirement requirement, string? scope, string userId, HttpContext httpContext)
+{
+ public string UserId { get; set; } = userId;
+ public HttpContext HttpContext { get; } = httpContext;
+ public Permission Permission { get; } = Permission.Create(requirement.Policy, scope);
+ public bool ExplicitScope { get; set; }
+ public PolicyRequirement Requirement { get; } = requirement;
+}
+public interface IPermissionHandler
+{
+ Task HandleAsync(AuthorizationHandlerContext authContext, PermissionAuthorizationContext permContext);
+}
diff --git a/BTCPayServer/Security/IPermissionScopeProvider.cs b/BTCPayServer/Security/IPermissionScopeProvider.cs
new file mode 100644
index 0000000..93b5cfe
--- /dev/null
+++ b/BTCPayServer/Security/IPermissionScopeProvider.cs
@@ -0,0 +1,18 @@
+#nullable enable
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace BTCPayServer.Security;
+
+public class ScopeProviderAuthorizationContext(string userId, PolicyRequirement requirement, HttpContext httpContext)
+{
+ public string UserId { get; } = userId;
+ public PolicyRequirement Requirement { get; } = requirement;
+ public HttpContext HttpContext { get; } = httpContext;
+}
+
+public interface IPermissionScopeProvider
+{
+ Task<string?> GetScope(AuthorizationHandlerContext authContext, ScopeProviderAuthorizationContext providerContext);
+}
diff --git a/BTCPayServer/Security/PermissionAuthorizationHandler.cs b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
new file mode 100644
index 0000000..53c764a
--- /dev/null
+++ b/BTCPayServer/Security/PermissionAuthorizationHandler.cs
@@ -0,0 +1,85 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Data;
+using BTCPayServer.Security.Greenfield;
+using BTCPayServer.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+
+namespace BTCPayServer.Security;
+
+public class PermissionAuthorizationHandler(
+ PermissionService permissionService,
+ IHttpContextAccessor httpContext,
+ IEnumerable<IPermissionHandler> permissionHandlers,
+ IEnumerable<IPermissionScopeProvider> implicitScopeProviders,
+ UserManager<ApplicationUser> userManager)
+ : AuthorizationHandler<PolicyRequirement>
+{
+ public const string PolicyRequirementKey = nameof(PolicyRequirementKey);
+ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
+ {
+ if (context.User.Identity is not ({ AuthenticationType: AuthenticationSchemes.Cookie } or { AuthenticationType: GreenfieldConstants.AuthenticationType }))
+ return;
+ var userId = userManager.GetUserId(context.User);
+ if (userId is null || httpContext.HttpContext is null)
+ return;
+ httpContext.HttpContext.Items[PolicyRequirementKey] = requirement;
+
+ string? scope = null;
+ var explicitScope = false;
+ if (!requirement.RequireUnscoped)
+ {
+ scope = context.Resource as string;
+ explicitScope = scope is not null;
+ if (!explicitScope)
+ scope = await GetImplicitScope(userId, context, requirement, httpContext.HttpContext);
+ }
+ await Handle(context, requirement, scope, userId, explicitScope, httpContext.HttpContext);
+ }
+
+ private async Task Handle(AuthorizationHandlerContext context, PolicyRequirement requirement, string? scope,
+ string userId, bool explicitScope, HttpContext httpContext2)
+ {
+ var ctx = new PermissionAuthorizationContext(requirement, scope, userId, httpContext2)
+ {
+ ExplicitScope = explicitScope
+ };
+
+ if (scope is not null || ctx.Requirement.RequireUnscoped)
+ {
+ if (!context.HasPermission(ctx.Permission, permissionService))
+ return;
+ }
+ // Imagine `ListStores` with `btcpay.store.canviewstoresettings`.
+ // Because it lists all the stores, the permission doesn't match any scope.
+ else
+ {
+ // Now imagine that the api key has "btcpay.store.canviewstoresettings:StoreA"
+ // The route should still be accessible by the API key.
+ // However, the action is responsible for only allowing `StoreA` to be shown in the list.
+ if (!context.HasPermission(ctx.Permission, permissionService, anyScope: true))
+ return;
+ }
+
+ foreach (var handler in permissionHandlers)
+ {
+ await handler.HandleAsync(context, ctx);
+ }
+ }
+
+ protected async Task<string?> GetImplicitScope(string userId, AuthorizationHandlerContext context, PolicyRequirement requirement, HttpContext httpContext2)
+ {
+ var ctx = new ScopeProviderAuthorizationContext(userId, requirement, httpContext2);
+ foreach (var implicitScopeProvider in implicitScopeProviders)
+ {
+ var scope = await implicitScopeProvider.GetScope(context, ctx);
+ if (scope is not null)
+ return scope;
+ }
+ return null;
+ }
+}
diff --git a/BTCPayServer/Security/PermissionAuthorizationOptionsSetup.cs b/BTCPayServer/Security/PermissionAuthorizationOptionsSetup.cs
new file mode 100644
index 0000000..3a4dc58
--- /dev/null
+++ b/BTCPayServer/Security/PermissionAuthorizationOptionsSetup.cs
@@ -0,0 +1,19 @@
+using BTCPayServer.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Extensions.Options;
+
+namespace BTCPayServer.Security
+{
+ public class PermissionAuthorizationOptionsSetup(PermissionService permissionService) : IConfigureOptions<AuthorizationOptions>
+ {
+ public void Configure(AuthorizationOptions options)
+ {
+ foreach (var def in permissionService.Definitions.Values)
+ {
+ options.AddPolicy(def.Policy, o => o.AddRequirements(new PolicyRequirement(def.Policy)));
+ options.AddPolicy(def.Policy + ":", o => o.AddRequirements(new PolicyRequirement(def.Policy, true)));
+ }
+ options.AddPolicy(ServerPolicies.CanGetRates.Key, o => o.AddRequirements(new PolicyRequirement(ServerPolicies.CanGetRates.Key)));
+ }
+ }
+}
diff --git a/BTCPayServer/Security/SecurityExtensions.cs b/BTCPayServer/Security/SecurityExtensions.cs
index a2cd4fd..a213e8c 100644
--- a/BTCPayServer/Security/SecurityExtensions.cs
+++ b/BTCPayServer/Security/SecurityExtensions.cs
@@ -8,10 +8,6 @@ namespace BTCPayServer.Security
{
public static class SecurityExtensions
{
- public static bool HasScopes(this AuthorizationHandlerContext context, params string[] scopes)
- {
- return scopes.All(s => context.User.HasClaim(c => c.Type.Equals("scope", StringComparison.InvariantCultureIgnoreCase) && c.Value.Split(' ').Contains(s)));
- }
public static string GetImplicitStoreId(this HttpContext httpContext)
{
diff --git a/BTCPayServer/Security/ServerPolicies.cs b/BTCPayServer/Security/ServerPolicies.cs
index 6718874..b2d7cc3 100644
--- a/BTCPayServer/Security/ServerPolicies.cs
+++ b/BTCPayServer/Security/ServerPolicies.cs
@@ -1,25 +1,7 @@
-using BTCPayServer.Client;
-using Microsoft.AspNetCore.Authorization;
-
namespace BTCPayServer.Security
{
public static class ServerPolicies
{
- public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
- {
- foreach (var p in Policies.AllPolicies)
- {
- options.AddPolicy(p);
- }
- options.AddPolicy(Policies.CanModifyStoreSettingsUnscoped);
- options.AddPolicy(CanGetRates.Key);
- return options;
- }
-
- public static void AddPolicy(this AuthorizationOptions options, string policy)
- {
- options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
- }
public class CanGetRates
{
public const string Key = "btcpay.store.cangetrates";
diff --git a/BTCPayServer/Security/SetContextFilter.cs b/BTCPayServer/Security/SetContextFilter.cs
new file mode 100644
index 0000000..c2c77a4
--- /dev/null
+++ b/BTCPayServer/Security/SetContextFilter.cs
@@ -0,0 +1,69 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BTCPayServer.Data;
+using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Services.Apps;
+using BTCPayServer.Services.Invoices;
+using BTCPayServer.Services.PaymentRequests;
+using BTCPayServer.Services.Stores;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace BTCPayServer.Security;
+
+public class SetContextFilter(
+ PaymentRequestRepository paymentRequestRepository,
+ StoreRepository storeRepository,
+ InvoiceRepository invoiceRepository,
+ AppService appService,
+ UserManager<ApplicationUser> userManager) : IAsyncActionFilter
+{
+ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ var httpContext = context.HttpContext;
+ var userId = userManager.GetUserId(context.HttpContext.User) ?? "??";
+ var isCookie = context.HttpContext.User.Identity is { AuthenticationType: AuthenticationSchemes.Cookie };
+ if (httpContext.Items.TryGetValue(BuiltInPermissionHandler.StoreKey, out var oo) && oo is StoreData store)
+ {
+ httpContext.SetStoreData(store);
+ if (isCookie)
+ httpContext.SetNavStoreData(store);
+ }
+ else if (isCookie && httpContext.GetUserPrefsCookie()?.CurrentStoreId is { } preferredStoreId)
+ {
+ httpContext.SetNavStoreData(await storeRepository.FindStore(preferredStoreId, userId));
+ }
+
+ if (httpContext.Items.TryGetValue(BuiltInPermissionHandler.StoresKey, out var ooo) && ooo is StoreData[] stores)
+ {
+ httpContext.SetStoresData(stores);
+ }
+ if (httpContext.Items.TryGetValue(BuiltInPermissionScopeProvider.AdditionalScopeKey, out var o) && o is IEnumerable<BuiltInPermissionScopeProvider.AdditionalScope> additionalScopes)
+ {
+ foreach (var additionalScope in additionalScopes)
+ {
+ switch (additionalScope.ScopeName)
+ {
+ case "appId":
+ var app = await appService.GetAppData(userId, additionalScope.Scope);
+ if (app is not null)
+ httpContext.SetAppData(app);
+ break;
+ case "payReqId":
+ var paymentRequest = await paymentRequestRepository.FindPaymentRequest(additionalScope.Scope, userId);
+ if (paymentRequest is not null)
+ httpContext.SetPaymentRequestData(paymentRequest);
+ break;
+ case "invoiceId":
+ var invoice = await invoiceRepository.GetInvoice(additionalScope.Scope);
+ if (invoice is not null)
+ httpContext.SetInvoiceData(invoice);
+ break;
+ }
+ }
+ }
+
+ await next();
+ }
+}
diff --git a/BTCPayServer/Services/PermissionService.cs b/BTCPayServer/Services/PermissionService.cs
new file mode 100644
index 0000000..1d29661
--- /dev/null
+++ b/BTCPayServer/Services/PermissionService.cs
@@ -0,0 +1,168 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using BTCPayServer.Client;
+
+namespace BTCPayServer.Services;
+
+public record PermissionDefinitionNode(
+ PolicyDefinition Definition,
+ IReadOnlyCollection<PermissionDefinitionNode> Children,
+ IReadOnlyCollection<PermissionDefinitionNode> Parents)
+{
+ public IEnumerable<PermissionDefinitionNode> EnumerateDescendants(bool includeSelf = true)
+ {
+ if (includeSelf)
+ yield return this;
+ foreach (var descendant in Children.SelectMany(c => c.EnumerateDescendants()))
+ yield return descendant;
+ }
+ public IEnumerable<PermissionDefinitionNode> EnumerateParents(bool includeSelf = true)
+ {
+ if (includeSelf)
+ yield return this;
+ foreach (var parent in Parents.SelectMany(p => p.EnumerateParents()))
+ yield return parent;
+ }
+}
+
+public class PermissionService
+{
+ record PermissionDefinitionNodeBuilder(
+ PolicyDefinition Definition,
+ List<PermissionDefinitionNodeBuilder> Children,
+ List<PermissionDefinitionNodeBuilder> Parents)
+ {
+ public void AddChild(PermissionDefinitionNodeBuilder node)
+ {
+ Children.Add(node);
+ node.Parents.Add(this);
+ }
+
+ public void Build(Dictionary<string, PermissionDefinitionNode> nodes)
+ {
+ HashSet<string> visited = new();
+ var unrestricted = Build(nodes, null, visited);
+ AddParents(unrestricted);
+ }
+
+ private void AddParents(PermissionDefinitionNode node)
+ {
+ foreach (var child in node.Children)
+ {
+ ((List<PermissionDefinitionNode>)child.Parents).Add(node);
+ AddParents(child);
+ }
+ }
+
+ PermissionDefinitionNode Build(Dictionary<string, PermissionDefinitionNode> nodes, PermissionDefinitionNode? parent, HashSet<string> visited)
+ {
+ if (!visited.Add($"{parent} -> {Definition.Policy}"))
+ throw new InvalidOperationException($"Circular reference detected in permissions [{Definition}]");
+ if (nodes.TryGetValue(Definition.Policy, out var n))
+ return n;
+
+ var children = new List<PermissionDefinitionNode>();
+ n = new PermissionDefinitionNode(Definition, children, new List<PermissionDefinitionNode>());
+ nodes.Add(Definition.Policy, n);
+ children.AddRange(Children.Select(c => c.Build(nodes, n, visited)));
+ return n;
+ }
+ }
+
+ private readonly IReadOnlyDictionary<string, PolicyDefinition> _definitions;
+
+ public PermissionService(IEnumerable<PolicyDefinition> definitions)
+ {
+ var definitionsByPermission = new Dictionary<string, PolicyDefinition>(StringComparer.OrdinalIgnoreCase);
+ var nodes = new Dictionary<PolicyDefinition, PermissionDefinitionNodeBuilder>();
+ foreach (var definition in definitions)
+ {
+ definitionsByPermission[definition.Policy] = definition;
+ nodes.Add(definition, new PermissionDefinitionNodeBuilder(definition, new(), new()));
+ }
+
+ _definitions = new ReadOnlyDictionary<string, PolicyDefinition>(definitionsByPermission);
+
+ foreach (var node in nodes)
+ {
+ foreach (var included in node.Key.IncludedPermissions)
+ node.Value.AddChild(nodes[GetPolicyDefinition(definitionsByPermission, included)]);
+
+ foreach (var includedBy in node.Key.IncludedByPermissions)
+ nodes[GetPolicyDefinition(definitionsByPermission, includedBy)].AddChild(node.Value);
+ }
+
+ var unrestricted = nodes[definitionsByPermission[Policies.Unrestricted]];
+ foreach (var node in nodes)
+ {
+ if (node.Key.Policy == Policies.Unrestricted)
+ continue;
+ if (node.Value.Parents.Count is 0)
+ unrestricted.AddChild(node.Value);
+ }
+
+ var permNodes = new Dictionary<string, PermissionDefinitionNode>();
+ unrestricted.Build(permNodes);
+ PermissionNodesByPolicy = new ReadOnlyDictionary<string, PermissionDefinitionNode>(permNodes);
+ UnrestrictedPermissionNode = permNodes[Policies.Unrestricted];
+ Definitions = _definitions;
+ }
+
+ private static PolicyDefinition GetPolicyDefinition(Dictionary<string, PolicyDefinition> definitionsByPermission, string included)
+ {
+ if (definitionsByPermission.TryGetValue(included, out var definition))
+ return definition;
+ throw new ArgumentException($"Permission '{included}' is not defined");
+ }
+
+ public IReadOnlyDictionary<string, PermissionDefinitionNode> PermissionNodesByPolicy { get; }
+ public PermissionDefinitionNode UnrestrictedPermissionNode { get; }
+
+ public IReadOnlyDictionary<string, PolicyDefinition> Definitions { get; }
+
+ public bool TryGetDefinition(string permission, [MaybeNullWhen(false)] out PolicyDefinition definition)
+ {
+ definition = null;
+ if (string.IsNullOrWhiteSpace(permission))
+ return false;
+ return _definitions.TryGetValue(permission, out definition);
+ }
+
+ public PolicyDefinition? TryGetDefinition(string permission)
+ {
+ this.TryGetDefinition(permission, out var definition);
+ return definition;
+ }
+
+ public bool IsValidPolicy(string policy)
+ {
+ if (string.IsNullOrWhiteSpace(policy))
+ return false;
+ return _definitions.ContainsKey(policy);
+ }
+
+ public bool Contains(Permission permission, Permission requestedPermission, bool anyScope = false)
+ {
+ if (permission is null)
+ throw new ArgumentNullException(nameof(permission));
+ if (requestedPermission is null)
+ throw new ArgumentNullException(nameof(requestedPermission));
+ if (!ContainsPolicy(permission.Policy, requestedPermission.Policy))
+ return false;
+ return permission.Scope == null ||
+ anyScope || requestedPermission.Scope == permission.Scope;
+ }
+
+ private bool ContainsPolicy(string policy, string subpolicy)
+ {
+ if (!PermissionNodesByPolicy.TryGetValue(policy, out var policyNode) ||
+ !PermissionNodesByPolicy.TryGetValue(subpolicy, out var subPolicyNode))
+ return false;
+
+ return subPolicyNode.EnumerateParents().Any(p => p == policyNode);
+ }
+}
diff --git a/BTCPayServer/Services/PolicyDefinition.cs b/BTCPayServer/Services/PolicyDefinition.cs
new file mode 100644
index 0000000..2576cc2
--- /dev/null
+++ b/BTCPayServer/Services/PolicyDefinition.cs
@@ -0,0 +1,46 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BTCPayServer.Client;
+
+namespace BTCPayServer.Services;
+
+public class PermissionDisplay(string title, string description)
+{
+ public string Title { get; } = title ?? throw new ArgumentNullException(nameof(title));
+ public string Description { get; } = description ?? throw new ArgumentNullException(nameof(description));
+}
+
+public class PolicyDefinition
+{
+ public PolicyDefinition(string policy,
+ PermissionDisplay display,
+ PermissionDisplay? scopeDisplay = null,
+ IEnumerable<string>? includedPermissions = null,
+ IEnumerable<string>? includedByPermissions = null)
+ {
+ Policy = policy switch
+ {
+ null => throw new ArgumentNullException(nameof(policy)),
+ _ when Permission.Parse(policy) is { Scope: null } p => p.Policy,
+ _ => throw new ArgumentException("Invalid policy (this should be a permission without scope)", nameof(policy))
+ };
+ Type = Permission.TryGetPolicyType(Policy);
+ Display = display ?? throw new ArgumentNullException(nameof(display));
+ ScopeDisplay = scopeDisplay;
+ IncludedPermissions = includedPermissions?.ToArray() ?? Array.Empty<string>();
+ IncludedByPermissions = includedByPermissions?.ToArray() ?? Array.Empty<string>();
+ }
+
+ public PolicyType? Type { get; }
+
+ public string Policy { get; }
+
+ public PermissionDisplay Display { get; }
+ public PermissionDisplay? ScopeDisplay { get; }
+ public IReadOnlyCollection<string> IncludedPermissions { get; }
+ public IReadOnlyCollection<string> IncludedByPermissions { get; }
+ public override string ToString() => Policy;
+}
+
diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs
index 0994c5e..d057fed 100644
--- a/BTCPayServer/Services/Stores/StoreRepository.cs
+++ b/BTCPayServer/Services/Stores/StoreRepository.cs
@@ -8,7 +8,6 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
-using BTCPayServer.Migrations;
using BTCPayServer.Payments;
using Dapper;
using Microsoft.EntityFrameworkCore;
@@ -168,11 +167,11 @@ namespace BTCPayServer.Services.Stores
return "Role not found";
}
- public async Task<StoreRole?> AddOrUpdateStoreRole(StoreRoleId role, List<string> policies)
+ public async Task<StoreRole?> AddOrUpdateStoreRole(StoreRoleId role, IEnumerable<string> permissions)
{
- policies = policies.Where(s => Policies.IsValidPolicy(s) && Policies.IsStorePolicy(s)).ToList();
+ var policiesList = permissions.Where(p => Permission.TryGetPolicyType(p) is PolicyType.Store).ToList();
await using var ctx = _ContextFactory.CreateContext();
- Data.StoreRole? match = await ctx.StoreRoles.FindAsync(role.Id);
+ var match = await ctx.StoreRoles.FindAsync(role.Id);
var added = false;
if (match is null)
{
@@ -180,7 +179,7 @@ namespace BTCPayServer.Services.Stores
ctx.StoreRoles.Add(match);
added = true;
}
- match.Permissions = policies;
+ match.Permissions = policiesList;
try
{
await ctx.SaveChangesAsync();
diff --git a/BTCPayServer/Views/Shared/CreateOrEditRole.cshtml b/BTCPayServer/Views/Shared/CreateOrEditRole.cshtml
index f20afe5..37b314b 100644
--- a/BTCPayServer/Views/Shared/CreateOrEditRole.cshtml
+++ b/BTCPayServer/Views/Shared/CreateOrEditRole.cshtml
@@ -1,10 +1,8 @@
@using BTCPayServer.Client
-@using BTCPayServer.Views.Server
-@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using BTCPayServer.Abstractions.TagHelpers
-@using BTCPayServer.Controllers
+@using BTCPayServer.Services
@using BTCPayServer.Views.Stores
@model UpdateRoleViewModel
+@inject PermissionService PermissionService
@{
var role = Context.GetRouteValue("role") as string;
@@ -15,7 +13,7 @@
var title = role is null ? StringLocalizer["Create role"] : StringLocalizer["Update Role"];
var category = storeId is null ? WellKnownCategories.Server : WellKnownCategories.Store;
ViewData.SetLayoutModel(new LayoutModel(nameof(StoreNavPages.Roles), title).SetCategory(category));
- var storePolicies = Policies.AllPolicies.Where(Policies.IsStorePolicy).ToArray();
+ var storePolicies = PermissionService.Definitions.Values.Where(p => p.Type is PolicyType.Store).Select(s => s.Policy.ToString()).ToArray();
}
<form method="post">
@@ -53,43 +51,42 @@
</div>
<h4 class="mt-4 mb-3">Permissions</h4>
- <select multiple="multiple" asp-for="Policies" class="form-select hide-when-js">
+ <select multiple="multiple" asp-for="Permissions" class="form-select hide-when-js">
@foreach (var policy in storePolicies)
{
- <option value="@policy" class="text-truncate" asp-selected="@(Model.Policies?.Contains(policy) ?? false)">@policy</option>
+ <option value="@policy" class="text-truncate" asp-selected="@Model.Permissions.Contains(policy)">@policy</option>
}
</select>
<div class="list-group mb-2">
@{
- var storePolicyMap = Permission.PolicyMap.Where(pair => Policies.IsStorePolicy(pair.Key)).ToArray();
- var topMostPolicies = storePolicyMap.Where(pair => !storePolicyMap.Any(valuePair => valuePair.Value.Contains(pair.Key)));
+ var topMostPolicies = PermissionService.UnrestrictedPermissionNode.Children.Where(pair => pair.Definition.Type is PolicyType.Store);
@foreach (var policy in topMostPolicies)
{
- RenderTree(policy, storePolicyMap, Model.Policies.Contains(policy.Key));
+ RenderTree(policy, Model.Permissions.Contains(policy.Definition.Policy));
}
}
</div>
- <span asp-validation-for="Policies" class="text-danger"></span>
+ <span asp-validation-for="Permissions" class="text-danger"></span>
</div>
</div>
</form>
@{
- void RenderTree(KeyValuePair<string, HashSet<string>> policy, KeyValuePair<string, HashSet<string>>[] storePolicyMap, bool isChecked)
+ void RenderTree(PermissionDefinitionNode permission, bool isChecked)
{
+ var policy = permission.Definition.Policy;
<div class="form-check mb-0">
- <input type="checkbox" class="form-check-input policy-cb" checked="@isChecked" value="@policy.Key" id="Policy-@policy.Key.Replace(".", "_")" />
- <label class="h5 fw-semibold form-check-label mb-1" for="Policy-@policy.Key.Replace(".", "_")" data-bs-toggle="tooltip" title="@policy.Key">
- @UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions[policy.Key].Title
+ <input type="checkbox" class="form-check-input policy-cb" checked="@isChecked" value="@policy" id="Policy-@policy.Replace(".", "_")" />
+ <label class="h5 fw-semibold form-check-label mb-1" for="Policy-@policy.Replace(".", "_")" data-bs-toggle="tooltip" title="@policy">
+ @permission.Definition.Display.Title
</label>
- <p class="text-muted">@UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions[policy.Key].Description</p>
- @if (policy.Value?.Any() is true)
+ <p class="text-muted">@permission.Definition.Display.Description</p>
+ @if (permission.Children.Count != 0)
{
<div class="list-group">
- @foreach (var subPolicy in policy.Value)
+ @foreach (var subPermission in permission.Children)
{
- var match = storePolicyMap.SingleOrDefault(pair => pair.Key == subPolicy);
- RenderTree(match.Key is not null ? match : new KeyValuePair<string, HashSet<string>>(subPolicy, null), storePolicyMap, !isChecked && Model.Policies.Contains(subPolicy));
+ RenderTree(subPermission, !isChecked && Model.Permissions.Contains(subPermission.Definition.Policy));
}
</div>
}
@@ -98,8 +95,9 @@
}
<script>
function handleCheckboxChange(element) {
- const { checked, value: policy } = element;
- const policySelect = document.getElementById('Policies');
+ const {checked, value} = element;
+ const policy = value;
+ const policySelect = document.getElementById('Permissions');
const subPolicies = element.parentElement.querySelectorAll(`.list-group .policy-cb:not([value="${policy}"])`);
policySelect.querySelector(`option[value="${policy}"]`).selected = checked;
diff --git a/BTCPayServer/Views/Shared/LNURL/LightningAddressNav.cshtml b/BTCPayServer/Views/Shared/LNURL/LightningAddressNav.cshtml
index b17fd03..8cfcf5c 100644
--- a/BTCPayServer/Views/Shared/LNURL/LightningAddressNav.cshtml
+++ b/BTCPayServer/Views/Shared/LNURL/LightningAddressNav.cshtml
@@ -1,15 +1,14 @@
-@using BTCPayServer.Views.Stores
@using BTCPayServer.Client
+@model BTCPayServer.Components.MainNav.MainNavViewModel
@{
const string cryptoCode = "BTC";
- var store = Context.GetStoreData();
}
-@if (store.IsLightningEnabled(cryptoCode) && store.IsLNUrlEnabled(cryptoCode))
+@if (Model.Store.IsLightningEnabled(cryptoCode) && Model.Store.IsLNUrlEnabled(cryptoCode))
{
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
- <a layout-menu-item="LightningAddress" asp-area="" asp-controller="UILNURL" asp-action="EditLightningAddress" asp-route-storeId="@store.Id">
- <vc:icon symbol="nav-lightning-address "/>
+ <a layout-menu-item="LightningAddress" asp-area="" asp-controller="UILNURL" asp-action="EditLightningAddress" asp-route-storeId="@Model.Store.Id">
+ <vc:icon symbol="nav-lightning-address" />
<span text-translate="true">Lightning Address</span>
</a>
</li>
diff --git a/BTCPayServer/Views/Shared/ListRoles.cshtml b/BTCPayServer/Views/Shared/ListRoles.cshtml
index c4f5720..ae6b015 100644
--- a/BTCPayServer/Views/Shared/ListRoles.cshtml
+++ b/BTCPayServer/Views/Shared/ListRoles.cshtml
@@ -1,7 +1,8 @@
-@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
-@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Client
+@using BTCPayServer.Services
+@inject PermissionService PermissionService
+
@model BTCPayServer.Models.ServerViewModels.RolesViewModel
@{
var storeId = Context.GetRouteValue("storeId") as string;
@@ -95,7 +96,7 @@
{
@foreach (var policy in role.Permissions)
{
- <code class="d-block text-break">@Policies.DisplayName(policy)</code>
+ <code class="d-block text-break">@(PermissionService.TryGetDefinition(policy)?.Display?.Title ?? policy)</code>
}
}
</td>
diff --git a/BTCPayServer/Views/UIManage/AddApiKey.cshtml b/BTCPayServer/Views/UIManage/AddApiKey.cshtml
index 12d53b7..a1472ad 100644
--- a/BTCPayServer/Views/UIManage/AddApiKey.cshtml
+++ b/BTCPayServer/Views/UIManage/AddApiKey.cshtml
@@ -64,7 +64,7 @@
{
<div class="list-group-item py-3">
<input type="hidden" asp-for="PermissionValues[i].Permission" />
- @if (Policies.IsStorePolicy(Model.PermissionValues[i].Permission))
+ @if (Model.PermissionValues[i].IsStorePolicy)
{
<input type="hidden" asp-for="PermissionValues[i].StoreMode" value="@Model.PermissionValues[i].StoreMode" />
@if (Model.PermissionValues[i].StoreMode == UIManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json
index 929486f..3b30680 100644
--- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json
+++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json
@@ -40,7 +40,7 @@
"required": true,
"description": "The invoice ID",
"schema": {
- "$ref": "#/components/schemas/InvoiceId"
+ "$ref": "#/components/schemas/InvoiceId"
}
},
"UserIdOrEmail": {
@@ -216,7 +216,7 @@
"securitySchemes": {
"API_Key": {
"type": "apiKey",
- "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `unrestricted`: Unrestricted access\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canviewprofile`: View your profile\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.canviewusers`: View users\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canviewofferings`: View your offerings\n* `btcpay.store.canmodifyofferings`: Modify your offerings\n* `btcpay.store.canmanagesubscribers`: Manage your subscribers\n* `btcpay.store.cancreditsubscribers`: Credit your subscribers\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n",
+ "description": "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: `token {token}`. For a smoother experience, you can generate a url that redirects users to an API key creation screen.\n\n The following permissions are available to the context of the user creating the API Key:\n\n* `btcpay.user.candeleteuser`: Delete user\n* `btcpay.user.canmanagenotificationsforuser`: Manage your notifications\n* `btcpay.user.canmodifyprofile`: Manage your profile\n* `btcpay.user.canviewnotificationsforuser`: View your notifications\n* `btcpay.user.canviewprofile`: View your profile\n* `unrestricted`: Unrestricted access\n\nThe following permissions are available if the user is an administrator:\n\n* `btcpay.server.cancreatelightninginvoiceinternalnode`: Create invoices with internal lightning node\n* `btcpay.server.cancreateuser`: Create new users\n* `btcpay.server.canmanageusers`: Manage users\n* `btcpay.server.canmodifyserversettings`: Manage your server\n* `btcpay.server.canuseinternallightningnode`: Use the internal lightning node\n* `btcpay.server.canviewlightninginvoiceinternalnode`: View invoices from internal lightning node\n* `btcpay.server.canviewusers`: View users\n\nThe following permissions applies to all stores of the user, you can limit to a specific store with the following format: `btcpay.store.cancreateinvoice:6HSHAEU4iYWtjxtyRs9KyPjM9GAQp8kw2T9VWbGG1FnZ`:\n\n* `btcpay.store.canarchivepullpayments`: Archive your pull payments\n* `btcpay.store.cancreateinvoice`: Create an invoice\n* `btcpay.store.cancreatelightninginvoice`: Create invoices from the lightning nodes associated with your stores\n* `btcpay.store.cancreatenonapprovedpullpayments`: Create non-approved pull payments\n* `btcpay.store.cancreatepullpayments`: Create pull payments\n* `btcpay.store.cancreditsubscribers`: Credit your subscribers\n* `btcpay.store.canmanagepayouts`: Manage payouts\n* `btcpay.store.canmanagepullpayments`: Manage your pull payments\n* `btcpay.store.canmanagesubscribers`: Manage your subscribers\n* `btcpay.store.canmodifyinvoices`: Modify invoices\n* `btcpay.store.canmodifyofferings`: Modify your offerings\n* `btcpay.store.canmodifypaymentrequests`: Modify your payment requests\n* `btcpay.store.canmodifystoresettings`: Modify your stores\n* `btcpay.store.canuselightningnode`: Use the lightning nodes associated with your stores\n* `btcpay.store.canviewinvoices`: View invoices\n* `btcpay.store.canviewlightninginvoice`: View the lightning invoices associated with your stores\n* `btcpay.store.canviewofferings`: View your offerings\n* `btcpay.store.canviewpaymentrequests`: View your payment requests\n* `btcpay.store.canviewpayouts`: View payouts\n* `btcpay.store.canviewpullpayments`: View your pull payments\n* `btcpay.store.canviewreports`: View your reports\n* `btcpay.store.canviewstoresettings`: View your stores\n* `btcpay.store.webhooks.canmodifywebhooks`: Modify stores webhooks\n\nNote that API Keys only limits permission of a user and can never expand it. If an API Key has the permission `btcpay.server.canmodifyserversettings` but that the user account creating this API Key is not administrator, the API Key will not be able to modify the server settings.\nSome permissions may include other permissions, see [this operation](#operation/permissionsMetadata).\n",
"name": "Authorization",
"in": "header"
},
@@ -233,4 +233,4 @@
"Basic": []
}
]
-}
+}
\ No newline at end of file
Why this scored 51/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.