mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +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:
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -11,15 +13,31 @@ namespace BTCPayServer.Data
|
|||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string Id
|
public string Id
|
||||||
{
|
{
|
||||||
get; set;
|
get;
|
||||||
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MaxLength(50)]
|
[MaxLength(50)] public string StoreId { get; set; }
|
||||||
public string StoreId
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
[MaxLength(50)] public string UserId { get; set; }
|
||||||
|
|
||||||
|
public APIKeyType Type { get; set; } = APIKeyType.Legacy;
|
||||||
|
public string Permissions { get; set; }
|
||||||
|
|
||||||
public StoreData StoreData { get; set; }
|
public StoreData StoreData { get; set; }
|
||||||
|
public ApplicationUser User { get; set; }
|
||||||
|
public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; }
|
||||||
|
|
||||||
|
public void SetPermissions(IEnumerable<string> permissions)
|
||||||
|
{
|
||||||
|
Permissions = string.Join(';',
|
||||||
|
permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum APIKeyType
|
||||||
|
{
|
||||||
|
Legacy,
|
||||||
|
Permanent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ namespace BTCPayServer.Data
|
|||||||
.HasOne(o => o.StoreData)
|
.HasOne(o => o.StoreData)
|
||||||
.WithMany(i => i.APIKeys)
|
.WithMany(i => i.APIKeys)
|
||||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.Entity<APIKeyData>()
|
||||||
|
.HasOne(o => o.User)
|
||||||
|
.WithMany(i => i.APIKeys)
|
||||||
|
.HasForeignKey(i => i.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
builder.Entity<APIKeyData>()
|
builder.Entity<APIKeyData>()
|
||||||
.HasIndex(o => o.StoreId);
|
.HasIndex(o => o.StoreId);
|
||||||
|
|
||||||
|
|||||||
@@ -30,5 +30,6 @@ namespace BTCPayServer.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<U2FDevice> U2FDevices { get; set; }
|
public List<U2FDevice> U2FDevices { get; set; }
|
||||||
|
public List<APIKeyData> APIKeys { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs
Normal file
74
BTCPayServer.Data/Migrations/20200119130108_ExtendApiKeys.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20200119130108_ExtendApiKeys")]
|
||||||
|
public partial class ExtendApiKeys : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Permissions",
|
||||||
|
table: "ApiKeys",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Type",
|
||||||
|
table: "ApiKeys",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "UserId",
|
||||||
|
table: "ApiKeys",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiKeys_UserId",
|
||||||
|
table: "ApiKeys",
|
||||||
|
column: "UserId");
|
||||||
|
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApiKeys_AspNetUsers_UserId",
|
||||||
|
table: "ApiKeys",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApiKeys_AspNetUsers_UserId",
|
||||||
|
table: "ApiKeys");
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ApiKeys_UserId",
|
||||||
|
table: "ApiKeys");
|
||||||
|
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Permissions",
|
||||||
|
table: "ApiKeys");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Type",
|
||||||
|
table: "ApiKeys");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserId",
|
||||||
|
table: "ApiKeys");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,14 +22,26 @@ namespace BTCPayServer.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasMaxLength(50);
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.Property<string>("Permissions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("StoreId")
|
b.Property<string>("StoreId")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasMaxLength(50);
|
.HasMaxLength(50);
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasMaxLength(50);
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("StoreId");
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("ApiKeys");
|
b.ToTable("ApiKeys");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -842,6 +854,11 @@ namespace BTCPayServer.Migrations
|
|||||||
.WithMany("APIKeys")
|
.WithMany("APIKeys")
|
||||||
.HasForeignKey("StoreId")
|
.HasForeignKey("StoreId")
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.ApplicationUser", "User")
|
||||||
|
.WithMany("APIKeys")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ namespace BTCPayServer.Migrations
|
|||||||
{
|
{
|
||||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||||
}
|
}
|
||||||
|
public static bool SupportAddForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||||
|
{
|
||||||
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||||
|
}
|
||||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||||
{
|
{
|
||||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||||
|
|||||||
300
BTCPayServer.Tests/ApiKeysTests.cs
Normal file
300
BTCPayServer.Tests/ApiKeysTests.cs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Security.APIKeys;
|
||||||
|
using BTCPayServer.Tests.Logging;
|
||||||
|
using BTCPayServer.Views.Manage;
|
||||||
|
using ExchangeSharp;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Tests
|
||||||
|
{
|
||||||
|
public class ApiKeysTests
|
||||||
|
{
|
||||||
|
public const int TestTimeout = TestUtils.TestTimeout;
|
||||||
|
|
||||||
|
public const string TestApiPath = "api/test/apikey";
|
||||||
|
public ApiKeysTests(ITestOutputHelper helper)
|
||||||
|
{
|
||||||
|
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||||
|
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Timeout = TestTimeout)]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
public async Task CanCreateApiKeys()
|
||||||
|
{
|
||||||
|
//there are 2 ways to create api keys:
|
||||||
|
//as a user through your profile
|
||||||
|
//as an external application requesting an api key from a user
|
||||||
|
|
||||||
|
using (var s = SeleniumTester.Create())
|
||||||
|
{
|
||||||
|
await s.StartAsync();
|
||||||
|
var tester = s.Server;
|
||||||
|
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
|
||||||
|
await user.CreateStoreAsync();
|
||||||
|
s.GoToLogin();
|
||||||
|
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||||
|
s.GoToProfile(ManageNavPages.APIKeys);
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
if (!user.IsAdmin)
|
||||||
|
{
|
||||||
|
//not an admin, so this permission should not show
|
||||||
|
Assert.DoesNotContain("ServerManagementPermission", s.Driver.PageSource);
|
||||||
|
await user.MakeAdmin();
|
||||||
|
s.Logout();
|
||||||
|
s.GoToLogin();
|
||||||
|
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||||
|
s.GoToProfile(ManageNavPages.APIKeys);
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
}
|
||||||
|
|
||||||
|
//server management should show now
|
||||||
|
s.SetCheckbox(s, "ServerManagementPermission", true);
|
||||||
|
s.SetCheckbox(s, "StoreManagementPermission", true);
|
||||||
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||||
|
var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||||
|
|
||||||
|
//this api key has access to everything
|
||||||
|
await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement,
|
||||||
|
APIKeyConstants.Permissions.StoreManagement);
|
||||||
|
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
s.SetCheckbox(s, "ServerManagementPermission", true);
|
||||||
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||||
|
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||||
|
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
|
||||||
|
APIKeyConstants.Permissions.ServerManagement);
|
||||||
|
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
s.SetCheckbox(s, "StoreManagementPermission", true);
|
||||||
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||||
|
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||||
|
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
|
||||||
|
APIKeyConstants.Permissions.StoreManagement);
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click();
|
||||||
|
//there should be a store already by default in the dropdown
|
||||||
|
var dropdown = s.Driver.FindElement(By.Name("SpecificStores[0]"));
|
||||||
|
var option = dropdown.FindElement(By.TagName("option"));
|
||||||
|
var storeId = option.GetAttribute("value");
|
||||||
|
option.Click();
|
||||||
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||||
|
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||||
|
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
|
||||||
|
APIKeyConstants.Permissions.GetStorePermission(storeId));
|
||||||
|
|
||||||
|
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||||
|
var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
|
||||||
|
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
||||||
|
|
||||||
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
|
{
|
||||||
|
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//let's test the authorized screen now
|
||||||
|
//options for authorize are:
|
||||||
|
//applicationName
|
||||||
|
//redirect
|
||||||
|
//permissions
|
||||||
|
//strict
|
||||||
|
//selectiveStores
|
||||||
|
UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri);
|
||||||
|
authorize.Path = "api-keys/authorize";
|
||||||
|
|
||||||
|
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{"redirect", "https://local.local/callback"},
|
||||||
|
{"applicationName", "kukksappname"},
|
||||||
|
{"strict", true},
|
||||||
|
{"selectiveStores", false},
|
||||||
|
{
|
||||||
|
"permissions",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
APIKeyConstants.Permissions.StoreManagement,
|
||||||
|
APIKeyConstants.Permissions.ServerManagement
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var authUrl = authorize.ToString();
|
||||||
|
var perms = new[]
|
||||||
|
{
|
||||||
|
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
|
||||||
|
};
|
||||||
|
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
|
||||||
|
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
|
||||||
|
s.Driver.Navigate().GoToUrl(authUrl);
|
||||||
|
s.Driver.PageSource.Contains("kukksappname");
|
||||||
|
Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
|
||||||
|
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
|
||||||
|
Assert.NotNull(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
|
||||||
|
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
|
||||||
|
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
|
||||||
|
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||||
|
var url = s.Driver.Url;
|
||||||
|
IEnumerable<KeyValuePair<string, string>> results = url.Split("?").Last().Split("&")
|
||||||
|
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||||
|
|
||||||
|
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
|
||||||
|
|
||||||
|
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||||
|
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
|
||||||
|
|
||||||
|
authorize = new UriBuilder(tester.PayTester.ServerUri);
|
||||||
|
authorize.Path = "api-keys/authorize";
|
||||||
|
authorize.AppendPayloadToQuery(new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{"strict", false},
|
||||||
|
{"selectiveStores", true},
|
||||||
|
{
|
||||||
|
"permissions",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
APIKeyConstants.Permissions.StoreManagement,
|
||||||
|
APIKeyConstants.Permissions.ServerManagement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
authUrl = authorize.ToString();
|
||||||
|
perms = new[]
|
||||||
|
{
|
||||||
|
APIKeyConstants.Permissions.StoreManagement, APIKeyConstants.Permissions.ServerManagement
|
||||||
|
};
|
||||||
|
authUrl = authUrl.Replace("permissions=System.String%5B%5D",
|
||||||
|
string.Join("&", perms.Select(s1 => $"permissions={s1}")));
|
||||||
|
s.Driver.Navigate().GoToUrl(authUrl);
|
||||||
|
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
|
||||||
|
|
||||||
|
Assert.Null(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
|
||||||
|
Assert.True(s.Driver.FindElement(By.Id("StoreManagementPermission")).Selected);
|
||||||
|
Assert.Null(s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("readonly"));
|
||||||
|
Assert.True(s.Driver.FindElement(By.Id("ServerManagementPermission")).Selected);
|
||||||
|
|
||||||
|
s.SetCheckbox(s, "ServerManagementPermission", false);
|
||||||
|
Assert.Contains("change-store-mode", s.Driver.PageSource);
|
||||||
|
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||||
|
url = s.Driver.Url;
|
||||||
|
results = url.Split("?").Last().Split("&")
|
||||||
|
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||||
|
|
||||||
|
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||||
|
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
|
||||||
|
params string[] permissions)
|
||||||
|
{
|
||||||
|
var resultUser =
|
||||||
|
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
Assert.Equal(testAccount.UserId, resultUser);
|
||||||
|
|
||||||
|
//create a second user to see if any of its data gets messed upin our results.
|
||||||
|
var secondUser = tester.NewAccount();
|
||||||
|
secondUser.GrantAccess();
|
||||||
|
|
||||||
|
var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions);
|
||||||
|
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any())
|
||||||
|
{
|
||||||
|
var resultStores =
|
||||||
|
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
|
||||||
|
foreach (string selectiveStorePermission in selectiveStorePermissions)
|
||||||
|
{
|
||||||
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit",
|
||||||
|
tester.PayTester.HttpClient));
|
||||||
|
|
||||||
|
Assert.Contains(resultStores,
|
||||||
|
data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement))
|
||||||
|
{
|
||||||
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/stores/actions",
|
||||||
|
tester.PayTester.HttpClient));
|
||||||
|
|
||||||
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
||||||
|
tester.PayTester.HttpClient));
|
||||||
|
Assert.Contains(resultStores,
|
||||||
|
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
|
{
|
||||||
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/stores/actions",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.DoesNotContain(resultStores,
|
||||||
|
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
|
{
|
||||||
|
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
|
{
|
||||||
|
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
||||||
|
tester.PayTester.HttpClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement))
|
||||||
|
{
|
||||||
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
|
$"{TestApiPath}/me/is-admin",
|
||||||
|
tester.PayTester.HttpClient));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
|
||||||
|
{
|
||||||
|
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||||
|
new Uri(client.BaseAddress, url));
|
||||||
|
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
|
||||||
|
var result = await client.SendAsync(httpRequest);
|
||||||
|
result.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var rawJson = await result.Content.ReadAsStringAsync();
|
||||||
|
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
|
||||||
|
{
|
||||||
|
return (T)Convert.ChangeType(rawJson, typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<T>(rawJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
public class AuthenticationTests
|
public class AuthenticationTests
|
||||||
{
|
{
|
||||||
|
public const string TestApiPath = "api/test/openid";
|
||||||
public const int TestTimeout = TestUtils.TestTimeout;
|
public const int TestTimeout = TestUtils.TestTimeout;
|
||||||
public AuthenticationTests(ITestOutputHelper helper)
|
public AuthenticationTests(ITestOutputHelper helper)
|
||||||
{
|
{
|
||||||
@@ -130,12 +131,12 @@ namespace BTCPayServer.Tests
|
|||||||
await TestApiAgainstAccessToken(results["access_token"], tester, user);
|
await TestApiAgainstAccessToken(results["access_token"], tester, user);
|
||||||
|
|
||||||
var stores = await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
var stores = await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
||||||
$"api/test/me/stores",
|
$"{TestApiPath}/me/stores",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
Assert.NotEmpty(stores);
|
Assert.NotEmpty(stores);
|
||||||
|
|
||||||
Assert.True(await TestApiAgainstAccessToken<bool>(results["access_token"],
|
Assert.True(await TestApiAgainstAccessToken<bool>(results["access_token"],
|
||||||
$"api/test/me/stores/{stores[0].Id}/can-edit",
|
$"{TestApiPath}/me/stores/{stores[0].Id}/can-edit",
|
||||||
tester.PayTester.HttpClient));
|
tester.PayTester.HttpClient));
|
||||||
|
|
||||||
//we dont ask for consent after acquiring it the first time for the same scopes.
|
//we dont ask for consent after acquiring it the first time for the same scopes.
|
||||||
@@ -166,13 +167,13 @@ namespace BTCPayServer.Tests
|
|||||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
{
|
{
|
||||||
await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
|
||||||
$"api/test/me/stores",
|
$"{TestApiPath}/me/stores",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
});
|
});
|
||||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
{
|
{
|
||||||
await TestApiAgainstAccessToken<bool>(results["access_token"],
|
await TestApiAgainstAccessToken<bool>(results["access_token"],
|
||||||
$"api/test/me/stores/{stores[0].Id}/can-edit",
|
$"{TestApiPath}/me/stores/{stores[0].Id}/can-edit",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -377,7 +378,7 @@ namespace BTCPayServer.Tests
|
|||||||
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
|
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
|
||||||
{
|
{
|
||||||
var resultUser =
|
var resultUser =
|
||||||
await TestApiAgainstAccessToken<string>(accessToken, "api/test/me/id",
|
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
Assert.Equal(testAccount.UserId, resultUser);
|
Assert.Equal(testAccount.UserId, resultUser);
|
||||||
|
|
||||||
@@ -385,7 +386,7 @@ namespace BTCPayServer.Tests
|
|||||||
secondUser.GrantAccess();
|
secondUser.GrantAccess();
|
||||||
|
|
||||||
var resultStores =
|
var resultStores =
|
||||||
await TestApiAgainstAccessToken<StoreData[]>(accessToken, "api/test/me/stores",
|
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
Assert.Contains(resultStores,
|
Assert.Contains(resultStores,
|
||||||
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
@@ -393,16 +394,16 @@ namespace BTCPayServer.Tests
|
|||||||
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
$"api/test/me/stores/{testAccount.StoreId}/can-edit",
|
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
|
||||||
tester.PayTester.HttpClient));
|
tester.PayTester.HttpClient));
|
||||||
|
|
||||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||||
$"api/test/me/is-admin",
|
$"{TestApiPath}/me/is-admin",
|
||||||
tester.PayTester.HttpClient));
|
tester.PayTester.HttpClient));
|
||||||
|
|
||||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||||
{
|
{
|
||||||
await TestApiAgainstAccessToken<bool>(accessToken, $"api/test/me/stores/{secondUser.StoreId}/can-edit",
|
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
||||||
tester.PayTester.HttpClient);
|
tester.PayTester.HttpClient);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Lightning.CLightning;
|
using BTCPayServer.Lightning.CLightning;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
|
using BTCPayServer.Views.Manage;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -71,19 +72,20 @@ namespace BTCPayServer.Tests
|
|||||||
Driver.AssertNoError();
|
Driver.AssertNoError();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
internal IWebElement AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(20_000);
|
using var cts = new CancellationTokenSource(20_000);
|
||||||
while (!cts.IsCancellationRequested)
|
while (!cts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed);
|
var result = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Where(el => el.Displayed);
|
||||||
if (success)
|
if (result.Any())
|
||||||
return;
|
return result.First();
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
Logs.Tester.LogInformation(this.Driver.PageSource);
|
Logs.Tester.LogInformation(this.Driver.PageSource);
|
||||||
Assert.True(false, $"Should have shown {severity} message");
|
Assert.True(false, $"Should have shown {severity} message");
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
|
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
|
||||||
public string Link(string relativeLink)
|
public string Link(string relativeLink)
|
||||||
@@ -271,6 +273,20 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
Driver.FindElement(By.Id("Invoices")).Click();
|
Driver.FindElement(By.Id("Invoices")).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id("MySettings")).Click();
|
||||||
|
if (navPages != ManageNavPages.Index)
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id(navPages.ToString())).Click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GoToLogin()
|
||||||
|
{
|
||||||
|
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "Account/Login"));
|
||||||
|
}
|
||||||
|
|
||||||
public void GoToCreateInvoicePage()
|
public void GoToCreateInvoicePage()
|
||||||
{
|
{
|
||||||
|
|||||||
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.Security;
|
||||||
using BTCPayServer.U2F;
|
using BTCPayServer.U2F;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Security.APIKeys;
|
||||||
|
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers
|
namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
@@ -34,6 +36,8 @@ namespace BTCPayServer.Controllers
|
|||||||
IWebHostEnvironment _Env;
|
IWebHostEnvironment _Env;
|
||||||
public U2FService _u2FService;
|
public U2FService _u2FService;
|
||||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||||
|
private readonly APIKeyRepository _apiKeyRepository;
|
||||||
|
private readonly IAuthorizationService _authorizationService;
|
||||||
StoreRepository _StoreRepository;
|
StoreRepository _StoreRepository;
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +52,10 @@ namespace BTCPayServer.Controllers
|
|||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
IWebHostEnvironment env,
|
IWebHostEnvironment env,
|
||||||
U2FService u2FService,
|
U2FService u2FService,
|
||||||
BTCPayServerEnvironment btcPayServerEnvironment)
|
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||||
|
APIKeyRepository apiKeyRepository,
|
||||||
|
IAuthorizationService authorizationService
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
@@ -58,6 +65,8 @@ namespace BTCPayServer.Controllers
|
|||||||
_Env = env;
|
_Env = env;
|
||||||
_u2FService = u2FService;
|
_u2FService = u2FService;
|
||||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||||
|
_apiKeyRepository = apiKeyRepository;
|
||||||
|
_authorizationService = authorizationService;
|
||||||
_StoreRepository = storeRepository;
|
_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>
|
/// <summary>
|
||||||
/// this controller serves as a testing endpoint for our OpenId unit tests
|
/// this controller serves as a testing endpoint for our OpenId unit tests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/[controller]")]
|
[Route("api/test/openid")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||||
public class TestController : ControllerBase
|
public class TestOpenIdController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
private readonly StoreRepository _storeRepository;
|
private readonly StoreRepository _storeRepository;
|
||||||
|
|
||||||
public TestController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
public TestOpenIdController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_storeRepository = storeRepository;
|
_storeRepository = storeRepository;
|
||||||
@@ -54,7 +54,6 @@ namespace BTCPayServer.Controllers.RestApi
|
|||||||
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("me/stores/{storeId}/can-edit")]
|
[HttpGet("me/stores/{storeId}/can-edit")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
|
||||||
@@ -32,6 +32,7 @@ using BTCPayServer.Payments.Bitcoin;
|
|||||||
using BTCPayServer.Payments.Changelly;
|
using BTCPayServer.Payments.Changelly;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
|
using BTCPayServer.Security.APIKeys;
|
||||||
using BTCPayServer.Services.PaymentRequests;
|
using BTCPayServer.Services.PaymentRequests;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
@@ -241,11 +242,11 @@ namespace BTCPayServer.Hosting
|
|||||||
services.AddTransient<PaymentRequestController>();
|
services.AddTransient<PaymentRequestController>();
|
||||||
// Add application services.
|
// Add application services.
|
||||||
services.AddSingleton<EmailSenderFactory>();
|
services.AddSingleton<EmailSenderFactory>();
|
||||||
// bundling
|
|
||||||
|
services.AddAPIKeyAuthentication();
|
||||||
services.AddBtcPayServerAuthenticationSchemes(configuration);
|
services.AddBtcPayServerAuthenticationSchemes();
|
||||||
services.AddAuthorization(o => o.AddBTCPayPolicies());
|
services.AddAuthorization(o => o.AddBTCPayPolicies());
|
||||||
|
// bundling
|
||||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||||
services.AddTransient<BundleOptions>(provider =>
|
services.AddTransient<BundleOptions>(provider =>
|
||||||
{
|
{
|
||||||
@@ -292,12 +293,12 @@ namespace BTCPayServer.Hosting
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
private const long MAX_DEBUG_LOG_FILE_SIZE = 2000000; // If debug log is in use roll it every N MB.
|
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,
|
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services)
|
||||||
IConfiguration configuration)
|
|
||||||
{
|
{
|
||||||
services.AddAuthentication()
|
services.AddAuthentication()
|
||||||
.AddCookie()
|
.AddCookie()
|
||||||
.AddBitpayAuthentication();
|
.AddBitpayAuthentication()
|
||||||
|
.AddAPIKeyAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
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 Cookie = "Identity.Application";
|
||||||
public const string Bitpay = "Bitpay";
|
public const string Bitpay = "Bitpay";
|
||||||
public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||||
|
public const string ApiKey = "GreenfieldApiKey";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ namespace BTCPayServer.Security.Bitpay
|
|||||||
|
|
||||||
using (var ctx = _Factory.CreateContext())
|
using (var ctx = _Factory.CreateContext())
|
||||||
{
|
{
|
||||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId && o.Type == APIKeyType.Legacy).ToListAsync();
|
||||||
if (existing != null)
|
if (existing.Any())
|
||||||
{
|
{
|
||||||
ctx.ApiKeys.Remove(existing);
|
ctx.ApiKeys.RemoveRange(existing);
|
||||||
}
|
}
|
||||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||||
@@ -95,7 +95,7 @@ namespace BTCPayServer.Security.Bitpay
|
|||||||
{
|
{
|
||||||
using (var ctx = _Factory.CreateContext())
|
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)
|
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||||
{
|
{
|
||||||
options.AddPolicy(CanModifyStoreSettings.Key);
|
options.AddPolicy(CanModifyStoreSettings.Key);
|
||||||
|
options.AddPolicy(CanListStoreSettings.Key);
|
||||||
options.AddPolicy(CanCreateInvoice.Key);
|
options.AddPolicy(CanCreateInvoice.Key);
|
||||||
options.AddPolicy(CanGetRates.Key);
|
options.AddPolicy(CanGetRates.Key);
|
||||||
options.AddPolicy(CanModifyServerSettings.Key);
|
options.AddPolicy(CanModifyServerSettings.Key);
|
||||||
@@ -30,6 +31,10 @@ namespace BTCPayServer.Security
|
|||||||
{
|
{
|
||||||
public const string Key = "btcpay.store.canmodifystoresettings";
|
public const string Key = "btcpay.store.canmodifystoresettings";
|
||||||
}
|
}
|
||||||
|
public class CanListStoreSettings
|
||||||
|
{
|
||||||
|
public const string Key = "btcpay.store.canliststoresettings";
|
||||||
|
}
|
||||||
public class CanCreateInvoice
|
public class CanCreateInvoice
|
||||||
{
|
{
|
||||||
public const string Key = "btcpay.store.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())
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
return (await ctx.UserStore
|
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 })
|
.Select(u => new { u.StoreData, u.Role })
|
||||||
.ToArrayAsync())
|
.ToArrayAsync())
|
||||||
.Select(u =>
|
.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
|
public enum ManageNavPages
|
||||||
{
|
{
|
||||||
Index, ChangePassword, TwoFactorAuthentication, U2F
|
Index, ChangePassword, TwoFactorAuthentication, U2F, APIKeys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
@inject SignInManager<ApplicationUser> SignInManager
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
|
||||||
<div class="nav flex-column nav-pills">
|
<div class="nav flex-column nav-pills">
|
||||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
<a id="@ManageNavPages.Index.ToString()" 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 id="@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
<a id="@ManageNavPages.TwoFactorAuthentication.ToString()" 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.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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user