Refactor permissions of GreenField

This commit is contained in:
nicolas.dorier 2020-03-19 19:11:15 +09:00
parent eac33d494a
commit 29a807696b
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
31 changed files with 581 additions and 404 deletions

View file

@ -5,25 +5,183 @@ using System.Text.Json.Serialization;
namespace BTCPayServer.Client
{
public static class Permissions
public class Permission
{
public const string ServerManagement = nameof(ServerManagement);
public const string StoreManagement = nameof(StoreManagement);
public const string ProfileManagement = nameof(ProfileManagement);
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
public const string CanViewProfile = "btcpay.user.canviewprofile";
public const string CanCreateUser = "btcpay.server.cancreateuser";
public const string Unrestricted = "unrestricted";
public static string[] GetAllPermissionKeys()
public static IEnumerable<string> AllPolicies
{
return new[]
get
{
ServerManagement,
StoreManagement,
ProfileManagement
};
yield return CanCreateInvoice;
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
yield return CanViewStoreSettings;
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanCreateUser;
yield return Unrestricted;
}
}
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]);
public static Permission Create(string policy, string storeId = null)
{
if (TryCreatePermission(policy, storeId, out var r))
return r;
throw new ArgumentException("Invalid Permission");
}
public static bool TryCreatePermission(string policy, string storeId, out Permission permission)
{
permission = null;
if (policy == null)
throw new ArgumentNullException(nameof(policy));
policy = policy.Trim().ToLowerInvariant();
if (!IsValidPolicy(policy))
return false;
if (storeId != null && !IsStorePolicy(policy))
return false;
permission = new Permission(policy, storeId);
return true;
}
public static bool TryParse(string str, out Permission permission)
{
permission = null;
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
var separator = str.IndexOf(':');
if (separator == -1)
{
str = str.ToLowerInvariant();
if (!IsValidPolicy(str))
return false;
permission = new Permission(str, null);
return true;
}
else
{
var policy = str.Substring(0, separator).ToLowerInvariant();
if (!IsValidPolicy(policy))
return false;
if (!IsStorePolicy(policy))
return false;
var storeId = str.Substring(separator + 1);
if (storeId.Length == 0)
return false;
permission = new Permission(policy, storeId);
return true;
}
}
private static bool IsValidPolicy(string policy)
{
return AllPolicies.Any(p => p.Equals(policy, StringComparison.OrdinalIgnoreCase));
}
private static bool IsStorePolicy(string policy)
{
return policy.StartsWith("btcpay.store", StringComparison.OrdinalIgnoreCase);
}
internal Permission(string policy, string storeId)
{
Policy = policy;
StoreId = storeId;
}
public bool Contains(Permission subpermission)
{
if (subpermission is null)
throw new ArgumentNullException(nameof(subpermission));
if (!ContainsPolicy(subpermission.Policy))
{
return false;
}
if (!IsStorePolicy(subpermission.Policy))
return true;
return StoreId == null || subpermission.StoreId == this.StoreId;
}
public static IEnumerable<Permission> ToPermissions(string[] permissions)
{
if (permissions == null)
throw new ArgumentNullException(nameof(permissions));
foreach (var p in permissions)
{
if (TryParse(p, out var pp))
yield return pp;
}
}
public static IEnumerable<Permission> ToPermissions(string permissionsFormatted)
{
foreach(var part in permissionsFormatted.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
if (Permission.TryParse(part, out var p))
yield return p;
}
}
private bool ContainsPolicy(string subpolicy)
{
if (this.Policy == Unrestricted)
return true;
if (this.Policy == subpolicy)
return true;
if (subpolicy == CanViewStoreSettings && this.Policy == CanModifyStoreSettings)
return true;
if (subpolicy == CanCreateInvoice && this.Policy == CanModifyStoreSettings)
return true;
if (subpolicy == CanViewProfile && this.Policy == CanModifyProfile)
return true;
return false;
}
public string StoreId { get; }
public string Policy { get; }
public override string ToString()
{
if (StoreId != null)
{
return $"{Policy}:{StoreId}";
}
return Policy;
}
public override bool Equals(object obj)
{
Permission item = obj as Permission;
if (item == null)
return false;
return ToString().Equals(item.ToString());
}
public static bool operator ==(Permission a, Permission b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(Permission a, Permission b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
}
}

View file

@ -27,13 +27,6 @@ namespace BTCPayServer.Data
public StoreData StoreData { get; set; }
public ApplicationUser User { get; set; }
public string Label { get; set; }
public string[] GetPermissions() { return Permissions?.Split(';') ?? new string[0]; }
public void SetPermissions(IEnumerable<string> permissions)
{
Permissions = string.Join(';',
permissions?.Select(s => s.Replace(";", string.Empty)) ?? new string[0]);
}
}
public enum APIKeyType

View file

