mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-04 09:58:13 +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:
parent
6f2b673021
commit
fae1dc8dbb
16 changed files with 298 additions and 85 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
|
@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
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue