From e288ca97a3ac6ad293799d4a63c82a76d30e69ce Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 7 Oct 2024 14:39:57 -0500 Subject: [PATCH] [PM-12358] New Verified Organization Domain SSO Detail endpoint (#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 --- .../OrganizationDomainController.cs | 16 +++++++++++++ ...rganizationDomainSsoDetailResponseModel.cs | 23 ++++++++++++++++++ ...ganizationDomainSsoDetailsResponseModel.cs | 8 +++++++ src/Core/Constants.cs | 1 + .../VerifiedOrganizationDomainSsoDetail.cs | 22 +++++++++++++++++ .../IOrganizationDomainRepository.cs | 1 + .../OrganizationDomainRepository.cs | 11 +++++++++ .../OrganizationDomainRepository.cs | 23 ++++++++++++++++++ ...anaizationDomainSsoDetails_ReadByEmail.sql | 23 ++++++++++++++++++ .../OrganizationDomainControllerTests.cs | 22 +++++++++++++++++ ...ganizationDomainSsoDetails_ReadByEmail.sql | 24 +++++++++++++++++++ 11 files changed, 174 insertions(+) create mode 100644 src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs create mode 100644 src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs create mode 100644 src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs create mode 100644 src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql create mode 100644 util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index 35c927d5a..af7a162d8 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -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; @@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller return new OrganizationDomainSsoDetailsResponseModel(ssoResult); } + [AllowAnonymous] + [HttpPost("domain/sso/verified")] + [RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)] + public async Task 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)) diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs new file mode 100644 index 000000000..be4d8865d --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs @@ -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; } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs new file mode 100644 index 000000000..3488eab2c --- /dev/null +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs @@ -0,0 +1,8 @@ +using Bit.Api.Models.Response; + +namespace Bit.Api.AdminConsole.Models.Response.Organizations; + +public class VerifiedOrganizationDomainSsoDetailsResponseModel( + IEnumerable data, + string continuationToken = null) + : ListResponseModel(data, continuationToken); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1003a65b5..617cd81b0 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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 GetAllKeys() { diff --git a/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs new file mode 100644 index 000000000..0a07af66b --- /dev/null +++ b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs @@ -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; } +} diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index 3fde08a54..f8b45574a 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository> GetDomainsByOrganizationIdAsync(Guid orgId); Task> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); + Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email); Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 31d599f0c..1a7085eb1 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -71,6 +71,17 @@ public class OrganizationDomainRepository : Repository } } + public async Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email) + { + await using var connection = new SqlConnection(ConnectionString); + + return await connection + .QueryAsync( + $"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]", + new { Email = email }, + commandType: CommandType.StoredProcedure); + } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 9135c8bd1..3e2d6e44a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -95,6 +95,29 @@ public class OrganizationDomainRepository : Repository> 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 GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..a32b42f6c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql @@ -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 diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs index 2f7430341..1ff4b519c 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs @@ -316,4 +316,26 @@ public class OrganizationDomainControllerTests Assert.IsType(result); } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetOrgDomainSsoDetails(model)); + } + + [Theory, BitAutoData] + public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain( + OrganizationDomainSsoDetailsRequestModel model, IEnumerable ssoDetailsData, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData); + + var result = await sutProvider.Sut.GetVerifiedOrgDomainSsoDetailsAsync(model); + + Assert.IsType(result); + } } diff --git a/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql new file mode 100644 index 000000000..e36ea1f46 --- /dev/null +++ b/util/Migrator/DbScripts/2024-09-26_00_AddVerifiedOrganizationDomainSsoDetails_ReadByEmail.sql @@ -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