diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 980b57cf6..faec08309 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -4,6 +4,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; +using BTCPayServer.Services.Stores; using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Mocks; using Microsoft.AspNetCore.Hosting; @@ -142,7 +143,7 @@ namespace BTCPayServer.Tests return _Host.Services.GetRequiredService(); } - public T GetController(string userId = null) where T : Controller + public T GetController(string userId = null, string storeId = null) where T : Controller { var context = new DefaultHttpContext(); context.Request.Host = new HostString("127.0.0.1"); @@ -152,6 +153,10 @@ namespace BTCPayServer.Tests { context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) })); } + if(storeId != null) + { + context.SetStoreData(GetService().FindStore(storeId, userId).GetAwaiter().GetResult()); + } var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var provider = scope.CreateScope().ServiceProvider; context.RequestServices = provider; diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index dd886b407..7c62bc233 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -56,7 +56,7 @@ namespace BTCPayServer.Tests public T GetController() where T : Controller { - return parent.PayTester.GetController(UserId); + return parent.PayTester.GetController(UserId, StoreId); } public async Task CreateStoreAsync() @@ -78,10 +78,10 @@ namespace BTCPayServer.Tests public async Task RegisterDerivationSchemeAsync(string cryptoCode) { SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); - var store = parent.PayTester.GetController(UserId); + var store = parent.PayTester.GetController(UserId, StoreId); ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); - var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model; + var vm = (StoreViewModel)((ViewResult)store.UpdateStore(StoreId)).Model; vm.SpeedPolicy = SpeedPolicy.MediumSpeed; await store.UpdateStore(StoreId, vm); @@ -127,7 +127,7 @@ namespace BTCPayServer.Tests public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType) { - var storeController = parent.PayTester.GetController(UserId); + var storeController = this.GetController(); await storeController.AddLightningNode(StoreId, new LightningNodeViewModel() { Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri : diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 1b7eccc60..f174becf1 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -306,9 +306,9 @@ namespace BTCPayServer.Tests tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); - var storeController = tester.PayTester.GetController(user.UserId); - Assert.IsType(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()); - Assert.IsType(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult()); + var storeController = user.GetController(); + Assert.IsType(storeController.UpdateStore(user.StoreId)); + Assert.IsType(storeController.AddLightningNode(user.StoreId, "BTC")); var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() { @@ -322,7 +322,7 @@ namespace BTCPayServer.Tests Url = tester.MerchantCharge.Client.Uri.AbsoluteUri }, "save", "BTC").GetAwaiter().GetResult()); - var storeVm = Assert.IsType(Assert.IsType(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model); + var storeVm = Assert.IsType(Assert.IsType(storeController.UpdateStore(user.StoreId)).Model); Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); } } @@ -468,7 +468,7 @@ namespace BTCPayServer.Tests acc.Register(); acc.CreateStore(); - var controller = tester.PayTester.GetController(acc.UserId); + var controller = acc.GetController(); var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() { Facade = Facade.Merchant.ToString(), @@ -685,8 +685,8 @@ namespace BTCPayServer.Tests private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) { - var storeController = tester.PayTester.GetController(user.UserId); - var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + var storeController = user.GetController(); + var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model; vm.PreferredExchange = exchange; storeController.UpdateStore(user.StoreId, vm).Wait(); var invoice2 = user.BitPay.CreateInvoice(new Invoice() @@ -724,8 +724,8 @@ namespace BTCPayServer.Tests }, Facade.Merchant); - var storeController = tester.PayTester.GetController(user.UserId); - var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + var storeController = user.GetController(); + var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model; Assert.Equal(1.0, vm.RateMultiplier); vm.RateMultiplier = 0.5; storeController.UpdateStore(user.StoreId, vm).Wait(); @@ -963,7 +963,7 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("BTC"); user.RegisterLightningNode("BTC", LightningConnectionType.Charge); - var vm = Assert.IsType(Assert.IsType(user.GetController().CheckoutExperience(user.StoreId).Result).Model); + var vm = Assert.IsType(Assert.IsType(user.GetController().CheckoutExperience(user.StoreId)).Model); vm.LightningMaxValue = "2 USD"; vm.OnChainMinValue = "5 USD"; Assert.IsType(user.GetController().CheckoutExperience(user.StoreId, vm).Result); diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 26c291ace..ddf40e28f 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -19,7 +19,7 @@ using BTCPayServer.Logging; namespace BTCPayServer.Controllers { - [Authorize] + [Authorize(AuthenticationSchemes = "Identity.Application")] [Route("[controller]/[action]")] public class AccountController : Controller { diff --git a/BTCPayServer/Controllers/AppsController.cs b/BTCPayServer/Controllers/AppsController.cs index 5e79741fc..92cce3217 100644 --- a/BTCPayServer/Controllers/AppsController.cs +++ b/BTCPayServer/Controllers/AppsController.cs @@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) .FirstOrDefaultAsync(); + if (app == null) + return null; if (type != null && type.Value.ToString() != app.AppType) return null; return app; diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 356ed2486..1daf39f1e 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -22,6 +22,7 @@ using BTCPayServer.Events; using NBXplorer; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Security; namespace BTCPayServer.Controllers { @@ -394,7 +395,7 @@ namespace BTCPayServer.Controllers [BitpayAPIConstraint(false)] public async Task CreateInvoice() { - var stores = await GetStores(GetUserId()); + var stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id), nameof(StoreData.StoreName), null); if (stores.Count() == 0) { StatusMessage = "Error: You need to create at least one store before creating a transaction"; @@ -409,14 +410,19 @@ namespace BTCPayServer.Controllers [BitpayAPIConstraint(false)] public async Task CreateInvoice(CreateInvoiceModel model) { - model.Stores = await GetStores(GetUserId(), model.StoreId); + var stores = await _StoreRepository.GetStoresByUserId(GetUserId()); + model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId); + var store = stores.FirstOrDefault(s => s.Id == model.StoreId); + if(store == null) + { + ModelState.AddModelError(nameof(model.StoreId), "Store not found"); + } if (!ModelState.IsValid) { return View(model); } - var store = await _StoreRepository.FindStore(model.StoreId, GetUserId()); StatusMessage = null; - if (store.Role != StoreRoles.Owner) + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice"); return View(model); @@ -461,11 +467,6 @@ namespace BTCPayServer.Controllers } } - private async Task GetStores(string userId, string storeId = null) - { - return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId); - } - [HttpPost] [Authorize(AuthenticationSchemes = "Identity.Application")] [BitpayAPIConstraint(false)] diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index 800e1a3d3..44daccb46 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -24,7 +24,7 @@ using System.Globalization; namespace BTCPayServer.Controllers { - [Authorize] + [Authorize(AuthenticationSchemes = "Identity.Application")] [Route("[controller]/[action]")] public class ManageController : Controller { diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 08c41e878..71bdb98d6 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -19,7 +19,7 @@ using System.Threading.Tasks; namespace BTCPayServer.Controllers { - [Authorize(Roles = Roles.ServerAdmin)] + [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)] public class ServerController : Controller { private UserManager _UserManager; diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 0bb73d207..6a480a39a 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -21,9 +21,9 @@ namespace BTCPayServer.Controllers { [HttpGet] [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, string cryptoCode) + public IActionResult AddDerivationScheme(string storeId, string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); @@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers { vm.ServerUrl = GetStoreUrl(storeId); vm.CryptoCode = cryptoCode; - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); @@ -188,7 +188,7 @@ namespace BTCPayServer.Controllers { if (!HttpContext.WebSockets.IsWebSocketRequest) return NotFound(); - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 546c2d757..1b51b51b4 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/lightning/{cryptoCode}")] - public async Task AddLightningNode(string storeId, string cryptoCode) + public IActionResult AddLightningNode(string storeId, string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); LightningNodeViewModel vm = new LightningNodeViewModel(); @@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers public async Task AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) { vm.CryptoCode = cryptoCode; - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 69b911d5c..f97defb01 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -4,6 +4,7 @@ using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; @@ -28,7 +29,7 @@ namespace BTCPayServer.Controllers { [Route("stores")] [Authorize(AuthenticationSchemes = "Identity.Application")] - [Authorize(Policy = StorePolicies.OwnStore)] + [Authorize(Policy = Policies.CanModifyStoreSettings.Key)] [AutoValidateAntiforgeryToken] public partial class StoresController : Controller { @@ -93,9 +94,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/wallet/{cryptoCode}")] - public async Task Wallet(string storeId, string cryptoCode) + public IActionResult Wallet(string storeId, string cryptoCode) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); WalletModel model = new WalletModel(); @@ -164,7 +165,7 @@ namespace BTCPayServer.Controllers public async Task DeleteStoreUser(string storeId, string userId) { StoreUsersViewModel vm = new StoreUsersViewModel(); - var store = await _Repo.FindStore(storeId, userId); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var user = await _UserManager.FindByIdAsync(userId); @@ -173,7 +174,7 @@ namespace BTCPayServer.Controllers return View("Confirm", new ConfirmModel() { Title = $"Remove store user", - Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?", + Description = $"Are you sure to remove access to remove access to {user.Email}?", Action = "Delete" }); } @@ -189,9 +190,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/checkout")] - public async Task CheckoutExperience(string storeId) + public IActionResult CheckoutExperience(string storeId) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var storeBlob = store.GetStoreBlob(); @@ -229,7 +230,7 @@ namespace BTCPayServer.Controllers } } - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); bool needUpdate = false; @@ -271,9 +272,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}")] - public async Task UpdateStore(string storeId) + public IActionResult UpdateStore(string storeId) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); @@ -338,7 +339,7 @@ namespace BTCPayServer.Controllers } if (model.PreferredExchange != null) model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); AddPaymentMethods(store, model); @@ -450,10 +451,10 @@ namespace BTCPayServer.Controllers var userId = GetUserId(); if (userId == null) return Unauthorized(); - var store = await _Repo.FindStore(storeId, userId); + var store = HttpContext.GetStoreData(); if (store == null) return Unauthorized(); - if (store.Role != StoreRoles.Owner) + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { StatusMessage = "Error: You need to be owner of this store to request pairing codes"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); @@ -535,7 +536,7 @@ namespace BTCPayServer.Controllers [Route("{storeId}/tokens/apikey")] public async Task GenerateAPIKey(string storeId) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); await _TokenRepository.GenerateLegacyAPIKey(storeId); @@ -585,7 +586,7 @@ namespace BTCPayServer.Controllers if (store == null || pairing == null) return NotFound(); - if (store.Role != StoreRoles.Owner) + if (!store.HasClaim(Policies.CanModifyStoreSettings.Key)) { StatusMessage = "Error: You can't approve a pairing without being owner of the store"; return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); diff --git a/BTCPayServer/Controllers/UserStoresController.cs b/BTCPayServer/Controllers/UserStoresController.cs index b0000ee07..8f8d87e1c 100644 --- a/BTCPayServer/Controllers/UserStoresController.cs +++ b/BTCPayServer/Controllers/UserStoresController.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Security; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; @@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers } [HttpGet] [Route("{storeId}/delete")] - public async Task DeleteStore(string storeId) + public IActionResult DeleteStore(string storeId) { - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); return View("Confirm", new ConfirmModel() @@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers public async Task DeleteStorePost(string storeId) { var userId = GetUserId(); - var store = await _Repo.FindStore(storeId, GetUserId()); + var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); await _Repo.RemoveStore(storeId, userId); @@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers Id = store.Id, Name = store.StoreName, WebSite = store.StoreWebsite, - IsOwner = store.Role == StoreRoles.Owner, - Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty() + IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key), + Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty() }); } return View(result); diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 52c19e26a..1446e5ad6 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -15,6 +15,9 @@ using BTCPayServer.Services.Rates; using BTCPayServer.Payments; using BTCPayServer.JsonConverters; using System.ComponentModel.DataAnnotations; +using BTCPayServer.Services; +using System.Security.Claims; +using BTCPayServer.Security; namespace BTCPayServer.Data { @@ -152,10 +155,35 @@ namespace BTCPayServer.Data } [NotMapped] + [Obsolete] public string Role { get; set; } + + public Claim[] GetClaims() + { + List claims = new List(); +#pragma warning disable CS0612 // Type or member is obsolete + var role = Role; +#pragma warning restore CS0612 // Type or member is obsolete + if (role == StoreRoles.Owner) + { + claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id)); + claims.Add(new Claim(Policies.CanUseStore.Key, Id)); + } + if (role == StoreRoles.Guest) + { + claims.Add(new Claim(Policies.CanUseStore.Key, Id)); + } + return claims.ToArray(); + } + + public bool HasClaim(string claim) + { + return GetClaims().Any(c => c.Type == claim); + } + public byte[] StoreBlob { get; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index f2ce5b78a..9f2052fbc 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -38,55 +38,13 @@ using Microsoft.Extensions.Caching.Memory; using BTCPayServer.Logging; using BTCPayServer.HostedServices; using Meziantou.AspNetCore.BundleTagHelpers; +using System.Security.Claims; +using BTCPayServer.Security; namespace BTCPayServer.Hosting { public static class BTCPayServerServices { - public class OwnStoreAuthorizationRequirement : IAuthorizationRequirement - { - public OwnStoreAuthorizationRequirement() - { - } - - public OwnStoreAuthorizationRequirement(string role) - { - Role = role; - } - - public string Role - { - get; set; - } - } - - public class OwnStoreHandler : AuthorizationHandler - { - StoreRepository _StoreRepository; - UserManager _UserManager; - public OwnStoreHandler(StoreRepository storeRepository, UserManager userManager) - { - _StoreRepository = storeRepository; - _UserManager = userManager; - } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OwnStoreAuthorizationRequirement requirement) - { - object storeId = null; - if (!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId)) - context.Succeed(requirement); - else if (storeId != null) - { - var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User); - if (user != null) - { - var store = await _StoreRepository.FindStore((string)storeId, user); - if (store != null) - if (requirement.Role == null || requirement.Role == store.Role) - context.Succeed(requirement); - } - } - } - } public static IServiceCollection AddBTCPayServer(this IServiceCollection services) { services.AddDbContext((provider, o) => @@ -160,6 +118,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient, BTCPayClaimsFilter>(); services.TryAddSingleton(); services.TryAddSingleton(o => @@ -172,27 +131,14 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddScoped(); - services.TryAddSingleton(); services.AddTransient(); services.AddTransient(); // Add application services. services.AddTransient(); - - services.AddAuthorization(o => - { - o.AddPolicy(StorePolicies.CanAccessStores, builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement()); - }); - - o.AddPolicy(StorePolicies.OwnStore, builder => - { - builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner)); - }); - }); - // bundling + services.AddAuthorization(o => Policies.AddBTCPayPolicies(o)); + services.AddBundles(); services.AddTransient(provider => { diff --git a/BTCPayServer/Security/BTCPayClaimsPrincipalFactory.cs b/BTCPayServer/Security/BTCPayClaimsPrincipalFactory.cs new file mode 100644 index 000000000..1a0cdb47b --- /dev/null +++ b/BTCPayServer/Security/BTCPayClaimsPrincipalFactory.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using BTCPayServer.Models; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Options; + +namespace BTCPayServer.Security +{ + + public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions + { + UserManager _UserManager; + StoreRepository _StoreRepository; + public BTCPayClaimsFilter( + UserManager userManager, + StoreRepository storeRepository) + { + _UserManager = userManager; + _StoreRepository = storeRepository; + } + + void IConfigureOptions.Configure(MvcOptions options) + { + options.Filters.Add(typeof(BTCPayClaimsFilter)); + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var principal = context.HttpContext.User; + if (!context.HttpContext.GetIsBitpayAPI()) + { + var identity = ((ClaimsIdentity)principal.Identity); + if (principal.IsInRole(Roles.ServerAdmin)) + { + identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); + } + if (context.RouteData.Values.TryGetValue("storeId", out var storeId)) + { + var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + if (claim != null) + { + var store = await _StoreRepository.FindStore((string)storeId, claim.Value); + context.HttpContext.SetStoreData(store); + if (store != null) + { + identity.AddClaims(store.GetClaims()); + } + } + } + } + } + } + public class BTCPayClaimsPrincipalFactory : UserClaimsPrincipalFactory + { + IHttpContextAccessor httpContext; + StoreRepository _StoreRepository; + public BTCPayClaimsPrincipalFactory( + UserManager userManager, + IHttpContextAccessor httpContext, + StoreRepository storeRepository, + IOptions options) : base(userManager, options) + { + this.httpContext = httpContext; + _StoreRepository = storeRepository; + } + + public override async Task CreateAsync(ApplicationUser user) + { + var ctx = (IActionContextAccessor)httpContext.HttpContext.RequestServices.GetService(typeof(IActionContextAccessor)); + var principal = await base.CreateAsync(user); + if (ctx.ActionContext.HttpContext.GetIsBitpayAPI()) + return principal; + var identity = ((ClaimsIdentity)principal.Identity); + if (principal.IsInRole(Roles.ServerAdmin)) + { + identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true")); + } + if (ctx.ActionContext.RouteData.Values.TryGetValue("storeId", out var storeId)) + { + var store = await _StoreRepository.FindStore((string)storeId, await UserManager.GetUserIdAsync(user)); + if (store != null) + { + identity.AddClaims(store.GetClaims()); + } + } + return principal; + } + } +} diff --git a/BTCPayServer/Security/Policies.cs b/BTCPayServer/Security/Policies.cs new file mode 100644 index 000000000..cc13921da --- /dev/null +++ b/BTCPayServer/Security/Policies.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace BTCPayServer.Security +{ + public static class Policies + { + public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) + { + AddClaim(options, CanUseStore.Key); + AddClaim(options, CanModifyStoreSettings.Key); + AddClaim(options, CanModifyServerSettings.Key); + return options; + } + + private static void AddClaim(AuthorizationOptions options, string key) + { + options.AddPolicy(key, o => o.RequireClaim(key)); + } + + public class CanModifyServerSettings + { + public const string Key = "btcpay.store.canmodifyserversettings"; + } + public class CanUseStore + { + public const string Key = "btcpay.store.canusestore"; + } + public class CanModifyStoreSettings + { + public const string Key = "btcpay.store.canmodifystoresettings"; + } + } +} diff --git a/BTCPayServer/Services/Stores/StoreRepository.cs b/BTCPayServer/Services/Stores/StoreRepository.cs index f938f4bef..9f4e0fbee 100644 --- a/BTCPayServer/Services/Stores/StoreRepository.cs +++ b/BTCPayServer/Services/Stores/StoreRepository.cs @@ -44,7 +44,9 @@ namespace BTCPayServer.Services.Stores }).ToArrayAsync()) .Select(us => { +#pragma warning disable CS0612 // Type or member is obsolete us.Store.Role = us.Role; +#pragma warning restore CS0612 // Type or member is obsolete return us.Store; }).FirstOrDefault(); } @@ -84,7 +86,9 @@ namespace BTCPayServer.Services.Stores .ToArrayAsync()) .Select(u => { +#pragma warning disable CS0612 // Type or member is obsolete u.StoreData.Role = u.Role; +#pragma warning restore CS0612 // Type or member is obsolete return u.StoreData; }).ToArray(); } diff --git a/BTCPayServer/StorePolicies.cs b/BTCPayServer/StorePolicies.cs index 876e58700..76957920e 100644 --- a/BTCPayServer/StorePolicies.cs +++ b/BTCPayServer/StorePolicies.cs @@ -5,11 +5,6 @@ using System.Threading.Tasks; namespace BTCPayServer { - public class StorePolicies - { - public const string CanAccessStores = "CanAccessStore"; - public const string OwnStore = "OwnStore"; - } public class StoreRoles { public const string Owner = "Owner";