Adapt cookie auth to work with same API permission system (#4595)

* Adapt cookie auth to work with same API permission system

* Handle unscoped store permission case

* Do not consider Unscoped as a valid policy

* Add tests

* Refactor permissions scopes

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2023-03-20 02:46:46 +01:00 committed by GitHub
parent 6f2b673021
commit fae1dc8dbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 298 additions and 85 deletions

View file

@ -98,6 +98,37 @@ namespace BTCPayServer.Client
{ {
return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase); return policy.StartsWith("btcpay.plugin", StringComparison.OrdinalIgnoreCase);
} }
public static bool IsUserPolicy(string policy)
{
return policy.StartsWith("btcpay.user", StringComparison.OrdinalIgnoreCase);
}
}
public class PermissionSet
{
public PermissionSet() : this(Array.Empty<Permission>())
{
}
public PermissionSet(Permission[] permissions)
{
Permissions = permissions;
}
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 class Permission public class Permission
{ {
@ -105,7 +136,7 @@ namespace BTCPayServer.Client
{ {
Init(); Init();
} }
public static Permission Create(string policy, string scope = null) public static Permission Create(string policy, string scope = null)
{ {
if (TryCreatePermission(policy, scope, out var r)) if (TryCreatePermission(policy, scope, out var r))
@ -121,7 +152,7 @@ namespace BTCPayServer.Client
policy = policy.Trim().ToLowerInvariant(); policy = policy.Trim().ToLowerInvariant();
if (!Policies.IsValidPolicy(policy)) if (!Policies.IsValidPolicy(policy))
return false; return false;
if (scope != null && !Policies.IsStorePolicy(policy)) if (!string.IsNullOrEmpty(scope) && !Policies.IsStorePolicy(policy))
return false; return false;
permission = new Permission(policy, scope); permission = new Permission(policy, scope);
return true; return true;
@ -174,7 +205,7 @@ namespace BTCPayServer.Client
} }
if (!Policies.IsStorePolicy(subpermission.Policy)) if (!Policies.IsStorePolicy(subpermission.Policy))
return true; return true;
return Scope == null || subpermission.Scope == this.Scope; return Scope == null || subpermission.Scope == Scope;
} }
public static IEnumerable<Permission> ToPermissions(string[] permissions) public static IEnumerable<Permission> ToPermissions(string[] permissions)
@ -199,7 +230,8 @@ namespace BTCPayServer.Client
return true; return true;
if (policy == subpolicy) if (policy == subpolicy)
return true; return true;
if (!PolicyMap.TryGetValue(policy, out var subPolicies)) return false; if (!PolicyMap.TryGetValue(policy, out var subPolicies))
return false;
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy)); return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
} }
@ -213,23 +245,23 @@ namespace BTCPayServer.Client
Policies.CanModifyInvoices, Policies.CanModifyInvoices,
Policies.CanViewStoreSettings, Policies.CanViewStoreSettings,
Policies.CanModifyStoreWebhooks, Policies.CanModifyStoreWebhooks,
Policies.CanModifyPaymentRequests); Policies.CanModifyPaymentRequests,
Policies.CanUseLightningNodeInStore);
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser); PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments ); PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments ); PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests ); PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile ); PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore ); PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser ); PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
PolicyHasChild(Policies.CanModifyServerSettings, PolicyHasChild(Policies.CanModifyServerSettings,
Policies.CanUseInternalLightningNode, Policies.CanUseInternalLightningNode,
Policies.CanManageUsers); Policies.CanManageUsers);
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode,Policies.CanViewLightningInvoiceInternalNode ); PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts ); PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice ); PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests ); PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
} }
private static void PolicyHasChild(string policy, params string[] subPolicies) private static void PolicyHasChild(string policy, params string[] subPolicies)
@ -243,33 +275,26 @@ namespace BTCPayServer.Client
} }
else else
{ {
PolicyMap.Add(policy,subPolicies.ToHashSet()); PolicyMap.Add(policy, subPolicies.ToHashSet());
} }
} }
public string Scope { get; } public string Scope { get; }
public string Policy { get; } public string Policy { get; }
public override string ToString() public override string ToString()
{ {
if (Scope != null) return Scope != null ? $"{Policy}:{Scope}" : Policy;
{
return $"{Policy}:{Scope}";
}
return Policy;
} }
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
Permission item = obj as Permission; Permission item = obj as Permission;
if (item == null) return item != null && ToString().Equals(item.ToString());
return false;
return ToString().Equals(item.ToString());
} }
public static bool operator ==(Permission a, Permission b) public static bool operator ==(Permission a, Permission b)
{ {
if (System.Object.ReferenceEquals(a, b)) if (ReferenceEquals(a, b))
return true; return true;
if (((object)a == null) || ((object)b == null)) if (((object)a == null) || ((object)b == null))
return false; return false;

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text; using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;

View file

@ -4,11 +4,13 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Reflection.Metadata;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
@ -831,6 +833,105 @@ namespace BTCPayServer.Tests
AssertUrlHasPairingCode(s); AssertUrlHasPairingCode(s);
} }
[Fact(Timeout = TestTimeout)]
public async Task CookieReflectProperPermissions()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
var alice = s.Server.NewAccount();
alice.Register(false);
await alice.CreateStoreAsync();
var bob = s.Server.NewAccount();
await bob.CreateStoreAsync();
await bob.AddGuest(alice.UserId);
s.GoToLogin();
s.LogIn(alice.Email, alice.Password);
s.GoToUrl($"/cheat/permissions/stores/{bob.StoreId}");
var pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser
});
AssertPermissions(pageSource, false,
new[]
{
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments,
Policies.CanModifyServerSettings
});
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser,
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments
});
AssertPermissions(pageSource, false,
new[]
{
Policies.CanModifyServerSettings
});
await alice.MakeAdmin();
s.Logout();
s.GoToLogin();
s.LogIn(alice.Email, alice.Password);
s.GoToUrl($"/cheat/permissions/stores/{alice.StoreId}");
pageSource = s.Driver.PageSource;
AssertPermissions(pageSource, true,
new[]
{
Policies.CanViewInvoices,
Policies.CanModifyInvoices,
Policies.CanViewPaymentRequests,
Policies.CanViewStoreSettings,
Policies.CanModifyStoreSettingsUnscoped,
Policies.CanDeleteUser,
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments,
Policies.CanModifyServerSettings,
Policies.CanCreateUser,
Policies.CanManageUsers
});
}
void AssertPermissions(string source, bool expected, string[] permissions)
{
if (expected)
{
foreach (var p in permissions)
Assert.Contains(p + "<", source);
}
else
{
foreach (var p in permissions)
Assert.DoesNotContain(p + "<", source);
}
}
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
public async Task CanCreateAppPoS() public async Task CanCreateAppPoS()
{ {

View file

@ -220,8 +220,8 @@ namespace BTCPayServer.Tests
RegisterDetails = new RegisterViewModel() RegisterDetails = new RegisterViewModel()
{ {
Email = Utils.GenerateEmail(), Email = Utils.GenerateEmail(),
ConfirmPassword = "Kitten0@", ConfirmPassword = Password,
Password = "Kitten0@", Password = Password,
IsAdmin = isAdmin IsAdmin = isAdmin
}; };
await account.Register(RegisterDetails); await account.Register(RegisterDetails);
@ -240,6 +240,7 @@ namespace BTCPayServer.Tests
Email = RegisterDetails.Email; Email = RegisterDetails.Email;
IsAdmin = account.RegisteredAdmin; IsAdmin = account.RegisteredAdmin;
} }
public string Password { get; set; } = "Kitten0@";
public RegisterViewModel RegisterDetails { get; set; } public RegisterViewModel RegisterDetails { get; set; }

