Refactor permissions of GreenField

This commit is contained in:
nicolas.dorier
2020-03-19 19:11:15 +09:00
parent eac33d494a
commit 29a807696b
31 changed files with 581 additions and 404 deletions

View File

@@ -5,25 +5,183 @@ using System.Text.Json.Serialization;
namespace BTCPayServer.Client namespace BTCPayServer.Client
{ {
public static class Permissions public class Permission
{ {
public const string ServerManagement = nameof(ServerManagement); public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
public const string StoreManagement = nameof(StoreManagement); public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
public const string ProfileManagement = nameof(ProfileManagement); 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, yield return CanCreateInvoice;
StoreManagement, yield return CanModifyServerSettings;
ProfileManagement 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 public static Permission Create(string policy, string storeId = null)
.Where(s => s.StartsWith($"{nameof(StoreManagement)}:", StringComparison.InvariantCulture)) {
.Select(s => s.Split(":")[1]); 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 StoreData StoreData { get; set; }
public ApplicationUser User { get; set; } public ApplicationUser User { get; set; }
public string Label { 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 public enum APIKeyType

View File

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

View File

@@ -38,7 +38,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
await user.MakeAdmin(); 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 //Get current api key
var apiKeyData = await client.GetCurrentAPIKeyInfo(); var apiKeyData = await client.GetCurrentAPIKeyInfo();
Assert.NotNull(apiKeyData); Assert.NotNull(apiKeyData);
@@ -97,14 +97,14 @@ namespace BTCPayServer.Tests
var adminAcc = tester.NewAccount(); var adminAcc = tester.NewAccount();
adminAcc.UserId = admin.Id; adminAcc.UserId = admin.Id;
adminAcc.IsAdmin = true; 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 // 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" }));
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true })); 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 // 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" }); await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
// Even creating new admin should be ok // Even creating new admin should be ok
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }); await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
@@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
var user1Acc = tester.NewAccount(); var user1Acc = tester.NewAccount();
user1Acc.UserId = user1.Id; user1Acc.UserId = user1.Id;
user1Acc.IsAdmin = false; 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 // 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" })); 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(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
await user.MakeAdmin(); await user.MakeAdmin();
var clientProfile = await user.CreateClient(Permissions.ProfileManagement); var clientProfile = await user.CreateClient(Permission.CanModifyProfile);
var clientServer = await user.CreateClient(Permissions.ServerManagement); var clientServer = await user.CreateClient(Permission.CanModifyServerSettings, Permission.CanViewProfile);
var clientInsufficient = await user.CreateClient(Permissions.StoreManagement); var clientInsufficient = await user.CreateClient(Permission.CanModifyStoreSettings);
var apiKeyProfileUserData = await clientProfile.GetCurrentUser(); var apiKeyProfileUserData = await clientProfile.GetCurrentUser();
@@ -153,6 +153,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser()); await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
await clientServer.GetCurrentUser(); await clientServer.GetCurrentUser();
await clientProfile.GetCurrentUser();
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest() 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 IWebDriver Driver { get; set; }
public ServerTester Server { 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() return new SeleniumTester()
{ {
Server = server Server = server
@@ -259,7 +259,7 @@ namespace BTCPayServer.Tests
public void SetCheckbox(SeleniumTester s, string inputName, bool value) 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) public void ScrollToElement(IWebElement element)

View File

@@ -38,11 +38,14 @@ namespace BTCPayServer.Tests
GrantAccessAsync().GetAwaiter().GetResult(); GrantAccessAsync().GetAwaiter().GetResult();
} }
public async Task MakeAdmin() public async Task MakeAdmin(bool isAdmin = true)
{ {
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>(); var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId); 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; IsAdmin = true;
} }

View File

