BTCPayServer.Client library + Revoke API Key

This commit is contained in:
Kukks 2020-03-02 16:50:28 +01:00
parent c74f52a61c
commit 233fa8a4a1
18 changed files with 285 additions and 136 deletions

View file

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,22 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ApiKeyData> GetCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current"), token);
return await HandleResponse<ApiKeyData>(response);
}
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
HandleResponse(response);
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public static Uri GenerateAuthorizeUri(Uri btcpayHost, string[] permissions, bool strict = true,
bool selectiveStores = false)
{
var result = new UriBuilder(btcpayHost);
result.Path = "api-keys/authorize";
AppendPayloadToQuery(result,
new Dictionary<string, object>()
{
{"strict", strict}, {"selectiveStores", selectiveStores}, {"permissions", permissions}
});
return result.Uri;
}
}
}

View file

@ -0,0 +1,96 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
private readonly string _apiKey;
private readonly Uri _btcpayHost;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public BTCPayServerClient(Uri btcpayHost, string APIKey, HttpClient httpClient = null)
{
_apiKey = APIKey;
_btcpayHost = btcpayHost;
_httpClient = httpClient ?? new HttpClient();
}
protected void HandleResponse(HttpResponseMessage message)
{
message.EnsureSuccessStatusCode();
}
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
{
HandleResponse(message);
return JsonSerializer.Deserialize<T>(await message.Content.ReadAsStringAsync(), _serializerOptions);
}
protected virtual HttpRequestMessage CreateHttpRequest(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null)
{
UriBuilder uriBuilder = new UriBuilder(_btcpayHost) {Path = path};
if (queryPayload != null && queryPayload.Any())
{
AppendPayloadToQuery(uriBuilder, queryPayload);
}
var httpRequest = new HttpRequestMessage(method ?? HttpMethod.Get, uriBuilder.Uri);
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", _apiKey);
return httpRequest;
}
protected virtual HttpRequestMessage CreateHttpRequest<T>(string path,
Dictionary<string, object> queryPayload = null,
T bodyPayload = default, HttpMethod method = null)
{
var request = CreateHttpRequest(path, queryPayload, method);
if (typeof(T).IsPrimitive || !EqualityComparer<T>.Default.Equals(bodyPayload, default(T)))
{
request.Content = new StringContent(JsonSerializer.Serialize(bodyPayload, _serializerOptions));
}
return request;
}
private static void AppendPayloadToQuery(UriBuilder uri, Dictionary<string, object> payload)
{
if (uri.Query.Length > 1)
uri.Query += "&";
foreach (KeyValuePair<string, object> keyValuePair in payload)
{
UriBuilder uriBuilder = uri;
if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
{
foreach (var item in (IEnumerable)keyValuePair.Value)
{
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
Uri.EscapeDataString(item.ToString()) + "&";
}
}
else
{
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&";
}
}
uri.Query = uri.Query.Trim('&');
}
}
}

View file

