mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-01-28 02:14:23 +01:00
Api keys with openiddict (#1262)
* Remove OpenIddict * Add API Key system * Revert removing OpenIddict * fix rebase * fix tests * pr changes * fix tests * fix apikey test * pr change * fix db * add migration attrs * fix migration error * PR Changes * Fix sqlite migration * change api key to use Authorization Header * add supportAddForeignKey * use tempdata status message * fix add api key css * remove redirect url + app identifier feature :(
This commit is contained in:
295
BTCPayServer/Controllers/ManageController.APIKeys.cs
Normal file
295
BTCPayServer/Controllers/ManageController.APIKeys.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> APIKeys()
|
||||
{
|
||||
return View(new ApiKeysViewModel()
|
||||
{
|
||||
ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
|
||||
{
|
||||
UserId = new[] {_userManager.GetUserId(User)}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RemoveAPIKey(string id)
|
||||
{
|
||||
await _apiKeyRepository.Remove(id, _userManager.GetUserId(User));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "API Key removed"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> AddApiKey()
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel()));
|
||||
}
|
||||
|
||||
[HttpGet("~/api-keys/authorize")]
|
||||
public async Task<IActionResult> AuthorizeAPIKey( string[] permissions, string applicationName = null,
|
||||
bool strict = true, bool selectiveStores = false)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot generate api keys while not on https or tor"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
permissions ??= Array.Empty<string>();
|
||||
|
||||
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
|
||||
{
|
||||
ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement),
|
||||
StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement),
|
||||
PermissionsFormatted = permissions,
|
||||
ApplicationName = applicationName,
|
||||
SelectiveStores = selectiveStores,
|
||||
Strict = strict,
|
||||
});
|
||||
|
||||
vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("~/api-keys/authorize")]
|
||||
public async Task<IActionResult> AuthorizeAPIKey([FromForm] AuthorizeApiKeysViewModel viewModel)
|
||||
{
|
||||
await SetViewModelValues(viewModel);
|
||||
var ar = HandleCommands(viewModel);
|
||||
|
||||
if (ar != null)
|
||||
{
|
||||
return ar;
|
||||
}
|
||||
|
||||
|
||||
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement))
|
||||
{
|
||||
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
|
||||
{
|
||||
viewModel.ServerManagementPermission = false;
|
||||
}
|
||||
|
||||
if (!viewModel.ServerManagementPermission && viewModel.Strict)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.ServerManagementPermission),
|
||||
"This permission is required for this application.");
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
if (!viewModel.SelectiveStores &&
|
||||
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
viewModel.StoreMode = AddApiKeyViewModel.ApiKeyStoreMode.AllStores;
|
||||
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
|
||||
"This application does not allow selective store permissions.");
|
||||
}
|
||||
|
||||
if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
|
||||
"This permission is required for this application.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
switch (viewModel.Command.ToLowerInvariant())
|
||||
{
|
||||
case "no":
|
||||
return RedirectToAction("APIKeys");
|
||||
case "yes":
|
||||
var key = await CreateKey(viewModel);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys", new { key = key.Id});
|
||||
default: return View(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddApiKey(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
await SetViewModelValues(viewModel);
|
||||
|
||||
var ar = HandleCommands(viewModel);
|
||||
|
||||
if (ar != null)
|
||||
{
|
||||
return ar;
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var key = await CreateKey(viewModel);
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code>{key.Id}</code>"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
private IActionResult HandleCommands(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
switch (viewModel.Command)
|
||||
{
|
||||
case "change-store-mode":
|
||||
viewModel.StoreMode = viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
|
||||
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
|
||||
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
|
||||
|
||||
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
|
||||
!viewModel.SpecificStores.Any() && viewModel.Stores.Any())
|
||||
{
|
||||
viewModel.SpecificStores.Add(null);
|
||||
}
|
||||
return View(viewModel);
|
||||
case "add-store":
|
||||
viewModel.SpecificStores.Add(null);
|
||||
return View(viewModel);
|
||||
|
||||
case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase):
|
||||
{
|
||||
ModelState.Clear();
|
||||
var index = int.Parse(
|
||||
viewModel.Command.Substring(
|
||||
viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
|
||||
CultureInfo.InvariantCulture);
|
||||
viewModel.SpecificStores.RemoveAt(index);
|
||||
return View(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<APIKeyData> CreateKey(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User)
|
||||
};
|
||||
key.SetPermissions(GetPermissionsFromViewModel(viewModel));
|
||||
await _apiKeyRepository.CreateKey(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
var permissions = new List<string>();
|
||||
|
||||
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission));
|
||||
}
|
||||
else if (viewModel.StoreManagementPermission)
|
||||
{
|
||||
permissions.Add(APIKeyConstants.Permissions.StoreManagement);
|
||||
}
|
||||
|
||||
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
|
||||
{
|
||||
permissions.Add(APIKeyConstants.Permissions.ServerManagement);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel
|
||||
{
|
||||
viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||
viewModel.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public class AddApiKeyViewModel
|
||||
{
|
||||
public StoreData[] Stores { get; set; }
|
||||
public ApiKeyStoreMode StoreMode { get; set; }
|
||||
public List<string> SpecificStores { get; set; } = new List<string>();
|
||||
public bool IsServerAdmin { get; set; }
|
||||
public bool ServerManagementPermission { get; set; }
|
||||
public bool StoreManagementPermission { get; set; }
|
||||
public string Command { get; set; }
|
||||
|
||||
public enum ApiKeyStoreMode
|
||||
{
|
||||
AllStores,
|
||||
Specific
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel
|
||||
{
|
||||
public string ApplicationName { get; set; }
|
||||
public bool Strict { get; set; }
|
||||
public bool SelectiveStores { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
|
||||
public string[] PermissionsFormatted
|
||||
{
|
||||
get
|
||||
{
|
||||
return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
set
|
||||
{
|
||||
Permissions = string.Join(';', value ?? Array.Empty<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class ApiKeysViewModel
|
||||
{
|
||||
public List<APIKeyData> ApiKeyDatas { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.U2F;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@@ -34,6 +36,8 @@ namespace BTCPayServer.Controllers
|
||||
IWebHostEnvironment _Env;
|
||||
public U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
||||
@@ -48,7 +52,10 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
IWebHostEnvironment env,
|
||||
U2FService u2FService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment)
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IAuthorizationService authorizationService
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
@@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers
|
||||
_Env = env;
|
||||
_u2FService = u2FService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
|
||||
73
BTCPayServer/Controllers/RestApi/TestApiKeyController.cs
Normal file
73
BTCPayServer/Controllers/RestApi/TestApiKeyController.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.RestApi
|
||||
{
|
||||
/// <summary>
|
||||
/// this controller serves as a testing endpoint for our api key unit tests
|
||||
/// </summary>
|
||||
[Route("api/test/apikey")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public class TestApiKeyController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public TestApiKeyController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet("me/id")]
|
||||
public string GetCurrentUserId()
|
||||
{
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
public async Task<ApplicationUser> GetCurrentUser()
|
||||
{
|
||||
return await _userManager.GetUserAsync(User);
|
||||
}
|
||||
|
||||
[HttpGet("me/is-admin")]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool AmIAnAdmin()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[HttpGet("me/stores")]
|
||||
[Authorize(Policy = Policies.CanListStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public async Task<StoreData[]> GetCurrentUserStores()
|
||||
{
|
||||
return await User.GetStores(_userManager, _storeRepository);
|
||||
}
|
||||
|
||||
[HttpGet("me/stores/actions")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool CanDoNonImplicitStoreActions()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("me/stores/{storeId}/can-edit")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
|
||||
public bool CanEdit(string storeId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,15 @@ namespace BTCPayServer.Controllers.RestApi
|
||||
/// <summary>
|
||||
/// this controller serves as a testing endpoint for our OpenId unit tests
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/test/openid")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
public class TestController : ControllerBase
|
||||
public class TestOpenIdController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public TestController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
public TestOpenIdController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
@@ -54,7 +54,6 @@ namespace BTCPayServer.Controllers.RestApi
|
||||
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("me/stores/{storeId}/can-edit")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||
@@ -32,6 +32,7 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.APIKeys;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@@ -241,11 +242,11 @@ namespace BTCPayServer.Hosting
|
||||
services.AddTransient<PaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
// bundling
|
||||
|
||||
services.AddBtcPayServerAuthenticationSchemes(configuration);
|
||||
|
||||
services.AddAPIKeyAuthentication();
|
||||
services.AddBtcPayServerAuthenticationSchemes();
|
||||
services.AddAuthorization(o => o.AddBTCPayPolicies());
|
||||
|
||||
// bundling
|
||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
@@ -292,12 +293,12 @@ namespace BTCPayServer.Hosting
|
||||
return services;
|
||||
}
|
||||
private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB.
|
||||
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services)
|
||||
{
|
||||
services.AddAuthentication()
|
||||
.AddCookie()
|
||||
.AddBitpayAuthentication();
|
||||
.AddBitpayAuthentication()
|
||||
.AddAPIKeyAuthentication();
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
|
||||
56
BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs
Normal file
56
BTCPayServer/Security/APIKeys/APIKeyAuthenticationHandler.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class APIKeyAuthenticationHandler : AuthenticationHandler<APIKeyAuthenticationOptions>
|
||||
{
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IOptionsMonitor<IdentityOptions> _identityOptions;
|
||||
|
||||
public APIKeyAuthenticationHandler(
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IOptionsMonitor<IdentityOptions> identityOptions,
|
||||
IOptionsMonitor<APIKeyAuthenticationOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_identityOptions = identityOptions;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Context.Request.HttpContext.GetAPIKey(out var apiKey) || string.IsNullOrEmpty(apiKey))
|
||||
return AuthenticateResult.NoResult();
|
||||
|
||||
var key = await _apiKeyRepository.GetKey(apiKey);
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
return AuthenticateResult.Fail("ApiKey authentication failed");
|
||||
}
|
||||
|
||||
List<Claim> claims = new List<Claim>();
|
||||
|
||||
claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId));
|
||||
claims.AddRange(key.GetPermissions()
|
||||
.Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permissions, permission)));
|
||||
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace BTCPayServer.Security.Bitpay
|
||||
{
|
||||
public class APIKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
}
|
||||
}
|
||||
84
BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs
Normal file
84
BTCPayServer/Security/APIKeys/APIKeyAuthorizationHandler.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class APIKeyAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
|
||||
|
||||
{
|
||||
private readonly HttpContext _HttpContext;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public APIKeyAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_HttpContext = httpContextAccessor.HttpContext;
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
PolicyRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity.AuthenticationType != APIKeyConstants.AuthenticationType)
|
||||
return;
|
||||
|
||||
bool success = false;
|
||||
switch (requirement.Policy)
|
||||
{
|
||||
case Policies.CanListStoreSettings.Key:
|
||||
var selectiveStorePermissions =
|
||||
APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions());
|
||||
success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) ||
|
||||
selectiveStorePermissions.Any();
|
||||
break;
|
||||
case Policies.CanModifyStoreSettings.Key:
|
||||
string storeId = _HttpContext.GetImplicitStoreId();
|
||||
if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) &&
|
||||
!context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId)))
|
||||
break;
|
||||
|
||||
if (storeId == null)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userid = _userManager.GetUserId(context.User);
|
||||
if (string.IsNullOrEmpty(userid))
|
||||
break;
|
||||
var store = await _storeRepository.FindStore((string)storeId, userid);
|
||||
if (store == null)
|
||||
break;
|
||||
success = true;
|
||||
_HttpContext.SetStoreData(store);
|
||||
}
|
||||
|
||||
break;
|
||||
case Policies.CanModifyServerSettings.Key:
|
||||
if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement))
|
||||
break;
|
||||
// For this authorization, we stil check in database because it is super sensitive.
|
||||
var user = await _userManager.GetUserAsync(context.User);
|
||||
if (user == null)
|
||||
break;
|
||||
if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
|
||||
break;
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
BTCPayServer/Security/APIKeys/APIKeyConstants.cs
Normal file
36
BTCPayServer/Security/APIKeys/APIKeyConstants.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public static class APIKeyConstants
|
||||
{
|
||||
public const string AuthenticationType = "APIKey";
|
||||
|
||||
public static class ClaimTypes
|
||||
{
|
||||
public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions);
|
||||
}
|
||||
|
||||
public static class Permissions
|
||||
{
|
||||
public const string ServerManagement = nameof(ServerManagement);
|
||||
public const string StoreManagement = nameof(StoreManagement);
|
||||
|
||||
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
|
||||
{
|
||||
{StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")},
|
||||
{$"{nameof(StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")},
|
||||
{ServerManagement, ("Manage your server", "The app will have total control on your server")},
|
||||
};
|
||||
|
||||
public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}";
|
||||
|
||||
public static IEnumerable<string> ExtractStorePermissionsIds(IEnumerable<string> permissions) => permissions
|
||||
.Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture))
|
||||
.Select(s => s.Split(":")[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
BTCPayServer/Security/APIKeys/APIKeyExtensions.cs
Normal file
75
BTCPayServer/Security/APIKeys/APIKeyExtensions.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public static class APIKeyExtensions
|
||||
{
|
||||
public static bool GetAPIKey(this HttpContext httpContext, out StringValues apiKey)
|
||||
{
|
||||
if (httpContext.Request.Headers.TryGetValue("Authorization", out var value) &&
|
||||
value.ToString().StartsWith("token ", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
apiKey = value.ToString().Substring("token ".Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Task<StoreData[]> GetStores(this ClaimsPrincipal claimsPrincipal,
|
||||
UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||
{
|
||||
var permissions =
|
||||
claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions)
|
||||
.Select(claim => claim.Value).ToList();
|
||||
|
||||
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal));
|
||||
}
|
||||
|
||||
var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions);
|
||||
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds);
|
||||
}
|
||||
|
||||
public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
|
||||
{
|
||||
builder.AddScheme<APIKeyAuthenticationOptions, APIKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey,
|
||||
o => { });
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAPIKeyAuthentication(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<APIKeyRepository>();
|
||||
serviceCollection.AddScoped<IAuthorizationHandler, APIKeyAuthorizationHandler>();
|
||||
return serviceCollection;
|
||||
}
|
||||
|
||||
public static string[] GetPermissions(this AuthorizationHandlerContext context)
|
||||
{
|
||||
return context.User.Claims.Where(c =>
|
||||
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(claim => claim.Value).ToArray();
|
||||
}
|
||||
|
||||
public static bool HasPermissions(this AuthorizationHandlerContext context, params string[] scopes)
|
||||
{
|
||||
return scopes.All(s => context.User.HasClaim(c =>
|
||||
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
c.Value.Split(' ').Contains(s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
72
BTCPayServer/Security/APIKeys/APIKeyRepository.cs
Normal file
72
BTCPayServer/Security/APIKeys/APIKeyRepository.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Security.APIKeys
|
||||
{
|
||||
public class APIKeyRepository
|
||||
{
|
||||
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
|
||||
|
||||
public APIKeyRepository(ApplicationDbContextFactory applicationDbContextFactory)
|
||||
{
|
||||
_applicationDbContextFactory = applicationDbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<APIKeyData> GetKey(string apiKey)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
return await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
|
||||
data => data.Id == apiKey && data.Type != APIKeyType.Legacy);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<APIKeyData>> GetKeys(APIKeyQuery query)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var queryable = context.ApiKeys.AsQueryable();
|
||||
if (query?.UserId != null && query.UserId.Any())
|
||||
{
|
||||
queryable = queryable.Where(data => query.UserId.Contains(data.UserId));
|
||||
}
|
||||
|
||||
return await queryable.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateKey(APIKeyData key)
|
||||
{
|
||||
if (key.Type == APIKeyType.Legacy || !string.IsNullOrEmpty(key.StoreId) || string.IsNullOrEmpty(key.UserId))
|
||||
{
|
||||
throw new InvalidOperationException("cannot save a bitpay legacy api key with this repository");
|
||||
}
|
||||
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
await context.ApiKeys.AddAsync(key);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Remove(string id, string getUserId)
|
||||
{
|
||||
using (var context = _applicationDbContextFactory.CreateContext())
|
||||
{
|
||||
var key = await EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(context.ApiKeys,
|
||||
data => data.Id == id && data.UserId == getUserId);
|
||||
context.ApiKeys.Remove(key);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class APIKeyQuery
|
||||
{
|
||||
public string[] UserId { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,6 @@ namespace BTCPayServer.Security
|
||||
public const string Cookie = "Identity.Application";
|
||||
public const string Bitpay = "Bitpay";
|
||||
public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
public const string ApiKey = "GreenfieldApiKey";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,10 @@ namespace BTCPayServer.Security.Bitpay
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync();
|
||||
if (existing.Any())
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
ctx.ApiKeys.RemoveRange(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
@@ -95,7 +95,7 @@ namespace BTCPayServer.Security.Bitpay
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type== APIKeyType.Legacy).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace BTCPayServer.Security
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
options.AddPolicy(CanModifyStoreSettings.Key);
|
||||
options.AddPolicy(CanListStoreSettings.Key);
|
||||
options.AddPolicy(CanCreateInvoice.Key);
|
||||
options.AddPolicy(CanGetRates.Key);
|
||||
options.AddPolicy(CanModifyServerSettings.Key);
|
||||
@@ -30,6 +31,10 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||
}
|
||||
public class CanListStoreSettings
|
||||
{
|
||||
public const string Key = "btcpay.store.canliststoresettings";
|
||||
}
|
||||
public class CanCreateInvoice
|
||||
{
|
||||
public const string Key = "btcpay.store.cancreateinvoice";
|
||||
|
||||
@@ -78,12 +78,12 @@ namespace BTCPayServer.Services.Stores
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId)
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId, IEnumerable<string> storeIds = null)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return (await ctx.UserStore
|
||||
.Where(u => u.ApplicationUserId == userId)
|
||||
.Where(u => u.ApplicationUserId == userId && (storeIds == null || storeIds.Contains(u.StoreDataId)))
|
||||
.Select(u => new { u.StoreData, u.Role })
|
||||
.ToArrayAsync())
|
||||
.Select(u =>
|
||||
|
||||
50
BTCPayServer/Views/Manage/APIKeys.cshtml
Normal file
50
BTCPayServer/Views/Manage/APIKeys.cshtml
Normal file
@@ -0,0 +1,50 @@
|
||||
@model BTCPayServer.Controllers.ManageController.ApiKeysViewModel
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Manage your API Keys");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
<h4>API Keys</h4>
|
||||
<table class="table table-lg">
|
||||
<thead>
|
||||
<tr>
|
||||
<th >Key</th>
|
||||
<th >Permissions</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var keyData in Model.ApiKeyDatas)
|
||||
{
|
||||
<tr>
|
||||
<td>@keyData.Id</td>
|
||||
<td>
|
||||
@if (string.IsNullOrEmpty(keyData.Permissions))
|
||||
{
|
||||
<span>No permissions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@string.Join(", ", keyData.GetPermissions())</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a asp-action="RemoveAPIKey" asp-route-id="@keyData.Id">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.ApiKeyDatas.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2" class="text-center h5 py-2">
|
||||
No API keys
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="bg-gray">
|
||||
<td colspan="3">
|
||||
<a class="btn btn-primary" asp-action="AddApiKey" id="AddApiKey">Generate new key</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
120
BTCPayServer/Views/Manage/AddApiKey.cshtml
Normal file
120
BTCPayServer/Views/Manage/AddApiKey.cshtml
Normal file
@@ -0,0 +1,120 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Security.APIKeys
|
||||
@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel
|
||||
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key");
|
||||
|
||||
string GetDescription(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
|
||||
string GetTitle(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage"/>
|
||||
<p >
|
||||
Generate a new api key to use BTCPay through its API.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form method="post" asp-action="AddApiKey" class="list-group">
|
||||
|
||||
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode"/>
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
@if (Model.IsServerAdmin)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input asp-for="ServerManagementPermission" class="form-check-inline"/>
|
||||
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label>
|
||||
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
|
||||
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p>
|
||||
</div>
|
||||
}
|
||||
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
@Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary<string, string>() {{"class", "form-check-inline"}})
|
||||
|
||||
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label>
|
||||
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
|
||||
</div>
|
||||
}
|
||||
else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
<div class="list-group-item p-0 border-0 mb-2">
|
||||
<li class="list-group-item ">
|
||||
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
|
||||
</li>
|
||||
@if (!Model.Stores.Any())
|
||||
{
|
||||
<li class="list-group-item alert-warning">
|
||||
You currently have no stores configured.
|
||||
</li>
|
||||
}
|
||||
@for (var index = 0; index < Model.SpecificStores.Count; index++)
|
||||
{
|
||||
<div class="list-group-item transaction-output-form p-0 pl-lg-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
|
||||
<div class="form-group my-0">
|
||||
@if (Model.SpecificStores[index] == null)
|
||||
{
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
|
||||
}
|
||||
else
|
||||
{
|
||||
var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]);
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
|
||||
}
|
||||
|
||||
<span asp-validation-for="SpecificStores[index]" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
|
||||
Remove
|
||||
</button>
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.SpecificStores.Count < Model.Stores.Length)
|
||||
{
|
||||
<div class="list-group-item">
|
||||
<button type="submit" name="command" value="add-store" class="ml-1 btn btn-secondary">Add another store </button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" id="Generate">Generate API Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
|
||||
<style>
|
||||
.remove-btn{
|
||||
font-size: 1.5rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.remove-btn:hover{
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
137
BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml
Normal file
137
BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml
Normal file
@@ -0,0 +1,137 @@
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Security.APIKeys
|
||||
@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel
|
||||
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}";
|
||||
|
||||
string GetDescription(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
|
||||
string GetTitle(string permission)
|
||||
{
|
||||
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
|
||||
}
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
<form method="post" asp-action="AuthorizeAPIKey">
|
||||
<input type="hidden" asp-for="Permissions" value="@Model.Permissions"/>
|
||||
<input type="hidden" asp-for="Strict" value="@Model.Strict"/>
|
||||
<input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/>
|
||||
<input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/>
|
||||
<section>
|
||||
<div class="card container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 section-heading">
|
||||
<h2>Authorization Request</h2>
|
||||
<hr class="primary">
|
||||
<p class="mb-1">@(Model.ApplicationName ?? "An application") is requesting access to your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 list-group px-2">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict))
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input asp-for="ServerManagementPermission" class="form-check-inline" readonly="@(Model.Strict || !Model.IsServerAdmin)"/>
|
||||
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label>
|
||||
@if (!Model.IsServerAdmin)
|
||||
{
|
||||
<span class="text-danger">
|
||||
The server management permission is being requested but your account is not an administrator
|
||||
</span>
|
||||
}
|
||||
|
||||
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
|
||||
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||
{
|
||||
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="list-group-item form-group">
|
||||
<input type="checkbox" asp-for="StoreManagementPermission" class="form-check-inline" readonly="@Model.Strict"/>
|
||||
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label>
|
||||
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p>
|
||||
@if (Model.SelectiveStores)
|
||||
{
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific)
|
||||
{
|
||||
<div class="list-group-item p-0 border-0 mb-2">
|
||||
<li class="list-group-item">
|
||||
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5>
|
||||
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p>
|
||||
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
|
||||
</li>
|
||||
@if (!Model.Stores.Any())
|
||||
{
|
||||
<li class="list-group-item alert-warning">
|
||||
You currently have no stores configured.
|
||||
</li>
|
||||
}
|
||||
@for (var index = 0; index < Model.SpecificStores.Count; index++)
|
||||
{
|
||||
<div class="list-group-item transaction-output-form p-0 pl-lg-2">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-10 py-2 ">
|
||||
<div class="form-group my-0">
|
||||
@if (Model.SpecificStores[index] == null)
|
||||
{
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(Model.Stores.Where(data => !Model.SpecificStores.Contains(data.Id)), nameof(StoreData.Id), nameof(StoreData.StoreName)))"></select>
|
||||
}
|
||||
else
|
||||
{
|
||||
var store = Model.Stores.SingleOrDefault(data => data.Id == Model.SpecificStores[index]);
|
||||
<select asp-for="SpecificStores[index]" class="form-control" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
|
||||
}
|
||||
|
||||
<span asp-validation-for="SpecificStores[index]" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
|
||||
Remove
|
||||
</button>
|
||||
<button type="submit" title="Remove Store Permission" name="command" value="@($"remove-store:{index}")"
|
||||
class="d-none d-lg-block remove-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.SpecificStores.Count < Model.Stores.Length)
|
||||
{
|
||||
<div class="list-group-item">
|
||||
<button type="submit" name="command" value="add-store" class="ml-1 btn btn-secondary">Add another store </button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div class="col-lg-12 text-center">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" name="command" id="consent-yes" type="submit" value="Yes">Authorize app</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="consent-no" name="command" type="submit" value="No">Cancel</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
@@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Manage
|
||||
{
|
||||
public enum ManageNavPages
|
||||
{
|
||||
Index, ChangePassword, TwoFactorAuthentication, U2F
|
||||
Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword" id="ChangePassword">Password</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||
<a id="@ManageNavPages.Index.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a id="@ManageNavPages.U2F.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.U2F)" asp-action="U2FAuthentication">U2F Authentication</a>
|
||||
<a id="@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-action="APIKeys">API Keys</a>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user