@@ -62,6 +62,7 @@ using BTCPayServer.U2F.Models;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache; using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
using Newtonsoft.Json.Schema; using Newtonsoft.Json.Schema;
using BTCPayServer.Client;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -3000,6 +3001,22 @@ noninventoryitem:
await new ApplicationDbContext(builder.Options).Database.MigrateAsync(); 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)] [Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")] [Trait("Fast", "Fast")]
public void CheckRatesProvider() public void CheckRatesProvider()

View File

@@ -223,5 +223,5 @@
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" /> <_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
</ItemGroup> </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> </Project>

View File

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

View File

@@ -6,6 +6,7 @@ using System.Net.Mime;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Filters; using BTCPayServer.Filters;
@@ -510,7 +511,7 @@ namespace BTCPayServer.Controllers
[HttpPost] [HttpPost]
[Route("invoices/create")] [Route("invoices/create")]
[Authorize(Policy = Policies.CanCreateInvoice.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Permission.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken) 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() ApiKeyDatas = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
{ {
UserId = new[] {_userManager.GetUserId(User)} UserId = new[] { _userManager.GetUserId(User) }
}) })
}); });
} }
[HttpGet("api-keys/{id}/delete")] [HttpGet("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKey(string id) public async Task<IActionResult> RemoveAPIKey(string id)
{ {
@@ -96,22 +96,13 @@ namespace BTCPayServer.Controllers
permissions ??= Array.Empty<string>(); permissions ??= Array.Empty<string>();
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel() var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel(Permission.ToPermissions(permissions))
{ {
Label = applicationName, 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, ApplicationName = applicationName,
SelectiveStores = selectiveStores, SelectiveStores = selectiveStores,
Strict = strict, Strict = strict,
}); });
vm.ServerManagementPermission = vm.ServerManagementPermission && vm.IsServerAdmin;
return View(vm); return View(vm);
} }
@@ -126,22 +117,20 @@ namespace BTCPayServer.Controllers
return ar; return ar;
} }
if (viewModel.Strict)
if (viewModel.PermissionsFormatted.Contains(Permissions.ServerManagement))
{ {
if (!viewModel.IsServerAdmin && viewModel.ServerManagementPermission) for (int i = 0; i < viewModel.PermissionValues.Count; i++)
{ {
viewModel.ServerManagementPermission = false; if (viewModel.PermissionValues[i].Forbidden)
} {
ModelState.AddModelError($"{viewModel.PermissionValues}[{i}].Value",
if (!viewModel.ServerManagementPermission && viewModel.Strict) $"The permission '{viewModel.PermissionValues[i].Title}' is required for this application.");
{ }
ModelState.AddModelError(nameof(viewModel.ServerManagementPermission),
"This permission 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 && if (!viewModel.SelectiveStores &&
viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific) viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
@@ -151,10 +140,10 @@ namespace BTCPayServer.Controllers
"This application does not allow selective store permissions."); "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), 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, Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"API key generated! <code>{key.Id}</code>" Html = $"API key generated! <code>{key.Id}</code>"
}); });
return RedirectToAction("APIKeys", new { key = key.Id}); return RedirectToAction("APIKeys", new { key = key.Id });
default: return View(viewModel); default:
return View(viewModel);
} }
} }
@@ -225,15 +215,15 @@ namespace BTCPayServer.Controllers
return View(viewModel); return View(viewModel);
case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase): case string x when x.StartsWith("remove-store", StringComparison.InvariantCultureIgnoreCase):
{ {
ModelState.Clear(); ModelState.Clear();
var index = int.Parse( var index = int.Parse(
viewModel.Command.Substring( viewModel.Command.Substring(
viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), viewModel.Command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1),
CultureInfo.InvariantCulture); CultureInfo.InvariantCulture);
viewModel.SpecificStores.RemoveAt(index); viewModel.SpecificStores.RemoveAt(index);
return View(viewModel); return View(viewModel);
} }
} }
return null; return null;
@@ -248,53 +238,104 @@ namespace BTCPayServer.Controllers
UserId = _userManager.GetUserId(User), UserId = _userManager.GetUserId(User),
Label = viewModel.Label Label = viewModel.Label
}; };
key.SetPermissions(GetPermissionsFromViewModel(viewModel)); key.Permissions = string.Join(";", GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray());
await _apiKeyRepository.CreateKey(key); await _apiKeyRepository.CreateKey(key);
return 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(); List<Permission> permissions = new List<Permission>();
foreach (var p in viewModel.PermissionValues.Where(tuple => tuple.Value && !tuple.Forbidden))
if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{ {
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));
} }
else if (viewModel.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific)
if (viewModel.IsServerAdmin && viewModel.ServerManagementPermission)
{ {
permissions.Add(Permissions.ServerManagement); permissions.AddRange(viewModel.SpecificStores.Select(s => Permission.Create(Permission.CanModifyStoreSettings, s)));
} }
return permissions.Distinct(); return permissions.Distinct();
} }
private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel private async Task<T> SetViewModelValues<T>(T viewModel) where T : AddApiKeyViewModel
{ {
viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User)); viewModel.Stores = await _StoreRepository.GetStoresByUserId(_userManager.GetUserId(User));
viewModel.IsServerAdmin = var isAdmin = (await _authorizationService.AuthorizeAsync(User, Permission.CanModifyServerSettings)).Succeeded;
(await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded; viewModel.PermissionValues ??= Permission.AllPolicies.Where(p => p != Permission.CanModifyStoreSettings)
viewModel.PermissionValues ??= Permissions.GetAllPermissionKeys().Where(s => .Select(s => new AddApiKeyViewModel.PermissionValueItem() { Permission = s, Value = false }).ToList();
!s.Contains(Permissions.StoreManagement, StringComparison.InvariantCultureIgnoreCase) && if (!isAdmin)
s != Permissions.ServerManagement) {
.Select(s => new AddApiKeyViewModel.PermissionValueItem() {Permission = s, Value = true}).ToList(); foreach (var p in viewModel.PermissionValues)
{
if (p.Permission == Permission.CanCreateUser ||
p.Permission == Permission.CanModifyServerSettings)
{
p.Forbidden = true;
}
}
}
return viewModel; return viewModel;
} }
public class AddApiKeyViewModel 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 string Label { get; set; }
public StoreData[] Stores { get; set; } public StoreData[] Stores { get; set; }
ApiKeyStoreMode _StoreMode;
public ApiKeyStoreMode StoreMode { get; set; } public ApiKeyStoreMode StoreMode { get; set; }
public List<string> SpecificStores { get; set; } = new List<string>(); public List<string> SpecificStores { get; set; } = new List<string>();
public bool IsServerAdmin { get; set; } public PermissionValueItem StoreManagementPermission { get; set; }
public bool ServerManagementPermission { get; set; } public PermissionValueItem StoreManagementSelectivePermission { get; set; }
public bool StoreManagementPermission { get; set; }
public string Command { get; set; } public string Command { get; set; }
public List<PermissionValueItem> PermissionValues { get; set; } public List<PermissionValueItem> PermissionValues { get; set; }
@@ -306,29 +347,52 @@ namespace BTCPayServer.Controllers
public class PermissionValueItem 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 string Permission { get; set; }
public bool Value { get; set; } public bool Value { get; set; }
public bool Forbidden { get; set; }
} }
} }
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel 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 string ApplicationName { get; set; }
public bool Strict { get; set; } public bool Strict { get; set; }
public bool SelectiveStores { get; set; } public bool SelectiveStores { get; set; }
public string Permissions { 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.Threading.Tasks;
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Security; using BTCPayServer.Security;
@@ -43,7 +45,7 @@ namespace BTCPayServer.Controllers.RestApi.ApiKeys
{ {
return new ApiKeyData() return new ApiKeyData()
{ {
Permissions = data.GetPermissions(), Permissions = Permission.ToPermissions(data.Permissions).Select(c => c.ToString()).ToArray(),
ApiKey = data.Id, ApiKey = data.Id,
UserId = data.UserId, UserId = data.UserId,
Label = data.Label Label = data.Label

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using BTCPayServer.Client;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -386,7 +387,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet() 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) if (isAdmin)
return true; return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>(); var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();

View File

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

View File

@@ -7,6 +7,7 @@ using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Hwi; using BTCPayServer.Hwi;
using BTCPayServer.ModelBinders; using BTCPayServer.ModelBinders;
@@ -127,7 +128,7 @@ namespace BTCPayServer.Controllers
} }
await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken); await websocketHelper.Send("{ \"info\": \"ready\"}", cancellationToken);
o = JObject.Parse(await websocketHelper.NextMessageAsync(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) if (!authorization.Succeeded)
{ {
await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken); await websocketHelper.Send("{ \"error\": \"not-authorized\"}", cancellationToken);

View File

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

View File

@@ -425,6 +425,15 @@ namespace BTCPayServer
ctx.Items["BTCPAY.STOREDATA"] = storeData; 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() }; private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o) public static string ToJson(this object o)
{ {

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
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;
@@ -44,11 +45,8 @@ namespace BTCPayServer.Security.APIKeys
} }
List<Claim> claims = new List<Claim>(); List<Claim> claims = new List<Claim>();
claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId)); claims.Add(new Claim(_identityOptions.CurrentValue.ClaimsIdentity.UserIdClaimType, key.UserId));
claims.AddRange(key.GetPermissions() claims.AddRange(Permission.ToPermissions(key.Permissions).Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permission, permission.ToString())));
.Select(permission => new Claim(APIKeyConstants.ClaimTypes.Permissions, permission)));
return AuthenticateResult.Success(new AuthenticationTicket( return AuthenticateResult.Success(new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType)); new ClaimsPrincipal(new ClaimsIdentity(claims, APIKeyConstants.AuthenticationType)), APIKeyConstants.AuthenticationType));
} }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -35,52 +36,54 @@ namespace BTCPayServer.Security.APIKeys
bool success = false; bool success = false;
switch (requirement.Policy) switch (requirement.Policy)
{ {
case Policies.CanModifyProfile.Key: case Permission.CanModifyProfile:
success = context.HasPermissions(Permissions.ProfileManagement); case Permission.CanViewProfile:
success = context.HasPermission(Permission.Create(requirement.Policy));
break; 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 else
{ {
var userid = _userManager.GetUserId(context.User); var stores = await _storeRepository.GetStoresByUserId(userid);
if (string.IsNullOrEmpty(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; break;
var store = await _storeRepository.FindStore((string)storeId, userid); if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
if (store == null)
break; break;
success = true; success = true;
_HttpContext.SetStoreData(store);
} }
break; 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) if (success)
@@ -88,15 +91,5 @@ namespace BTCPayServer.Security.APIKeys
context.Succeed(requirement); 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 System.Collections.Generic;
using BTCPayServer.Client;
namespace BTCPayServer.Security.APIKeys namespace BTCPayServer.Security.APIKeys
{ {
@@ -8,19 +9,7 @@ namespace BTCPayServer.Security.APIKeys
public static class ClaimTypes public static class ClaimTypes
{ {
public const string Permissions = nameof(APIKeys) + "." + nameof(Permissions); public const string Permission = "APIKey.Permission";
}
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.")},
};
} }
} }
} }