@ -0,0 +1,10 @@
namespace BTCPayServer.Client.Models
{
public class ApiKeyData
{
public string ApiKey { get; set; }
public string Label { get; set; }
public string UserId { get; set; }
public string[] Permissions { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace BTCPayServer.Client
{
public static class Permissions
{
public const string ServerManagement = nameof(ServerManagement);
public const string StoreManagement = nameof(StoreManagement);
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]);
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Security.APIKeys; using BTCPayServer.Security.APIKeys;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
@ -67,8 +68,8 @@ namespace BTCPayServer.Tests
var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
//this api key has access to everything //this api key has access to everything
await TestApiAgainstAccessToken(superApiKey, tester, user, APIKeyConstants.Permissions.ServerManagement, await TestApiAgainstAccessToken(superApiKey, tester, user, Permissions.ServerManagement,
APIKeyConstants.Permissions.StoreManagement); Permissions.StoreManagement);
s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("AddApiKey")).Click();
@ -76,7 +77,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Generate")).Click(); s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
APIKeyConstants.Permissions.ServerManagement); Permissions.ServerManagement);
s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("AddApiKey")).Click();
@ -84,7 +85,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Generate")).Click(); s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
APIKeyConstants.Permissions.StoreManagement); Permissions.StoreManagement);
s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click(); s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click();
@ -96,7 +97,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Generate")).Click(); s.Driver.FindElement(By.Id("Generate")).Click();
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
APIKeyConstants.Permissions.GetStorePermission(storeId)); Permissions.GetStorePermission(storeId));
s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.Id("Generate")).Click(); s.Driver.FindElement(By.Id("Generate")).Click();
@ -117,31 +118,8 @@ namespace BTCPayServer.Tests
//permissions //permissions
//strict //strict
//selectiveStores //selectiveStores
UriBuilder authorize = new UriBuilder(tester.PayTester.ServerUri); var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
authorize.Path = "api-keys/authorize"; new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString();
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.Navigate().GoToUrl(authUrl);
s.Driver.PageSource.Contains("kukksappname"); s.Driver.PageSource.Contains("kukksappname");
Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly")); Assert.NotNull(s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("readonly"));
@ -159,28 +137,9 @@ namespace BTCPayServer.Tests
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user, await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions()); (await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetPermissions());
authorize = new UriBuilder(tester.PayTester.ServerUri); authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
authorize.Path = "api-keys/authorize"; new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString();
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); s.Driver.Navigate().GoToUrl(authUrl);
Assert.DoesNotContain("kukksappname", s.Driver.PageSource); Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
@ -214,8 +173,8 @@ namespace BTCPayServer.Tests
var secondUser = tester.NewAccount(); var secondUser = tester.NewAccount();
secondUser.GrantAccess(); secondUser.GrantAccess();
var selectiveStorePermissions = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(permissions);
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement) || selectiveStorePermissions.Any()) if (permissions.Contains(Permissions.StoreManagement) || selectiveStorePermissions.Any())
{ {
var resultStores = var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores", await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
@ -231,7 +190,7 @@ namespace BTCPayServer.Tests
data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase)); data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase));
} }
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) if (permissions.Contains(Permissions.StoreManagement))
{ {
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken, Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/actions", $"{TestApiPath}/me/stores/actions",
@ -272,7 +231,7 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient); tester.PayTester.HttpClient);
}); });
if (permissions.Contains(APIKeyConstants.Permissions.ServerManagement)) if (permissions.Contains(Permissions.ServerManagement))
{ {
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken, Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin", $"{TestApiPath}/me/is-admin",

View file

@ -1,14 +1,10 @@
using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Controllers.RestApi.ApiKeys;
using BTCPayServer.Data;
using BTCPayServer.Security.APIKeys;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
using Microsoft.AspNet.SignalR.Client;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -37,17 +33,20 @@ namespace BTCPayServer.Tests
user.GrantAccess(); user.GrantAccess();
await user.MakeAdmin(); await user.MakeAdmin();
string apiKey = await GenerateAPIKey(tester, user); string apiKey = await GenerateAPIKey(tester, user);
var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey);
//Get current api key //Get current api key
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/api-keys/current"); var apiKeyData = await client.GetCurrentAPIKeyInfo();
request.Headers.Authorization = new AuthenticationHeaderValue("token", apiKey);
var result = await tester.PayTester.HttpClient.SendAsync(request);
Assert.True(result.IsSuccessStatusCode);
var apiKeyData = JObject.Parse(await result.Content.ReadAsStringAsync()).ToObject<ApiKeyData>();
Assert.NotNull(apiKeyData); Assert.NotNull(apiKeyData);
Assert.Equal(apiKey, apiKeyData.ApiKey); Assert.Equal(apiKey, apiKeyData.ApiKey);
Assert.Equal(user.UserId, apiKeyData.UserId); Assert.Equal(user.UserId, apiKeyData.UserId);
Assert.Equal(2, apiKeyData.Permissions.Length); Assert.Equal(2, apiKeyData.Permissions.Length);
//revoke current api key
await client.RevokeCurrentAPIKeyInfo();
await Assert.ThrowsAsync<HttpRequestException>(async () =>
{
await client.GetCurrentAPIKeyInfo();
});
} }
} }

View file