@ -42,49 +42,46 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.CreateStoreAsync();
await user.MakeAdmin(false);
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();
}
//not an admin, so this permission should not show
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", 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();
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.SetCheckbox(s, "ServerManagementPermission", true);
s.SetCheckbox(s, "StoreManagementPermission", true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", 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, Permissions.ServerManagement,
Permissions.StoreManagement);
await TestApiAgainstAccessToken(superApiKey, tester, user, $"{Permission.CanModifyServerSettings};{Permission.CanModifyStoreSettings}");
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "ServerManagementPermission", true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
Permissions.ServerManagement);
Permission.CanModifyServerSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.SetCheckbox(s, "StoreManagementPermission", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
Permissions.StoreManagement);
Permission.CanModifyStoreSettings);
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value=change-store-mode]")).Click();
@ -96,12 +93,12 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Generate")).Click();
var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
Permissions.GetStorePermission(storeId));
Permission.Create(Permission.CanModifyStoreSettings, storeId).ToString());
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 TestApiAgainstAccessToken(noPermissionsApiKey, tester, user, string.Empty);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
@ -118,13 +115,13 @@ namespace BTCPayServer.Tests
//strict
//selectiveStores
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
new[] {Permissions.StoreManagement, Permissions.ServerManagement}).ToString();
new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
s.Driver.PageSource.Contains("kukksappname");
Assert.Equal("hidden", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("hidden", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
var url = s.Driver.Url;
@ -134,20 +131,20 @@ namespace BTCPayServer.Tests
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());
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions);
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
new[] {Permissions.StoreManagement, Permissions.ServerManagement}, false, true).ToString();
new[] {Permission.CanModifyStoreSettings, Permission.CanModifyServerSettings}, false, true).ToString();
s.Driver.Navigate().GoToUrl(authUrl);
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("StoreManagementPermission")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true",s.Driver.FindElement(By.Id("ServerManagementPermission")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true",s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
s.SetCheckbox(s, "ServerManagementPermission", false);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false);
Assert.Contains("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
url = s.Driver.Url;
@ -155,14 +152,15 @@ namespace BTCPayServer.Tests
.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());
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).Permissions);
}
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount,
params string[] permissions)
string permissionFormatted)
{
var permissions = Permission.ToPermissions(permissionFormatted);
var resultUser =
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
@ -172,49 +170,68 @@ namespace BTCPayServer.Tests
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
var selectiveStorePermissions = Permissions.ExtractStorePermissionsIds(permissions);
if (permissions.Contains(Permissions.StoreManagement) || selectiveStorePermissions.Any())
var canModifyAllStores = Permission.Create(Permission.CanModifyStoreSettings, null);
var canModifyServer = Permission.Create(Permission.CanModifyServerSettings, null);
var unrestricted = Permission.Create(Permission.Unrestricted, null);
var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Permission.CanModifyStoreSettings);
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
{
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
foreach (string selectiveStorePermission in selectiveStorePermissions)
foreach (var selectiveStorePermission in selectiveStorePermissions)
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{selectiveStorePermission}/can-edit",
$"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(selectiveStorePermission, StringComparison.InvariantCultureIgnoreCase));
data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
if (permissions.Contains(Permissions.StoreManagement))
bool shouldBeAuthorized = false;
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanViewStoreSettings, testAccount.StoreId)))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/actions",
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient));
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
shouldBeAuthorized = true;
}
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Contains(Permission.Create(Permission.CanModifyStoreSettings, testAccount.StoreId)))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
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));
shouldBeAuthorized = true;
}
else
if (!shouldBeAuthorized)
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/actions",
tester.PayTester.HttpClient);
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-view",
tester.PayTester.HttpClient);
});
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
}
else if(!permissions.Contains(Permissions.ServerManagement))
else if(!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
@ -231,7 +248,7 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient);
}
if (!permissions.Contains(Permissions.ServerManagement))
if (!permissions.Contains(unrestricted))
{
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
@ -245,7 +262,7 @@ namespace BTCPayServer.Tests
tester.PayTester.HttpClient);
}
if (permissions.Contains(Permissions.ServerManagement))
if (permissions.Contains(canModifyServer))
{
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",

View file

@ -38,7 +38,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Permissions.ServerManagement, Permissions.StoreManagement);
var client = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanModifyStoreSettings);
//Get current api key
var apiKeyData = await client.GetCurrentAPIKeyInfo();
Assert.NotNull(apiKeyData);
@ -97,14 +97,14 @@ namespace BTCPayServer.Tests
var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id;
adminAcc.IsAdmin = true;
var adminClient = await adminAcc.CreateClient(Permissions.ProfileManagement);
var adminClient = await adminAcc.CreateClient(Permission.CanModifyProfile);
// We should be forbidden to create a new user without proper admin permissions
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
// However, should be ok with the server management permissions
adminClient = await adminAcc.CreateClient(Permissions.ServerManagement);
adminClient = await adminAcc.CreateClient(Permission.CanModifyServerSettings);
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
// Even creating new admin should be ok
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false;
var user1Client = await user1Acc.CreateClient(Permissions.ServerManagement);
var user1Client = await user1Acc.CreateClient(Permission.CanModifyServerSettings);
// User1 trying to get server management would still fail to create user
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }));
@ -141,9 +141,9 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var clientProfile = await user.CreateClient(Permissions.ProfileManagement);
var clientServer = await user.CreateClient(Permissions.ServerManagement);
var clientInsufficient = await user.CreateClient(Permissions.StoreManagement);
var clientProfile = await user.CreateClient(Permission.CanModifyProfile);
var clientServer = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanViewProfile);
var clientInsufficient = await user.CreateClient(Permission.CanModifyStoreSettings);
var apiKeyProfileUserData = await clientProfile.GetCurrentUser();
@ -153,6 +153,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
await clientServer.GetCurrentUser();
await clientProfile.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
{

View file

@ -32,9 +32,9 @@ namespace BTCPayServer.Tests
public IWebDriver Driver { get; set; }
public ServerTester Server { get; set; }
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null)
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false)
{
var server = ServerTester.Create(scope);
var server = ServerTester.Create(scope, newDb);
return new SeleniumTester()
{
Server = server
@ -259,7 +259,7 @@ namespace BTCPayServer.Tests
public void SetCheckbox(SeleniumTester s, string inputName, bool value)
{
SetCheckbox(s.Driver.FindElement(By.Name(inputName)), value);
SetCheckbox(s.Driver.FindElement(By.Id(inputName)), value);
}
public void ScrollToElement(IWebElement element)

View file

@ -38,11 +38,14 @@ namespace BTCPayServer.Tests
GrantAccessAsync().GetAwaiter().GetResult();
}
public async Task MakeAdmin()
public async Task MakeAdmin(bool isAdmin = true)
{
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId);
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
if (isAdmin)
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
else
await userManager.RemoveFromRoleAsync(u, Roles.ServerAdmin);
IsAdmin = true;
}

