Merge branch 'validate-scope'

This commit is contained in:
Nicolas Dorier
2025-11-18 18:46:14 +09:00
15 changed files with 56 additions and 1494 deletions

View File

@@ -1,12 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IBTCPayServerClientFactory
{
Task<BTCPayServerClient> Create(string userId, params string[] storeIds);
Task<BTCPayServerClient> Create(string userId, string[] storeIds, HttpContext httpRequest);
}
}

View File

@@ -169,6 +169,10 @@ namespace BTCPayServer.Tests
#endif
var conf = confBuilder.Build();
_Host = new WebHostBuilder()
.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
})
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
.UseWebRoot(Path.Combine(FindBTCPayServerDirectory(), "wwwroot"))
@@ -284,10 +288,7 @@ namespace BTCPayServer.Tests
public string IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{
return _Host.Services.GetRequiredService<T>();
}
public T GetService<T>() => _Host.Services.GetRequiredService<T>();
public IServiceProvider ServiceProvider => _Host.Services;

View File

@@ -52,41 +52,6 @@ namespace BTCPayServer.Tests
{
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
public async Task LocalClientTests()
{
using var tester = CreateServerTester();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var factory = tester.PayTester.GetService<IBTCPayServerClientFactory>();
Assert.NotNull(factory);
var client = await factory.Create(user.UserId, user.StoreId);
await client.GetCurrentUser();
await client.GetStores();
var store = await client.GetStore(user.StoreId);
Assert.NotNull(store);
var addr = await client.GetLightningDepositAddress(user.StoreId, "BTC");
Assert.NotNull(BitcoinAddress.Create(addr, Network.RegTest));
await user.CreateStoreAsync();
var store1 = user.StoreId;
await user.CreateStoreAsync();
var store2 = user.StoreId;
var store1Client = await factory.Create(null, store1);
var store2Client = await factory.Create(null, store2);
var store1Res = await store1Client.GetStore(store1);
var store2Res = await store2Client.GetStore(store2);
Assert.Equal(store1, store1Res.Id);
Assert.Equal(store2, store2Res.Id);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task MissingPermissionTest()

View File

@@ -28,6 +28,7 @@ using ExchangeSharp;
using LNURL;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright;
using static Microsoft.Playwright.Assertions;
using NBitcoin;
@@ -200,7 +201,8 @@ namespace BTCPayServer.Tests
await s.Page.FillAsync("#Email", changedEmail);
await s.ClickPagePrimary();
await s.FindAlertMessage();
var manager = tester.PayTester.GetService<UserManager<ApplicationUser>>();
using var scope = tester.PayTester.ServiceProvider.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
Assert.NotNull(await manager.FindByNameAsync(changedEmail));
Assert.NotNull(await manager.FindByEmailAsync(changedEmail));
}
@@ -1242,13 +1244,9 @@ namespace BTCPayServer.Tests
await s.LogIn(admin.RegisterDetails.Email, admin.RegisterDetails.Password);
await s.GoToHome();
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal("1", await s.Page.Locator("#NotificationsBadge").TextContentAsync());
});
await Expect(s.Page.Locator("#NotificationsBadge")).ToContainTextAsync("1");
await s.Page.ClickAsync("#NotificationsHandle");
await s.Page.Locator($"#NotificationsList .notification:has-text('New user {unapproved.RegisterDetails.Email} requires approval')").WaitForAsync();
await Expect(s.Page.Locator("#NotificationsList .notification")).ToContainTextAsync($"New user {unapproved.RegisterDetails.Email} requires approval");
await s.Page.ClickAsync("#NotificationsMarkAllAsSeen");
await s.GoToServer(ServerNavPages.Policies);
@@ -1754,8 +1752,8 @@ namespace BTCPayServer.Tests
opening = s.Page.Context.WaitForPageAsync();
await s.Page.ClickAsync("text=View");
newPage = await opening;
await Expect(newPage.Locator("body")).ToContainTextAsync("Description Edit");
await Expect(newPage.Locator("body")).ToContainTextAsync("PP1 Edited");
await Expect(newPage.GetByTestId("description")).ToContainTextAsync("Description Edit");
await Expect(newPage.GetByTestId("title")).ToContainTextAsync("PP1 Edited");
}
[Fact]
@@ -2288,22 +2286,23 @@ namespace BTCPayServer.Tests
await s.FindAlertMessage();
Assert.DoesNotContain("Unarchive", await s.Page.Locator("#btn-archive-toggle").InnerTextAsync());
await s.GoToInvoices(storeId);
await s.Page.WaitForSelectorAsync($"tr[id=invoice_{invoiceId}]");
Assert.Contains(invoiceId, await s.Page.ContentAsync());
// archive via list
await s.Page.Locator($".mass-action-select[value=\"{invoiceId}\"]").ClickAsync();
await s.Page.Locator("#ArchiveSelected").ClickAsync();
Assert.Contains("1 invoice archived", await (await s.FindAlertMessage()).InnerTextAsync());
await s.Page.ClickAsync($".mass-action-select[value=\"{invoiceId}\"]");
await s.Page.ClickAsync("#ArchiveSelected");
await s.FindAlertMessage(partialText: "1 invoice archived");
Assert.DoesNotContain(invoiceId, await s.Page.ContentAsync());
// unarchive via list
await s.Page.Locator("#StatusOptionsToggle").ClickAsync();
await s.Page.Locator("#StatusOptionsIncludeArchived").ClickAsync();
Assert.Contains(invoiceId, await s.Page.ContentAsync());
await s.Page.Locator($".mass-action-select[value=\"{invoiceId}\"]").ClickAsync();
await s.Page.Locator("#UnarchiveSelected").ClickAsync();
Assert.Contains("1 invoice unarchived", await (await s.FindAlertMessage()).InnerTextAsync());
Assert.Contains(invoiceId, await s.Page.ContentAsync());
await s.Page.ClickAsync($".mass-action-select[value=\"{invoiceId}\"]");
await s.Page.ClickAsync("#UnarchiveSelected");
await s.FindAlertMessage(partialText: "1 invoice unarchived");
await s.Page.WaitForSelectorAsync($"tr[id=invoice_{invoiceId}]");
// When logout out we should not be able to access store and invoice details
await s.GoToUrl("/account");