@ -124,6 +124,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
<ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" /> <ProjectReference Include="..\BTCPayServer.Data\BTCPayServer.Data.csproj" />
<ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" /> <ProjectReference Include="..\BTCPayServer.Rating\BTCPayServer.Rating.csproj" />
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" /> <ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.Models; using BTCPayServer.Models;
@ -109,8 +110,8 @@ namespace BTCPayServer.Controllers
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
{ {
Label = applicationName, Label = applicationName,
ServerManagementPermission = permissions.Contains(APIKeyConstants.Permissions.ServerManagement), ServerManagementPermission = permissions.Contains(Permissions.ServerManagement),
StoreManagementPermission = permissions.Contains(APIKeyConstants.Permissions.StoreManagement), StoreManagementPermission = permissions.Contains(Permissions.StoreManagement),
PermissionsFormatted = permissions, PermissionsFormatted = permissions,
ApplicationName = applicationName, ApplicationName = applicationName,
SelectiveStores = selectiveStores, SelectiveStores = selectiveStores,
@ -133,7 +134,7 @@ namespace BTCPayServer.Controllers
} }
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement)) if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement))
{ {
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
{ {
@ -147,7 +148,7 @@ namespace BTCPayServer.Controllers
} }
} }
if (viewModel.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) if (viewModel.PermissionsFormatted.Contains(Permissions.StoreManagement))
{ {
if (!viewModel.SelectiveStores && if (!viewModel.SelectiveStores &&
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
@ -265,16 +266,16 @@ namespace BTCPayServer.Controllers
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{ {
permissions.AddRange(viewModel.SpecificStores.Select(APIKeyConstants.Permissions.GetStorePermission)); permissions.AddRange(viewModel.SpecificStores.Select(Permissions.GetStorePermission));
} }
else if (viewModel.StoreManagementPermission) else if (viewModel.StoreManagementPermission)
{ {
permissions.Add(APIKeyConstants.Permissions.StoreManagement); permissions.Add(Permissions.StoreManagement);
} }
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission) if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
{ {
permissions.Add(APIKeyConstants.Permissions.ServerManagement); permissions.Add(Permissions.ServerManagement);
} }
return permissions; return permissions;

View file

@ -1,23 +0,0 @@
using BTCPayServer.Data;
namespace BTCPayServer.Controllers.RestApi.ApiKeys
{
public class ApiKeyData
{
public string ApiKey { get; set; }
public string Label { get; set; }
public string UserId { get; set; }
public string[] Permissions { get; set; }
public static ApiKeyData FromModel(APIKeyData data)
{
return new ApiKeyData()
{
Permissions = data.GetPermissions(),
ApiKey = data.Id,
UserId = data.UserId,
Label = data.Label
};
}
}
}

View file

@ -1,9 +1,12 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Hosting.OpenApi; using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys; using BTCPayServer.Security.APIKeys;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
@ -16,10 +19,12 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys
public class ApiKeysController : ControllerBase public class ApiKeysController : ControllerBase
{ {
private readonly APIKeyRepository _apiKeyRepository; private readonly APIKeyRepository _apiKeyRepository;
private readonly UserManager<ApplicationUser> _userManager;
public ApiKeysController(APIKeyRepository apiKeyRepository) public ApiKeysController(APIKeyRepository apiKeyRepository, UserManager<ApplicationUser> userManager)
{ {
_apiKeyRepository = apiKeyRepository; _apiKeyRepository = apiKeyRepository;
_userManager = userManager;
} }
[OpenApiOperation("Get current API Key information", "View information about the current API key")] [OpenApiOperation("Get current API Key information", "View information about the current API key")]
@ -31,7 +36,30 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys
{ {
ControllerContext.HttpContext.GetAPIKey(out var apiKey); ControllerContext.HttpContext.GetAPIKey(out var apiKey);
var data = await _apiKeyRepository.GetKey(apiKey); var data = await _apiKeyRepository.GetKey(apiKey);
return Ok(ApiKeyData.FromModel(data)); return Ok(FromModel(data));
}
[OpenApiOperation("Revoke the current API Key", "Revoke the current API key so that it cannot be used anymore")]
[SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData),
Description = "The key was revoked and is no longer usable")]
[HttpDelete("~/api/v1/api-keys/current")]
[HttpDelete("~/api/v1/users/me/api-keys/current")]
public async Task<ActionResult<ApiKeyData>> RevokeKey()
{
ControllerContext.HttpContext.GetAPIKey(out var apiKey);
await _apiKeyRepository.Remove(apiKey, _userManager.GetUserId(User));
return Ok();
}
private static ApiKeyData FromModel(APIKeyData data)
{
return new ApiKeyData()
{
Permissions = data.GetPermissions(),
ApiKey = data.Id,
UserId = data.UserId,
Label = data.Label
};
} }
} }
} }

