1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[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
This commit is contained in:
Jared McCannon 2024-10-07 14:39:57 -05:00 committed by GitHub
parent 452a45b00b
commit e288ca97a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 174 additions and 0 deletions

View File

@ -2,11 +2,13 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller
return new OrganizationDomainSsoDetailsResponseModel(ssoResult); 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) private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{ {
if (!await _currentContext.ManageSso(orgIdGuid)) if (!await _currentContext.ManageSso(orgIdGuid))

View File

@ -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; }
}

View File

@ -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);

View File

@ -144,6 +144,7 @@ public static class FeatureFlagKeys
public const string TrialPayment = "PM-8163-trial-payment"; public const string TrialPayment = "PM-8163-trial-payment";
public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api"; public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api";
public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -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; }
}

View File

@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository<OrganizationDomain,
Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId); Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);
Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date); Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date);
Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email); Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email);
Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email);
Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId);
Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);
Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync(); Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();

View File

@ -71,6 +71,17 @@ public class OrganizationDomainRepository : Repository<OrganizationDomain, Guid>
} }
} }
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) public async Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{ {
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))

View File

@ -95,6 +95,29 @@ public class OrganizationDomainRepository : Repository<Core.Entities.Organizatio
return ssoDetails; 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) public async Task<Core.Entities.OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{ {
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();

View File

@ -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

View File

@ -316,4 +316,26 @@ public class OrganizationDomainControllerTests
Assert.IsType<OrganizationDomainSsoDetailsResponseModel>(result); 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);
}
} }

View File

@ -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