View File

@@ -29,22 +29,6 @@ namespace BTCPayServer.Security.APIKeys
return false; 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) public static AuthenticationBuilder AddAPIKeyAuthentication(this AuthenticationBuilder builder)
{ {
builder.AddScheme<APIKeyAuthenticationOptions, APIKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey, builder.AddScheme<APIKeyAuthenticationOptions, APIKeyAuthenticationHandler>(AuthenticationSchemes.ApiKey,
@@ -62,15 +46,24 @@ namespace BTCPayServer.Security.APIKeys
public static string[] GetPermissions(this AuthorizationHandlerContext context) public static string[] GetPermissions(this AuthorizationHandlerContext context)
{ {
return context.User.Claims.Where(c => 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(); .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 => foreach (var claim in context.User.Claims.Where(c =>
c.Type.Equals(APIKeyConstants.ClaimTypes.Permissions, StringComparison.InvariantCultureIgnoreCase) && c.Type.Equals(APIKeyConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
c.Value.Split(' ').Contains(s))); {
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 Microsoft.AspNetCore.Authentication;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using BTCPayServer.Client;
namespace BTCPayServer.Security.Bitpay namespace BTCPayServer.Security.Bitpay
{ {
@@ -54,7 +55,7 @@ namespace BTCPayServer.Security.Bitpay
var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice; var anyoneCanInvoice = store.GetStoreBlob().AnyoneCanInvoice;
switch (requirement.Policy) switch (requirement.Policy)
{ {
case Policies.CanCreateInvoice.Key: case Permission.CanCreateInvoice:
if (!isAnonymous || (isAnonymous && anyoneCanInvoice)) if (!isAnonymous || (isAnonymous && anyoneCanInvoice))
{ {
context.Succeed(requirement); context.Succeed(requirement);

View File

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

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization; using BTCPayServer.Client;
using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer.Security namespace BTCPayServer.Security
{ {
@@ -6,14 +7,11 @@ namespace BTCPayServer.Security
{ {
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options) public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{ {
options.AddPolicy(CanModifyStoreSettings.Key); foreach (var p in Permission.AllPolicies)
options.AddPolicy(CanListStoreSettings.Key); {
options.AddPolicy(CanCreateInvoice.Key); options.AddPolicy(p);
}
options.AddPolicy(CanGetRates.Key); options.AddPolicy(CanGetRates.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyServerSettings.Key);
options.AddPolicy(CanModifyProfile.Key);
options.AddPolicy(CanCreateUser.Key);
return options; return options;
} }
@@ -21,36 +19,9 @@ namespace BTCPayServer.Security
{ {
options.AddPolicy(policy, o => o.AddRequirements(new PolicyRequirement(policy))); 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 class CanGetRates
{ {
public const string Key = "btcpay.store.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;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Client;
namespace BTCPayServer.Services.Altcoins.Monero.UI namespace BTCPayServer.Services.Altcoins.Monero.UI
{ {
[Route("stores/{storeId}/monerolike")] [Route("stores/{storeId}/monerolike")]
[OnlyIfSupportAttribute("XMR")] [OnlyIfSupportAttribute("XMR")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Permission.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Permission.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class MoneroLikeStoreController : Controller public class MoneroLikeStoreController : Controller
{ {
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;

View File

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

View File

@@ -5,16 +5,6 @@
@{ @{
ViewData.SetActivePageAndTitle(ManageNavPages.APIKeys, "Add API Key"); 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> <h4>@ViewData["Title"]</h4>
@@ -26,53 +16,47 @@
<div class="col-md-12"> <div class="col-md-12">
<form method="post" asp-action="AddApiKey" class="list-group"> <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 asp-validation-summary="All" class="text-danger"></div>
<div class="list-group-item "> <div class="list-group-item ">
<div class="form-group"> <div class="form-group">
<label asp-for="Label"></label> <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> <span asp-validation-for="Label" class="text-danger"></span>
</div> </div>
</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++) @for (int i = 0; i < Model.PermissionValues.Count; i++)
{ {
@if (!Model.PermissionValues[i].Forbidden)
{
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<input type="hidden" asp-for="PermissionValues[i].Permission"> <input type="hidden" asp-for="PermissionValues[i].Permission">
<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> <label asp-for="PermissionValues[i].Value" class="h5">@Model.PermissionValues[i].Title</label>
<span asp-validation-for="PermissionValues[i].Value" class="text-danger"></span> <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> </div>
} }
}
@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">
@Html.CheckBoxFor(model => model.StoreManagementPermission, new Dictionary<string, string>() {{"class", "form-check-inline"}}) <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> <label asp-for="StoreManagementPermission" class="h5">@Model.StoreManagementPermission.Title</label>
<span asp-validation-for="StoreManagementPermission" class="text-danger"></span> <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>
<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>
} }
else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific) else if (Model.StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific)
{ {
<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(Permissions.StoreManagement + ":")</h5> <h5 class="mb-1">@Model.StoreManagementSelectivePermission.Title</h5>
<p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p> <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> <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

@@ -6,24 +6,17 @@
@{ @{
Layout = "_Layout"; Layout = "_Layout";
ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}"; ViewData["Title"] = $"Authorize {(Model.ApplicationName ?? "Application")}";
var permissions = Permission.ToPermissions(Model.Permissions);
string GetDescription(string permission) var hasStorePermission = permissions.Any(p => p.Policy == Permission.CanModifyStoreSettings);
{
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
}
string GetTitle(string permission)
{
return APIKeyConstants.Permissions.PermissionDescriptions[permission].Description;
}
} }
<partial name="_StatusMessage"/> <partial name="_StatusMessage"/>
<form method="post" asp-action="AuthorizeAPIKey"> <form method="post" asp-action="AuthorizeAPIKey">
<input type="hidden" asp-for="Permissions" value="@Model.Permissions"/> <input type="hidden" asp-for="Permissions" value="@Model.Permissions" />
<input type="hidden" asp-for="Strict" value="@Model.Strict"/> <input type="hidden" asp-for="Strict" value="@Model.Strict" />
<input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName"/> <input type="hidden" asp-for="ApplicationName" value="@Model.ApplicationName" />
<input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores"/> <input type="hidden" asp-for="SelectiveStores" value="@Model.SelectiveStores" />
<input type="hidden" asp-for="StoreMode" value="@Model.StoreMode" />
<section> <section>
<div class="card container"> <div class="card container">
<div class="row"> <div class="row">
@@ -39,76 +32,60 @@
<div class="list-group-item "> <div class="list-group-item ">
<div class="form-group"> <div class="form-group">
<label asp-for="Label"></label> <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> <span asp-validation-for="Label" class="text-danger"></span>
</div> </div>
</div> </div>
@if (!Model.PermissionsFormatted.Any()) @if (!permissions.Any())
{ {
<div class="list-group-item form-group"> <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> </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++) @for (int i = 0; i < Model.PermissionValues.Count; i++)
{ {
<div class="list-group-item form-group"> <div class="list-group-item form-group">
<input type="hidden" asp-for="PermissionValues[i].Permission"> <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 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/> <input type="checkbox" class="form-check-inline" checked="@Model.PermissionValues[i].Value" disabled />
} }
else 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> <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> @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> </div>
} }
@if (Model.PermissionsFormatted.Contains(Permissions.StoreManagement)) @if (hasStorePermission)
{ {
@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">
@if (Model.Strict) @if (Model.Strict)
{ {
<input type="hidden" asp-for="StoreManagementPermission"/> <input id="@Model.StoreManagementPermission.Permission" type="hidden" asp-for="StoreManagementPermission.Value" />
<input type="checkbox" class="form-check-inline" checked="@Model.StoreManagementPermission" disabled/> <input type="checkbox" class="form-check-inline" checked="@Model.StoreManagementPermission.Value" disabled />
} }
else 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> <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) @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>
@@ -119,8 +96,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(Permissions.StoreManagement + ":")</h5> <h5 class="mb-1">@Model.StoreManagementSelectivePermission.Title</h5>
<p class="mb-0">@GetDescription(Permissions.StoreManagement + ":").</p> <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> <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())