View file

@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -35,14 +36,14 @@ namespace BTCPayServer.Security.APIKeys
{ {
case Policies.CanListStoreSettings.Key: case Policies.CanListStoreSettings.Key:
var selectiveStorePermissions = var selectiveStorePermissions =
APIKeyConstants.Permissions.ExtractStorePermissionsIds(context.GetPermissions()); Permissions.ExtractStorePermissionsIds(context.GetPermissions());
success = context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) || success = context.HasPermissions(Permissions.StoreManagement) ||
selectiveStorePermissions.Any(); selectiveStorePermissions.Any();
break; break;
case Policies.CanModifyStoreSettings.Key: case Policies.CanModifyStoreSettings.Key:
string storeId = _HttpContext.GetImplicitStoreId(); string storeId = _HttpContext.GetImplicitStoreId();
if (!context.HasPermissions(APIKeyConstants.Permissions.StoreManagement) && if (!context.HasPermissions(Permissions.StoreManagement) &&
!context.HasPermissions(APIKeyConstants.Permissions.GetStorePermission(storeId))) !context.HasPermissions(Permissions.GetStorePermission(storeId)))
break; break;
if (storeId == null) if (storeId == null)
@ -63,7 +64,7 @@ namespace BTCPayServer.Security.APIKeys
break; break;
case Policies.CanModifyServerSettings.Key: case Policies.CanModifyServerSettings.Key:
if (!context.HasPermissions(APIKeyConstants.Permissions.ServerManagement)) if (!context.HasPermissions(Permissions.ServerManagement))
break; break;
// For this authorization, we stil check in database because it is super sensitive. // For this authorization, we stil check in database because it is super sensitive.
var user = await _userManager.GetUserAsync(context.User); var user = await _userManager.GetUserAsync(context.User);

View file

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace BTCPayServer.Security.APIKeys namespace BTCPayServer.Security.APIKeys
{ {
@ -16,21 +13,13 @@ namespace BTCPayServer.Security.APIKeys
public static class 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)>() 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.")}, {Client.Permissions.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.")}, {$"{nameof(Client.Permissions.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")}, {Client.Permissions.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]);
} }
} }
} }

View file

@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@ -35,12 +36,12 @@ namespace BTCPayServer.Security.APIKeys
claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions) claimsPrincipal.Claims.Where(claim => claim.Type == APIKeyConstants.ClaimTypes.Permissions)
.Select(claim => claim.Value).ToList(); .Select(claim => claim.Value).ToList();
if (permissions.Contains(APIKeyConstants.Permissions.StoreManagement)) if (permissions.Contains(Permissions.StoreManagement))
{ {
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal)); return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal));
} }
var storeIds = APIKeyConstants.Permissions.ExtractStorePermissionsIds(permissions); var storeIds = Permissions.ExtractStorePermissionsIds(permissions);
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds); return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds);
} }

View file

