mirror of
https://github.com/bitwarden/server.git
synced 2026-06-01 01:55:55 -05:00
* Add email domain validation to OrganizationInviteLink - Introduced IsEmailDomainAllowed method to check if an email's domain is permitted based on allowed domains. - Added necessary using directives for MailAddress and domain sanitization utilities. * Refactor OrganizationInviteLink by removing email domain validation - Removed the IsEmailDomainAllowed method and associated using directives for MailAddress. - Cleaned up the code by eliminating unused methods related to email domain validation. * Add InviteLinkDomainValidator for email domain validation - Introduced InviteLinkDomainValidator class with IsEmailDomainAllowed method to validate if an email's domain is in the list of allowed domains. - Utilized MailAddress for email parsing and added domain sanitization logic. * Add email domain validation endpoint for organization invite links - Implemented ValidateEmailDomain method in OrganizationInviteLinksController to check if an email's domain is allowed based on the invite link's permitted domains. - Created OrganizationInviteLinkValidateEmailDomainRequestModel for request validation and OrganizationInviteLinkValidateEmailDomainResponseModel for response formatting. - Integrated IOrganizationInviteLinkRepository to retrieve invite link details by code. * Add unit tests for InviteLinkDomainValidator - Created InviteLinkDomainValidatorTests class to validate email domain functionality. - Added tests for various scenarios including invalid emails, empty domain lists, and matching domains. - Ensured comprehensive coverage of the IsEmailDomainAllowed method's behavior. * Add integration test for email domain validation in OrganizationInviteLinksController - Implemented a test to validate that an allowed email domain returns the expected result when checked against an organization invite link. - Ensured the test verifies the creation of an invite link and the subsequent validation of an email domain against the allowed domains list. * Add validation query and interface for organization invite link email domain - Introduced ValidateOrganizationInviteLinkEmailDomainQuery class to validate if an email's domain is allowed based on the invite link's permitted domains. - Created IValidateOrganizationInviteLinkEmailDomainQuery interface to define the validation method. - Added unit tests for the validation query to ensure correct behavior for various scenarios, including link not found and domain matching. * Refactor OrganizationInviteLinksController to use validation query for email domain - Updated ValidateEmailDomain method to utilize IValidateOrganizationInviteLinkEmailDomainQuery for domain validation instead of directly accessing the repository. - Removed unnecessary repository dependency and streamlined the response handling for validation results. - Registered the new validation query in OrganizationServiceCollectionExtensions for dependency injection. * Refactor InviteLinkDomainValidator and replace MailAddress usage with existing email validation method
267 lines
11 KiB
C#
267 lines
11 KiB
C#
using System.Net;
|
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
|
using Bit.Api.IntegrationTest.Factories;
|
|
using Bit.Api.IntegrationTest.Helpers;
|
|
using Bit.Core;
|
|
using Bit.Core.AdminConsole.Entities;
|
|
using Bit.Core.Billing.Enums;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Models.Data.Organizations;
|
|
using Bit.Core.Services;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
|
|
|
public class OrganizationInviteLinksControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
|
{
|
|
private readonly HttpClient _client;
|
|
private readonly ApiApplicationFactory _factory;
|
|
private readonly LoginHelper _loginHelper;
|
|
|
|
private const string _validEncryptedKey =
|
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
|
|
|
private Organization _organization = null!;
|
|
private string _ownerEmail = null!;
|
|
|
|
public OrganizationInviteLinksControllerTests(ApiApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
_factory.SubstituteService<IFeatureService>(featureService =>
|
|
{
|
|
featureService
|
|
.IsEnabled(FeatureFlagKeys.GenerateInviteLink)
|
|
.Returns(true);
|
|
});
|
|
_factory.SubstituteService<IApplicationCacheService>(cacheService =>
|
|
{
|
|
cacheService
|
|
.GetOrganizationAbilityAsync(Arg.Any<Guid>())
|
|
.Returns(new OrganizationAbility { UseInviteLinks = true });
|
|
});
|
|
_client = factory.CreateClient();
|
|
_loginHelper = new LoginHelper(_factory, _client);
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_ownerEmail = $"integration-test{Guid.NewGuid()}@example.com";
|
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
|
|
|
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(
|
|
_factory,
|
|
plan: PlanType.EnterpriseAnnually,
|
|
ownerEmail: _ownerEmail,
|
|
passwordManagerSeats: 10,
|
|
paymentMethod: PaymentMethodType.Card);
|
|
|
|
await _loginHelper.LoginAsync(_ownerEmail);
|
|
}
|
|
|
|
public Task DisposeAsync()
|
|
{
|
|
_client.Dispose();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateEmailDomain_WithAllowedEmail_ReturnsIsAllowedTrue()
|
|
{
|
|
var createRequest = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", createRequest);
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
|
|
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(created);
|
|
|
|
var validateRequest = new OrganizationInviteLinkValidateEmailDomainRequestModel
|
|
{
|
|
Code = created.Code,
|
|
Email = "user@acme.com",
|
|
};
|
|
using var anonymousClient = _factory.CreateClient();
|
|
var validateResponse = await anonymousClient.PostAsJsonAsync(
|
|
"/organizations/invite-link/validate-email-domain", validateRequest);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, validateResponse.StatusCode);
|
|
var result = await validateResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkValidateEmailDomainResponseModel>();
|
|
Assert.NotNull(result);
|
|
Assert.True(result.IsAllowed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateThenGet_AsOwner_ReturnsCreatedAndOk()
|
|
{
|
|
var request = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com", "example.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
|
|
static void AssertInviteLink(OrganizationInviteLinkResponseModel? content, Organization organization)
|
|
{
|
|
Assert.NotNull(content);
|
|
Assert.NotEqual(Guid.Empty, content.Id);
|
|
Assert.NotEqual(Guid.Empty, content.Code);
|
|
Assert.Equal(organization.Id, content.OrganizationId);
|
|
Assert.Equal(["acme.com", "example.com"], content.AllowedDomains);
|
|
Assert.Equal(_validEncryptedKey, content.EncryptedInviteKey);
|
|
}
|
|
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", request);
|
|
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
|
|
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
AssertInviteLink(created, _organization);
|
|
|
|
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
|
|
var content = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
AssertInviteLink(content, _organization);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateThenUpdateThenGet_AsOwner_ReturnsCreatedAndOkAndOk()
|
|
{
|
|
var createRequest = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", createRequest);
|
|
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
|
|
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(created);
|
|
|
|
var updateRequest = new UpdateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["example.com", "new.com"],
|
|
};
|
|
|
|
var updateResponse = await _client.PutAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", updateRequest);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
|
|
|
|
var updated = await updateResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(updated);
|
|
Assert.Equal(created.Id, updated.Id);
|
|
Assert.Equal(created.Code, updated.Code);
|
|
Assert.Equal(_organization.Id, updated.OrganizationId);
|
|
Assert.Equal(_validEncryptedKey, updated.EncryptedInviteKey);
|
|
Assert.Equal(["example.com", "new.com"], updated.AllowedDomains);
|
|
|
|
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
|
|
var content = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(content);
|
|
Assert.Equal(created.Id, content.Id);
|
|
Assert.Equal(created.Code, content.Code);
|
|
Assert.Equal(_validEncryptedKey, content.EncryptedInviteKey);
|
|
Assert.Equal(["example.com", "new.com"], content.AllowedDomains);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Delete_AsOwner_ReturnsNoContentAndRemovesLink()
|
|
{
|
|
var createRequest = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", createRequest);
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
|
|
var deleteResponse = await _client.DeleteAsync(
|
|
$"/organizations/{_organization.Id}/invite-link");
|
|
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
|
|
|
var deleteAgainResponse = await _client.DeleteAsync(
|
|
$"/organizations/{_organization.Id}/invite-link");
|
|
Assert.Equal(HttpStatusCode.NotFound, deleteAgainResponse.StatusCode);
|
|
|
|
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
|
|
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Refresh_AsOwner_ReplacesLink()
|
|
{
|
|
var createRequest = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com", "example.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", createRequest);
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
var original = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(original);
|
|
|
|
var refreshRequest = new RefreshOrganizationInviteLinkRequestModel
|
|
{
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
var refreshResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link/refresh", refreshRequest);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, refreshResponse.StatusCode);
|
|
var refreshed = await refreshResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(refreshed);
|
|
Assert.NotEqual(original.Id, refreshed.Id);
|
|
Assert.NotEqual(original.Code, refreshed.Code);
|
|
Assert.Equal(original.AllowedDomains, refreshed.AllowedDomains);
|
|
Assert.Equal(_organization.Id, refreshed.OrganizationId);
|
|
|
|
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
|
|
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
var current = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(current);
|
|
Assert.Equal(refreshed.Id, current.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatus_WithExistingLink_ReturnsData()
|
|
{
|
|
var createRequest = new CreateOrganizationInviteLinkRequestModel
|
|
{
|
|
AllowedDomains = ["acme.com"],
|
|
EncryptedInviteKey = _validEncryptedKey,
|
|
};
|
|
var createResponse = await _client.PostAsJsonAsync(
|
|
$"/organizations/{_organization.Id}/invite-link", createRequest);
|
|
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
|
|
Assert.NotNull(created);
|
|
|
|
var anonClient = _factory.CreateClient();
|
|
var statusResponse = await anonClient.PostAsJsonAsync(
|
|
"/organizations/invite-link/status",
|
|
new GetOrganizationInviteLinkStatusRequestModel { Code = created.Code });
|
|
|
|
Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);
|
|
var status = await statusResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkStatusResponseModel>();
|
|
Assert.NotNull(status);
|
|
Assert.Equal(_organization.Name, status.OrganizationName);
|
|
Assert.True(status.SeatsAvailable);
|
|
}
|
|
}
|