mirror of
https://github.com/bitwarden/server
synced 2025-12-23 19:53:40 +00:00
[PM-25913] Fix owners unable to rename provider-managed organization (#6599)
And other refactors: - move update organization method to a command - separate authorization from business logic - add tests - move Billing Team logic into their service
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private Organization _organization = null!;
|
||||
private string _ownerEmail = null!;
|
||||
private readonly string _billingEmail = "billing@example.com";
|
||||
private readonly string _organizationName = "Organizations Controller Test Org";
|
||||
|
||||
public OrganizationsControllerTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"org-integration-test-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
|
||||
name: _organizationName,
|
||||
billingEmail: _billingEmail,
|
||||
plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: _ownerEmail,
|
||||
passwordManagerSeats: 5,
|
||||
paymentMethod: PaymentMethodType.Card);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithoutProvider_CanUpdateOrganization()
|
||||
{
|
||||
// Arrange - Regular organization owner (no provider)
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsProvider_CanUpdateOrganization()
|
||||
{
|
||||
// Create and login as a new account to be the provider user (not the owner)
|
||||
var providerUserEmail = $"provider-{Guid.NewGuid()}@example.com";
|
||||
var (token, _) = await _factory.LoginWithNewAccount(providerUserEmail);
|
||||
|
||||
// Set up provider linked to org and ProviderUser entry
|
||||
var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id,
|
||||
ProviderType.Msp);
|
||||
await ProviderTestHelpers.CreateProviderUserAsync(_factory, provider.Id, providerUserEmail,
|
||||
ProviderUserType.ProviderAdmin);
|
||||
|
||||
await _loginHelper.LoginAsync(providerUserEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_NotMemberOrProvider_CannotUpdateOrganization()
|
||||
{
|
||||
// Create and login as a new account to be unrelated to the org
|
||||
var userEmail = "stranger@example.com";
|
||||
await _factory.LoginWithNewAccount(userEmail);
|
||||
await _loginHelper.LoginAsync(userEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "newbillingemail@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
|
||||
// Verify the organization name was not updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal(_organizationName, updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithProvider_CanRenameOrganization()
|
||||
{
|
||||
// Arrange - Create provider and link to organization
|
||||
// The active user is ONLY an org owner, NOT a provider user
|
||||
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify the organization name was actually updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal("Updated Organization Name", updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_AsOwner_WithProvider_CannotChangeBillingEmail()
|
||||
{
|
||||
// Arrange - Create provider and link to organization
|
||||
// The active user is ONLY an org owner, NOT a provider user
|
||||
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var updateRequest = new OrganizationUpdateRequestModel
|
||||
{
|
||||
Name = "Updated Organization Name",
|
||||
BillingEmail = "updatedbilling@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
|
||||
// Verify the organization was not updated
|
||||
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
|
||||
Assert.NotNull(updatedOrg);
|
||||
Assert.Equal(_organizationName, updatedOrg.Name);
|
||||
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
|
||||
}
|
||||
}
|
||||
77
test/Api.IntegrationTest/Helpers/ProviderTestHelpers.cs
Normal file
77
test/Api.IntegrationTest/Helpers/ProviderTestHelpers.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Helpers;
|
||||
|
||||
public static class ProviderTestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a provider and links it to an organization.
|
||||
/// This does NOT create any provider users.
|
||||
/// </summary>
|
||||
/// <param name="factory">The API application factory</param>
|
||||
/// <param name="organizationId">The organization ID to link to the provider</param>
|
||||
/// <param name="providerType">The type of provider to create</param>
|
||||
/// <param name="providerStatus">The provider status (defaults to Created)</param>
|
||||
/// <returns>The created provider</returns>
|
||||
public static async Task<Provider> CreateProviderAndLinkToOrganizationAsync(
|
||||
ApiApplicationFactory factory,
|
||||
Guid organizationId,
|
||||
ProviderType providerType,
|
||||
ProviderStatusType providerStatus = ProviderStatusType.Created)
|
||||
{
|
||||
var providerRepository = factory.GetService<IProviderRepository>();
|
||||
var providerOrganizationRepository = factory.GetService<IProviderOrganizationRepository>();
|
||||
|
||||
// Create the provider
|
||||
var provider = await providerRepository.CreateAsync(new Provider
|
||||
{
|
||||
Name = $"Test {providerType} Provider",
|
||||
BusinessName = $"Test {providerType} Provider Business",
|
||||
BillingEmail = $"provider-{providerType.ToString().ToLower()}@example.com",
|
||||
Type = providerType,
|
||||
Status = providerStatus,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
// Link the provider to the organization
|
||||
await providerOrganizationRepository.CreateAsync(new ProviderOrganization
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
OrganizationId = organizationId,
|
||||
Key = "test-provider-key"
|
||||
});
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a providerUser for a provider.
|
||||
/// </summary>
|
||||
public static async Task<ProviderUser> CreateProviderUserAsync(
|
||||
ApiApplicationFactory factory,
|
||||
Guid providerId,
|
||||
string userEmail,
|
||||
ProviderUserType providerUserType)
|
||||
{
|
||||
var userRepository = factory.GetService<IUserRepository>();
|
||||
var user = await userRepository.GetByEmailAsync(userEmail);
|
||||
if (user is null)
|
||||
{
|
||||
throw new Exception("No user found in test setup.");
|
||||
}
|
||||
|
||||
var providerUserRepository = factory.GetService<IProviderUserRepository>();
|
||||
return await providerUserRepository.CreateAsync(new ProviderUser
|
||||
{
|
||||
ProviderId = providerId,
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
UserId = user.Id,
|
||||
Key = Guid.NewGuid().ToString(),
|
||||
Type = providerUserType
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user