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:
parent
452a45b00b
commit
e288ca97a3
@ -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))
|
||||||
|
@ -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; }
|
||||||
|
}
|
@ -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);
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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; }
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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))
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user