View File

@@ -26,6 +26,7 @@ using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
@@ -58,13 +59,14 @@ namespace BTCPayServer.Tests
public async Task MakeAdmin(bool isAdmin = true)
{
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
using var scope = parent.PayTester.ServiceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId);
if (isAdmin)
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
else
await userManager.RemoveFromRoleAsync(u, Roles.ServerAdmin);
IsAdmin = true;
IsAdmin = isAdmin;
}
public Task<BTCPayServerClient> CreateClient()

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,6 @@ namespace BTCPayServer.Controllers
public partial class UIInvoiceController : Controller
{
readonly InvoiceRepository _InvoiceRepository;
private readonly WalletRepository _walletRepository;
readonly RateFetcher _RateProvider;
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
@@ -71,14 +70,12 @@ namespace BTCPayServer.Controllers
public UIInvoiceController(
InvoiceRepository invoiceRepository,
WalletRepository walletRepository,
DisplayFormatter displayFormatter,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
@@ -105,7 +102,6 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_walletRepository = walletRepository;
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;

View File

@@ -1,27 +1,20 @@
#nullable enable
using System;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayServer.Data.Payouts.LightningLike
{
public class BoltInvoiceClaimDestination : ILightningLikeLikeClaimDestination
public class BoltInvoiceClaimDestination(string bolt11, BOLT11PaymentRequest paymentRequest) : ILightningLikeLikeClaimDestination
{
public BoltInvoiceClaimDestination(string bolt11, BOLT11PaymentRequest paymentRequest)
{
Bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
PaymentRequest = paymentRequest;
PaymentHash = paymentRequest.Hash;
Amount = paymentRequest.MinimumAmount.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
}
public override string ToString()
{
return Bolt11;
}
public string Bolt11 { get; }
public BOLT11PaymentRequest PaymentRequest { get; }
public uint256 PaymentHash { get; }
public string Bolt11 { get; } = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
public BOLT11PaymentRequest PaymentRequest { get; } = paymentRequest;
public uint256 PaymentHash { get; } = paymentRequest.Hash;
public string Id => PaymentHash.ToString();
public decimal? Amount { get; }
public decimal? Amount { get; } = paymentRequest.MinimumAmount.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
};
}

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -14,13 +12,10 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using LNURL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MimeKit;
using NBitcoin;
namespace BTCPayServer.Data.Payouts.LightningLike
@@ -32,7 +27,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public PaymentMethodId PaymentMethodId { get; }
private readonly IOptions<LightningNetworkOptions> _options;
private PaymentMethodHandlerDictionary _paymentHandlers;
private readonly PaymentMethodHandlerDictionary _paymentHandlers;
public BTCPayNetwork Network { get; }
public string[] DefaultRateRules => Network.DefaultRateRules;
@@ -43,15 +38,13 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService;
private readonly IAuthorizationService _authorizationService;
public LightningLikePayoutHandler(
PayoutMethodId payoutMethodId,
IOptions<LightningNetworkOptions> options,
BTCPayNetwork network,
PaymentMethodHandlerDictionary paymentHandlers,
IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService)
IHttpClientFactory httpClientFactory)
{
_paymentHandlers = paymentHandlers;
Network = network;
@@ -59,8 +52,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_options = options;
PaymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
_httpClientFactory = httpClientFactory;
_userService = userService;
_authorizationService = authorizationService;
Currency = network.CryptoCode;
}
@@ -112,7 +103,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
}
var result =
BOLT11PaymentRequest.TryParse(destination, out var invoice, Network.NBitcoinNetwork)
BOLT11PaymentRequest.TryParse(destination, out var invoice, Network.NBitcoinNetwork) && invoice is not null
? new BoltInvoiceClaimDestination(destination, invoice)
: null;