@ -1,6 +1,7 @@
@using BTCPayServer.Client
@using BTCPayServer.Controllers @using BTCPayServer.Controllers
@using BTCPayServer.Security.APIKeys @using BTCPayServer.Security.APIKeys
@model BTCPayServer.Controllers.ManageController.AddApiKeyViewModel @model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel
@{ @{
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key");
@ -39,9 +40,9 @@
{ {
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<input asp-for="ServerManagementPermission" class="form-check-inline"/> <input asp-for="ServerManagementPermission" class="form-check-inline"/>
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label> <label asp-for="ServerManagementPermission" class="h5">@GetTitle(Permissions.ServerManagement)</label>
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span> <span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p> <p>@GetDescription(Permissions.ServerManagement).</p>
</div> </div>
} }
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
@ -49,9 +50,9 @@
<div class="list-group-item form-group"> <div class="list-group-item form-group">
@Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary<string, string>() {{"class", "form-check-inline"}}) @Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary<string, string>() {{"class", "form-check-inline"}})
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label> <label asp-for="StoreManagementPermission" class="h5">@GetTitle(Permissions.StoreManagement)</label>
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span> <span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p> <p class="mb-0">@GetDescription(Permissions.StoreManagement).</p>
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button> <button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
</div> </div>
} }
@ -59,8 +60,8 @@
{ {
<div class="list-group-item p-0 border-0 mb-2"> <div class="list-group-item p-0 border-0 mb-2">
<li class="list-group-item "> <li class="list-group-item ">
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5> <h5 class="mb-1">@GetTitle(Permissions.StoreManagement + ":")</h5>
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p> <p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p>
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button> <button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
</li> </li>
@if (!Model.Stores.Any()) @if (!Model.Stores.Any())

View file

@ -1,3 +1,4 @@
@using BTCPayServer.Client
@using BTCPayServer.Controllers @using BTCPayServer.Controllers
@using BTCPayServer.Security.APIKeys @using BTCPayServer.Security.APIKeys
@model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel @model BTCPayServer.Controllers.ManageController.AuthorizeApiKeysViewModel
@ -48,11 +49,11 @@
<p >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.</p> <p >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.</p>
</div> </div>
} }
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict)) @if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict))
{ {
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<input asp-for="ServerManagementPermission" class="form-check-inline" readonly="@(Model.Strict || !Model.IsServerAdmin)"/> <input asp-for="ServerManagementPermission" class="form-check-inline" readonly="@(Model.Strict || !Model.IsServerAdmin)"/>
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.ServerManagement)</label> <label asp-for="ServerManagementPermission" class="h5">@GetTitle(Permissions.ServerManagement)</label>
@if (!Model.IsServerAdmin) @if (!Model.IsServerAdmin)
{ {
<span class="text-danger"> <span class="text-danger">
@ -61,19 +62,19 @@
} }
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span> <span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
<p>@GetDescription(APIKeyConstants.Permissions.ServerManagement).</p> <p>@GetDescription(Permissions.ServerManagement).</p>
</div> </div>
} }
@if (Model.PermissionsFormatted.Contains(APIKeyConstants.Permissions.StoreManagement)) @if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement))
{ {
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores) @if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{ {
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<input type="checkbox" asp-for="StoreManagementPermission" class="form-check-inline" readonly="@Model.Strict"/> <input type="checkbox" asp-for="StoreManagementPermission" class="form-check-inline" readonly="@Model.Strict"/>
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(APIKeyConstants.Permissions.StoreManagement)</label> <label asp-for="StoreManagementPermission" class="h5">@GetTitle(Permissions.StoreManagement)</label>
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span> <span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement).</p> <p class="mb-0">@GetDescription(Permissions.StoreManagement).</p>
@if (Model.SelectiveStores) @if (Model.SelectiveStores)
{ {
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button> <button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
@ -84,8 +85,8 @@
{ {
<div class="list-group-item p-0 border-0 mb-2"> <div class="list-group-item p-0 border-0 mb-2">
<li class="list-group-item"> <li class="list-group-item">
<h5 class="mb-1">@GetTitle(APIKeyConstants.Permissions.StoreManagement + ":")</h5> <h5 class="mb-1">@GetTitle(Permissions.StoreManagement + ":")</h5>
<p class="mb-0">@GetDescription(APIKeyConstants.Permissions.StoreManagement + ":").</p> <p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p>
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button> <button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to all stores instead</button>
</li> </li>
@if (!Model.Stores.Any()) @if (!Model.Stores.Any())

View file

@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Common", "BTCP
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Data", "BTCPayServer.Data\BTCPayServer.Data.csproj", "{4D7A865D-3945-4C70-9CC8-B09A274A697E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Data", "BTCPayServer.Data\BTCPayServer.Data.csproj", "{4D7A865D-3945-4C70-9CC8-B09A274A697E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "BTCPayServer.Client\BTCPayServer.Client.csproj", "{21A13304-7168-49A0-86C2-0A1A9453E9C7}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -95,6 +97,18 @@ Global
{4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x64.Build.0 = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x64.Build.0 = Release|Any CPU
{4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.ActiveCfg = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.ActiveCfg = Release|Any CPU
{4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.Build.0 = Release|Any CPU {4D7A865D-3945-4C70-9CC8-B09A274A697E}.Release|x86.Build.0 = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.ActiveCfg = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x64.Build.0 = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.ActiveCfg = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Debug|x86.Build.0 = Debug|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|Any CPU.Build.0 = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.ActiveCfg = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x64.Build.0 = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.ActiveCfg = Release|Any CPU
{21A13304-7168-49A0-86C2-0A1A9453E9C7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE