diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 050a8a002..2d4030460 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -9,6 +9,7 @@ using BTCPayServer.Client.Models; using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.JsonConverters; +using BTCPayServer.Lightning; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Tests.Logging; @@ -872,6 +873,96 @@ namespace BTCPayServer.Tests } } + + [Fact(Timeout = 60 * 2 * 1000)] + [Trait("Integration", "Integration")] + [Trait("Lightning", "Lightning")] + public async Task CanUseLightningAPI() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); + + var merchant = tester.NewAccount(); + merchant.GrantAccess(true); + merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); + var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}"); + var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60))); + tester.PayTester.GetService().DevelopmentOverride = false; + // The default client is using charge, so we should not be able to query channels + var client = await user.CreateClient(Policies.CanUseInternalLightningNode); + + var info = await client.GetLightningNodeInfo("BTC"); + Assert.Single(info.NodeURIs); + Assert.NotEqual(0, info.BlockHeight); + + var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); + Assert.Contains("503", err.Message); + // Not permission for the store! + err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); + Assert.Contains("403", err.Message); + var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); + var chargeInvoice = invoiceData; + Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); + + client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); + // Not permission for the server + err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); + Assert.Contains("403", err.Message); + + var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); + Assert.Equal(2, data.Count()); + BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); + + invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); + + Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); + + await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = merchantInvoice.BOLT11 + }); + await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = "lol" + })); + + var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = -1, + Expiry = TimeSpan.FromSeconds(-1), + Description = null + })); + Assert.Equal(2, validationErr.ValidationErrors.Length); + + var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); + Assert.NotNull(invoice.PaidAt); + Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); + // Amount received might be bigger because of internal implementation shit from lightning + Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); + + info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); + Assert.Single(info.NodeURIs); + Assert.NotEqual(0, info.BlockHeight); + } + } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6788b6d00..ab03e923d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -836,92 +836,6 @@ namespace BTCPayServer.Tests .ToArray()); } } - - [Fact(Timeout = 60 * 2 * 1000)] - [Trait("Integration", "Integration")] - [Trait("Lightning", "Lightning")] - public async Task CanUseLightningAPI() - { - using (var tester = ServerTester.Create()) - { - tester.ActivateLightning(); - await tester.StartAsync(); - await tester.EnsureChannelsSetup(); - var user = tester.NewAccount(); - user.GrantAccess(true); - user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); - - var merchant = tester.NewAccount(); - merchant.GrantAccess(true); - merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); - var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}"); - var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60))); - - // The default client is using charge, so we should not be able to query channels - var client = await user.CreateClient("btcpay.server.canuseinternallightningnode"); - var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); - Assert.Contains("503", err.Message); - // Not permission for the store! - err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); - Assert.Contains("403", err.Message); - var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() - { - Amount = LightMoney.Satoshis(1000), - Description = "lol", - Expiry = TimeSpan.FromSeconds(400), - PrivateRouteHints = false - }); - var chargeInvoice = invoiceData; - Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); - - client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}"); - // Not permission for the server - err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); - Assert.Contains("403", err.Message); - - var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); - Assert.Equal(2, data.Count()); - BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); - - invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() - { - Amount = LightMoney.Satoshis(1000), - Description = "lol", - Expiry = TimeSpan.FromSeconds(400), - PrivateRouteHints = false - }); - - Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); - - await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() - { - BOLT11 = merchantInvoice.BOLT11 - }); - await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() - { - BOLT11 = "lol" - })); - - var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() - { - Amount = -1, - Expiry = TimeSpan.FromSeconds(-1), - Description = null - })); - Assert.Equal(2, validationErr.ValidationErrors.Length); - - var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); - Assert.NotNull(invoice.PaidAt); - Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); - // Amount received might be bigger because of internal implementation shit from lightning - Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); - - var info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); - Assert.Single(info.NodeURIs); - Assert.NotEqual(0, info.BlockHeight); - } - } - async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user) { var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice() diff --git a/BTCPayServer/Security/GreenField/APIKeyRepository.cs b/BTCPayServer/Security/GreenField/APIKeyRepository.cs index 94e48e561..278c09fa3 100644 --- a/BTCPayServer/Security/GreenField/APIKeyRepository.cs +++ b/BTCPayServer/Security/GreenField/APIKeyRepository.cs @@ -16,13 +16,14 @@ namespace BTCPayServer.Security.GreenField _applicationDbContextFactory = applicationDbContextFactory; } - public async Task GetKey(string apiKey) + public async Task GetKey(string apiKey, bool includeUser = false) { - using (var context = _applicationDbContextFactory.CreateContext()) + await using var context = _applicationDbContextFactory.CreateContext(); + if (includeUser) { - return await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys, - data => data.Id == apiKey && data.Type != APIKeyType.Legacy); + return await context.ApiKeys.Include(data => data.User).SingleOrDefaultAsync(data => data.Id == apiKey && data.Type != APIKeyType.Legacy); } + return await context.ApiKeys.SingleOrDefaultAsync(data => data.Id == apiKey && data.Type != APIKeyType.Legacy); } public async Task> GetKeys(APIKeyQuery query) diff --git a/BTCPayServer/Security/GreenField/APIKeysAuthenticationHandler.cs b/BTCPayServer/Security/GreenField/APIKeysAuthenticationHandler.cs index df9dd9003..8a22a1600 100644 --- a/BTCPayServer/Security/GreenField/APIKeysAuthenticationHandler.cs +++ b/BTCPayServer/Security/GreenField/APIKeysAuthenticationHandler.cs @@ -40,15 +40,16 @@ namespace BTCPayServer.Security.GreenField if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey)) return AuthenticateResult.NoResult(); - var key = await _apiKeyRepository.GetKey(apiKey); + var key = await _apiKeyRepository.GetKey(apiKey, true); if (key == null) { return AuthenticateResult.Fail("ApiKey authentication failed"); } - List claims = new List(); claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId)); + + claims.AddRange((await _userManager.GetRolesAsync(key.User)).Select(s => new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s))); claims.AddRange(Permission.ToPermissions(key.GetBlob().Permissions).Select(permission => new Claim(GreenFieldConstants.ClaimTypes.Permission, permission.ToString()))); return AuthenticateResult.Success(new AuthenticationTicket( diff --git a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs index ed976092f..53f35b36a 100644 --- a/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs +++ b/BTCPayServer/Security/GreenField/BasicAuthenticationHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; @@ -67,6 +68,7 @@ namespace BTCPayServer.Security.GreenField new Claim(GreenFieldConstants.ClaimTypes.Permission, Permission.Create(Policies.Unrestricted).ToString()) }; + claims.AddRange((await _userManager.GetRolesAsync(user)).Select(s => new Claim(_identityOptions.CurrentValue.ClaimsIdentity.RoleClaimType, s))); return AuthenticateResult.Success(new AuthenticationTicket( new ClaimsPrincipal(new ClaimsIdentity(claims, GreenFieldConstants.AuthenticationType)), diff --git a/BTCPayServer/Services/BTCPayServerEnvironment.cs b/BTCPayServer/Services/BTCPayServerEnvironment.cs index 175c4ccbe..ba8e9624a 100644 --- a/BTCPayServer/Services/BTCPayServerEnvironment.cs +++ b/BTCPayServer/Services/BTCPayServerEnvironment.cs @@ -58,7 +58,7 @@ namespace BTCPayServer.Services { get { - return NetworkType == NetworkType.Regtest && Environment.IsDevelopment(); + return DevelopmentOverride?? NetworkType == NetworkType.Regtest && Environment.IsDevelopment(); } } @@ -87,5 +87,7 @@ namespace BTCPayServer.Services } return txt.ToString(); } + + public bool? DevelopmentOverride; } }