mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-06 07:34:26 +01:00
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:
@@ -139,6 +139,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
|
||||
@@ -7,10 +7,12 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
@@ -83,6 +85,24 @@ namespace BTCPayServer.Controllers
|
||||
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")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||
|
||||
@@ -222,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public RedirectToActionResult RedirectToStore(StoreData store)
|
||||
{
|
||||
return store.Role == StoreRoles.Owner
|
||||
return store.HasPermission(Policies.CanModifyStoreSettings)
|
||||
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
|
||||
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
@@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
@@ -385,18 +385,21 @@ namespace BTCPayServer.Controllers
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
var authorizedForAutoApprove = (await
|
||||
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
|
||||
.Succeeded;
|
||||
switch (model.SelectedRefundOption)
|
||||
{
|
||||
case "RateThen":
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = model.CryptoAmountThen;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
|
||||
break;
|
||||
|
||||
case "CurrentRate":
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = model.CryptoAmountNow;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove;
|
||||
break;
|
||||
|
||||
case "Fiat":
|
||||
@@ -441,7 +444,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
createPullPayment.Currency = model.CustomCurrency;
|
||||
createPullPayment.Amount = model.CustomAmount;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency;
|
||||
createPullPayment.AutoApproveClaims = authorizedForAutoApprove && paymentMethodId.CryptoCode == model.CustomCurrency;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -24,6 +24,7 @@ using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -56,6 +57,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly InvoiceActivator _invoiceActivator;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
@@ -78,7 +80,8 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerClients,
|
||||
UIWalletsController walletsController,
|
||||
InvoiceActivator invoiceActivator,
|
||||
LinkGenerator linkGenerator)
|
||||
LinkGenerator linkGenerator,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
@@ -98,6 +101,7 @@ namespace BTCPayServer.Controllers
|
||||
_walletsController = walletsController;
|
||||
_invoiceActivator = invoiceActivator;
|
||||
_linkGenerator = linkGenerator;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@@ -14,6 +16,33 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
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
|
||||
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
@@ -112,49 +113,49 @@ namespace BTCPayServer.Security
|
||||
}
|
||||
|
||||
// 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))
|
||||
storeId = null;
|
||||
if (storeId != null)
|
||||
if (!string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
store = await _storeRepository.FindStore(storeId, userId);
|
||||
}
|
||||
|
||||
switch (requirement.Policy)
|
||||
if (Policies.IsServerPolicy(policy) && isAdmin)
|
||||
{
|
||||
case Policies.CanModifyServerSettings:
|
||||
if (isAdmin)
|
||||
success = true;
|
||||
break;
|
||||
case Policies.CanModifyStoreSettings:
|
||||
if (store != null && (store.Role == StoreRoles.Owner))
|
||||
success = true;
|
||||
break;
|
||||
case Policies.CanViewInvoices:
|
||||
case Policies.CanViewStoreSettings:
|
||||
case Policies.CanCreateInvoice:
|
||||
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))
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsUserPolicy(policy) && userId is not null)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else if (Policies.IsStorePolicy(policy))
|
||||
{
|
||||
if (store is not null)
|
||||
{
|
||||
if (store.HasPermission(policy))
|
||||
{
|
||||
var handle = (AuthorizationFilterHandle)await _pluginHookService.ApplyFilter("handle-authorization-requirement",
|
||||
new AuthorizationFilterHandle(context, requirement, _httpContext));
|
||||
success = handle.Success;
|
||||
success = true;
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -48,18 +48,12 @@ namespace BTCPayServer.Security.Greenfield
|
||||
.Select(claim => claim.Value).ToArray();
|
||||
}
|
||||
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 =>
|
||||
c.Type.Equals(GreenfieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
if (Permission.TryParse(claim.Value, out var claimPermission))
|
||||
{
|
||||
if (requireUnscoped && claimPermission.Scope is not null)
|
||||
continue;
|
||||
if (claimPermission.Contains(permission))
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -87,22 +87,19 @@ namespace BTCPayServer.Security.Greenfield
|
||||
switch (policy)
|
||||
{
|
||||
case { } when Policies.IsStorePolicy(policy):
|
||||
var storeId = _httpContext.GetImplicitStoreId();
|
||||
var storeId = requiredUnscoped ? null : _httpContext.GetImplicitStoreId();
|
||||
// Specific store action
|
||||
if (storeId != null)
|
||||
{
|
||||
if (context.HasPermission(Permission.Create(policy, storeId), requiredUnscoped))
|
||||
if (context.HasPermission(Permission.Create(policy, storeId)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(userid))
|
||||
break;
|
||||
var store = await _storeRepository.FindStore(storeId, userid);
|
||||
if (store == null)
|
||||
break;
|
||||
if (Policies.IsStoreModifyPolicy(policy) || policy == Policies.CanUseLightningNodeInStore)
|
||||
{
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
break;
|
||||
}
|
||||
if (!store.HasPermission(policy))
|
||||
break;
|
||||
success = true;
|
||||
_httpContext.SetStoreData(store);
|
||||
}
|
||||
@@ -115,7 +112,7 @@ namespace BTCPayServer.Security.Greenfield
|
||||
List<StoreData> permissionedStores = new List<StoreData>();
|
||||
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);
|
||||
}
|
||||
_httpContext.SetStoresData(permissionedStores.ToArray());
|
||||
@@ -144,7 +141,7 @@ namespace BTCPayServer.Security.Greenfield
|
||||
case Policies.CanViewProfile:
|
||||
case Policies.CanDeleteUser:
|
||||
case Policies.Unrestricted:
|
||||
success = context.HasPermission(Permission.Create(policy), requiredUnscoped);
|
||||
success = context.HasPermission(Permission.Create(policy));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
22
BTCPayServer/Views/UIAccount/CheatPermissions.cshtml
Normal file
22
BTCPayServer/Views/UIAccount/CheatPermissions.cshtml
Normal 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>
|
||||
@@ -1,3 +1,7 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model InvoiceDetailsModel
|
||||
@{
|
||||
ViewData["Title"] = $"Invoice {Model.Id}";
|
||||
@@ -102,7 +106,7 @@
|
||||
}
|
||||
@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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user