View file

@ -139,6 +139,7 @@
<ItemGroup> <ItemGroup>
<Watch Include="Views\**\*.*"></Watch> <Watch Include="Views\**\*.*"></Watch>
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml"> <Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>

View file

@ -7,10 +7,12 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Fido2; using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models; using BTCPayServer.Fido2.Models;
using BTCPayServer.Filters;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services; using BTCPayServer.Services;
@ -83,6 +85,24 @@ namespace BTCPayServer.Controllers
get; set; get; set;
} }
[HttpGet("/cheat/permissions")]
[HttpGet("/cheat/permissions/stores/{storeId}")]
[CheatModeRoute]
public async Task<IActionResult> CheatPermissions([FromServices]IAuthorizationService authorizationService, 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 }))
{
results.Add((p, authorizationService.AuthorizeAsync(User, storeId, p)));
}
await Task.WhenAll(results.Select(r => r.Item2));
results = results.OrderBy(r => r.Item1).ToList();
vm.Permissions = results.Select(r => (r.Item1, r.Item2.Result)).ToArray();
return View(vm);
}
[HttpGet("/login")] [HttpGet("/login")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null, string email = null) public async Task<IActionResult> Login(string returnUrl = null, string email = null)

View file

@ -222,7 +222,7 @@ namespace BTCPayServer.Controllers
public RedirectToActionResult RedirectToStore(StoreData store) public RedirectToActionResult RedirectToStore(StoreData store)
{ {
return store.Role == StoreRoles.Owner return store.HasPermission(Policies.CanModifyStoreSettings)
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id }) ? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id }); : RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
} }

