Greenfield API: Get current User

Builds on #1368
This PR adds a new endpoint: Get current user.. It only returns the current user's id and email for now( let's extend later)
It also adds a new permission: `ProfileManagement` which is needed for this endpoint (and for update endpoints later)
This commit is contained in:
Kukks
2020-03-12 14:59:24 +01:00
parent 2002c6750b
commit 8173296c96
12 changed files with 174 additions and 17 deletions

View File

@@ -0,0 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<ApplicationUserData> GetCurrentUser(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users/me"), token);
return await HandleResponse<ApplicationUserData>(response);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace BTCPayServer.Client.Models
{
public class ApplicationUserData
{
public string Id { get; set; }
public string Email { get; set; }
}
}

View File

@@ -9,6 +9,17 @@ namespace BTCPayServer.Client
{
public const string ServerManagement = nameof(ServerManagement);
public const string StoreManagement = nameof(StoreManagement);
public const string ProfileManagement = nameof(ProfileManagement);
public static string[] GetAllPermissionKeys()
{
return new[]
{
ServerManagement,
StoreManagement,
ProfileManagement
};
}
public static string GetStorePermission(string storeId) => $"{nameof(StoreManagement)}:{storeId}";
public static IEnumerable<string> ExtractStorePermissionsIds(IEnumerable<string> permissions) => permissions

View File

@@ -9,7 +9,6 @@ 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;

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Client;
@@ -22,7 +23,7 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ApiKeysControllerTests()
{
@@ -32,7 +33,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
string apiKey = await GenerateAPIKey(tester, user);
string apiKey = await GenerateAPIKey(tester, user, Permissions.ServerManagement, Permissions.StoreManagement);
var client = new BTCPayServerClient(tester.PayTester.ServerUri, apiKey);
//Get current api key
var apiKeyData = await client.GetCurrentAPIKeyInfo();
@@ -50,14 +51,41 @@ namespace BTCPayServer.Tests
}
}
private static async Task<string> GenerateAPIKey(ServerTester tester, TestAccount user)
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task UsersControllerTests()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
string apiKeyProfile = await GenerateAPIKey(tester, user, Permissions.ProfileManagement);
string apiKeyInsufficient = await GenerateAPIKey(tester, user, Permissions.StoreManagement);
var clientProfile = new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyProfile);
var clientInsufficient= new BTCPayServerClient(tester.PayTester.ServerUri, apiKeyInsufficient);
var apiKeyProfileUserData = await clientProfile.GetCurrentUser();
Assert.NotNull(apiKeyProfileUserData);
Assert.Equal(apiKeyProfileUserData.Id, user.UserId);
Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email);
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
}
}
private static async Task<string> GenerateAPIKey(ServerTester tester, TestAccount user, params string[] permissions)
{
var manageController = tester.PayTester.GetController<ManageController>(user.UserId, user.StoreId, user.IsAdmin);
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new ManageController.AddApiKeyViewModel()
{
ServerManagementPermission = true,
StoreManagementPermission = true,
PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem()
{
Permission = s,
Value = true
}).ToList(),
StoreMode = ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores
}));
var statusMessage = manageController.TempData.GetStatusMessageModel();

View File

@@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
using NSwag.Annotations;
using YamlDotNet.Core.Tokens;
namespace BTCPayServer.Controllers
{
@@ -31,8 +32,6 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKey(string id)
{
@@ -113,6 +112,10 @@ namespace BTCPayServer.Controllers
ServerManagementPermission = permissions.Contains(Permissions.ServerManagement),
StoreManagementPermission = permissions.Contains(Permissions.StoreManagement),
PermissionsFormatted = permissions,
PermissionValues = permissions.Where(s =>
!s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) &&
s != Permissions.ServerManagement)
.Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(),
ApplicationName = applicationName,
SelectiveStores = selectiveStores,
Strict = strict,
@@ -262,7 +265,7 @@ namespace BTCPayServer.Controllers
private IEnumerable<string> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
{
var permissions = new List<string>();
var permissions = viewModel.PermissionValues.Where(tuple => tuple.Value).Select(tuple => tuple.Permission).ToList();
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{
@@ -278,13 +281,18 @@ namespace BTCPayServer.Controllers
permissions.Add(Permissions.ServerManagement);
}
return permissions;
return permissions.Distinct();
}
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;
viewModel.IsServerAdmin =
(await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
viewModel.PermissionValues ??= Permissions.GetAllPermissionKeys().Where(s =>
!s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) &&
s != Permissions.ServerManagement)
.Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList();
return viewModel;
}
@@ -298,12 +306,19 @@ namespace BTCPayServer.Controllers
public bool ServerManagementPermission { get; set; }
public bool StoreManagementPermission { get; set; }
public string Command { get; set; }
public List<PermissionValueItem> PermissionValues { get; set; }
public enum ApiKeyStoreMode
{
AllStores,
Specific
}
public class PermissionValueItem
{
public string Permission { get; set; }
public bool Value { get; set; }
}
}
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel

