mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Fix: Create store could be called with a scoped store's modify apikey (#1696)
This commit is contained in:
@@ -18,6 +18,8 @@ namespace BTCPayServer.Client
|
||||
private readonly string _password;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Uri Host => _btcpayHost;
|
||||
|
||||
public string APIKey => _apiKey;
|
||||
|
||||
public BTCPayServerClient(Uri btcpayHost, HttpClient httpClient = null)
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
|
||||
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
||||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
|
||||
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
|
||||
|
||||
@@ -69,6 +69,32 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task SpecificCanModifyStoreCantCreateNewStore()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
await acc.GrantAccessAsync();
|
||||
var unrestricted = await acc.CreateClient();
|
||||
var response = await unrestricted.CreateStore(new CreateStoreRequest() { Name = "mystore" });
|
||||
var apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings", response.Id) } })).ApiKey;
|
||||
var restricted = new BTCPayServerClient(unrestricted.Host, apiKey);
|
||||
|
||||
// Unscoped permission should be required for create store
|
||||
await this.AssertHttpError(403, async () => await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" }));
|
||||
// Unrestricted should work fine
|
||||
await unrestricted.CreateStore(new CreateStoreRequest() { Name = "store2" });
|
||||
// Restricted but unscoped should work fine
|
||||
apiKey = (await unrestricted.CreateAPIKey(new CreateApiKeyRequest() { Permissions = new[] { Permission.Create("btcpay.store.canmodifystoresettings") } })).ApiKey;
|
||||
restricted = new BTCPayServerClient(unrestricted.Host, apiKey);
|
||||
await restricted.CreateStore(new CreateStoreRequest() { Name = "store2" });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteAPIKeyViaAPI()
|
||||
@@ -135,7 +161,9 @@ namespace BTCPayServer.Tests
|
||||
// Let's make an admin
|
||||
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true
|
||||
Email = "admin@gmail.com",
|
||||
Password = "abceudhqw",
|
||||
IsAdministrator = true
|
||||
});
|
||||
|
||||
// Creating a new user without proper creds is now impossible (unauthorized)
|
||||
@@ -155,7 +183,9 @@ namespace BTCPayServer.Tests
|
||||
await AssertHttpError(403,
|
||||
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
Email = "admin2@gmail.com",
|
||||
Password = "afewfoiewiou",
|
||||
IsAdministrator = true
|
||||
}));
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = true });
|
||||
|
||||
@@ -171,7 +201,9 @@ namespace BTCPayServer.Tests
|
||||
await AssertHttpError(403,
|
||||
async () => await adminClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
Email = "test4@gmail.com",
|
||||
Password = "afewfoiewiou",
|
||||
IsAdministrator = true
|
||||
}));
|
||||
|
||||
// However, should be ok with the unrestricted permissions of an admin
|
||||
@@ -181,7 +213,9 @@ namespace BTCPayServer.Tests
|
||||
// Even creating new admin should be ok
|
||||
await adminClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
Email = "admin4@gmail.com",
|
||||
Password = "afewfoiewiou",
|
||||
IsAdministrator = true
|
||||
});
|
||||
|
||||
var user1Acc = tester.NewAccount();
|
||||
@@ -203,7 +237,9 @@ namespace BTCPayServer.Tests
|
||||
await AssertHttpError(403,
|
||||
async () => await user1Client.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
Email = "admin8@gmail.com",
|
||||
Password = "afewfoiewiou",
|
||||
IsAdministrator = true
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -534,25 +570,29 @@ namespace BTCPayServer.Tests
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
|
||||
var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
});
|
||||
Assert.NotNull(newUser);
|
||||
|
||||
var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
});
|
||||
Assert.NotNull(newUser2);
|
||||
|
||||
await AssertValidationError(new[] { "Email" }, async () =>
|
||||
await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
|
||||
await AssertValidationError(new[] { "Password" }, async () =>
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
|
||||
@@ -46,14 +46,19 @@ namespace BTCPayServer.Security.GreenField
|
||||
c.Type.Equals(GreenFieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(claim => claim.Value).ToArray();
|
||||
}
|
||||
|
||||
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission)
|
||||
{
|
||||
return HasPermission(context, permission, false);
|
||||
}
|
||||
public static bool HasPermission(this AuthorizationHandlerContext context, Permission permission, bool requireUnscoped)
|
||||
{
|
||||
foreach (var claim in context.User.Claims.Where(c =>
|
||||
c.Type.Equals(GreenFieldConstants.ClaimTypes.Permission, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
if (Permission.TryParse(claim.Value, out var claimPermission))
|
||||
{
|
||||
if (requireUnscoped && claimPermission.Scope is string)
|
||||
continue;
|
||||
if (claimPermission.Contains(permission))
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -35,14 +35,22 @@ namespace BTCPayServer.Security.GreenField
|
||||
return;
|
||||
var userid = _userManager.GetUserId(context.User);
|
||||
bool success = false;
|
||||
switch (requirement.Policy)
|
||||
var policy = requirement.Policy;
|
||||
var requiredUnscoped = false;
|
||||
if (policy.EndsWith(':'))
|
||||
{
|
||||
case { } policy when Policies.IsStorePolicy(policy):
|
||||
policy = policy.Substring(0, policy.Length - 1);
|
||||
requiredUnscoped = true;
|
||||
}
|
||||
|
||||
switch (policy)
|
||||
{
|
||||
case { } when Policies.IsStorePolicy(policy):
|
||||
var storeId = _HttpContext.GetImplicitStoreId();
|
||||
// Specific store action
|
||||
if (storeId != null)
|
||||
{
|
||||
if (context.HasPermission(Permission.Create(requirement.Policy, storeId)))
|
||||
if (context.HasPermission(Permission.Create(policy, storeId), requiredUnscoped))
|
||||
{
|
||||
if (string.IsNullOrEmpty(userid))
|
||||
break;
|
||||
@@ -60,19 +68,21 @@ namespace BTCPayServer.Security.GreenField
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requiredUnscoped && !context.HasPermission(Permission.Create(policy)))
|
||||
break;
|
||||
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)))
|
||||
if (context.HasPermission(Permission.Create(policy, store.Id), requiredUnscoped))
|
||||
permissionedStores.Add(store);
|
||||
}
|
||||
_HttpContext.SetStoresData(permissionedStores.ToArray());
|
||||
success = true;
|
||||
}
|
||||
break;
|
||||
case { } policy when Policies.IsServerPolicy(policy):
|
||||
if (context.HasPermission(Permission.Create(requirement.Policy)))
|
||||
case { } when Policies.IsServerPolicy(policy):
|
||||
if (context.HasPermission(Permission.Create(policy)))
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(context.User);
|
||||
if (user == null)
|
||||
@@ -85,7 +95,7 @@ namespace BTCPayServer.Security.GreenField
|
||||
case Policies.CanModifyProfile:
|
||||
case Policies.CanViewProfile:
|
||||
case Policies.Unrestricted:
|
||||
success = context.HasPermission(Permission.Create(requirement.Policy));
|
||||
success = context.HasPermission(Permission.Create(policy), requiredUnscoped);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
options.AddPolicy(p);
|
||||
}
|
||||
options.AddPolicy(Policies.CanModifyStoreSettingsUnscoped);
|
||||
options.AddPolicy(CanGetRates.Key);
|
||||
return options;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user