View File

@@ -73,7 +73,6 @@ using BTCPayServer.Payouts;
using ExchangeSharp;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.Localization;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Hosting
@@ -427,7 +426,7 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<PayoutMethodHandlerDictionary>();
services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>();
services.AddSingleton<NotificationSender>();
RegisterExchangeRecommendations(services);
services.AddSingleton<DefaultRulesCollection>();
@@ -481,10 +480,6 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<EmailSenderFactory>();
services.AddSingleton<InvoiceActivator>();
//create a simple client which hooks up to the http scope
services.AddScoped<BTCPayServerClient, LocalBTCPayServerClient>();
//also provide a factory that can impersonate user/store id
services.AddSingleton<IBTCPayServerClientFactory, BTCPayServerClientFactory>();
services.AddPayoutProcesors();
services.AddForms();

View File

@@ -11,6 +11,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using QRCoder;
@@ -18,14 +19,12 @@ namespace BTCPayServer.Plugins.Emails.HostedServices;
public class UserEventHostedService(
EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager,
IServiceScopeFactory serviceScopeFactory,
ISettingsAccessor<ServerSettings> serverSettings,
NotificationSender notificationSender,
Logs logs)
: EventHostedServiceBase(eventAggregator, logs)
{
public UserManager<ApplicationUser> UserManager { get; } = userManager;
protected override void SubscribeToEvents()
{
SubscribeAny<UserEvent>();
@@ -117,7 +116,9 @@ public class UserEventHostedService(
private async Task<TriggerEvent> CreateTriggerEvent(string trigger, JObject model, ApplicationUser user)
{
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
using var scope = serviceScopeFactory.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var admins = await userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var adminMailboxes = string.Join(", ", admins.Select(a => a.GetMailboxAddress().ToString()).ToArray());
model["Admins"] = new JObject()
{

View File

@@ -18,6 +18,7 @@ using BTCPayServer.Services.Rates;
using Dapper;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static BTCPayServer.Data.Subscriptions.SubscriberData;
@@ -30,7 +31,7 @@ public class SubscriptionHostedService(
EventAggregator eventAggregator,
ApplicationDbContextFactory applicationDbContextFactory,
SettingsRepository settingsRepository,
UIInvoiceController invoiceController,
IServiceScopeFactory scopeFactory,
CurrencyNameTable currencyNameTable,
LinkGenerator linkGenerator,
Logs logger) : EventHostedServiceBase(eventAggregator, logger), IPeriodicTask
@@ -162,6 +163,8 @@ public class SubscriptionHostedService(
if (amount > 0)
{
using var scope = scopeFactory.CreateScope();
var invoiceController = scope.ServiceProvider.GetRequiredService<UIInvoiceController>();
var request = await invoiceController.CreateInvoiceCoreRaw(new()
{
Currency = plan.Currency,

View File

@@ -26,6 +26,10 @@ public class BTCPayServerSecurityStampValidator(
ConcurrentDictionary<string, DateTimeOffset> _DisabledUsers = new ConcurrentDictionary<string, DateTimeOffset>();
public bool HasAny => !_DisabledUsers.IsEmpty;
/// <summary>
/// Note that you also need to invalidate the security stamp of the user
/// </summary>
/// <param name="user"></param>
public void Add(string user)
{
_DisabledUsers.TryAdd(user, DateTimeOffset.UtcNow);

View File

@@ -6,6 +6,7 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Services.Notifications
{
@@ -13,25 +14,15 @@ namespace BTCPayServer.Services.Notifications
{
public string UserId { get; set; }
}
public class NotificationSender
public class NotificationSender(ApplicationDbContextFactory contextFactory, IServiceScopeFactory scopeFactory, NotificationManager notificationManager)
{
private readonly ApplicationDbContextFactory _contextFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly NotificationManager _notificationManager;
public NotificationSender(ApplicationDbContextFactory contextFactory, UserManager<ApplicationUser> userManager, NotificationManager notificationManager)
{
_contextFactory = contextFactory;
_userManager = userManager;
_notificationManager = notificationManager;
}
public async Task SendNotification(INotificationScope scope, BaseNotification notification)
{
ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(notification);
var users = await GetUsers(scope, notification.Identifier);
await using (var db = _contextFactory.CreateContext())
await using (var db = contextFactory.CreateContext())
{
foreach (var uid in users)
{
@@ -48,7 +39,7 @@ namespace BTCPayServer.Services.Notifications
}
await db.SaveChangesAsync();
}
_notificationManager.InvalidateNotificationCache(users);
notificationManager.InvalidateNotificationCache(users);
}
public BaseNotification GetBaseNotification(NotificationData notificationData)
@@ -58,7 +49,7 @@ namespace BTCPayServer.Services.Notifications
private async Task<string[]> GetUsers(INotificationScope scope, string notificationIdentifier)
{
await using var ctx = _contextFactory.CreateContext();
await using var ctx = contextFactory.CreateContext();
var split = notificationIdentifier.Split('_', StringSplitOptions.None);
var terms = new List<string>();
@@ -71,8 +62,8 @@ namespace BTCPayServer.Services.Notifications
{
case AdminScope _:
{
query = _userManager.GetUsersInRoleAsync(Roles.ServerAdmin).Result.AsQueryable();
using var s = scopeFactory.CreateScope();
query = s.ServiceProvider.GetService<UserManager<ApplicationUser>>().GetUsersInRoleAsync(Roles.ServerAdmin).Result.AsQueryable();
break;
}
case StoreScope s:

View File

@@ -93,7 +93,7 @@
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
@if (!string.IsNullOrWhiteSpace(Model.Title))
{
<h2 class="h4 mb-3">@Model.Title</h2>
<h2 class="h4 mb-3" data-testid="title">@Model.Title</h2>
}
<div class="d-flex align-items-center gap-2">
<span class="text-muted text-nowrap" text-translate="true">Start Date</span>
@@ -122,7 +122,7 @@
}
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="mt-4">@Safe.Raw(Model.Description)</div>
<div class="mt-4" data-testid="description">@Safe.Raw(Model.Description)</div>
}
</div>
</div>