Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ public async Task<IResult> GetStatus([FromBody] GetOrganizationInviteLinkStatusR

return Handle(result, status =>
TypedResults.Ok(new OrganizationInviteLinkStatusResponseModel(
status.OrganizationId,
status.OrganizationName,
status.LinksEnabled,
status.SeatsAvailable,
status.Sso is null
? null
Expand All @@ -57,8 +59,9 @@ public async Task<IResult> 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("")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ο»Ώnamespace Bit.Api.AdminConsole.Models.Response.Organizations;

public record OrganizationInviteLinkValidateEmailDomainResponseModel(bool IsAllowed);
public record OrganizationInviteLinkValidateEmailDomainResponseModel(Guid OrganizationId, bool IsAllowed);
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public async Task<CommandResult<OrganizationInviteLinkStatus>> 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
Expand All @@ -41,7 +42,8 @@ public async Task<CommandResult<OrganizationInviteLinkStatus>> 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<OrganizationInviteLinkSsoStatus?> GetSsoStatusAsync(Organization organization)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public interface IGetOrganizationInviteLinkStatusQuery
/// </summary>
/// <param name="code">The public invite link code (from the URL).</param>
/// <returns>
/// An <see cref="OrganizationInviteLinkStatus"/> 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 <see cref="OrganizationInviteLinkStatus"/> 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.
/// </returns>
Task<CommandResult<OrganizationInviteLinkStatus>> GetStatusAsync(Guid code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces;
public interface IValidateOrganizationInviteLinkEmailDomainQuery
{
/// <summary>
/// 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.
/// </summary>
Task<CommandResult<bool>> ValidateAsync(Guid code, string email);
Task<CommandResult<OrganizationInviteLinkEmailDomainStatus>> ValidateAsync(Guid code, string email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ο»Ώnamespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks;

public record OrganizationInviteLinkEmailDomainStatus(
Guid OrganizationId,
bool IsAllowed);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
ο»Ώnamespace Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks;

public record OrganizationInviteLinkStatus(
Guid OrganizationId,
string OrganizationName,
bool LinksEnabled,
bool SeatsAvailable,
OrganizationInviteLinkSsoStatus? Sso);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ public class ValidateOrganizationInviteLinkEmailDomainQuery(
IOrganizationInviteLinkRepository organizationInviteLinkRepository)
: IValidateOrganizationInviteLinkEmailDomainQuery
{
public async Task<CommandResult<bool>> ValidateAsync(Guid code, string email)
public async Task<CommandResult<OrganizationInviteLinkEmailDomainStatus>> ValidateAsync(Guid code, string email)
{
var link = await organizationInviteLinkRepository.GetByCodeAsync(code);
if (link is null)
{
return new InviteLinkNotFound();
}

return InviteLinkDomainValidator.IsEmailDomainAllowed(email, link.GetAllowedDomains());
var isAllowed = InviteLinkDomainValidator.IsEmailDomainAllowed(email, link.GetAllowedDomains());
return new OrganizationInviteLinkEmailDomainStatus(link.OrganizationId, isAllowed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public async Task ValidateEmailDomain_WithAllowedEmail_ReturnsIsAllowedTrue()
Assert.Equal(HttpStatusCode.OK, validateResponse.StatusCode);
var result = await validateResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkValidateEmailDomainResponseModel>();
Assert.NotNull(result);
Assert.Equal(_organization.Id, result.OrganizationId);
Assert.True(result.IsAllowed);
}

Expand Down Expand Up @@ -262,7 +263,9 @@ public async Task GetStatus_WithExistingLink_ReturnsData()
Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);
var status = await statusResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkStatusResponseModel>();
Assert.NotNull(status);
Assert.Equal(_organization.Id, status.OrganizationId);
Assert.Equal(_organization.Name, status.OrganizationName);
Assert.True(status.LinksEnabled);
Assert.True(status.SeatsAvailable);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ public async Task GetStatus_WithValidQuery_Success(
var result = await sutProvider.Sut.GetStatus(model);

var okResult = Assert.IsType<Ok<OrganizationInviteLinkStatusResponseModel>>(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);
}

Expand All @@ -250,20 +252,6 @@ public async Task GetStatus_WithNotFoundError_ReturnsNotFound(
Assert.IsType<NotFound<ErrorResponseModel>>(result);
}

[Theory, BitAutoData]
public async Task GetStatus_WithNotAvailableError_ReturnsBadRequest(
GetOrganizationInviteLinkStatusRequestModel model,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkStatusQuery>()
.GetStatusAsync(model.Code)
.Returns(new CommandResult<OrganizationInviteLinkStatus>(new InviteLinkNotAvailable()));

var result = await sutProvider.Sut.GetStatus(model);

Assert.IsType<BadRequest<ErrorResponseModel>>(result);
}

[Theory, BitAutoData]
public async Task GetPolicies_WithValidInput_ReturnsOkWithList(
GetOrganizationInviteLinkPoliciesRequestModel model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -103,9 +105,15 @@ public async Task GetStatusAsync_UseInviteLinksFalse_ReturnsNotAvailableError(

var result = await sutProvider.Sut.GetStatusAsync(code);

Assert.True(result.IsError);
Assert.IsType<InviteLinkNotAvailable>(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<IOrganizationRepository>()
.DidNotReceiveWithAnyArgs()
.GetOccupiedSeatCountByOrganizationIdAsync(default);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidateOrganizationInviteLinkEmailDomainQuery> sutProvider)
{
var link = new OrganizationInviteLink();
var link = new OrganizationInviteLink { OrganizationId = organizationId };
link.SetAllowedDomains(["acme.com"]);

sutProvider.GetDependency<IOrganizationInviteLinkRepository>()
Expand All @@ -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<ValidateOrganizationInviteLinkEmailDomainQuery> sutProvider)
{
var link = new OrganizationInviteLink();
var link = new OrganizationInviteLink { OrganizationId = organizationId };
link.SetAllowedDomains(["acme.com"]);

sutProvider.GetDependency<IOrganizationInviteLinkRepository>()
Expand All @@ -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);
}
}
Loading