View file

@ -62,6 +62,7 @@ using BTCPayServer.U2F.Models;
using BTCPayServer.Security.Bitpay;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
using Newtonsoft.Json.Schema;
using BTCPayServer.Client;
namespace BTCPayServer.Tests
{
@ -3000,6 +3001,22 @@ noninventoryitem:
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CanUsePermission()
{
Assert.True(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyServerSettings)));
Assert.True(Permission.Create(Permission.CanModifyProfile).Contains(Permission.Create(Permission.CanViewProfile)));
Assert.True(Permission.Create(Permission.CanModifyStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings)));
Assert.False(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings)));
Assert.False(Permission.Create(Permission.CanModifyServerSettings).Contains(Permission.Create(Permission.CanModifyStoreSettings)));
Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings)));
Assert.True(Permission.Create(Permission.Unrestricted).Contains(Permission.Create(Permission.CanModifyStoreSettings, "abc")));
Assert.True(Permission.Create(Permission.CanViewStoreSettings).Contains(Permission.Create(Permission.CanViewStoreSettings, "abcd")));
Assert.False(Permission.Create(Permission.CanModifyStoreSettings, "abcd").Contains(Permission.Create(Permission.CanModifyStoreSettings)));
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()

View file

@ -223,5 +223,5 @@
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
<ProjectExtensions><VisualStudio><UserProperties /></VisualStudio></ProjectExtensions>
</Project>

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Security;
@ -12,7 +13,7 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Bitpay)]
[Authorize(Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Bitpay)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;

View file

@ -6,6 +6,7 @@ using System.Net.Mime;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
@ -510,7 +511,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{

View file

@ -25,11 +25,11 @@ namespace BTCPayServer.Controllers
{
ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
{
UserId = new[] {_userManager.GetUserId(User)}
UserId = new[] { _userManager.GetUserId(User) }
})
});
}
[HttpGet("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKey(string id)
{
@ -96,22 +96,13 @@ namespace BTCPayServer.Controllers
permissions ??= Array.Empty<string>();
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel(Permission.ToPermissions(permissions))
{
Label = applicationName,
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,
});
vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin;
return View(vm);
}
@ -126,22 +117,20 @@ namespace BTCPayServer.Controllers
return ar;
}
if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement))
if (viewModel.Strict)
{
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
for (int i = 0; i < viewModel.PermissionValues.Count; i++)
{
viewModel.ServerManagementPermission = false;
}
if (!viewModel.ServerManagementPermission && viewModel.Strict)
{
ModelState.AddModelError(nameof(viewModel.ServerManagementPermission),
"This permission is required for this application.");
if (viewModel.PermissionValues[i].Forbidden)
{
ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value",
$"The permission '{viewModel.PermissionValues[i].Title}' is required for this application.");
}
}
}
if (viewModel.PermissionsFormatted.Contains(Permissions.StoreManagement))
var permissions = Permission.ToPermissions(viewModel.Permissions).ToHashSet();
if (permissions.Contains(Permission.Create(Permission.CanModifyStoreSettings)))
{
if (!viewModel.SelectiveStores &&
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
@ -151,10 +140,10 @@ namespace BTCPayServer.Controllers
"This application does not allow selective store permissions.");
}
if (!viewModel.StoreManagementPermission && !viewModel.SpecificStores.Any() && viewModel.Strict)
if (!viewModel.StoreManagementPermission.Value && !viewModel.SpecificStores.Any() && viewModel.Strict)
{
ModelState.AddModelError(nameof(viewModel.StoreManagementPermission),
"This permission is required for this application.");
$"This permission '{viewModel.StoreManagementPermission.Title}' is required for this application.");
}
}
@ -174,8 +163,9 @@ namespace BTCPayServer.Controllers
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"API key generated! <code>{key.Id}</code>"
});
return RedirectToAction("APIKeys", new { key = key.Id});
default: return View(viewModel);
return RedirectToAction("APIKeys", new { key = key.Id });
default:
return View(viewModel);
}
}
@ -225,15 +215,15 @@ namespace BTCPayServer.Controllers
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);
}
{
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;
@ -248,53 +238,104 @@ namespace BTCPayServer.Controllers
UserId = _userManager.GetUserId(User),
Label = viewModel.Label
};
key.SetPermissions(GetPermissionsFromViewModel(viewModel));
key.Permissions = string.Join(";", GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray());
await _apiKeyRepository.CreateKey(key);
return key;
}
private IEnumerable<string> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
private IEnumerable<Permission> GetPermissionsFromViewModel(AddApiKeyViewModel viewModel)
{
var permissions = viewModel.PermissionValues.Where(tuple => tuple.Value).Select(tuple => tuple.Permission).ToList();
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
List<Permission> permissions = new List<Permission>();
foreach (var p in viewModel.PermissionValues.Where(tuple => tuple.Value && !tuple.Forbidden))
{
permissions.AddRange(viewModel.SpecificStores.Select(Permissions.GetStorePermission));
if (Permission.TryCreatePermission(p.Permission, null, out var pp))
permissions.Add(pp);
}
else if (viewModel.StoreManagementPermission)
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores && viewModel.StoreManagementPermission.Value)
{
permissions.Add(Permissions.StoreManagement);
permissions.Add(Permission.Create(Permission.CanModifyStoreSettings));
}
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
else if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{
permissions.Add(Permissions.ServerManagement);
permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Permission.CanModifyStoreSettings, s)));
}
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.PermissionValues ??= Permissions.GetAllPermissionKeys().Where(s =>
!s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) &&
s != Permissions.ServerManagement)
.Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList();
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded;
viewModel.PermissionValues ??= Permission.AllPolicies.Where(p => p != Permission.CanModifyStoreSettings)
.Select(s => new AddApiKeyViewModel.PermissionValueItem() { Permission = s, Value = false }).ToList();
if (!isAdmin)
{
foreach (var p in viewModel.PermissionValues)
{
if (p.Permission == Permission.CanCreateUser ||
p.Permission == Permission.CanModifyServerSettings)
{
p.Forbidden = true;
}
}
}
return viewModel;
}
public class AddApiKeyViewModel
{
public AddApiKeyViewModel()
{
StoreManagementPermission = new PermissionValueItem()
{
Permission = Permission.CanModifyStoreSettings,
Value = false
};
StoreManagementSelectivePermission = new PermissionValueItem()
{
Permission = $"{Permission.CanModifyStoreSettings}:",
Value = true
};
}
public AddApiKeyViewModel(IEnumerable<Permission> permissions):this()
{
StoreManagementPermission.Value = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings && p.StoreId == null);
PermissionValues = permissions.Where(p => p.Policy != Permission.CanModifyStoreSettings)
.Select(p => new PermissionValueItem() { Permission = p.ToString(), Value = true })
.ToList();
}
public IEnumerable<Permission> GetPermissions()
{
if (!(PermissionValues is null))
{
foreach (var p in PermissionValues.Where(o => o.Value))
{
if (Permission.TryCreatePermission(p.Permission, null, out var pp))
yield return pp;
}
}
if (this.StoreMode == ApiKeyStoreMode.AllStores)
{
if (StoreManagementPermission.Value)
yield return Permission.Create(Permission.CanModifyStoreSettings);
}
else if (this.StoreMode == ApiKeyStoreMode.Specific && SpecificStores is List<string>)
{
foreach (var p in SpecificStores)
{
if (Permission.TryCreatePermission(Permission.CanModifyStoreSettings, p, out var pp))
yield return pp;
}
}
}
public string Label { get; set; }
public StoreData[] Stores { get; set; }
ApiKeyStoreMode _StoreMode;
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 PermissionValueItem StoreManagementPermission { get; set; }
public PermissionValueItem StoreManagementSelectivePermission { get; set; }
public string Command { get; set; }
public List<PermissionValueItem> PermissionValues { get; set; }
@ -306,29 +347,52 @@ namespace BTCPayServer.Controllers
public class PermissionValueItem
{
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
{
{BTCPayServer.Client.Permission.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")},
{BTCPayServer.Client.Permission.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
{BTCPayServer.Client.Permission.CanModifyStoreSettings, ("Modify your stores", "The app will be able to create, view and modify, delete and create new invoices on the all your stores.")},
{BTCPayServer.Client.Permission.CanViewStoreSettings, ("View your stores", "The app will be able to create, view all your stores.")},
{$"{BTCPayServer.Client.Permission.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
{BTCPayServer.Client.Permission.CanModifyServerSettings, ("Manage your server", "The app will have total control on your server")},
{BTCPayServer.Client.Permission.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")},
{BTCPayServer.Client.Permission.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
{BTCPayServer.Client.Permission.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoice.")},
};
public string Title
{
get
{
return PermissionDescriptions[Permission].Title;
}
}
public string Description
{
get
{
return PermissionDescriptions[Permission].Description;
}
}
public string Permission { get; set; }
public bool Value { get; set; }
public bool Forbidden { get; set; }
}
}
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel
{
public AuthorizeApiKeysViewModel()
{
}
public AuthorizeApiKeysViewModel(IEnumerable<Permission> permissions) : base(permissions)
{
Permissions = string.Join(';', permissions.Select(p => p.ToString()).ToArray());
}
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)?? Array.Empty<string>();
}
set
{
Permissions = string.Join(';', value ?? Array.Empty<string>());
}
}
}

View file

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Security;
@ -43,7 +45,7 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys
{
return new ApiKeyData()
{
Permissions = data.GetPermissions(),
Permissions = Permission.ToPermissions(data.Permissions).Select(c => c.ToString()).ToArray(),
ApiKey = data.Id,
UserId = data.UserId,
Label = data.Label

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security;
using BTCPayServer.Security.APIKeys;
@ -27,45 +28,45 @@ namespace BTCPayServer.Controllers.RestApi
}
[HttpGet("me/id")]
[Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public string GetCurrentUserId()
{
return _userManager.GetUserId(User);
}
[HttpGet("me")]
[Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public async Task<ApplicationUser> GetCurrentUser()
{
return await _userManager.GetUserAsync(User);
}
[HttpGet("me/is-admin")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public bool AmIAnAdmin()
{
return true;
}
[HttpGet("me/stores")]
[Authorize(Policy = Policies.CanListStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public async Task<StoreData[]> GetCurrentUserStores()
[Authorize(Policy = Permission.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public StoreData[] GetCurrentUserStores()
{
return await User.GetStores(_userManager, _storeRepository);
return this.HttpContext.GetStoresData();
}
[HttpGet("me/stores/actions")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
[HttpGet("me/stores/{storeId}/can-view")]
[Authorize(Policy = Permission.CanViewStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public bool CanDoNonImplicitStoreActions()
public bool CanViewStore(string storeId)
{
return true;
}
[HttpGet("me/stores/{storeId}/can-edit")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
[Authorize(Policy = Permission.CanModifyStoreSettings,
AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
public bool CanEdit(string storeId)
public bool CanEditStore(string storeId)
{
return true;
}

View file

@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NicolasDorier.RateLimits;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers.RestApi.Users
{
@ -54,7 +55,7 @@ namespace BTCPayServer.Controllers.RestApi.Users
_authorizationService = authorizationService;
}
[Authorize(Policy = Policies.CanModifyProfile.Key, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[Authorize(Policy = Permission.CanViewProfile, AuthenticationSchemes = AuthenticationSchemes.ApiKey)]
[HttpGet("~/api/v1/users/me")]
public async Task<ActionResult<ApplicationUserData>> GetCurrentUser()
{
@ -86,26 +87,21 @@ namespace BTCPayServer.Controllers.RestApi.Users
if (anyAdmin && request.IsAdministrator is true && !isAuth)
return Forbid(AuthenticationSchemes.ApiKey);
// You are de-facto admin if there is no other admin, else you need to be auth and pass policy requirements
bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings.Key))).Succeeded
bool isAdmin = anyAdmin ? (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanModifyServerSettings))).Succeeded
&& isAuth
: true;
// You need to be admin to create an admin
if (request.IsAdministrator is true && !isAdmin)
return Forbid(AuthenticationSchemes.ApiKey);
var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser.Key))).Succeeded;
if (!isAdmin && policies.LockSubscription)
{
// If we are not admin and subscriptions are locked, we need to check the Policies.CanCreateUser.Key permission
var canCreateUser = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Permission.CanCreateUser))).Succeeded;
if (!isAuth || !canCreateUser)
return Forbid(AuthenticationSchemes.ApiKey);
}
// TODO: Check if needed to reenable
// Forbid non-admin users without CanCreateUser permission to create accounts
//if (isAuth && !isAdmin && !canCreateUser)
// return Forbid(AuthenticationSchemes.ApiKey);
var user = new ApplicationUser
{
UserName = request.Email,

View file

@ -35,10 +35,11 @@ using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key,
[Authorize(Policy = Permission.CanModifyServerSettings,
AuthenticationSchemes = BTCPayServer.Security.AuthenticationSchemes.Cookie)]
public partial class ServerController : Controller
{

View file

@ -24,6 +24,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers
{
@ -386,7 +387,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, BTCPayServer.Security.Policies.CanModifyServerSettings.Key)).Succeeded;
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
@ -33,7 +34,7 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{

View file

@ -7,6 +7,7 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders;
@ -127,7 +128,7 @@ namespace BTCPayServer.Controllers
}
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(cancellationToken));
var authorization = await _authorizationService.AuthorizeAsync(User, Policies.CanModifyStoreSettings.Key);
var authorization = await _authorizationService.AuthorizeAsync(User, Permission.CanModifyStoreSettings);
if (!authorization.Succeeded)
{
await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);

View file

@ -6,6 +6,7 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
@ -30,7 +31,7 @@ using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken]
public partial class WalletsController : Controller
{
@ -366,7 +367,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
@ -839,7 +840,7 @@ namespace BTCPayServer.Controllers
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded;
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
@ -869,7 +870,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)

View file

@ -425,6 +425,15 @@ namespace BTCPayServer
ctx.Items["BTCPAY.STOREDATA"] = storeData;
}
public static StoreData[] GetStoresData(this HttpContext ctx)
{
return ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[];
}
public static void SetStoresData(this HttpContext ctx, StoreData[] storeData)
{
ctx.Items["BTCPAY.STORESDATA"] = storeData;
}
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o)
{

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services.Stores;
@ -44,11 +45,8 @@ namespace BTCPayServer.Security.APIKeys
}
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)));
claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permission, permission.ToString())));
return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType));
}

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@ -35,52 +36,54 @@ namespace BTCPayServer.Security.APIKeys
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyProfile.Key:
success = context.HasPermissions(Permissions.ProfileManagement);
case Permission.CanModifyProfile:
case Permission.CanViewProfile:
success = context.HasPermission(Permission.Create(requirement.Policy));
break;
case Policies.CanListStoreSettings.Key:
var selectiveStorePermissions =
Permissions.ExtractStorePermissionsIds(context.GetPermissions());
success = context.HasPermissions(Permissions.StoreManagement) ||
selectiveStorePermissions.Any();
break;
case Policies.CanModifyStoreSettings.Key:
string storeId = _HttpContext.GetImplicitStoreId();
if (!context.HasPermissions(Permissions.StoreManagement) &&
!context.HasPermissions(Permissions.GetStorePermission(storeId)))
break;
if (storeId == null)
case Permission.CanViewStoreSettings:
case Permission.CanModifyStoreSettings:
var storeId = _HttpContext.GetImplicitStoreId();
var userid = _userManager.GetUserId(context.User);
// Specific store action
if (storeId != null)
{
success = true;
if (context.HasPermission(Permission.Create(requirement.Policy, storeId)))
{
if (string.IsNullOrEmpty(userid))
break;
var store = await _storeRepository.FindStore((string)storeId, userid);
if (store == null)
break;
success = true;
_HttpContext.SetStoreData(store);
}
}
else
{
var userid = _userManager.GetUserId(context.User);
if (string.IsNullOrEmpty(userid))
var stores = await _storeRepository.GetStoresByUserId(userid);
List<StoreData> permissionedStores = new List<StoreData>();
foreach (var store in stores)
{
if (context.HasPermission(Permission.Create(requirement.Policy, store.Id)))
permissionedStores.Add(store);
}
_HttpContext.SetStoresData(stores.ToArray());
success = true;
}
break;
case Permission.CanCreateUser:
case Permission.CanModifyServerSettings:
if (context.HasPermission(Permission.Create(requirement.Policy)))
{
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
break;
var store = await _storeRepository.FindStore((string)storeId, userid);
if (store == null)
if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
break;
success = true;
_HttpContext.SetStoreData(store);
}
break;
case Policies.CanCreateUser.Key:
case Policies.CanModifyServerSettings.Key:
if (!context.HasPermissions(Permissions.ServerManagement))
break;
// For this authorization, we still check in database because it is super sensitive.
success = await IsUserAdmin(context.User);
break;
}
//if you do not have the specific permissions, BUT you have server management, we enable god mode
if (!success && context.HasPermissions(Permissions.ServerManagement) &&
requirement.Policy != Policies.CanModifyServerSettings.Key)
{
success = await IsUserAdmin(context.User);
}
if (success)
@ -88,15 +91,5 @@ namespace BTCPayServer.Security.APIKeys
context.Succeed(requirement);
}
}
private async Task<bool> IsUserAdmin(ClaimsPrincipal contextUser)
{
var user = await _userManager.GetUserAsync(contextUser);
if (user == null)
return false;
if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
return false;
return true;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using BTCPayServer.Client;
namespace BTCPayServer.Security.APIKeys
{
@ -8,19 +9,7 @@ namespace BTCPayServer.Security.APIKeys
public static class ClaimTypes
{
public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions);
}
public static class Permissions
{
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
{
{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.")},
};
public const string Permission = "APIKey.Permission";
}
}
}