View File

@@ -0,0 +1,47 @@
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Hosting.OpenApi;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
namespace BTCPayServer.Controllers.RestApi.Users
{
[ApiController]
[IncludeInOpenApiDocs]
[OpenApiTags("Users")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public class UsersController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
public UsersController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[OpenApiOperation("Get current user information", "View information about the current user")]
[SwaggerResponse(StatusCodes.Status200OK, typeof(ApiKeyData),
Description = "Information about the current user")]
[Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[HttpGet("~/api/v1/users/me")]
public async Task<ActionResult<ApplicationUserData>> GetCurrentUser()
{
var user = await _userManager.GetUserAsync(User);
return FromModel(user);
}
private static ApplicationUserData FromModel(ApplicationUser data)
{
return new ApplicationUserData()
{
Id = data.Id,
Email = data.Email
};
}
}
}

View File

@@ -34,6 +34,9 @@ namespace BTCPayServer.Security.APIKeys
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyProfile.Key:
success = context.HasPermissions(Permissions.ProfileManagement);
break;
case Policies.CanListStoreSettings.Key:
var selectiveStorePermissions =
Permissions.ExtractStorePermissionsIds(context.GetPermissions());

View File

@@ -18,6 +18,7 @@ namespace BTCPayServer.Security.APIKeys
{Client.Permissions.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")},
{$"{nameof(Client.Permissions.StoreManagement)}:", ("Manage selected stores", "The app will be able to modify and delete selected stores.")},
{Client.Permissions.ServerManagement, ("Manage your server", "The app will have total control on your server")},
{Client.Permissions.ProfileManagement, ("Manage your profile", "The app will be able to view and modify your user profile.")},
};
}

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Security
{
@@ -15,6 +11,8 @@ namespace BTCPayServer.Security
options.AddPolicy(CanCreateInvoice.Key);
options.AddPolicy(CanGetRates.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyProfile.Key);
return options;
}
@@ -27,6 +25,10 @@ namespace BTCPayServer.Security
{
public const string Key = "btcpay.store.canmodifyserversettings";
}
public class CanModifyProfile
{
public const string Key = "btcpay.store.canmodifyprofile";
}
public class CanModifyStoreSettings
{
public const string Key = "btcpay.store.canmodifystoresettings";

View File

@@ -45,6 +45,17 @@
<p>@GetDescription(Permissions.ServerManagement).</p>
</div>
}
@for (int i = 0; i < Model.PermissionValues.Count; i++)
{
<div class="list-group-item form-group">
<input type="hidden" asp-for="PermissionValues[i].Permission">
<input type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-inline"/>
<label asp-for="PermissionValues[i].Value" class="h5">@GetTitle(Model.PermissionValues[i].Permission)</label>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<p>@GetDescription(Model.PermissionValues[i].Permission).</p>
</div>
}
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{
<div class="list-group-item form-group">

View File

@@ -74,7 +74,24 @@
<p>@GetDescription(Permissions.ServerManagement).</p>
</div>
}
@for (int i = 0; i < Model.PermissionValues.Count; i++)
{
<div class="list-group-item form-group">
<input type="hidden" asp-for="PermissionValues[i].Permission">
@if (Model.Strict || !Model.IsServerAdmin)
{
<input type="hidden" asp-for="PermissionValues[i].Value"/>
<input type="checkbox" class="form-check-inline" checked="@Model.PermissionValues[i].Value" disabled/>
}
else
{
<input type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-inline"/>
}
<label asp-for="PermissionValues[i].Value" class="h5">@GetTitle(Model.PermissionValues[i].Permission)</label>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<p>@GetDescription(Model.PermissionValues[i].Permission).</p>
</div>
}
@if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement))
{
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)