Move to a Claim based security

This commit is contained in:
nicolas.dorier
2018-04-30 02:33:42 +09:00
parent 3954ce2137
commit 1fc9a1a54b
18 changed files with 235 additions and 118 deletions

View File

@@ -4,6 +4,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
using BTCPayServer.Tests.Mocks; using BTCPayServer.Tests.Mocks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@@ -142,7 +143,7 @@ namespace BTCPayServer.Tests
return _Host.Services.GetRequiredService<T>(); return _Host.Services.GetRequiredService<T>();
} }
public T GetController<T>(string userId = null) where T : Controller public T GetController<T>(string userId = null, string storeId = null) where T : Controller
{ {
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1"); 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) })); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }));
} }
if(storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}
var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory)); var scope = (IServiceScopeFactory)_Host.Services.GetService(typeof(IServiceScopeFactory));
var provider = scope.CreateScope().ServiceProvider; var provider = scope.CreateScope().ServiceProvider;
context.RequestServices = provider; context.RequestServices = provider;

View File

@@ -56,7 +56,7 @@ namespace BTCPayServer.Tests
public T GetController<T>() where T : Controller public T GetController<T>() where T : Controller
{ {
return parent.PayTester.GetController<T>(UserId); return parent.PayTester.GetController<T>(UserId, StoreId);
} }
public async Task<StoresController> CreateStoreAsync() public async Task<StoresController> CreateStoreAsync()
@@ -78,10 +78,10 @@ namespace BTCPayServer.Tests
public async Task RegisterDerivationSchemeAsync(string cryptoCode) public async Task RegisterDerivationSchemeAsync(string cryptoCode)
{ {
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId); var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork); ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); 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; vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm); await store.UpdateStore(StoreId, vm);
@@ -127,7 +127,7 @@ namespace BTCPayServer.Tests
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType) public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{ {
var storeController = parent.PayTester.GetController<StoresController>(UserId); var storeController = this.GetController<StoresController>();
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel() await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{ {
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri : Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :

View File

@@ -306,9 +306,9 @@ namespace BTCPayServer.Tests
tester.Start(); tester.Start();
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()); Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId));
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult()); Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel() var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{ {
@@ -322,7 +322,7 @@ namespace BTCPayServer.Tests
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save", "BTC").GetAwaiter().GetResult()); }, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model); var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId)).Model);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
} }
} }
@@ -468,7 +468,7 @@ namespace BTCPayServer.Tests
acc.Register(); acc.Register();
acc.CreateStore(); acc.CreateStore();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId); var controller = acc.GetController<StoresController>();
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel() var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
{ {
Facade = Facade.Merchant.ToString(), Facade = Facade.Merchant.ToString(),
@@ -685,8 +685,8 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
{ {
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
vm.PreferredExchange = exchange; vm.PreferredExchange = exchange;
storeController.UpdateStore(user.StoreId, vm).Wait(); storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@@ -724,8 +724,8 @@ namespace BTCPayServer.Tests
}, Facade.Merchant); }, Facade.Merchant);
var storeController = tester.PayTester.GetController<StoresController>(user.UserId); var storeController = user.GetController<StoresController>();
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId)).Model;
Assert.Equal(1.0, vm.RateMultiplier); Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5; vm.RateMultiplier = 0.5;
storeController.UpdateStore(user.StoreId, vm).Wait(); storeController.UpdateStore(user.StoreId, vm).Wait();
@@ -963,7 +963,7 @@ namespace BTCPayServer.Tests
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge); user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId).Result).Model); var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId)).Model);
vm.LightningMaxValue = "2 USD"; vm.LightningMaxValue = "2 USD";
vm.OnChainMinValue = "5 USD"; vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result); Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result);

View File

@@ -19,7 +19,7 @@ using BTCPayServer.Logging;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize] [Authorize(AuthenticationSchemes = "Identity.Application")]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class AccountController : Controller public class AccountController : Controller
{ {

View File

@@ -140,6 +140,8 @@ namespace BTCPayServer.Controllers
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner) .Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId)) .SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType) if (type != null && type.Value.ToString() != app.AppType)
return null; return null;
return app; return app;

View File