View file

@ -254,7 +254,7 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("invoices/{invoiceId}/refund")] [HttpGet("invoices/{invoiceId}/refund")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund([FromServices] IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken) public async Task<IActionResult> Refund([FromServices] IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken)
{ {
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers
} }
[HttpPost("invoices/{invoiceId}/refund")] [HttpPost("invoices/{invoiceId}/refund")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken) public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
{ {
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
@ -385,18 +385,21 @@ namespace BTCPayServer.Controllers
StoreId = invoice.StoreId, StoreId = invoice.StoreId,
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
}; };
var authorizedForAutoApprove = (await
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
.Succeeded;
switch (model.SelectedRefundOption) switch (model.SelectedRefundOption)
{ {
case "RateThen": case "RateThen":
createPullPayment.Currency = paymentMethodId.CryptoCode; createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = model.CryptoAmountThen; createPullPayment.Amount = model.CryptoAmountThen;
createPullPayment.AutoApproveClaims = true; createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
break; break;
case "CurrentRate": case "CurrentRate":
createPullPayment.Currency = paymentMethodId.CryptoCode; createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = model.CryptoAmountNow; createPullPayment.Amount = model.CryptoAmountNow;
createPullPayment.AutoApproveClaims = true; createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
break; break;
case "Fiat": case "Fiat":
@ -441,7 +444,7 @@ namespace BTCPayServer.Controllers
createPullPayment.Currency = model.CustomCurrency; createPullPayment.Currency = model.CustomCurrency;
createPullPayment.Amount = model.CustomAmount; createPullPayment.Amount = model.CustomAmount;
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency; createPullPayment.AutoApproveClaims = authorizedForAutoApprove && paymentMethodId.CryptoCode == model.CustomCurrency;
break; break;
default: default:

View file

@ -24,6 +24,7 @@ using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Validation; using BTCPayServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -56,6 +57,7 @@ namespace BTCPayServer.Controllers
private readonly UIWalletsController _walletsController; private readonly UIWalletsController _walletsController;
private readonly InvoiceActivator _invoiceActivator; private readonly InvoiceActivator _invoiceActivator;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
public WebhookSender WebhookNotificationManager { get; } public WebhookSender WebhookNotificationManager { get; }
@ -78,7 +80,8 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerClients, ExplorerClientProvider explorerClients,
UIWalletsController walletsController, UIWalletsController walletsController,
InvoiceActivator invoiceActivator, InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator) LinkGenerator linkGenerator,
IAuthorizationService authorizationService)
{ {
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
@ -98,6 +101,7 @@ namespace BTCPayServer.Controllers
_walletsController = walletsController; _walletsController = walletsController;
_invoiceActivator = invoiceActivator; _invoiceActivator = invoiceActivator;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_authorizationService = authorizationService;
} }

View file

@ -1,8 +1,10 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@ -14,6 +16,33 @@ namespace BTCPayServer.Data
{ {
public static class StoreDataExtensions public static class StoreDataExtensions
{ {
public static PermissionSet GetPermissionSet(this StoreData store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.Role is null)
return new PermissionSet();
return new PermissionSet(store.Role == StoreRoles.Owner
? new[]
{
Permission.Create(Policies.CanModifyStoreSettings, store.Id),
Permission.Create(Policies.CanTradeCustodianAccount, store.Id),
Permission.Create(Policies.CanWithdrawFromCustodianAccounts, store.Id),
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
}
: new[]
{
Permission.Create(Policies.CanViewStoreSettings, store.Id),
Permission.Create(Policies.CanModifyInvoices, store.Id),
Permission.Create(Policies.CanViewCustodianAccounts, store.Id),
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
});
}
public static bool HasPermission(this StoreData store, string permission)
{
ArgumentNullException.ThrowIfNull(store);
return store.GetPermissionSet().Contains(permission, store.Id);
}
#pragma warning disable CS0618 #pragma warning disable CS0618
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData) public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
{ {

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Models.AccountViewModels
{
public class CheatPermissionsViewModel
{
public string StoreId { get; internal set; }
public (string, AuthorizationResult Result)[] Permissions { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
@ -112,49 +113,49 @@ namespace BTCPayServer.Security
} }
// Fall back to user prefs cookie // Fall back to user prefs cookie
if (storeId == null) storeId ??= _httpContext.GetUserPrefsCookie()?.CurrentStoreId;
var policy = requirement.Policy;
bool requiredUnscoped = false;
if (policy.EndsWith(':'))
{ {
storeId = _httpContext.GetUserPrefsCookie()?.CurrentStoreId; policy = policy.Substring(0, policy.Length - 1);
requiredUnscoped = true;
storeId = null;
} }
if (string.IsNullOrEmpty(storeId)) if (!string.IsNullOrEmpty(storeId))
storeId = null;
if (storeId != null)
{ {
store = await _storeRepository.FindStore(storeId, userId); store = await _storeRepository.FindStore(storeId, userId);
} }
switch (requirement.Policy) if (Policies.IsServerPolicy(policy) && isAdmin)
{ {
case Policies.CanModifyServerSettings: success = true;
if (isAdmin) }
success = true; else if (Policies.IsUserPolicy(policy) && userId is not null)
break; {
case Policies.CanModifyStoreSettings: success = true;
if (store != null && (store.Role == StoreRoles.Owner)) }
success = true; else if (Policies.IsStorePolicy(policy))
break; {
case Policies.CanViewInvoices: if (store is not null)
case Policies.CanViewStoreSettings: {
case Policies.CanCreateInvoice: if (store.HasPermission(policy))
if (store != null)
success = true;
break;
case Policies.CanViewProfile:
case Policies.CanViewNotificationsForUser:
case Policies.CanManageNotificationsForUser:
case Policies.CanModifyStoreSettingsUnscoped:
if (context.User != null)
success = true;
break;
default:
if (Policies.IsPluginPolicy(requirement.Policy))
{ {
var handle = (AuthorizationFilterHandle)await _pluginHookService.ApplyFilter("handle-authorization-requirement", success = true;
new AuthorizationFilterHandle(context, requirement, _httpContext));
success = handle.Success;
} }
break; }
else if (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) if (success)

View file

@ -48,18 +48,12 @@ namespace BTCPayServer.Security.Greenfield
.Select(claim => claim.Value).ToArray(); .Select(claim => claim.Value).ToArray();
} }
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission) public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission)
{
return HasPermission(context, permission, false);
}
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission, bool requireUnscoped)
{ {
foreach (var claim in context.User.Claims.Where(c => foreach (var claim in context.User.Claims.Where(c =>
c.Type.Equals(GreenfieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))) c.Type.Equals(GreenfieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
{ {
if (Permission.TryParse(claim.Value, out var claimPermission)) if (Permission.TryParse(claim.Value, out var claimPermission))
{ {
if (requireUnscoped && claimPermission.Scope is not null)
continue;
if (claimPermission.Contains(permission)) if (claimPermission.Contains(permission))
{ {
return true; return true;

View file

@ -87,22 +87,19 @@ namespace BTCPayServer.Security.Greenfield
switch (policy) switch (policy)
{ {
case { } when Policies.IsStorePolicy(policy): case { } when Policies.IsStorePolicy(policy):
var storeId = _httpContext.GetImplicitStoreId(); var storeId = requiredUnscoped ? null : _httpContext.GetImplicitStoreId();
// Specific store action // Specific store action
if (storeId != null) if (storeId != null)
{ {
if (context.HasPermission(Permission.Create(policy, storeId), requiredUnscoped)) if (context.HasPermission(Permission.Create(policy, storeId)))
{ {
if (string.IsNullOrEmpty(userid)) if (string.IsNullOrEmpty(userid))
break; break;
var store = await _storeRepository.FindStore(storeId, userid); var store = await _storeRepository.FindStore(storeId, userid);
if (store == null) if (store == null)
break; break;
if (Policies.IsStoreModifyPolicy(policy) || policy == Policies.CanUseLightningNodeInStore) if (!store.HasPermission(policy))
{ break;
if (store.Role != StoreRoles.Owner)
break;
}
success = true; success = true;
_httpContext.SetStoreData(store); _httpContext.SetStoreData(store);
} }
@ -115,7 +112,7 @@ namespace BTCPayServer.Security.Greenfield
List<StoreData> permissionedStores = new List<StoreData>(); List<StoreData> permissionedStores = new List<StoreData>();
foreach (var store in stores) foreach (var store in stores)
{ {
if (context.HasPermission(Permission.Create(policy, store.Id), requiredUnscoped)) if (context.HasPermission(Permission.Create(policy, store.Id)))
permissionedStores.Add(store); permissionedStores.Add(store);
} }
_httpContext.SetStoresData(permissionedStores.ToArray()); _httpContext.SetStoresData(permissionedStores.ToArray());
@ -144,7 +141,7 @@ namespace BTCPayServer.Security.Greenfield
case Policies.CanViewProfile: case Policies.CanViewProfile:
case Policies.CanDeleteUser: case Policies.CanDeleteUser:
case Policies.Unrestricted: case Policies.Unrestricted:
success = context.HasPermission(Permission.Create(policy), requiredUnscoped); success = context.HasPermission(Permission.Create(policy));
break; break;
} }

View file

@ -0,0 +1,22 @@
@model CheatPermissionsViewModel
@{
ViewData["Title"] = "Permissions";
Layout = "_LayoutSignedOut";
}
@if (Model.StoreId is not null)
{
<h1>Store: @Model.StoreId</h1>
}
else
{
<h1>No scope</h1>
}
<ul>
@foreach (var p in Model.Permissions.Where(o => o.Result.Succeeded))
{
<li>@p.Item1</li>
}
</ul>

View file

@ -1,3 +1,7 @@
@using BTCPayServer.Client.Models
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Client
@using BTCPayServer.Abstractions.TagHelpers
@model InvoiceDetailsModel @model InvoiceDetailsModel
@{ @{
ViewData["Title"] = $"Invoice {Model.Id}"; ViewData["Title"] = $"Invoice {Model.Id}";
@ -102,7 +106,7 @@
} }
@if (Model.CanRefund) @if (Model.CanRefund)
{ {
<a asp-action="Refund" asp-route-invoiceId="@Model.Id" id="IssueRefund" class="btn btn-primary text-nowrap" data-bs-toggle="modal" data-bs-target="#RefundModal">Issue Refund</a> <a asp-action="Refund" asp-route-invoiceId="@Model.Id" id="IssueRefund" class="btn btn-primary text-nowrap" data-bs-toggle="modal" data-bs-target="#RefundModal" permission="@Policies.CanCreateNonApprovedPullPayments">Issue Refund</a>
} }
else else
{ {