View file

@ -29,22 +29,6 @@ namespace BTCPayServer.Security.APIKeys
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(Permissions.StoreManagement))
{
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal));
}
var storeIds = Permissions.ExtractStorePermissionsIds(permissions);
return storeRepository.GetStoresByUserId(userManager.GetUserId(claimsPrincipal), storeIds);
}
public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
{
builder.AddScheme<APIKeyAuthenticationOptions, APIKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey,
@ -62,15 +46,24 @@ namespace BTCPayServer.Security.APIKeys
public static string[] GetPermissions(this AuthorizationHandlerContext context)
{
return context.User.Claims.Where(c =>
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase))
c.Type.Equals(APIKeyConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))
.Select(claim => claim.Value).ToArray();
}
public static bool HasPermissions(this AuthorizationHandlerContext context, params string[] scopes)
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission)
{
return scopes.All(s => context.User.HasClaim(c =>
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) &&
c.Value.Split(' ').Contains(s)));
foreach (var claim in context.User.Claims.Where(c =>
c.Type.Equals(APIKeyConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
{
if (Permission.TryParse(claim.Value, out var claimPermission))
{
if (claimPermission.Contains(permission))
{
return true;
}
}
}
return false;
}
}
}

View file

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authentication;
using BTCPayServer.Services;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Client;
namespace BTCPayServer.Security.Bitpay
{
@ -54,7 +55,7 @@ namespace BTCPayServer.Security.Bitpay
var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice;
switch (requirement.Policy)
{
case Policies.CanCreateInvoice.Key:
case Permission.CanCreateInvoice:
if (!isAnonymous || (isAnonymous && anyoneCanInvoice))
{
context.Succeed(requirement);

View file

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using BTCPayServer.Client;
namespace BTCPayServer.Security
{
@ -35,7 +36,7 @@ namespace BTCPayServer.Security
var isAdmin = context.User.IsInRole(Roles.ServerAdmin);
switch (requirement.Policy)
{
case Policies.CanModifyServerSettings.Key:
case Permission.CanModifyServerSettings:
if (isAdmin)
context.Succeed(requirement);
return;
@ -56,11 +57,11 @@ namespace BTCPayServer.Security
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyStoreSettings.Key:
case Permission.CanModifyStoreSettings:
if (store.Role == StoreRoles.Owner || isAdmin)
success = true;
break;
case Policies.CanCreateInvoice.Key:
case Permission.CanCreateInvoice:
if (store.Role == StoreRoles.Owner ||
store.Role == StoreRoles.Guest ||
isAdmin ||

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Security
{
@ -6,14 +7,11 @@ namespace BTCPayServer.Security
{
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{
options.AddPolicy(CanModifyStoreSettings.Key);
options.AddPolicy(CanListStoreSettings.Key);
options.AddPolicy(CanCreateInvoice.Key);
foreach (var p in Permission.AllPolicies)
{
options.AddPolicy(p);
}
options.AddPolicy(CanGetRates.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyProfile.Key);
options.AddPolicy(CanCreateUser.Key);
return options;
}
@ -21,36 +19,9 @@ namespace BTCPayServer.Security
{
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy)));
}
public class CanModifyServerSettings
{
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";
}
public class CanListStoreSettings
{
public const string Key = "btcpay.store.canliststoresettings";
}
public class CanCreateInvoice
{
public const string Key = "btcpay.store.cancreateinvoice";
}
public class CanGetRates
{
public const string Key = "btcpay.store.cangetrates";
}
public class CanCreateUser
{
public const string Key = "btcpay.store.cancreateuser";
}
}
}

View file

@ -20,14 +20,15 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Filters;
using BTCPayServer.Client;
namespace BTCPayServer.Services.Altcoins.Monero.UI
{
[Route("stores/{storeId}/monerolike")]
[OnlyIfSupportAttribute("XMR")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class MoneroLikeStoreController : Controller
{
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;

View file

@ -1,3 +1,4 @@
@namespace BTCPayServer.Client
@model BTCPayServer.Controllers.ManageController.ApiKeysViewModel
@{
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Manage your API Keys");
@ -27,7 +28,7 @@
}
else
{
<span>@string.Join(", ", keyData.GetPermissions())</span>
<span>@string.Join(", ", Permission.ToPermissions(keyData.Permissions).Select(c => c.ToString()).Distinct().ToArray())</span>
}
</td>
<td class="text-right">

View file

@ -5,16 +5,6 @@
@{
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>
@ -26,53 +16,47 @@
<div class="col-md-12">
<form method="post" asp-action="AddApiKey" class="list-group">
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode"/>
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode" />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="list-group-item ">
<div class="form-group">
<label asp-for="Label"></label>
<input asp-for="Label" class="form-control"/>
<input asp-for="Label" class="form-control" />
<span asp-validation-for="Label" class="text-danger"></span>
</div>
</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(Permissions.ServerManagement)</label>
<span asp-validation-for="ServerManagementPermission" class="text-danger"></span>
<p>@GetDescription(Permissions.ServerManagement).</p>
</div>
}
@for (int i = 0; i < Model.PermissionValues.Count; i++)
{
@if (!Model.PermissionValues[i].Forbidden)
{
<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>
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-inline" />
<label asp-for="PermissionValues[i].Value" class="h5">@Model.PermissionValues[i].Title</label>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span>
<p>@GetDescription(Model.PermissionValues[i].Permission).</p>
<p>@Model.PermissionValues[i].Description</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"}})
<div class="list-group-item form-group">
<input id="@Model.StoreManagementPermission.Permission" type="checkbox" asp-for="@Model.StoreManagementPermission.Value" class="form-check-inline" />
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(Permissions.StoreManagement)</label>
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
<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>
</div>
<label asp-for="StoreManagementPermission" class="h5">@Model.StoreManagementPermission.Title</label>
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
<p class="mb-0">@Model.StoreManagementPermission.Description</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(Permissions.StoreManagement + ":")</h5>
<p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p>
<h5 class="mb-1">@Model.StoreManagementSelectivePermission.Title</h5>
<p class="mb-0">@Model.StoreManagementSelectivePermission.Description</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())

View file

@ -6,24 +6,17 @@
@{
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;
}
var permissions = Permission.ToPermissions(Model.Permissions);
var hasStorePermission = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings);
}
<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"/>
<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" />
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode" />
<section>
<div class="card container">
<div class="row">
@ -39,76 +32,60 @@
<div class="list-group-item ">
<div class="form-group">
<label asp-for="Label"></label>
<input asp-for="Label" class="form-control"/>
<input asp-for="Label" class="form-control" />
<span asp-validation-for="Label" class="text-danger"></span>
</div>
</div>
@if (!Model.PermissionsFormatted.Any())
@if (!permissions.Any())
{
<div class="list-group-item form-group">
<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>
}
@if (Model.PermissionsFormatted.Contains(Permissions.ServerManagement) && (Model.IsServerAdmin || Model.Strict))
{
<div class="list-group-item form-group">
@if (Model.Strict || !Model.IsServerAdmin)
{
<input type="hidden" asp-for="ServerManagementPermission"/>
<input type="checkbox" class="form-check-inline" checked="@Model.ServerManagementPermission" disabled/>
}
else
{
<input type="checkbox" asp-for="ServerManagementPermission" class="form-check-inline"/>
}
<label asp-for="ServerManagementPermission" class="h5">@GetTitle(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(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)
@if (Model.Strict)
{
<input type="hidden" asp-for="PermissionValues[i].Value"/>
<input type="checkbox" class="form-check-inline" checked="@Model.PermissionValues[i].Value" disabled/>
<input id="@Model.PermissionValues[i].Permission" 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"/>
<input id="@Model.PermissionValues[i].Permission" 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>
<label asp-for="PermissionValues[i].Value" class="h5">@Model.PermissionValues[i].Title</label>
@if (Model.PermissionValues[i].Forbidden)
{
<br />
<span class="text-danger">
This permission is not available for your account.
</span>
}
<p>@Model.PermissionValues[i].Description</p>
</div>
}
@if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement))
@if (hasStorePermission)
{
@if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{
<div class="list-group-item form-group">
@if (Model.Strict)
{
<input type="hidden" asp-for="StoreManagementPermission"/>
<input type="checkbox" class="form-check-inline" checked="@Model.StoreManagementPermission" disabled/>
<input id="@Model.StoreManagementPermission.Permission" type="hidden" asp-for="StoreManagementPermission.Value" />
<input type="checkbox" class="form-check-inline" checked="@Model.StoreManagementPermission.Value" disabled />
}
else
{
<input type="checkbox" asp-for="StoreManagementPermission" class="form-check-inline"/>
<input id="@Model.StoreManagementPermission.Permission" type="checkbox" asp-for="StoreManagementPermission.Value" class="form-check-inline" />
}
<label asp-for="StoreManagementPermission" class="h5">@GetTitle(Permissions.StoreManagement)</label>
<label asp-for="StoreManagementPermission" class="h5">@Model.StoreManagementPermission.Title</label>
<br />
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span>
<p class="mb-0">@GetDescription(Permissions.StoreManagement).</p>
<p class="mb-0">@Model.StoreManagementPermission.Description</p>
@if (Model.SelectiveStores)
{
<button type="submit" class="btn btn-link" name="command" value="change-store-mode">Give permission to specific stores instead</button>
@ -119,8 +96,8 @@
{
<div class="list-group-item p-0 border-0 mb-2">
<li class="list-group-item">
<h5 class="mb-1">@GetTitle(Permissions.StoreManagement + ":")</h5>
<p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p>
<h5 class="mb-1">@Model.StoreManagementSelectivePermission.Title</h5>
<p class="mb-0">@Model.StoreManagementSelectivePermission.Description</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())