@@ -22,6 +22,7 @@ using BTCPayServer.Events;
using NBXplorer; using NBXplorer;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -394,7 +395,7 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice() public async Task<IActionResult> 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) if (stores.Count() == 0)
{ {
StatusMessage = "Error: You need to create at least one store before creating a transaction"; StatusMessage = "Error: You need to create at least one store before creating a transaction";
@@ -409,14 +410,19 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model) public async Task<IActionResult> 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) if (!ModelState.IsValid)
{ {
return View(model); return View(model);
} }
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
StatusMessage = null; 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"); ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model); return View(model);
@@ -461,11 +467,6 @@ namespace BTCPayServer.Controllers
} }
} }
private async Task<SelectList> GetStores(string userId, string storeId = null)
{
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
}
[HttpPost] [HttpPost]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]

View File

@@ -24,7 +24,7 @@ using System.Globalization;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize] [Authorize(AuthenticationSchemes = "Identity.Application")]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class ManageController : Controller public class ManageController : Controller
{ {

View File

@@ -19,7 +19,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(Roles = Roles.ServerAdmin)] [Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
public class ServerController : Controller public class ServerController : Controller
{ {
private UserManager<ApplicationUser> _UserManager; private UserManager<ApplicationUser> _UserManager;

View File

@@ -21,9 +21,9 @@ namespace BTCPayServer.Controllers
{ {
[HttpGet] [HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")] [Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> 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) if (store == null)
return NotFound(); return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
@@ -60,7 +60,7 @@ namespace BTCPayServer.Controllers
{ {
vm.ServerUrl = GetStoreUrl(storeId); vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode; vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
@@ -188,7 +188,7 @@ namespace BTCPayServer.Controllers
{ {
if (!HttpContext.WebSockets.IsWebSocketRequest) if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound(); return NotFound();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();

View File

@@ -19,9 +19,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/lightning/{cryptoCode}")] [Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> 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) if (store == null)
return NotFound(); return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel(); LightningNodeViewModel vm = new LightningNodeViewModel();
@@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{ {
vm.CryptoCode = cryptoCode; vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);

View File

@@ -4,6 +4,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@@ -28,7 +29,7 @@ namespace BTCPayServer.Controllers
{ {
[Route("stores")] [Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")] [Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = StorePolicies.OwnStore)] [Authorize(Policy = Policies.CanModifyStoreSettings.Key)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
public partial class StoresController : Controller public partial class StoresController : Controller
{ {
@@ -93,9 +94,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")] [Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> 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) if (store == null)
return NotFound(); return NotFound();
WalletModel model = new WalletModel(); WalletModel model = new WalletModel();
@@ -164,7 +165,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId) public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
{ {
StoreUsersViewModel vm = new StoreUsersViewModel(); StoreUsersViewModel vm = new StoreUsersViewModel();
var store = await _Repo.FindStore(storeId, userId); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var user = await _UserManager.FindByIdAsync(userId); var user = await _UserManager.FindByIdAsync(userId);
@@ -173,7 +174,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel() return View("Confirm", new ConfirmModel()
{ {
Title = $"Remove store user", 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" Action = "Delete"
}); });
} }
@@ -189,9 +190,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/checkout")] [Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId) public IActionResult CheckoutExperience(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var storeBlob = store.GetStoreBlob(); 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) if (store == null)
return NotFound(); return NotFound();
bool needUpdate = false; bool needUpdate = false;
@@ -271,9 +272,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}")] [Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId) public IActionResult UpdateStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
@@ -338,7 +339,7 @@ namespace BTCPayServer.Controllers
} }
if (model.PreferredExchange != null) if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant(); model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
AddPaymentMethods(store, model); AddPaymentMethods(store, model);
@@ -450,10 +451,10 @@ namespace BTCPayServer.Controllers
var userId = GetUserId(); var userId = GetUserId();
if (userId == null) if (userId == null)
return Unauthorized(); return Unauthorized();
var store = await _Repo.FindStore(storeId, userId); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return Unauthorized(); 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"; StatusMessage = "Error: You need to be owner of this store to request pairing codes";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
@@ -535,7 +536,7 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/tokens/apikey")] [Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey(string storeId) public async Task<IActionResult> GenerateAPIKey(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
await _TokenRepository.GenerateLegacyAPIKey(storeId); await _TokenRepository.GenerateLegacyAPIKey(storeId);
@@ -585,7 +586,7 @@ namespace BTCPayServer.Controllers
if (store == null || pairing == null) if (store == null || pairing == null)
return NotFound(); 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"; StatusMessage = "Error: You can't approve a pairing without being owner of the store";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -37,9 +38,9 @@ namespace BTCPayServer.Controllers
} }
[HttpGet] [HttpGet]
[Route("{storeId}/delete")] [Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId) public IActionResult DeleteStore(string storeId)
{ {
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
return View("Confirm", new ConfirmModel() return View("Confirm", new ConfirmModel()
@@ -67,7 +68,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> DeleteStorePost(string storeId) public async Task<IActionResult> DeleteStorePost(string storeId)
{ {
var userId = GetUserId(); var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId()); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
await _Repo.RemoveStore(storeId, userId); await _Repo.RemoveStore(storeId, userId);
@@ -102,8 +103,8 @@ namespace BTCPayServer.Controllers
Id = store.Id, Id = store.Id,
Name = store.StoreName, Name = store.StoreName,
WebSite = store.StoreWebsite, WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner, IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>() Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
}); });
} }
return View(result); return View(result);

View File

@@ -15,6 +15,9 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
using System.Security.Claims;
using BTCPayServer.Security;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@@ -152,10 +155,35 @@ namespace BTCPayServer.Data
} }
[NotMapped] [NotMapped]
[Obsolete]
public string Role public string Role
{ {
get; set; get; set;
} }
public Claim[] GetClaims()
{
List<Claim> claims = new List<Claim>();
#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 public byte[] StoreBlob
{ {
get; get;

View File

@@ -38,55 +38,13 @@ using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers; using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Security;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
public static class BTCPayServerServices 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<OwnStoreAuthorizationRequirement>
{
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
public OwnStoreHandler(StoreRepository storeRepository, UserManager<ApplicationUser> 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) public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
{ {
services.AddDbContext<ApplicationDbContext>((provider, o) => services.AddDbContext<ApplicationDbContext>((provider, o) =>
@@ -160,6 +118,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>(); services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>(); services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>(); services.AddSingleton<IHostedService, RatesHostedService>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>(); services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o => services.TryAddSingleton<Bitpay>(o =>
@@ -172,27 +131,14 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>(); services.TryAddSingleton<IRateProviderFactory, BTCPayRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>(); services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>(); services.AddTransient<InvoiceController>();
// Add application services. // Add application services.
services.AddTransient<IEmailSender, EmailSender>(); services.AddTransient<IEmailSender, EmailSender>();
services.AddAuthorization(o =>
{
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
});
o.AddPolicy(StorePolicies.OwnStore, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
});
});
// bundling // bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
services.AddBundles(); services.AddBundles();
services.AddTransient<BundleOptions>(provider => services.AddTransient<BundleOptions>(provider =>
{ {

View File

@@ -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<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
public BTCPayClaimsFilter(
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
}
void IConfigureOptions<MvcOptions>.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<ApplicationUser>
{
IHttpContextAccessor httpContext;
StoreRepository _StoreRepository;
public BTCPayClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
IHttpContextAccessor httpContext,
StoreRepository storeRepository,
IOptions<IdentityOptions> options) : base(userManager, options)
{
this.httpContext = httpContext;
_StoreRepository = storeRepository;
}
public override async Task<ClaimsPrincipal> 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;
}
}
}

View File

@@ -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";
}
}
}

View File

@@ -44,7 +44,9 @@ namespace BTCPayServer.Services.Stores
}).ToArrayAsync()) }).ToArrayAsync())
.Select(us => .Select(us =>
{ {
#pragma warning disable CS0612 // Type or member is obsolete
us.Store.Role = us.Role; us.Store.Role = us.Role;
#pragma warning restore CS0612 // Type or member is obsolete
return us.Store; return us.Store;
}).FirstOrDefault(); }).FirstOrDefault();
} }
@@ -84,7 +86,9 @@ namespace BTCPayServer.Services.Stores
.ToArrayAsync()) .ToArrayAsync())
.Select(u => .Select(u =>
{ {
#pragma warning disable CS0612 // Type or member is obsolete
u.StoreData.Role = u.Role; u.StoreData.Role = u.Role;
#pragma warning restore CS0612 // Type or member is obsolete
return u.StoreData; return u.StoreData;
}).ToArray(); }).ToArray();
} }

View File

@@ -5,11 +5,6 @@ using System.Threading.Tasks;
namespace BTCPayServer namespace BTCPayServer
{ {
public class StorePolicies
{
public const string CanAccessStores = "CanAccessStore";
public const string OwnStore = "OwnStore";
}
public class StoreRoles public class StoreRoles
{ {
public const string Owner = "Owner"; public const string Owner = "Owner";