From 21219262a212fbbd7dea6616fd9dc30dd74c9e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:57:59 +0100 Subject: [PATCH] [PM-3779] idor allow the attacker to delete the victim domain (#3308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [PM-3779] Added IOrganizationDomainRepository.GetDomainByIdAndOrganizationIdAsync and SQL stored procedure * [PM-3779] Changed GetOrganizationDomainByIdQuery to also take OrgId as a parameter. Updated existing unit tests and added new. Updated controller to match command changes * [PM-3779] Removed type from url routes * [PM-3779] Renamed IGetOrganizationDomainByIdAndOrganizationIdQuery to IGetOrganizationDomainByIdOrganizationIdQuery * [PM-3779] Renamed GetOrganizationDomainByIdOrganizationIdQueryTests file and added more tests --- .../OrganizationDomainController.cs | 113 ++++++++++-------- .../DeleteOrganizationDomainCommand.cs | 16 +-- ...anizationDomainByIdOrganizationIdQuery.cs} | 8 +- ...OrganizationDomainByOrganizationIdQuery.cs | 2 +- .../IDeleteOrganizationDomainCommand.cs | 6 +- ...ganizationDomainByIdOrganizationIdQuery.cs | 8 ++ .../IGetOrganizationDomainByIdQuery.cs | 8 -- ...OrganizationDomainByOrganizationIdQuery.cs | 2 +- .../IVerifyOrganizationDomainCommand.cs | 2 +- .../VerifyOrganizationDomainCommand.cs | 8 +- ...OrganizationServiceCollectionExtensions.cs | 2 +- .../IOrganizationDomainRepository.cs | 1 + .../OrganizationDomainRepository.cs | 14 +++ .../OrganizationDomainRepository.cs | 12 ++ ...anizationDomain_ReadByIdOrganizationId.sql | 16 +++ .../OrganizationDomainControllerTests.cs | 110 ++++++++++++----- .../DeleteOrganizationDomainCommandTests.cs | 16 +-- ...ationDomainByIdOrganizationIdQueryTests.cs | 80 +++++++++++++ .../GetOrganizationDomainByIdQueryTests.cs | 22 ---- ...izationDomainByOrganizationIdQueryTests.cs | 2 +- .../VerifyOrganizationDomainCommandTests.cs | 23 +--- .../2023-09-29_00_OrgDomainReadByIdOrgId.sql | 17 +++ 22 files changed, 312 insertions(+), 176 deletions(-) rename src/Core/OrganizationFeatures/OrganizationDomains/{GetOrganizationDomainByIdQuery.cs => GetOrganizationDomainByIdOrganizationIdQuery.cs} (52%) create mode 100644 src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdOrganizationIdQuery.cs delete mode 100644 src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdQuery.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByIdOrganizationId.sql create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQueryTests.cs delete mode 100644 test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQueryTests.cs create mode 100644 util/Migrator/DbScripts/2023-09-29_00_OrgDomainReadByIdOrgId.sql diff --git a/src/Api/Controllers/OrganizationDomainController.cs b/src/Api/Controllers/OrganizationDomainController.cs index 23e4f51bc..60abdad7d 100644 --- a/src/Api/Controllers/OrganizationDomainController.cs +++ b/src/Api/Controllers/OrganizationDomainController.cs @@ -19,7 +19,7 @@ public class OrganizationDomainController : Controller private readonly ICreateOrganizationDomainCommand _createOrganizationDomainCommand; private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand; private readonly IDeleteOrganizationDomainCommand _deleteOrganizationDomainCommand; - private readonly IGetOrganizationDomainByIdQuery _getOrganizationDomainByIdQuery; + private readonly IGetOrganizationDomainByIdOrganizationIdQuery _getOrganizationDomainByIdAndOrganizationIdQuery; private readonly IGetOrganizationDomainByOrganizationIdQuery _getOrganizationDomainByOrganizationIdQuery; private readonly ICurrentContext _currentContext; private readonly IOrganizationRepository _organizationRepository; @@ -29,7 +29,7 @@ public class OrganizationDomainController : Controller ICreateOrganizationDomainCommand createOrganizationDomainCommand, IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand, IDeleteOrganizationDomainCommand deleteOrganizationDomainCommand, - IGetOrganizationDomainByIdQuery getOrganizationDomainByIdQuery, + IGetOrganizationDomainByIdOrganizationIdQuery getOrganizationDomainByIdAndOrganizationIdQuery, IGetOrganizationDomainByOrganizationIdQuery getOrganizationDomainByOrganizationIdQuery, ICurrentContext currentContext, IOrganizationRepository organizationRepository, @@ -38,7 +38,7 @@ public class OrganizationDomainController : Controller _createOrganizationDomainCommand = createOrganizationDomainCommand; _verifyOrganizationDomainCommand = verifyOrganizationDomainCommand; _deleteOrganizationDomainCommand = deleteOrganizationDomainCommand; - _getOrganizationDomainByIdQuery = getOrganizationDomainByIdQuery; + _getOrganizationDomainByIdAndOrganizationIdQuery = getOrganizationDomainByIdAndOrganizationIdQuery; _getOrganizationDomainByOrganizationIdQuery = getOrganizationDomainByOrganizationIdQuery; _currentContext = currentContext; _organizationRepository = organizationRepository; @@ -46,71 +46,78 @@ public class OrganizationDomainController : Controller } [HttpGet("{orgId}/domain")] - public async Task> Get(string orgId) + public async Task> Get(Guid orgId) { - var orgIdGuid = new Guid(orgId); - await ValidateOrganizationAccessAsync(orgIdGuid); + await ValidateOrganizationAccessAsync(orgId); var domains = await _getOrganizationDomainByOrganizationIdQuery - .GetDomainsByOrganizationId(orgIdGuid); + .GetDomainsByOrganizationIdAsync(orgId); var response = domains.Select(x => new OrganizationDomainResponseModel(x)).ToList(); return new ListResponseModel(response); } [HttpGet("{orgId}/domain/{id}")] - public async Task Get(string orgId, string id) + public async Task Get(Guid orgId, Guid id) { - var orgIdGuid = new Guid(orgId); - var IdGuid = new Guid(id); - await ValidateOrganizationAccessAsync(orgIdGuid); + await ValidateOrganizationAccessAsync(orgId); - var domain = await _getOrganizationDomainByIdQuery.GetOrganizationDomainById(IdGuid); + var organizationDomain = await _getOrganizationDomainByIdAndOrganizationIdQuery + .GetOrganizationDomainByIdOrganizationIdAsync(id, orgId); + if (organizationDomain is null) + { + throw new NotFoundException(); + } + + return new OrganizationDomainResponseModel(organizationDomain); + } + + [HttpPost("{orgId}/domain")] + public async Task Post(Guid orgId, + [FromBody] OrganizationDomainRequestModel model) + { + await ValidateOrganizationAccessAsync(orgId); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = orgId, + Txt = model.Txt, + DomainName = model.DomainName.ToLower() + }; + + organizationDomain = await _createOrganizationDomainCommand.CreateAsync(organizationDomain); + + return new OrganizationDomainResponseModel(organizationDomain); + } + + [HttpPost("{orgId}/domain/{id}/verify")] + public async Task Verify(Guid orgId, Guid id) + { + await ValidateOrganizationAccessAsync(orgId); + + var organizationDomain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId); + if (organizationDomain is null) + { + throw new NotFoundException(); + } + + organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain); + + return new OrganizationDomainResponseModel(organizationDomain); + } + + [HttpDelete("{orgId}/domain/{id}")] + [HttpPost("{orgId}/domain/{id}/remove")] + public async Task RemoveDomain(Guid orgId, Guid id) + { + await ValidateOrganizationAccessAsync(orgId); + + var domain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId); if (domain is null) { throw new NotFoundException(); } - return new OrganizationDomainResponseModel(domain); - } - - [HttpPost("{orgId}/domain")] - public async Task Post(string orgId, - [FromBody] OrganizationDomainRequestModel model) - { - var orgIdGuid = new Guid(orgId); - await ValidateOrganizationAccessAsync(orgIdGuid); - - var organizationDomain = new OrganizationDomain - { - OrganizationId = orgIdGuid, - Txt = model.Txt, - DomainName = model.DomainName.ToLower() - }; - - var domain = await _createOrganizationDomainCommand.CreateAsync(organizationDomain); - return new OrganizationDomainResponseModel(domain); - } - - [HttpPost("{orgId}/domain/{id}/verify")] - public async Task Verify(string orgId, string id) - { - var orgIdGuid = new Guid(orgId); - var idGuid = new Guid(id); - await ValidateOrganizationAccessAsync(orgIdGuid); - - var domain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomain(idGuid); - return new OrganizationDomainResponseModel(domain); - } - - [HttpDelete("{orgId}/domain/{id}")] - [HttpPost("{orgId}/domain/{id}/remove")] - public async Task RemoveDomain(string orgId, string id) - { - var orgIdGuid = new Guid(orgId); - var idGuid = new Guid(id); - await ValidateOrganizationAccessAsync(orgIdGuid); - - await _deleteOrganizationDomainCommand.DeleteAsync(idGuid); + await _deleteOrganizationDomainCommand.DeleteAsync(domain); } [AllowAnonymous] diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommand.cs b/src/Core/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommand.cs index c42060913..f0162f7a5 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommand.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using Bit.Core.Exceptions; +using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -18,15 +18,9 @@ public class DeleteOrganizationDomainCommand : IDeleteOrganizationDomainCommand _eventService = eventService; } - public async Task DeleteAsync(Guid id) + public async Task DeleteAsync(OrganizationDomain organizationDomain) { - var domain = await _organizationDomainRepository.GetByIdAsync(id); - if (domain is null) - { - throw new NotFoundException(); - } - - await _organizationDomainRepository.DeleteAsync(domain); - await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Removed); + await _organizationDomainRepository.DeleteAsync(organizationDomain); + await _eventService.LogOrganizationDomainEventAsync(organizationDomain, EventType.OrganizationDomain_Removed); } } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQuery.cs b/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs similarity index 52% rename from src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQuery.cs rename to src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs index 8037fa8ec..1ed220768 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs @@ -4,15 +4,15 @@ using Bit.Core.Repositories; namespace Bit.Core.OrganizationFeatures.OrganizationDomains; -public class GetOrganizationDomainByIdQuery : IGetOrganizationDomainByIdQuery +public class GetOrganizationDomainByIdOrganizationIdQuery : IGetOrganizationDomainByIdOrganizationIdQuery { private readonly IOrganizationDomainRepository _organizationDomainRepository; - public GetOrganizationDomainByIdQuery(IOrganizationDomainRepository organizationDomainRepository) + public GetOrganizationDomainByIdOrganizationIdQuery(IOrganizationDomainRepository organizationDomainRepository) { _organizationDomainRepository = organizationDomainRepository; } - public async Task GetOrganizationDomainById(Guid id) - => await _organizationDomainRepository.GetByIdAsync(id); + public async Task GetOrganizationDomainByIdOrganizationIdAsync(Guid id, Guid organizationId) + => await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, organizationId); } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQuery.cs b/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQuery.cs index 6b94dbf17..c1fef0508 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQuery.cs @@ -13,6 +13,6 @@ public class GetOrganizationDomainByOrganizationIdQuery : IGetOrganizationDomain _organizationDomainRepository = organizationDomainRepository; } - public async Task> GetDomainsByOrganizationId(Guid orgId) + public async Task> GetDomainsByOrganizationIdAsync(Guid orgId) => await _organizationDomainRepository.GetDomainsByOrganizationIdAsync(orgId); } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IDeleteOrganizationDomainCommand.cs b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IDeleteOrganizationDomainCommand.cs index 4a5cc1c55..8b11bc61e 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IDeleteOrganizationDomainCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IDeleteOrganizationDomainCommand.cs @@ -1,6 +1,8 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; public interface IDeleteOrganizationDomainCommand { - Task DeleteAsync(Guid id); + Task DeleteAsync(OrganizationDomain organizationDomain); } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdOrganizationIdQuery.cs b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdOrganizationIdQuery.cs new file mode 100644 index 000000000..12fe643a6 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdOrganizationIdQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; + +public interface IGetOrganizationDomainByIdOrganizationIdQuery +{ + Task GetOrganizationDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); +} diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdQuery.cs b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdQuery.cs deleted file mode 100644 index 765007f42..000000000 --- a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Bit.Core.Entities; - -namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; - -public interface IGetOrganizationDomainByIdQuery -{ - Task GetOrganizationDomainById(Guid id); -} diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByOrganizationIdQuery.cs b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByOrganizationIdQuery.cs index 1377cb48f..d1fb642a5 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByOrganizationIdQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByOrganizationIdQuery.cs @@ -4,5 +4,5 @@ namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; public interface IGetOrganizationDomainByOrganizationIdQuery { - Task> GetDomainsByOrganizationId(Guid orgId); + Task> GetDomainsByOrganizationIdAsync(Guid orgId); } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs index 1d070cf3c..fe4c38aa8 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; public interface IVerifyOrganizationDomainCommand { - Task VerifyOrganizationDomain(Guid id); + Task VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain); } diff --git a/src/Core/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index cfa4ab148..508a085f6 100644 --- a/src/Core/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -27,14 +27,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand _logger = logger; } - public async Task VerifyOrganizationDomain(Guid id) + public async Task VerifyOrganizationDomainAsync(OrganizationDomain domain) { - var domain = await _organizationDomainRepository.GetByIdAsync(id); - if (domain is null) - { - throw new NotFoundException(); - } - if (domain.VerifiedDate is not null) { domain.SetLastCheckedDate(); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 175614923..39b3d33ee 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -118,7 +118,7 @@ public static class OrganizationServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index dfe535854..1308c4110 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -9,6 +9,7 @@ public interface IOrganizationDomainRepository : IRepository> GetDomainsByOrganizationIdAsync(Guid orgId); Task> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); + Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); Task DeleteExpiredAsync(int expirationPeriod); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index c46c994a3..6202a121d 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -69,6 +69,20 @@ public class OrganizationDomainRepository : Repository } } + public async Task GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection + .QueryAsync( + $"[{Schema}].[OrganizationDomain_ReadByIdOrganizationId]", + new { Id = id, OrganizationId = orgId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + public async Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index ea507d553..11ff8e047 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -93,6 +93,18 @@ public class OrganizationDomainRepository : Repository GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var domain = await dbContext.OrganizationDomains + .Where(x => x.Id == id && x.OrganizationId == orgId) + .AsNoTracking() + .FirstOrDefaultAsync(); + + return Mapper.Map(domain); + } + public async Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByIdOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByIdOrganizationId.sql new file mode 100644 index 000000000..7777fe683 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByIdOrganizationId.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByIdOrganizationId] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationDomain] +WHERE + [Id] = @Id + AND + [OrganizationId] = @OrganizationId +END diff --git a/test/Api.Test/Controllers/OrganizationDomainControllerTests.cs b/test/Api.Test/Controllers/OrganizationDomainControllerTests.cs index bd97aa20b..019dbdcba 100644 --- a/test/Api.Test/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationDomainControllerTests.cs @@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -13,8 +14,6 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; -using Organization = Bit.Core.Entities.Organization; -using OrganizationDomain = Bit.Core.Entities.OrganizationDomain; namespace Bit.Api.Test.Controllers; @@ -28,7 +27,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.Get(orgId.ToString()); + var requestAction = async () => await sutProvider.Sut.Get(orgId); await Assert.ThrowsAsync(requestAction); } @@ -40,7 +39,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Get(orgId.ToString()); + var requestAction = async () => await sutProvider.Sut.Get(orgId); await Assert.ThrowsAsync(requestAction); } @@ -52,7 +51,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).Returns(new Organization()); sutProvider.GetDependency() - .GetDomainsByOrganizationId(orgId).Returns(new List + .GetDomainsByOrganizationIdAsync(orgId).Returns(new List { new() { @@ -64,7 +63,7 @@ public class OrganizationDomainControllerTests } }); - var result = await sutProvider.Sut.Get(orgId.ToString()); + var result = await sutProvider.Sut.Get(orgId); Assert.IsType>(result); Assert.Equal(orgId, result.Data.Select(x => x.OrganizationId).FirstOrDefault()); @@ -76,7 +75,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.Get(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.Get(orgId, id); await Assert.ThrowsAsync(requestAction); } @@ -88,7 +87,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Get(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.Get(orgId, id); await Assert.ThrowsAsync(requestAction); } @@ -99,9 +98,24 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).Returns(new Organization()); - sutProvider.GetDependency().GetOrganizationDomainById(id).ReturnsNull(); + sutProvider.GetDependency().GetOrganizationDomainByIdOrganizationIdAsync(id, orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Get(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.Get(orgId, id); + + await Assert.ThrowsAsync(requestAction); + } + + [Theory, BitAutoData] + public async Task GetByOrgIdAndId_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageSso(organizationDomain.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization()); + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .ReturnsNull(); + + var requestAction = async () => await sutProvider.Sut.Get(organizationDomain.OrganizationId, organizationDomain.Id); await Assert.ThrowsAsync(requestAction); } @@ -112,7 +126,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).Returns(new Organization()); - sutProvider.GetDependency().GetOrganizationDomainById(id) + sutProvider.GetDependency().GetOrganizationDomainByIdOrganizationIdAsync(id, orgId) .Returns(new OrganizationDomain { Id = Guid.NewGuid(), @@ -122,7 +136,7 @@ public class OrganizationDomainControllerTests Txt = "btw+12342" }); - var result = await sutProvider.Sut.Get(orgId.ToString(), id.ToString()); + var result = await sutProvider.Sut.Get(orgId, id); Assert.IsType(result); Assert.Equal(orgId, result.OrganizationId); @@ -134,7 +148,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.Post(orgId.ToString(), model); + var requestAction = async () => await sutProvider.Sut.Post(orgId, model); await Assert.ThrowsAsync(requestAction); } @@ -146,7 +160,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Post(orgId.ToString(), model); + var requestAction = async () => await sutProvider.Sut.Post(orgId, model); await Assert.ThrowsAsync(requestAction); } @@ -160,7 +174,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().CreateAsync(Arg.Any()) .Returns(new OrganizationDomain()); - var result = await sutProvider.Sut.Post(orgId.ToString(), model); + var result = await sutProvider.Sut.Post(orgId, model); await sutProvider.GetDependency().ReceivedWithAnyArgs(1) .CreateAsync(Arg.Any()); @@ -173,7 +187,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.Verify(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.Verify(orgId, id); await Assert.ThrowsAsync(requestAction); } @@ -185,24 +199,42 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Verify(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.Verify(orgId, id); await Assert.ThrowsAsync(requestAction); } [Theory, BitAutoData] - public async Task Verify_WhenRequestIsValid(Guid orgId, Guid id, + public async Task VerifyOrganizationDomain_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain, SutProvider sutProvider) { - sutProvider.GetDependency().ManageSso(orgId).Returns(true); - sutProvider.GetDependency().GetByIdAsync(orgId).Returns(new Organization()); - sutProvider.GetDependency().VerifyOrganizationDomain(id) + sutProvider.GetDependency().ManageSso(organizationDomain.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization()); + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .ReturnsNull(); + + var requestAction = async () => await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id); + + await Assert.ThrowsAsync(requestAction); + } + + [Theory, BitAutoData] + public async Task Verify_WhenRequestIsValid(OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageSso(organizationDomain.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization()); + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + sutProvider.GetDependency().VerifyOrganizationDomainAsync(organizationDomain) .Returns(new OrganizationDomain()); - var result = await sutProvider.Sut.Verify(orgId.ToString(), id.ToString()); + var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id); await sutProvider.GetDependency().Received(1) - .VerifyOrganizationDomain(id); + .VerifyOrganizationDomainAsync(organizationDomain); Assert.IsType(result); } @@ -212,7 +244,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId, id); await Assert.ThrowsAsync(requestAction); } @@ -224,22 +256,40 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId, id); await Assert.ThrowsAsync(requestAction); } [Theory, BitAutoData] - public async Task RemoveDomain_WhenRequestIsValid(Guid orgId, Guid id, + public async Task RemoveDomain_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain, SutProvider sutProvider) { - sutProvider.GetDependency().ManageSso(orgId).Returns(true); - sutProvider.GetDependency().GetByIdAsync(orgId).Returns(new Organization()); + sutProvider.GetDependency().ManageSso(organizationDomain.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization()); + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .ReturnsNull(); - await sutProvider.Sut.RemoveDomain(orgId.ToString(), id.ToString()); + var requestAction = async () => await sutProvider.Sut.RemoveDomain(organizationDomain.OrganizationId, organizationDomain.Id); + + await Assert.ThrowsAsync(requestAction); + } + + [Theory, BitAutoData] + public async Task RemoveDomain_WhenRequestIsValid(OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency().ManageSso(organizationDomain.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization()); + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + + await sutProvider.Sut.RemoveDomain(organizationDomain.OrganizationId, organizationDomain.Id); await sutProvider.GetDependency().Received(1) - .DeleteAsync(id); + .DeleteAsync(organizationDomain); } [Theory, BitAutoData] diff --git a/test/Core.Test/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommandTests.cs index b9201d35a..3a559d5c3 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommandTests.cs @@ -1,13 +1,11 @@ using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationDomains; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; -using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; @@ -15,17 +13,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; [SutProviderCustomize] public class DeleteOrganizationDomainCommandTests { - [Theory, BitAutoData] - public async Task DeleteAsync_ShouldThrowNotFoundException_WhenIdDoesNotExist(Guid id, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(id).ReturnsNull(); - - var requestAction = async () => await sutProvider.Sut.DeleteAsync(id); - - await Assert.ThrowsAsync(requestAction); - } - [Theory, BitAutoData] public async Task DeleteAsync_Success(Guid id, SutProvider sutProvider) { @@ -36,9 +23,8 @@ public class DeleteOrganizationDomainCommandTests DomainName = "Test Domain", Txt = "btw+test18383838383" }; - sutProvider.GetDependency().GetByIdAsync(id).Returns(expected); - await sutProvider.Sut.DeleteAsync(id); + await sutProvider.Sut.DeleteAsync(expected); await sutProvider.GetDependency().Received(1).DeleteAsync(expected); await sutProvider.GetDependency().Received(1) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQueryTests.cs new file mode 100644 index 000000000..6480d8be0 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQueryTests.cs @@ -0,0 +1,80 @@ +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationDomains; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; + +[SutProviderCustomize] +public class GetOrganizationDomainByIdOrganizationIdQueryTests +{ + [Theory, BitAutoData] + public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithExistingParameters_ReturnsExpectedEntity( + OrganizationDomain organizationDomain, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + + var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId); + + await sutProvider.GetDependency().Received(1) + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId); + + Assert.Equal(organizationDomain, result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingParameters_ReturnsNull( + Guid id, Guid organizationId, OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + + var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(id, organizationId); + + await sutProvider.GetDependency().Received(1) + .GetDomainByIdOrganizationIdAsync(id, organizationId); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingId_ReturnsNull( + Guid id, OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + + var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(id, organizationDomain.OrganizationId); + + await sutProvider.GetDependency().Received(1) + .GetDomainByIdOrganizationIdAsync(id, organizationDomain.OrganizationId); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingOrgId_ReturnsNull( + Guid organizationId, OrganizationDomain organizationDomain, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) + .Returns(organizationDomain); + + var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationId); + + await sutProvider.GetDependency().Received(1) + .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationId); + + Assert.Null(result); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQueryTests.cs deleted file mode 100644 index 0cb77d243..000000000 --- a/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdQueryTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Bit.Core.OrganizationFeatures.OrganizationDomains; -using Bit.Core.Repositories; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; - -[SutProviderCustomize] -public class GetOrganizationDomainByIdQueryTests -{ - [Theory, BitAutoData] - public async Task GetOrganizationDomainById_CallsGetByIdAsync(Guid id, - SutProvider sutProvider) - { - await sutProvider.Sut.GetOrganizationDomainById(id); - - await sutProvider.GetDependency().Received(1) - .GetByIdAsync(id); - } -} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQueryTests.cs index 964a3a76d..d87667600 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQueryTests.cs @@ -14,7 +14,7 @@ public class GetOrganizationDomainByOrganizationIdQueryTests public async Task GetDomainsByOrganizationId_CallsGetDomainsByOrganizationIdAsync(Guid orgId, SutProvider sutProvider) { - await sutProvider.Sut.GetDomainsByOrganizationId(orgId); + await sutProvider.Sut.GetDomainsByOrganizationIdAsync(orgId); await sutProvider.GetDependency().Received(1) .GetDomainsByOrganizationIdAsync(orgId); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index 54783982a..07bc2b14e 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -7,8 +7,6 @@ using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; -using NSubstitute.ReceivedExtensions; -using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; @@ -16,19 +14,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationDomains; [SutProviderCustomize] public class VerifyOrganizationDomainCommandTests { - [Theory, BitAutoData] - public async Task VerifyOrganizationDomain_ShouldThrowNotFound_WhenDomainDoesNotExist(Guid id, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetByIdAsync(id) - .ReturnsNull(); - - var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomain(id); - - await Assert.ThrowsAsync(requestAction); - } - [Theory, BitAutoData] public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, SutProvider sutProvider) @@ -45,7 +30,7 @@ public class VerifyOrganizationDomainCommandTests .GetByIdAsync(id) .Returns(expected); - var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomain(id); + var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var exception = await Assert.ThrowsAsync(requestAction); Assert.Contains("Domain has already been verified.", exception.Message); @@ -69,7 +54,7 @@ public class VerifyOrganizationDomainCommandTests .GetClaimedDomainsByDomainNameAsync(expected.DomainName) .Returns(new List { expected }); - var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomain(id); + var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var exception = await Assert.ThrowsAsync(requestAction); Assert.Contains("The domain is not available to be claimed.", exception.Message); @@ -96,7 +81,7 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(true); - var result = await sutProvider.Sut.VerifyOrganizationDomain(id); + var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); Assert.NotNull(result.VerifiedDate); await sutProvider.GetDependency().Received(1) @@ -126,7 +111,7 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(expected.DomainName, Arg.Any()) .Returns(false); - var result = await sutProvider.Sut.VerifyOrganizationDomain(id); + var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); Assert.Null(result.VerifiedDate); await sutProvider.GetDependency().Received(1) diff --git a/util/Migrator/DbScripts/2023-09-29_00_OrgDomainReadByIdOrgId.sql b/util/Migrator/DbScripts/2023-09-29_00_OrgDomainReadByIdOrgId.sql new file mode 100644 index 000000000..501bf7010 --- /dev/null +++ b/util/Migrator/DbScripts/2023-09-29_00_OrgDomainReadByIdOrgId.sql @@ -0,0 +1,17 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationDomain_ReadByIdOrganizationId] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationDomain] +WHERE + [Id] = @Id + AND + [OrganizationId] = @OrganizationId +END +GO