diff --git a/BTCPayServer.Data/Data/APIKeyData.cs b/BTCPayServer.Data/Data/APIKeyData.cs index 715d3f2ec..c538a9bd9 100644 --- a/BTCPayServer.Data/Data/APIKeyData.cs +++ b/BTCPayServer.Data/Data/APIKeyData.cs @@ -26,6 +26,7 @@ namespace BTCPayServer.Data public StoreData StoreData { get; set; } public ApplicationUser User { get; set; } + public string Label { get; set; } public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; } public void SetPermissions(IEnumerable permissions) diff --git a/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs index 78f323529..d356af760 100644 --- a/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs +++ b/BTCPayServer.Data/Migrations/20200224134444_Remove_OpenIddict.cs @@ -1,8 +1,12 @@ using System; +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; namespace BTCPayServer.Migrations { + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200224134444_Remove_OpenIddict")] public partial class Remove_OpenIddict : Migration { protected override void Up(MigrationBuilder migrationBuilder) diff --git a/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs new file mode 100644 index 000000000..332c7a48f --- /dev/null +++ b/BTCPayServer.Data/Migrations/20200225133433_AddApiKeyLabel.cs @@ -0,0 +1,27 @@ +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200225133433_AddApiKeyLabel")] + public partial class AddApiKeyLabel : Migration + { + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Label", + table: "ApiKeys", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Label", + table: "ApiKeys"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index b2a26241a..3ee8f2a07 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,9 @@ namespace BTCPayServer.Migrations .HasColumnType("TEXT") .HasMaxLength(50); + b.Property("Label") + .HasColumnType("TEXT"); + b.Property("Permissions") .HasColumnType("TEXT"); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 749d9db8f..ae972afda 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -48,6 +48,7 @@ + diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index 26ff2aeec..23052fe9b 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -4,13 +4,13 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Data; +using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Models; using BTCPayServer.Security; using BTCPayServer.Security.APIKeys; -using ExchangeSharp; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; +using NSwag.Annotations; namespace BTCPayServer.Controllers { @@ -28,9 +28,33 @@ namespace BTCPayServer.Controllers }); } - [HttpGet] + + + [HttpGet("api-keys/{id}/delete")] public async Task RemoveAPIKey(string id) { + var key = await _apiKeyRepository.GetKey(id); + if (key == null || key.UserId != _userManager.GetUserId(User)) + { + return NotFound(); + } + return View("Confirm", new ConfirmModel() + { + Title = "Delete API Key "+ ( string.IsNullOrEmpty(key.Label)? string.Empty: key.Label) + "("+key.Id+")", + Description = "Any application using this api key will immediately lose access", + Action = "Delete", + ActionUrl = Request.GetCurrentUrl().Replace("RemoveAPIKey", "RemoveAPIKeyPost") + }); + } + + [HttpPost("api-keys/{id}/delete")] + public async Task RemoveAPIKeyPost(string id) + { + var key = await _apiKeyRepository.GetKey(id); + if (key == null || key.UserId != _userManager.GetUserId(User)) + { + return NotFound(); + } await _apiKeyRepository.Remove(id, _userManager.GetUserId(User)); TempData.SetStatusMessageModel(new StatusMessageModel() { @@ -56,8 +80,16 @@ namespace BTCPayServer.Controllers return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel())); } + /// The permissions to request. Current permissions available: ServerManagement, StoreManagement + /// The name of your application + /// If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting. + /// If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control. [HttpGet("~/api-keys/authorize")] - public async Task AuthorizeAPIKey( string[] permissions, string applicationName = null, + [OpenApiTags("Authorization")] + [OpenApiOperation("Authorize User", + "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions")] + [IncludeInOpenApiDocs] + public async Task AuthorizeAPIKey(string[] permissions, string applicationName = null, bool strict = true, bool selectiveStores = false) { if (!_btcPayServerEnvironment.IsSecure) @@ -74,6 +106,7 @@ namespace BTCPayServer.Controllers var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() { + Label = applicationName, ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), PermissionsFormatted = permissions, @@ -214,7 +247,10 @@ namespace BTCPayServer.Controllers { var key = new APIKeyData() { - Id = Guid.NewGuid().ToString(), Type = APIKeyType.Permanent, UserId = _userManager.GetUserId(User) + Id = Guid.NewGuid().ToString().Replace("-", string.Empty), + Type = APIKeyType.Permanent, + UserId = _userManager.GetUserId(User), + Label = viewModel.Label }; key.SetPermissions(GetPermissionsFromViewModel(viewModel)); await _apiKeyRepository.CreateKey(key); @@ -251,6 +287,7 @@ namespace BTCPayServer.Controllers public class AddApiKeyViewModel { + public string Label { get; set; } public StoreData[] Stores { get; set; } public ApiKeyStoreMode StoreMode { get; set; } public List SpecificStores { get; set; } = new List(); @@ -277,7 +314,7 @@ namespace BTCPayServer.Controllers { get { - return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries); + return Permissions?.Split(";", StringSplitOptions.RemoveEmptyEntries)?? Array.Empty(); } set { diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 8b01c240f..1926cecb1 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -141,8 +141,9 @@ namespace BTCPayServer.Controllers } [HttpPost] - [Route("{storeId}/derivations/{cryptoCode}")] - public async Task AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, + [Route("{storeId}/derivations/{cryptoCode}")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm, string cryptoCode) { vm.CryptoCode = cryptoCode; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 7072b63e9..d455efd65 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -26,6 +26,7 @@ using System.Threading; using BTCPayServer.Services.Wallets; using BTCPayServer.Logging; using BTCPayServer.HostedServices; +using BTCPayServer.Hosting.OpenApi; using BTCPayServer.PaymentRequest; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; @@ -262,7 +263,7 @@ namespace BTCPayServer.Hosting } return rateLimits; }); - + services.AddBTCPayOpenApi(); services.AddLogging(logBuilder => { @@ -291,6 +292,7 @@ namespace BTCPayServer.Hosting public static IApplicationBuilder UsePayServer(this IApplicationBuilder app) { app.UseMiddleware(); + app.UseBTCPayOpenApi(); return app; } public static IApplicationBuilder UseHeadersOverride(this IApplicationBuilder app) diff --git a/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs b/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs new file mode 100644 index 000000000..5921efdaf --- /dev/null +++ b/BTCPayServer/Hosting/OpenApi/IncludeInOpenApiDocs.cs @@ -0,0 +1,9 @@ +using System; + + +namespace BTCPayServer.Hosting.OpenApi +{ + public class IncludeInOpenApiDocs : Attribute + { + } +} diff --git a/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs new file mode 100644 index 000000000..60c05330d --- /dev/null +++ b/BTCPayServer/Hosting/OpenApi/OpenApiExtensions.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Payments; +using BTCPayServer.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; +using NSwag; +using NSwag.Generation.Processors.Security; + +namespace BTCPayServer.Hosting.OpenApi +{ + public static class OpenApiExtensions + { + public static IServiceCollection AddBTCPayOpenApi(this IServiceCollection serviceCollection) + { + + return serviceCollection.AddOpenApiDocument(config => + { + config.PostProcess = document => + { + document.Info.Version = "v1"; + document.Info.Title = "BTCPay Greenfield API"; + document.Info.Description = "A full API to use your BTCPay Server"; + document.Info.TermsOfService = null; + document.Info.Contact = new NSwag.OpenApiContact + { + Name = "BTCPay Server", Email = string.Empty, Url = "https://btcpayserver.org" + }; + }; + config.AddOperationFilter(context => + { + var methodInfo = context.MethodInfo; + if (methodInfo != null) + { + return methodInfo.CustomAttributes.Any(data => + data.AttributeType == typeof(IncludeInOpenApiDocs)) || + methodInfo.DeclaringType.CustomAttributes.Any(data => + data.AttributeType == typeof(IncludeInOpenApiDocs)); + } + + return false; + }); + + config.AddSecurity("APIKey", Enumerable.Empty(), + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = + "BTCPay Server supports authenticating and authorizing users through an API Key that is generated by them. Send the API Key as a header value to Authorization with the format: token {token}. For a smoother experience, you can generate a url that redirects users to an API key creation screen." + }); + + config.OperationProcessors.Add( + new BTCPayPolicyOperationProcessor("APIKey", AuthenticationSchemes.ApiKey)); + + config.TypeMappers.Add( + new PrimitiveTypeMapper(typeof(PaymentType), s => s.Type = JsonObjectType.String)); + config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(PaymentMethodId), + s => s.Type = JsonObjectType.String)); + }); + } + + public static IApplicationBuilder UseBTCPayOpenApi(this IApplicationBuilder builder) + { + return builder.UseOpenApi() + .UseReDoc(settings => settings.Path = "/docs"); + } + + + class BTCPayPolicyOperationProcessor : AspNetCoreOperationSecurityScopeProcessor + { + private readonly string _authScheme; + + public BTCPayPolicyOperationProcessor(string x, string authScheme) : base(x) + { + _authScheme = authScheme; + } + + protected override IEnumerable GetScopes(IEnumerable authorizeAttributes) + { + var result = authorizeAttributes + .Where(attribute => attribute?.AuthenticationSchemes != null && attribute.Policy != null && + attribute.AuthenticationSchemes.Equals(_authScheme, + StringComparison.InvariantCultureIgnoreCase)) + .Select(attribute => attribute.Policy); + + return result; + } + } + } +} diff --git a/BTCPayServer/Models/ConfirmModel.cs b/BTCPayServer/Models/ConfirmModel.cs index b56f4274a..65caf78f1 100644 --- a/BTCPayServer/Models/ConfirmModel.cs +++ b/BTCPayServer/Models/ConfirmModel.cs @@ -29,5 +29,6 @@ namespace BTCPayServer.Models get; set; } public string ButtonClass { get; set; } = "btn-danger"; + public string ActionUrl { get; set; } } } diff --git a/BTCPayServer/Views/Manage/APIKeys.cshtml b/BTCPayServer/Views/Manage/APIKeys.cshtml index 23db9ac73..ac197f44d 100644 --- a/BTCPayServer/Views/Manage/APIKeys.cshtml +++ b/BTCPayServer/Views/Manage/APIKeys.cshtml @@ -8,8 +8,9 @@ - - + + + @@ -17,6 +18,7 @@ @foreach (var keyData in Model.ApiKeyDatas) { + } @if (!Model.ApiKeyDatas.Any()) { - } - diff --git a/BTCPayServer/Views/Manage/AddApiKey.cshtml b/BTCPayServer/Views/Manage/AddApiKey.cshtml index e992c0a38..f5316c545 100644 --- a/BTCPayServer/Views/Manage/AddApiKey.cshtml +++ b/BTCPayServer/Views/Manage/AddApiKey.cshtml @@ -27,6 +27,14 @@
+ +
+
+ + + +
+
@if (Model.IsServerAdmin) {
diff --git a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml index 136d480a3..303e171aa 100644 --- a/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml +++ b/BTCPayServer/Views/Manage/AuthorizeAPIKey.cshtml @@ -35,6 +35,19 @@
+
+
+ + + +
+
+ @if (!Model.PermissionsFormatted.Any()) + { +
+

There are no associated permissions to the API key being requested here. The application cannot do anything with your BTCPay account other than validating your account exists.

+
+ } @if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) {
diff --git a/BTCPayServer/Views/Shared/Confirm.cshtml b/BTCPayServer/Views/Shared/Confirm.cshtml index 22f2975cb..d44105ae3 100644 --- a/BTCPayServer/Views/Shared/Confirm.cshtml +++ b/BTCPayServer/Views/Shared/Confirm.cshtml @@ -26,7 +26,7 @@ {
-
+
KeyPermissionsLabelKeyPermissions Actions
@keyData.Label @keyData.Id @if (string.IsNullOrEmpty(keyData.Permissions)) @@ -29,20 +31,20 @@ } - Remove + Remove
+ No API keys
+ Generate new key