Skip to content

Commit

Permalink
[PM-12358] New Verified Organization Domain SSO Detail endpoint (bitw…
Browse files Browse the repository at this point in the history
…arden#4838)

* Added /domain/sso/verified to organization controller

* Restricting sproc to only return verified domains if the org has sso. Adding name. corrected route. removed not found exception. Adding the sproc definition to the SQL project
  • Loading branch information
jrmccannon authored Oct 7, 2024
1 parent 452a45b commit e288ca9
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/Api/AdminConsole/Controllers/OrganizationDomainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand Down Expand Up @@ -133,6 +135,20 @@ public async Task<OrganizationDomainSsoDetailsResponseModel> GetOrgDomainSsoDeta
return new OrganizationDomainSsoDetailsResponseModel(ssoResult);
}

[AllowAnonymous]
[HttpPost("domain/sso/verified")]
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
{
var ssoResults = (await _organizationDomainRepository
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email))
.ToList();

return new VerifiedOrganizationDomainSsoDetailsResponseModel(
ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult)));
}

private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{
if (!await _currentContext.ManageSso(orgIdGuid))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations;

namespace Bit.Api.AdminConsole.Models.Response.Organizations;

public class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel
{
public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data)
: base("verifiedOrganizationDomainSsoDetails")
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}

DomainName = data.DomainName;
OrganizationIdentifier = data.OrganizationIdentifier;
OrganizationName = data.OrganizationName;
}
public string DomainName { get; }
public string OrganizationIdentifier { get; }
public string OrganizationName { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Bit.Api.Models.Response;

namespace Bit.Api.AdminConsole.Models.Response.Organizations;

public class VerifiedOrganizationDomainSsoDetailsResponseModel(
IEnumerable<VerifiedOrganizationDomainSsoDetailResponseModel> data,
string continuationToken = null)
: ListResponseModel<VerifiedOrganizationDomainSsoDetailResponseModel>(data, continuationToken);
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public static class FeatureFlagKeys
public const string TrialPayment = "PM-8163-trial-payment";
public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api";
public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";

public static List<string> GetAllKeys()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Bit.Core.Models.Data.Organizations;

public class VerifiedOrganizationDomainSsoDetail
{
public VerifiedOrganizationDomainSsoDetail()
{
}

public VerifiedOrganizationDomainSsoDetail(Guid organizationId, string organizationName, string domainName,
string organizationIdentifier)
{
OrganizationId = organizationId;
OrganizationName = organizationName;
DomainName = domainName;
OrganizationIdentifier = organizationIdentifier;
}

public Guid OrganizationId { get; init; }
public string OrganizationName { get; init; }
public string DomainName { get; init; }
public string OrganizationIdentifier { get; init; }
}
1 change: 1 addition & 0 deletions src/Core/Repositories/IOrganizationDomainRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository<OrganizationDomain,
Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);
Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date);
Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email);
Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email);
Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId);
Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);
Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ public async Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(Dat
}
}

public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)
{
await using var connection = new SqlConnection(ConnectionString);

return await connection
.QueryAsync<VerifiedOrganizationDomainSsoDetail>(
$"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]",
new { Email = email },
commandType: CommandType.StoredProcedure);
}

public async Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{
using (var connection = new SqlConnection(ConnectionString))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@ from s in sJoin.DefaultIfEmpty()
return ssoDetails;
}

public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)
{
var domainName = new MailAddress(email).Host;

using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
return await (from o in dbContext.Organizations
from od in o.Domains
join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin
from s in sJoin.DefaultIfEmpty()
where od.DomainName == domainName
&& o.Enabled
&& s.Enabled
&& od.VerifiedDate != null
select new VerifiedOrganizationDomainSsoDetail(
o.Id,
o.Name,
od.DomainName,
o.Identifier))
.AsNoTracking()
.ToListAsync();
}

public async Task<Core.Entities.OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{
using var scope = ServiceScopeFactory.CreateScope();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
CREATE PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]
@Email NVARCHAR(256)
AS
BEGIN
SET NOCOUNT ON

DECLARE @Domain NVARCHAR(256)

SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))

SELECT
O.Id AS OrganizationId,
O.Name AS OrganizationName,
O.Identifier AS OrganizationIdentifier,
OD.DomainName
FROM [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain
AND O.Enabled = 1
AND OD.VerifiedDate IS NOT NULL
AND S.Enabled = 1
END
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,26 @@ public async Task GetOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetail

Assert.IsType<OrganizationDomainSsoDetailsResponseModel>(result);
}

[Theory, BitAutoData]
public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain(
OrganizationDomainSsoDetailsRequestModel model, SutProvider<OrganizationDomainController> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty<VerifiedOrganizationDomainSsoDetail>());

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetOrgDomainSsoDetails(model));
}

[Theory, BitAutoData]
public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain(
OrganizationDomainSsoDetailsRequestModel model, IEnumerable<VerifiedOrganizationDomainSsoDetail> ssoDetailsData, SutProvider<OrganizationDomainController> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData);

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

Assert.IsType<VerifiedOrganizationDomainSsoDetailsResponseModel>(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE OR ALTER PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]
@Email NVARCHAR(256)
AS
BEGIN
SET NOCOUNT ON

DECLARE @Domain NVARCHAR(256)

SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))

SELECT
O.Id AS OrganizationId,
O.Name AS OrganizationName,
O.Identifier AS OrganizationIdentifier,
OD.DomainName
FROM [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain
AND O.Enabled = 1
AND OD.VerifiedDate IS NOT NULL
AND S.Enabled = 1
END
GO

0 comments on commit e288ca9

Please sign in to comment.