diff --git a/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs b/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs index a8f8387cbd6b..10e8f06781df 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationInviteLinksController.cs @@ -33,7 +33,9 @@ public async Task GetStatus([FromBody] GetOrganizationInviteLinkStatusR return Handle(result, status => TypedResults.Ok(new OrganizationInviteLinkStatusResponseModel( + status.OrganizationId, status.OrganizationName, + status.LinksEnabled, status.SeatsAvailable, status.Sso is null ? null @@ -57,8 +59,9 @@ public async Task ValidateEmailDomain( { var result = await validateOrganizationInviteLinkEmailDomainQuery.ValidateAsync(model.Code, model.Email); - return Handle(result, isAllowed => - TypedResults.Ok(new OrganizationInviteLinkValidateEmailDomainResponseModel(isAllowed))); + return Handle(result, status => + TypedResults.Ok(new OrganizationInviteLinkValidateEmailDomainResponseModel( + status.OrganizationId, status.IsAllowed))); } [HttpGet("")] diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkStatusResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkStatusResponseModel.cs index 2f99952ed1f5..c2caf2add85d 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkStatusResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkStatusResponseModel.cs @@ -4,16 +4,22 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations; public class OrganizationInviteLinkStatusResponseModel : ResponseModel { + public Guid OrganizationId { get; set; } public string OrganizationName { get; set; } = null!; + public bool LinksEnabled { get; set; } public bool SeatsAvailable { get; set; } public OrganizationInviteLinkSsoResponseModel? Sso { get; set; } public OrganizationInviteLinkStatusResponseModel( + Guid organizationId, string organizationName, + bool linksEnabled, bool seatsAvailable, OrganizationInviteLinkSsoResponseModel? sso) : base("inviteLinkStatus") { + OrganizationId = organizationId; OrganizationName = organizationName; + LinksEnabled = linksEnabled; SeatsAvailable = seatsAvailable; Sso = sso; } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkValidateEmailDomainResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkValidateEmailDomainResponseModel.cs index ec912a115c16..fe1e78285608 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkValidateEmailDomainResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationInviteLinkValidateEmailDomainResponseModel.cs @@ -1,3 +1,3 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations; -public record OrganizationInviteLinkValidateEmailDomainResponseModel(bool IsAllowed); +public record OrganizationInviteLinkValidateEmailDomainResponseModel(Guid OrganizationId, bool IsAllowed); diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQuery.cs index 9df0177b7305..f21bfd3fd4d4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQuery.cs @@ -32,7 +32,8 @@ public async Task> GetStatusAsync(Gu if (!organization.UseInviteLinks) { - return new InviteLinkNotAvailable(); + return new OrganizationInviteLinkStatus( + organization.Id, organization.Name, LinksEnabled: false, SeatsAvailable: false, Sso: null); } var occupied = (await organizationRepository @@ -41,7 +42,8 @@ public async Task> GetStatusAsync(Gu var sso = seatsAvailable ? await GetSsoStatusAsync(organization) : null; - return new OrganizationInviteLinkStatus(organization.Name, seatsAvailable, sso); + return new OrganizationInviteLinkStatus( + organization.Id, organization.Name, LinksEnabled: true, seatsAvailable, sso); } private async Task GetSsoStatusAsync(Organization organization) diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IGetOrganizationInviteLinkStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IGetOrganizationInviteLinkStatusQuery.cs index ae9f6847a5af..56b142c26755 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IGetOrganizationInviteLinkStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IGetOrganizationInviteLinkStatusQuery.cs @@ -9,8 +9,8 @@ public interface IGetOrganizationInviteLinkStatusQuery /// /// The public invite link code (from the URL). /// - /// An if the link is valid and the organization has the - /// invite links feature, or an error if the link is not found or the feature is unavailable. + /// An if the link is valid, or an error if the link is not + /// found or the organization is disabled. The status indicates whether the invite links feature is enabled. /// Task> GetStatusAsync(Guid code); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IValidateOrganizationInviteLinkEmailDomainQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IValidateOrganizationInviteLinkEmailDomainQuery.cs index 7d7868200586..c3d74ea9e566 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IValidateOrganizationInviteLinkEmailDomainQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/Interfaces/IValidateOrganizationInviteLinkEmailDomainQuery.cs @@ -5,8 +5,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces; public interface IValidateOrganizationInviteLinkEmailDomainQuery { /// - /// Returns whether the email's domain is allowed by the invite link, + /// Returns whether the email's domain is allowed by the invite link and the organization id /// or an error if the invite link does not exist. /// - Task> ValidateAsync(Guid code, string email); + Task> ValidateAsync(Guid code, string email); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkEmailDomainStatus.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkEmailDomainStatus.cs new file mode 100644 index 000000000000..9e088e64733b --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkEmailDomainStatus.cs @@ -0,0 +1,5 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks; + +public record OrganizationInviteLinkEmailDomainStatus( + Guid OrganizationId, + bool IsAllowed); diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkStatus.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkStatus.cs index d608b2912f87..2fbcdb31ca1c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkStatus.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/OrganizationInviteLinkStatus.cs @@ -1,7 +1,9 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks; public record OrganizationInviteLinkStatus( + Guid OrganizationId, string OrganizationName, + bool LinksEnabled, bool SeatsAvailable, OrganizationInviteLinkSsoStatus? Sso); diff --git a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQuery.cs index 37ef37ea9de3..a97f60077df5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQuery.cs @@ -9,7 +9,7 @@ public class ValidateOrganizationInviteLinkEmailDomainQuery( IOrganizationInviteLinkRepository organizationInviteLinkRepository) : IValidateOrganizationInviteLinkEmailDomainQuery { - public async Task> ValidateAsync(Guid code, string email) + public async Task> ValidateAsync(Guid code, string email) { var link = await organizationInviteLinkRepository.GetByCodeAsync(code); if (link is null) @@ -17,6 +17,7 @@ public async Task> ValidateAsync(Guid code, string email) return new InviteLinkNotFound(); } - return InviteLinkDomainValidator.IsEmailDomainAllowed(email, link.GetAllowedDomains()); + var isAllowed = InviteLinkDomainValidator.IsEmailDomainAllowed(email, link.GetAllowedDomains()); + return new OrganizationInviteLinkEmailDomainStatus(link.OrganizationId, isAllowed); } } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs index 66893a3539ff..c02f81de6459 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs @@ -95,6 +95,7 @@ public async Task ValidateEmailDomain_WithAllowedEmail_ReturnsIsAllowedTrue() Assert.Equal(HttpStatusCode.OK, validateResponse.StatusCode); var result = await validateResponse.Content.ReadFromJsonAsync(); Assert.NotNull(result); + Assert.Equal(_organization.Id, result.OrganizationId); Assert.True(result.IsAllowed); } @@ -262,7 +263,9 @@ public async Task GetStatus_WithExistingLink_ReturnsData() Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode); var status = await statusResponse.Content.ReadFromJsonAsync(); Assert.NotNull(status); + Assert.Equal(_organization.Id, status.OrganizationId); Assert.Equal(_organization.Name, status.OrganizationName); + Assert.True(status.LinksEnabled); Assert.True(status.SeatsAvailable); } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs index f89c7b4cda1c..7f8669052bbf 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs @@ -232,7 +232,9 @@ public async Task GetStatus_WithValidQuery_Success( var result = await sutProvider.Sut.GetStatus(model); var okResult = Assert.IsType>(result); - Assert.Equal(status.OrganizationName, okResult.Value!.OrganizationName); + Assert.Equal(status.OrganizationId, okResult.Value!.OrganizationId); + Assert.Equal(status.OrganizationName, okResult.Value.OrganizationName); + Assert.Equal(status.LinksEnabled, okResult.Value.LinksEnabled); Assert.Equal(status.SeatsAvailable, okResult.Value.SeatsAvailable); } @@ -250,20 +252,6 @@ public async Task GetStatus_WithNotFoundError_ReturnsNotFound( Assert.IsType>(result); } - [Theory, BitAutoData] - public async Task GetStatus_WithNotAvailableError_ReturnsBadRequest( - GetOrganizationInviteLinkStatusRequestModel model, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetStatusAsync(model.Code) - .Returns(new CommandResult(new InviteLinkNotAvailable())); - - var result = await sutProvider.Sut.GetStatus(model); - - Assert.IsType>(result); - } - [Theory, BitAutoData] public async Task GetPolicies_WithValidInput_ReturnsOkWithList( GetOrganizationInviteLinkPoliciesRequestModel model, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs index 53f60c406b3b..e26e7b8c9104 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs @@ -34,7 +34,9 @@ public async Task GetStatusAsync_WithValidInput_Success( Assert.True(result.IsSuccess); var status = result.AsSuccess; + Assert.Equal(organization.Id, status.OrganizationId); Assert.Equal(organization.Name, status.OrganizationName); + Assert.True(status.LinksEnabled); } [Theory, BitAutoData] @@ -90,7 +92,7 @@ public async Task GetStatusAsync_OrganizationDisabled_ReturnsNotFoundError( } [Theory, BitAutoData] - public async Task GetStatusAsync_UseInviteLinksFalse_ReturnsNotAvailableError( + public async Task GetStatusAsync_UseInviteLinksFalse_ReturnsLinksDisabled( Guid code, OrganizationInviteLink inviteLink, Organization organization, @@ -103,9 +105,15 @@ public async Task GetStatusAsync_UseInviteLinksFalse_ReturnsNotAvailableError( var result = await sutProvider.Sut.GetStatusAsync(code); - Assert.True(result.IsError); - Assert.IsType(result.AsError); + Assert.True(result.IsSuccess); + var status = result.AsSuccess; + Assert.Equal(organization.Id, status.OrganizationId); + Assert.Equal(organization.Name, status.OrganizationName); + Assert.False(status.LinksEnabled); + Assert.False(status.SeatsAvailable); + Assert.Null(status.Sso); + // When links are disabled we short-circuit without touching seats or SSO. await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .GetOccupiedSeatCountByOrganizationIdAsync(default); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQueryTests.cs index 2745938259ff..b1871439da65 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/ValidateOrganizationInviteLinkEmailDomainQueryTests.cs @@ -28,11 +28,12 @@ public async Task ValidateAsync_WhenLinkNotFound_ReturnsNotFoundError( } [Theory, BitAutoData] - public async Task ValidateAsync_WhenEmailDomainMatches_ReturnsTrue( + public async Task ValidateAsync_WhenEmailDomainMatches_ReturnsAllowed( Guid code, + Guid organizationId, SutProvider sutProvider) { - var link = new OrganizationInviteLink(); + var link = new OrganizationInviteLink { OrganizationId = organizationId }; link.SetAllowedDomains(["acme.com"]); sutProvider.GetDependency() @@ -42,15 +43,17 @@ public async Task ValidateAsync_WhenEmailDomainMatches_ReturnsTrue( var result = await sutProvider.Sut.ValidateAsync(code, "user@acme.com"); Assert.True(result.IsSuccess); - Assert.True(result.AsSuccess); + Assert.Equal(organizationId, result.AsSuccess.OrganizationId); + Assert.True(result.AsSuccess.IsAllowed); } [Theory, BitAutoData] - public async Task ValidateAsync_WhenEmailDomainDoesNotMatch_ReturnsFalse( + public async Task ValidateAsync_WhenEmailDomainDoesNotMatch_ReturnsNotAllowed( Guid code, + Guid organizationId, SutProvider sutProvider) { - var link = new OrganizationInviteLink(); + var link = new OrganizationInviteLink { OrganizationId = organizationId }; link.SetAllowedDomains(["acme.com"]); sutProvider.GetDependency() @@ -60,6 +63,7 @@ public async Task ValidateAsync_WhenEmailDomainDoesNotMatch_ReturnsFalse( var result = await sutProvider.Sut.ValidateAsync(code, "user@other.com"); Assert.True(result.IsSuccess); - Assert.False(result.AsSuccess); + Assert.Equal(organizationId, result.AsSuccess.OrganizationId); + Assert.False(result.AsSuccess.IsAllowed); } }