diff --git a/SETUP.md b/SETUP.md index 939a63dc7..77c9b43b6 100644 --- a/SETUP.md +++ b/SETUP.md @@ -13,6 +13,8 @@ Each service is built and run separately. The Bitwarden clients can use differen This means that you don't need to run all services locally for a development environment. You can run only those services that you intend to modify, and use Bitwarden.com or a self-hosted instance for all other services required. +By default some of the services depends on the Bitwarden licensed `CommCore`, however it can easily be disabled by including the `/p:DefineConstants="OSS"` as an argument to `dotnet`. + # Local Development Environment Setup This guide will show you how to set up the Api, Identity and SQL projects for development. These are the minimum projects for any development work. You may need to set up additional projects depending on the changes you want to make. diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 0ae64a7f2..75d09e19d 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -61,7 +61,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Portal", "bitwarden_license EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sso", "bitwarden_license\src\Sso\Sso.csproj", "{4866AF64-6640-4C65-A662-A31E02FF9064}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Icons.Test", "test\Icons.Test\Icons.Test.csproj", "{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icons.Test", "test\Icons.Test\Icons.Test.csproj", "{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore", "bitwarden_license\src\CommCore\CommCore.csproj", "{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}" + ProjectSection(ProjectDependencies) = postProject + {3973D21B-A692-4B60-9B70-3631C057423A} = {3973D21B-A692-4B60-9B70-3631C057423A} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommCore.Test", "bitwarden_license\test\CmmCore.Test\CommCore.Test.csproj", "{0E99A21B-684B-4C59-9831-90F775CAB6F7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test - Bitwarden License", "test - Bitwarden License", "{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -143,6 +152,14 @@ Global {C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.Build.0 = Release|Any CPU + {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.Build.0 = Release|Any CPU + {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -166,6 +183,8 @@ Global {BA852F18-852F-4154-973B-77D577B8CA04} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A} {4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A} {C7BA2255-C1B1-4789-8BB9-C27540DA6FB8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A} + {0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/bitwarden_license/src/CommCore/CommCore.csproj b/bitwarden_license/src/CommCore/CommCore.csproj new file mode 100644 index 000000000..d77902c60 --- /dev/null +++ b/bitwarden_license/src/CommCore/CommCore.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Core/Services/Implementations/ProviderService.cs b/bitwarden_license/src/CommCore/Services/ProviderService.cs similarity index 88% rename from src/Core/Services/Implementations/ProviderService.cs rename to bitwarden_license/src/CommCore/Services/ProviderService.cs index 925fd31ad..26ce56db2 100644 --- a/src/Core/Services/Implementations/ProviderService.cs +++ b/bitwarden_license/src/CommCore/Services/ProviderService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Bit.Core.Enums; using Bit.Core.Enums.Provider; @@ -10,11 +9,12 @@ using Bit.Core.Models.Business.Provider; using Bit.Core.Models.Table; using Bit.Core.Models.Table.Provider; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; -namespace Bit.Core.Services +namespace Bit.CommCore.Services { public class ProviderService : IProviderService { @@ -24,15 +24,18 @@ namespace Bit.Core.Services private readonly GlobalSettings _globalSettings; private readonly IProviderRepository _providerRepository; private readonly IProviderUserRepository _providerUserRepository; + private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IUserRepository _userRepository; private readonly IUserService _userService; public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, - IUserRepository userRepository, IUserService userService, IMailService mailService, - IDataProtectionProvider dataProtectionProvider, IEventService eventService, GlobalSettings globalSettings) + IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, + IUserService userService, IMailService mailService, IDataProtectionProvider dataProtectionProvider, + IEventService eventService, GlobalSettings globalSettings) { _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; + _providerOrganizationRepository = providerOrganizationRepository; _userRepository = userRepository; _userService = userService; _mailService = mailService; @@ -55,12 +58,21 @@ namespace Bit.Core.Services Enabled = true, }; await _providerRepository.CreateAsync(provider); - + + var providerUser = new ProviderUser + { + ProviderId = provider.Id, + UserId = owner.Id, + Type = ProviderUserType.ProviderAdmin, + Status = ProviderUserStatusType.Confirmed, + }; + await _providerUserRepository.CreateAsync(providerUser); + var token = _dataProtector.Protect($"ProviderSetupInvite {provider.Id} {owner.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); await _mailService.SendProviderSetupInviteEmailAsync(provider, token, owner.Email); } - public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) + public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) { var owner = await _userService.GetUserByIdAsync(ownerUserId); if (owner == null) @@ -68,23 +80,29 @@ namespace Bit.Core.Services throw new BadRequestException("Invalid owner."); } + if (provider.Status != ProviderStatusType.Pending) + { + throw new BadRequestException("Provider is already setup."); + } + if (!CoreHelpers.TokenIsValid("ProviderSetupInvite", _dataProtector, token, owner.Email, provider.Id, _globalSettings)) { throw new BadRequestException("Invalid token."); } - - await _providerRepository.UpsertAsync(provider); - - var providerUser = new ProviderUser - { - ProviderId = provider.Id, - UserId = owner.Id, - Key = key, - Status = ProviderUserStatusType.Confirmed, - Type = ProviderUserType.ProviderAdmin, - }; - await _providerUserRepository.CreateAsync(providerUser); + var providerUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, ownerUserId); + if (!(providerUser is {Type: ProviderUserType.ProviderAdmin})) + { + throw new BadRequestException("Invalid owner."); + } + + provider.Status = ProviderStatusType.Created; + await _providerRepository.UpsertAsync(provider); + + providerUser.Key = key; + await _providerUserRepository.ReplaceAsync(providerUser); + + return provider; } public async Task UpdateAsync(Provider provider, bool updateBilling = false) @@ -129,14 +147,6 @@ namespace Bit.Core.Services RevisionDate = DateTime.UtcNow, }; - if (invite.Permissions != null) - { - providerUser.Permissions = JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - } - await _providerUserRepository.CreateAsync(providerUser); await SendInviteAsync(providerUser, provider); @@ -322,8 +332,17 @@ namespace Bit.Core.Services return result; } - // TODO: Implement this - public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException(); + public async Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) + { + var providerOrganization = new ProviderOrganization + { + ProviderId = providerId, + OrganizationId = organizationId, + Key = key, + }; + + await _providerOrganizationRepository.CreateAsync(providerOrganization); + } // TODO: Implement this public Task RemoveOrganization(Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException(); diff --git a/bitwarden_license/src/CommCore/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/CommCore/Utilities/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..bd8447120 --- /dev/null +++ b/bitwarden_license/src/CommCore/Utilities/ServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Bit.CommCore.Services; +using Bit.Core.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.CommCore.Utilities +{ + public static class ServiceCollectionExtensions + { + public static void AddCommCoreServices(this IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/test/Core.Test/AutoFixture/ProviderUserFixtures.cs b/bitwarden_license/test/CmmCore.Test/AutoFixture/ProviderUserFixtures.cs similarity index 95% rename from test/Core.Test/AutoFixture/ProviderUserFixtures.cs rename to bitwarden_license/test/CmmCore.Test/AutoFixture/ProviderUserFixtures.cs index 8cf0d9c97..8dd7a94c6 100644 --- a/test/Core.Test/AutoFixture/ProviderUserFixtures.cs +++ b/bitwarden_license/test/CmmCore.Test/AutoFixture/ProviderUserFixtures.cs @@ -3,7 +3,7 @@ using AutoFixture; using AutoFixture.Xunit2; using Bit.Core.Enums.Provider; -namespace Bit.Core.Test.AutoFixture.ProviderUserFixtures +namespace Bit.CommCore.Test.AutoFixture.ProviderUserFixtures { internal class ProviderUser : ICustomization { diff --git a/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj b/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj new file mode 100644 index 000000000..50d12b4f8 --- /dev/null +++ b/bitwarden_license/test/CmmCore.Test/CommCore.Test.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.1 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Core.Test/Services/ProviderServiceTests.cs b/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs similarity index 96% rename from test/Core.Test/Services/ProviderServiceTests.cs rename to bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs index d4f5fae13..1ec208dc0 100644 --- a/test/Core.Test/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Bit.CommCore.Test.AutoFixture.ProviderUserFixtures; +using Bit.CommCore.Services; using Bit.Core.Enums; using Bit.Core.Enums.Provider; using Bit.Core.Exceptions; @@ -12,14 +14,13 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.ProviderUserFixtures; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; using NSubstitute; using Xunit; using ProviderUser = Bit.Core.Models.Table.Provider.ProviderUser; -namespace Bit.Core.Test.Services +namespace Bit.CommCore.Test.Services { public class ProviderServiceTests { @@ -62,27 +63,33 @@ namespace Bit.Core.Test.Services () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default)); Assert.Contains("Invalid token.", exception.Message); } - + [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task CompleteSetupAsync_Success(User user, Provider provider, + public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, + [ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)]ProviderUser providerUser, SutProvider sutProvider) { + providerUser.ProviderId = provider.Id; + providerUser.UserId = user.Id; var userService = sutProvider.GetDependency(); userService.GetUserByIdAsync(user.Id).Returns(user); + var providerUserRepository = sutProvider.GetDependency(); + providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); + var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") .Returns(protector); sutProvider.Create(); - - var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, default); + var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); + + await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key); await sutProvider.GetDependency().Received().UpsertAsync(provider); await sutProvider.GetDependency().Received() - .CreateAsync(Arg.Is(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id)); + .ReplaceAsync(Arg.Is(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key)); } [Theory, CustomAutoData(typeof(SutProviderCustomization))] diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index 95686cc19..5fcedae05 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -9,6 +9,14 @@ + + + + + + + + diff --git a/src/Admin/Controllers/ProvidersController.cs b/src/Admin/Controllers/ProvidersController.cs index b97f50a9f..3267ac2ab 100644 --- a/src/Admin/Controllers/ProvidersController.cs +++ b/src/Admin/Controllers/ProvidersController.cs @@ -85,7 +85,7 @@ namespace Bit.Admin.Controllers return RedirectToAction("Index"); } - var users = await _providerUserRepository.GetManyByProviderAsync(id); + var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); return View(new ProviderViewModel(provider, users)); } @@ -98,7 +98,7 @@ namespace Bit.Admin.Controllers return RedirectToAction("Index"); } - var users = await _providerUserRepository.GetManyByProviderAsync(id); + var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id); return View(new ProviderEditModel(provider, users)); } diff --git a/src/Admin/Models/ProviderEditModel.cs b/src/Admin/Models/ProviderEditModel.cs index 652234e84..7eac1bf42 100644 --- a/src/Admin/Models/ProviderEditModel.cs +++ b/src/Admin/Models/ProviderEditModel.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using System.Linq; using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; namespace Bit.Admin.Models { public class ProviderEditModel : ProviderViewModel { - public ProviderEditModel(Provider provider, IEnumerable providerUsers) + public ProviderEditModel(Provider provider, IEnumerable providerUsers) : base(provider, providerUsers) { Name = provider.Name; diff --git a/src/Admin/Models/ProviderViewModel.cs b/src/Admin/Models/ProviderViewModel.cs index 9f77e4197..48ed3b27e 100644 --- a/src/Admin/Models/ProviderViewModel.cs +++ b/src/Admin/Models/ProviderViewModel.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using System.Linq; using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; namespace Bit.Admin.Models { public class ProviderViewModel { - public ProviderViewModel(Provider provider, IEnumerable providerUsers) + public ProviderViewModel(Provider provider, IEnumerable providerUsers) { Provider = provider; UserCount = providerUsers.Count(); diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 2409989b8..49c2aaf8e 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -13,6 +13,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Stripe; +#if !OSS +using Bit.CommCore.Utilities; +#endif + namespace Bit.Admin { public class Startup @@ -65,6 +69,12 @@ namespace Bit.Admin // Services services.AddBaseServices(); services.AddDefaultServices(globalSettings); + + #if OSS + services.AddOosServices(); + #else + services.AddCommCoreServices(); + #endif // Mvc services.AddMvc(config => diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 41fc38fa2..84264f890 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -19,6 +19,14 @@ + + + + + + + + diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index dac1a70b5..83eb7b281 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Bit.Core.Enums.Provider; namespace Bit.Api.Controllers { @@ -30,6 +31,7 @@ namespace Bit.Api.Controllers private readonly IFolderRepository _folderRepository; private readonly IOrganizationService _organizationService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; private readonly IPaymentService _paymentService; private readonly IUserRepository _userRepository; private readonly IUserService _userService; @@ -40,6 +42,7 @@ namespace Bit.Api.Controllers IFolderRepository folderRepository, IOrganizationService organizationService, IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository, IPaymentService paymentService, ISsoUserRepository ssoUserRepository, IUserRepository userRepository, @@ -50,6 +53,7 @@ namespace Bit.Api.Controllers _globalSettings = globalSettings; _organizationService = organizationService; _organizationUserRepository = organizationUserRepository; + _providerUserRepository = providerUserRepository; _paymentService = paymentService; _userRepository = userRepository; _userService = userService; @@ -358,7 +362,9 @@ namespace Bit.Api.Controllers var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed); - var response = new ProfileResponseModel(user, organizationUserDetails, + var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id, + ProviderUserStatusType.Confirmed); + var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, await _userService.TwoFactorIsEnabledAsync(user)); return response; } @@ -384,7 +390,7 @@ namespace Bit.Api.Controllers } await _userService.SaveUserAsync(model.ToUser(user)); - var response = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); + var response = new ProfileResponseModel(user, null, null, await _userService.TwoFactorIsEnabledAsync(user)); return response; } @@ -535,7 +541,7 @@ namespace Bit.Api.Controllers BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode, }); - var profile = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); + var profile = new ProfileResponseModel(user, null, null, await _userService.TwoFactorIsEnabledAsync(user)); return new PaymentResponseModel { UserProfile = profile, diff --git a/src/Api/Controllers/ProviderOrganizationsController.cs b/src/Api/Controllers/ProviderOrganizationsController.cs new file mode 100644 index 000000000..7a181cc70 --- /dev/null +++ b/src/Api/Controllers/ProviderOrganizationsController.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [Route("providers/{providerId:guid}/organizations")] + [Authorize("Application")] + public class ProviderOrganizationsController : Controller + { + + private readonly IProviderOrganizationRepository _providerOrganizationRepository; + private readonly IProviderService _providerService; + private readonly IUserService _userService; + private readonly ICurrentContext _currentContext; + + public ProviderOrganizationsController( + IProviderOrganizationRepository providerOrganizationRepository, + IProviderService providerService, + IUserService userService, + ICurrentContext currentContext) + { + _providerOrganizationRepository = providerOrganizationRepository; + _providerService = providerService; + _userService = userService; + _currentContext = currentContext; + } + + [HttpGet("")] + public async Task> Get(Guid providerId) + { + if (!_currentContext.AccessProviderOrganizations(providerId)) + { + throw new NotFoundException(); + } + + var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); + var responses = providerOrganizations.Select(o => new ProviderOrganizationOrganizationDetailsResponseModel(o)); + return new ListResponseModel(responses); + } + + [HttpPost("add")] + public async Task Add(Guid providerId, [FromBody]ProviderOrganizationAddRequestModel model) + { + if (!_currentContext.ManageProviderOrganizations(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + + await _providerService.AddOrganization(providerId, model.OrganizationId, userId, model.Key); + } + } +} diff --git a/src/Api/Controllers/ProviderUsersController.cs b/src/Api/Controllers/ProviderUsersController.cs new file mode 100644 index 000000000..3dc131d4a --- /dev/null +++ b/src/Api/Controllers/ProviderUsersController.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Authorization; +using Bit.Core.Models.Api; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Bit.Core.Context; +using Bit.Core.Models.Business.Provider; + +namespace Bit.Api.Controllers +{ + [Route("providers/{providerId:guid}/users")] + [Authorize("Application")] + public class ProviderUsersController : Controller + { + private readonly IProviderUserRepository _providerUserRepository; + private readonly IProviderService _providerService; + private readonly IUserService _userService; + private readonly ICurrentContext _currentContext; + + public ProviderUsersController( + IProviderUserRepository providerUserRepository, + IProviderService providerService, + IUserService userService, + ICurrentContext currentContext) + { + _providerUserRepository = providerUserRepository; + _providerService = providerService; + _userService = userService; + _currentContext = currentContext; + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid providerId, Guid id) + { + var providerUser = await _providerUserRepository.GetByIdAsync(id); + if (providerUser == null || !_currentContext.ManageProviderUsers(providerUser.ProviderId)) + { + throw new NotFoundException(); + } + + return new ProviderUserResponseModel(providerUser); + } + + [HttpGet("")] + public async Task> Get(Guid providerId) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var providerUsers = await _providerUserRepository.GetManyDetailsByProviderAsync(providerId); + var responses = providerUsers.Select(o => new ProviderUserUserDetailsResponseModel(o)); + return new ListResponseModel(responses); + } + + [HttpPost("invite")] + public async Task Invite(Guid providerId, [FromBody]ProviderUserInviteRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _providerService.InviteUserAsync(providerId, userId.Value, new ProviderUserInvite(model)); + } + + [HttpPost("reinvite")] + public async Task> BulkReinvite(Guid providerId, [FromBody]ProviderUserBulkRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + var result = await _providerService.ResendInvitesAsync(providerId, userId.Value, model.Ids); + return new ListResponseModel( + result.Select(t => new ProviderUserBulkResponseModel(t.Item1.Id, t.Item2))); + } + + [HttpPost("{id:guid}/reinvite")] + public async Task Reinvite(Guid providerId, Guid id) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _providerService.ResendInvitesAsync(providerId, userId.Value, new [] { id }); + } + + [HttpPost("{id:guid}/accept")] + public async Task Accept(Guid providerId, Guid id, [FromBody]ProviderUserAcceptRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + await _providerService.AcceptUserAsync(id, user, model.Token); + } + + [HttpPost("{id:guid}/confirm")] + public async Task Confirm(Guid providerId, Guid id, [FromBody]ProviderUserConfirmRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _providerService.ConfirmUsersAsync(providerId, new Dictionary { [id] = model.Key }, userId.Value); + } + + [HttpPost("confirm")] + public async Task> BulkConfirm(Guid providerId, + [FromBody]ProviderUserBulkConfirmRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + var results = await _providerService.ConfirmUsersAsync(providerId, model.ToDictionary(), userId.Value); + + return new ListResponseModel(results.Select(r => + new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2))); + } + + [HttpPost("public-keys")] + public async Task> UserPublicKeys(Guid providerId, [FromBody]ProviderUserBulkRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var result = await _providerUserRepository.GetManyPublicKeysByProviderUserAsync(providerId, model.Ids); + var responses = result.Select(r => new ProviderUserPublicKeyResponseModel(r.Id, r.PublicKey)).ToList(); + return new ListResponseModel(responses); + } + + [HttpPut("{id:guid}")] + [HttpPost("{id:guid}")] + public async Task Put(Guid providerId, Guid id, [FromBody]ProviderUserUpdateRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var providerUser = await _providerUserRepository.GetByIdAsync(id); + if (providerUser == null || providerUser.ProviderId != providerId) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _providerService.SaveUserAsync(model.ToProviderUser(providerUser), userId.Value); + } + + [HttpDelete("{id:guid}")] + [HttpPost("{id:guid}/delete")] + public async Task Delete(Guid providerId, Guid id) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _providerService.DeleteUsersAsync(providerId, new [] { id }, userId.Value); + } + + [HttpDelete("")] + [HttpPost("delete")] + public async Task> BulkDelete(Guid providerId, [FromBody]ProviderUserBulkRequestModel model) + { + if (!_currentContext.ManageProviderUsers(providerId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + var result = await _providerService.DeleteUsersAsync(providerId, model.Ids, userId.Value); + return new ListResponseModel(result.Select(r => + new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2))); + } + } +} diff --git a/src/Api/Controllers/ProvidersController.cs b/src/Api/Controllers/ProvidersController.cs new file mode 100644 index 000000000..9f4eb4d92 --- /dev/null +++ b/src/Api/Controllers/ProvidersController.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [Route("providers")] + [Authorize("Application")] + public class ProvidersController : Controller + { + private readonly IUserService _userService; + private readonly IProviderRepository _providerRepository; + private readonly IProviderService _providerService; + private readonly ICurrentContext _currentContext; + + public ProvidersController(IUserService userService, IProviderRepository providerRepository, + IProviderService providerService, ICurrentContext currentContext) + { + _userService = userService; + _providerRepository = providerRepository; + _providerService = providerService; + _currentContext = currentContext; + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + if (!_currentContext.ProviderUser(id)) + { + throw new NotFoundException(); + } + + var provider = await _providerRepository.GetByIdAsync(id); + if (provider == null) + { + throw new NotFoundException(); + } + + return new ProviderResponseModel(provider); + } + + [HttpPost("{id:guid}/setup")] + public async Task Setup(Guid id, [FromBody]ProviderSetupRequestModel model) + { + if (!_currentContext.ProviderProviderAdmin(id)) + { + throw new NotFoundException(); + } + + var provider = await _providerRepository.GetByIdAsync(id); + if (provider == null) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + + var response = + await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key); + + return new ProviderResponseModel(response); + } + } +} diff --git a/src/Api/Controllers/SyncController.cs b/src/Api/Controllers/SyncController.cs index 5dbcd372a..3ae545338 100644 --- a/src/Api/Controllers/SyncController.cs +++ b/src/Api/Controllers/SyncController.cs @@ -10,6 +10,7 @@ using Bit.Core.Exceptions; using System.Linq; using Bit.Core.Models.Table; using System.Collections.Generic; +using Bit.Core.Enums.Provider; using Bit.Core.Models.Data; using Bit.Core.Settings; @@ -25,6 +26,7 @@ namespace Bit.Api.Controllers private readonly ICollectionRepository _collectionRepository; private readonly ICollectionCipherRepository _collectionCipherRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; private readonly IPolicyRepository _policyRepository; private readonly ISendRepository _sendRepository; private readonly GlobalSettings _globalSettings; @@ -36,6 +38,7 @@ namespace Bit.Api.Controllers ICollectionRepository collectionRepository, ICollectionCipherRepository collectionCipherRepository, IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository, IPolicyRepository policyRepository, ISendRepository sendRepository, GlobalSettings globalSettings) @@ -46,6 +49,7 @@ namespace Bit.Api.Controllers _collectionRepository = collectionRepository; _collectionCipherRepository = collectionCipherRepository; _organizationUserRepository = organizationUserRepository; + _providerUserRepository = providerUserRepository; _policyRepository = policyRepository; _sendRepository = sendRepository; _globalSettings = globalSettings; @@ -62,6 +66,8 @@ namespace Bit.Api.Controllers var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed); + var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id, + ProviderUserStatusType.Confirmed); var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, hasEnabledOrgs); @@ -80,7 +86,8 @@ namespace Bit.Api.Controllers var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, organizationUserDetails, - folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); + providerUserDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, + policies, sends); return response; } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index aaf748c78..101df5c6c 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -20,6 +20,10 @@ using Microsoft.OpenApi.Models; using System.Collections.Generic; using System; +#if !OSS +using Bit.CommCore.Utilities; +#endif + namespace Bit.Api { public class Startup @@ -119,6 +123,12 @@ namespace Bit.Api services.AddDefaultServices(globalSettings); services.AddCoreLocalizationServices(); + #if OSS + services.AddOosServices(); + #else + services.AddCommCoreServices(); + #endif + // MVC services.AddMvc(config => { diff --git a/src/Core/Context/CurrentContentProvider.cs b/src/Core/Context/CurrentContentProvider.cs new file mode 100644 index 000000000..a0cb235a3 --- /dev/null +++ b/src/Core/Context/CurrentContentProvider.cs @@ -0,0 +1,26 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; +using Bit.Core.Models.Table; +using Bit.Core.Models.Table.Provider; +using Bit.Core.Utilities; + +namespace Bit.Core.Context +{ + public class CurrentContentProvider + { + public CurrentContentProvider() { } + + public CurrentContentProvider(ProviderUser providerUser) + { + Id = providerUser.ProviderId; + Type = providerUser.Type; + Permissions = CoreHelpers.LoadClassFromJsonData(providerUser.Permissions); + } + + public Guid Id { get; set; } + public ProviderUserType Type { get; set; } + public Permissions Permissions { get; set; } + } +} diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 2e27aaf92..7cf992f22 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Bit.Core.Repositories; using System.Threading.Tasks; using System.Security.Claims; +using Bit.Core.Enums.Provider; using Bit.Core.Utilities; using Bit.Core.Models.Data; using Bit.Core.Settings; @@ -25,6 +26,7 @@ namespace Bit.Core.Context public virtual DeviceType? DeviceType { get; set; } public virtual string IpAddress { get; set; } public virtual List Organizations { get; set; } + public virtual List Providers { get; set; } public virtual Guid? InstallationId { get; set; } public virtual Guid? OrganizationId { get; set; } public virtual bool CloudflareWorkerProxied { get; set; } @@ -127,10 +129,19 @@ namespace Bit.Core.Context DeviceIdentifier = GetClaimValue(claimsDict, "device"); - Organizations = new List(); + Organizations = GetOrganizations(claimsDict, orgApi); + + Providers = GetProviders(claimsDict); + + return Task.FromResult(0); + } + + private List GetOrganizations(Dictionary> claimsDict, bool orgApi) + { + var organizations = new List(); if (claimsDict.ContainsKey("orgowner")) { - Organizations.AddRange(claimsDict["orgowner"].Select(c => + organizations.AddRange(claimsDict["orgowner"].Select(c => new CurrentContentOrganization { Id = new Guid(c.Value), @@ -139,7 +150,7 @@ namespace Bit.Core.Context } else if (orgApi && OrganizationId.HasValue) { - Organizations.Add(new CurrentContentOrganization + organizations.Add(new CurrentContentOrganization { Id = OrganizationId.Value, Type = OrganizationUserType.Owner @@ -148,7 +159,7 @@ namespace Bit.Core.Context if (claimsDict.ContainsKey("orgadmin")) { - Organizations.AddRange(claimsDict["orgadmin"].Select(c => + organizations.AddRange(claimsDict["orgadmin"].Select(c => new CurrentContentOrganization { Id = new Guid(c.Value), @@ -158,7 +169,7 @@ namespace Bit.Core.Context if (claimsDict.ContainsKey("orguser")) { - Organizations.AddRange(claimsDict["orguser"].Select(c => + organizations.AddRange(claimsDict["orguser"].Select(c => new CurrentContentOrganization { Id = new Guid(c.Value), @@ -168,7 +179,7 @@ namespace Bit.Core.Context if (claimsDict.ContainsKey("orgmanager")) { - Organizations.AddRange(claimsDict["orgmanager"].Select(c => + organizations.AddRange(claimsDict["orgmanager"].Select(c => new CurrentContentOrganization { Id = new Guid(c.Value), @@ -178,7 +189,7 @@ namespace Bit.Core.Context if (claimsDict.ContainsKey("orgcustom")) { - Organizations.AddRange(claimsDict["orgcustom"].Select(c => + organizations.AddRange(claimsDict["orgcustom"].Select(c => new CurrentContentOrganization { Id = new Guid(c.Value), @@ -186,8 +197,34 @@ namespace Bit.Core.Context Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict) })); } + + return organizations; + } + + private List GetProviders(Dictionary> claimsDict) + { + var providers = new List(); + if (claimsDict.ContainsKey("providerprovideradmin")) + { + providers.AddRange(claimsDict["providerprovideradmin"].Select(c => + new CurrentContentProvider + { + Id = new Guid(c.Value), + Type = ProviderUserType.ProviderAdmin + })); + } - return Task.FromResult(0); + if (claimsDict.ContainsKey("providerserviceuser")) + { + providers.AddRange(claimsDict["providerserviceuser"].Select(c => + new CurrentContentProvider + { + Id = new Guid(c.Value), + Type = ProviderUserType.ServiceUser + })); + } + + return providers; } public bool OrganizationUser(Guid orgId) @@ -284,6 +321,31 @@ namespace Bit.Core.Context && (o.Permissions?.ManageResetPassword ?? false)) ?? false); } + public bool ProviderProviderAdmin(Guid providerId) + { + return Providers?.Any(o => o.Id == providerId && o.Type == ProviderUserType.ProviderAdmin) ?? false; + } + + public bool ManageProviderUsers(Guid providerId) + { + return ProviderProviderAdmin(providerId); + } + + public bool AccessProviderOrganizations(Guid providerId) + { + return ProviderUser(providerId); + } + + public bool ManageProviderOrganizations(Guid providerId) + { + return ProviderProviderAdmin(providerId); + } + + public bool ProviderUser(Guid providerId) + { + return Providers?.Any(o => o.Id == providerId) ?? false; + } + public async Task> OrganizationMembershipAsync( IOrganizationUserRepository organizationUserRepository, Guid userId) { @@ -295,6 +357,18 @@ namespace Bit.Core.Context } return Organizations; } + + public async Task> ProviderMembershipAsync( + IProviderUserRepository providerUserRepository, Guid userId) + { + if (Providers == null) + { + var userProviders = await providerUserRepository.GetManyByUserAsync(userId); + Providers = userProviders.Where(ou => ou.Status == ProviderUserStatusType.Confirmed) + .Select(ou => new CurrentContentProvider(ou)).ToList(); + } + return Providers; + } private string GetClaimValue(Dictionary> claims, string type) { diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index d6eb8686a..b50481565 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -47,8 +47,16 @@ namespace Bit.Core.Context bool ManageSso(Guid orgId); bool ManageUsers(Guid orgId); bool ManageResetPassword(Guid orgId); + bool ProviderProviderAdmin(Guid providerId); + bool ProviderUser(Guid providerId); + bool ManageProviderUsers(Guid providerId); + bool AccessProviderOrganizations(Guid providerId); + bool ManageProviderOrganizations(Guid providerId); Task> OrganizationMembershipAsync( IOrganizationUserRepository organizationUserRepository, Guid userId); + + Task> ProviderMembershipAsync( + IProviderUserRepository providerUserRepository, Guid userId); } } diff --git a/src/Core/IdentityServer/ApiResources.cs b/src/Core/IdentityServer/ApiResources.cs index 0583747c6..d0898b430 100644 --- a/src/Core/IdentityServer/ApiResources.cs +++ b/src/Core/IdentityServer/ApiResources.cs @@ -22,11 +22,14 @@ namespace Bit.Core.IdentityServer "orgmanager", "orguser", "orgcustom", + "providerprovideradmin", + "providerserviceuser", }), new ApiResource("internal", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }), - new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }) + new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }), + new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }), }; } } diff --git a/src/Core/IdentityServer/ClientStore.cs b/src/Core/IdentityServer/ClientStore.cs index 7bac1d917..225e9776c 100644 --- a/src/Core/IdentityServer/ClientStore.cs +++ b/src/Core/IdentityServer/ClientStore.cs @@ -25,6 +25,7 @@ namespace Bit.Core.IdentityServer private readonly ILicensingService _licensingService; private readonly ICurrentContext _currentContext; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; public ClientStore( IInstallationRepository installationRepository, @@ -34,7 +35,8 @@ namespace Bit.Core.IdentityServer StaticClientStore staticClientStore, ILicensingService licensingService, ICurrentContext currentContext, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository) { _installationRepository = installationRepository; _organizationRepository = organizationRepository; @@ -44,6 +46,7 @@ namespace Bit.Core.IdentityServer _licensingService = licensingService; _currentContext = currentContext; _organizationUserRepository = organizationUserRepository; + _providerUserRepository = providerUserRepository; } public async Task FindClientByIdAsync(string clientId) @@ -138,8 +141,9 @@ namespace Bit.Core.IdentityServer new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external") }; var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id); + var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id); var isPremium = await _licensingService.ValidateUserPremiumAsync(user); - foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, isPremium)) + foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium)) { var upperValue = claim.Value.ToUpperInvariant(); var isBool = upperValue == "TRUE" || upperValue == "FALSE"; diff --git a/src/Core/IdentityServer/ProfileService.cs b/src/Core/IdentityServer/ProfileService.cs index 97b7672d4..0ea17e444 100644 --- a/src/Core/IdentityServer/ProfileService.cs +++ b/src/Core/IdentityServer/ProfileService.cs @@ -18,17 +18,20 @@ namespace Bit.Core.IdentityServer { private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; private readonly ILicensingService _licensingService; private readonly ICurrentContext _currentContext; public ProfileService( IUserService userService, IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository, ILicensingService licensingService, ICurrentContext currentContext) { _userService = userService; _organizationUserRepository = organizationUserRepository; + _providerUserRepository = providerUserRepository; _licensingService = licensingService; _currentContext = currentContext; } @@ -43,7 +46,8 @@ namespace Bit.Core.IdentityServer { var isPremium = await _licensingService.ValidateUserPremiumAsync(user); var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id); - foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, isPremium)) + var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id); + foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium)) { var upperValue = claim.Value.ToUpperInvariant(); var isBool = upperValue == "TRUE" || upperValue == "FALSE"; diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs index e4100d91b..e8d74da37 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs @@ -5,47 +5,20 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json; +using Bit.Core.Utilities; namespace Bit.Core.Models.Api { - public class OrganizationUserInviteRequestModel : IValidatableObject + public class OrganizationUserInviteRequestModel { [Required] + [EmailAddressList] public IEnumerable Emails { get; set; } [Required] public Enums.OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } public Permissions Permissions { get; set; } public IEnumerable Collections { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (!Emails.Any()) - { - yield return new ValidationResult("An email is required."); - } - - if (Emails.Count() > 20) - { - yield return new ValidationResult("You can only invite up to 20 users at a time."); - } - - var attr = new EmailAddressAttribute(); - for (var i = 0; i < Emails.Count(); i++) - { - var email = Emails.ElementAt(i); - if (!attr.IsValid(email) || email.Contains(" ") || email.Contains("<")) - { - yield return new ValidationResult($"Email #{i + 1} is not valid.", - new string[] { nameof(Emails) }); - } - else if (email.Length > 256) - { - yield return new ValidationResult($"Email #{i + 1} is longer than 256 characters.", - new string[] { nameof(Emails) }); - } - } - } } public class OrganizationUserAcceptRequestModel diff --git a/src/Core/Models/Api/Request/Providers/ProviderOrganizationAddRequestModel.cs b/src/Core/Models/Api/Request/Providers/ProviderOrganizationAddRequestModel.cs new file mode 100644 index 000000000..7499c0b76 --- /dev/null +++ b/src/Core/Models/Api/Request/Providers/ProviderOrganizationAddRequestModel.cs @@ -0,0 +1,14 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class ProviderOrganizationAddRequestModel + { + [Required] + public Guid OrganizationId { get; set; } + + [Required] + public string Key { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/Providers/ProviderSetupRequestModel.cs b/src/Core/Models/Api/Request/Providers/ProviderSetupRequestModel.cs new file mode 100644 index 000000000..dfb70af4f --- /dev/null +++ b/src/Core/Models/Api/Request/Providers/ProviderSetupRequestModel.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Models.Table.Provider; + +namespace Bit.Core.Models.Api +{ + public class ProviderSetupRequestModel + { + [Required] + [StringLength(50)] + public string Name { get; set; } + [StringLength(50)] + public string BusinessName { get; set; } + [Required] + [StringLength(256)] + [EmailAddress] + public string BillingEmail { get; set; } + [Required] + public string Token { get; set; } + [Required] + public string Key { get; set; } + + public virtual Provider ToProvider(Provider provider) + { + provider.Name = Name; + provider.BusinessName = BusinessName; + provider.BillingEmail = BillingEmail; + + return provider; + } + } +} diff --git a/src/Core/Models/Api/Request/Providers/ProviderUserRequestModels.cs b/src/Core/Models/Api/Request/Providers/ProviderUserRequestModels.cs new file mode 100644 index 000000000..f7afa59f6 --- /dev/null +++ b/src/Core/Models/Api/Request/Providers/ProviderUserRequestModels.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Bit.Core.Enums.Provider; +using Bit.Core.Models.Table.Provider; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api +{ + public class ProviderUserInviteRequestModel + { + [Required] + [EmailAddressList] + public IEnumerable Emails { get; set; } + [Required] + public ProviderUserType? Type { get; set; } + } + + public class ProviderUserAcceptRequestModel + { + [Required] + public string Token { get; set; } + } + + public class ProviderUserConfirmRequestModel + { + [Required] + public string Key { get; set; } + } + + public class ProviderUserBulkConfirmRequestModelEntry + { + [Required] + public Guid Id { get; set; } + [Required] + public string Key { get; set; } + } + + public class ProviderUserBulkConfirmRequestModel + { + [Required] + public IEnumerable Keys { get; set; } + + public Dictionary ToDictionary() + { + return Keys.ToDictionary(e => e.Id, e => e.Key); + } + } + + public class ProviderUserUpdateRequestModel + { + [Required] + public ProviderUserType? Type { get; set; } + + public ProviderUser ToProviderUser(ProviderUser existingUser) + { + existingUser.Type = Type.Value; + return existingUser; + } + } + + public class ProviderUserBulkRequestModel + { + [Required] + public IEnumerable Ids { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs index c0e6532ed..e7b563b04 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -34,6 +34,8 @@ namespace Bit.Core.Models.Api Permissions = CoreHelpers.LoadClassFromJsonData(organization.Permissions); ResetPasswordEnrolled = organization.ResetPasswordKey != null; UserId = organization.UserId?.ToString(); + ProviderId = organization.ProviderId?.ToString(); + ProviderName = organization.ProviderName; } public string Id { get; set; } @@ -63,5 +65,7 @@ namespace Bit.Core.Models.Api public bool ResetPasswordEnrolled { get; set; } public string UserId { get; set; } public bool HasPublicAndPrivateKeys { get; set; } + public string ProviderId { get; set; } + public string ProviderName { get; set; } } } diff --git a/src/Core/Models/Api/Response/ProfileResponseModel.cs b/src/Core/Models/Api/Response/ProfileResponseModel.cs index 323087174..59b5ec5a5 100644 --- a/src/Core/Models/Api/Response/ProfileResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileResponseModel.cs @@ -10,8 +10,8 @@ namespace Bit.Core.Models.Api public class ProfileResponseModel : ResponseModel { public ProfileResponseModel(User user, - IEnumerable organizationsUserDetails, bool twoFactorEnabled) - : base("profile") + IEnumerable organizationsUserDetails, + IEnumerable providerUserDetails, bool twoFactorEnabled) : base("profile") { if (user == null) { @@ -30,6 +30,7 @@ namespace Bit.Core.Models.Api PrivateKey = user.PrivateKey; SecurityStamp = user.SecurityStamp; Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o)); + Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); } public string Id { get; set; } @@ -44,5 +45,6 @@ namespace Bit.Core.Models.Api public string PrivateKey { get; set; } public string SecurityStamp { get; set; } public IEnumerable Organizations { get; set; } + public IEnumerable Providers { get; set; } } } diff --git a/src/Core/Models/Api/Response/Providers/ProfileProviderResponseModel.cs b/src/Core/Models/Api/Response/Providers/ProfileProviderResponseModel.cs new file mode 100644 index 000000000..daa7f9d84 --- /dev/null +++ b/src/Core/Models/Api/Response/Providers/ProfileProviderResponseModel.cs @@ -0,0 +1,31 @@ +using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api +{ + public class ProfileProviderResponseModel : ResponseModel + { + public ProfileProviderResponseModel(ProviderUserProviderDetails provider) + : base("profileProvider") + { + Id = provider.ProviderId.ToString(); + Name = provider.Name; + Key = provider.Key; + Status = provider.Status; + Type = provider.Type; + Enabled = provider.Enabled; + Permissions = CoreHelpers.LoadClassFromJsonData(provider.Permissions); + UserId = provider.UserId?.ToString(); + } + + public string Id { get; set; } + public string Name { get; set; } + public string Key { get; set; } + public ProviderUserStatusType Status { get; set; } + public ProviderUserType Type { get; set; } + public bool Enabled { get; set; } + public Permissions Permissions { get; set; } + public string UserId { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/Providers/ProviderOrganizationResponseModel.cs b/src/Core/Models/Api/Response/Providers/ProviderOrganizationResponseModel.cs new file mode 100644 index 000000000..ad47d772e --- /dev/null +++ b/src/Core/Models/Api/Response/Providers/ProviderOrganizationResponseModel.cs @@ -0,0 +1,35 @@ +using System; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Api +{ + public class ProviderOrganizationOrganizationDetailsResponseModel : ResponseModel + { + public ProviderOrganizationOrganizationDetailsResponseModel(ProviderOrganizationOrganizationDetails providerOrganization, + string obj = "providerOrganization") : base(obj) + { + if (providerOrganization == null) + { + throw new ArgumentNullException(nameof(providerOrganization)); + } + + Id = providerOrganization.Id; + ProviderId = providerOrganization.ProviderId; + OrganizationId = providerOrganization.OrganizationId; + OrganizationName = providerOrganization.OrganizationName; + Key = providerOrganization.Key; + Settings = providerOrganization.Settings; + CreationDate = providerOrganization.CreationDate; + RevisionDate = providerOrganization.RevisionDate; + } + + public Guid Id { get; set; } + public Guid ProviderId { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string Key { get; set; } + public string Settings { get; set; } + public DateTime CreationDate { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/Providers/ProviderResponseModel.cs b/src/Core/Models/Api/Response/Providers/ProviderResponseModel.cs new file mode 100644 index 000000000..279abdb74 --- /dev/null +++ b/src/Core/Models/Api/Response/Providers/ProviderResponseModel.cs @@ -0,0 +1,36 @@ +using System; +using Bit.Core.Models.Table.Provider; + +namespace Bit.Core.Models.Api +{ + public class ProviderResponseModel : ResponseModel + { + public ProviderResponseModel(Provider provider, string obj = "provider") : base(obj) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + Id = provider.Id; + Name = provider.Name; + BusinessName = provider.BusinessName; + BusinessAddress1 = provider.BusinessAddress1; + BusinessAddress2 = provider.BusinessAddress2; + BusinessAddress3 = provider.BusinessAddress3; + BusinessCountry = provider.BusinessCountry; + BusinessTaxNumber = provider.BusinessTaxNumber; + BillingEmail = provider.BillingEmail; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public string BusinessName { get; set; } + public string BusinessAddress1 { get; set; } + public string BusinessAddress2 { get; set; } + public string BusinessAddress3 { get; set; } + public string BusinessCountry { get; set; } + public string BusinessTaxNumber { get; set; } + public string BillingEmail { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/Providers/ProviderUserResponseModel.cs b/src/Core/Models/Api/Response/Providers/ProviderUserResponseModel.cs new file mode 100644 index 000000000..df81b1467 --- /dev/null +++ b/src/Core/Models/Api/Response/Providers/ProviderUserResponseModel.cs @@ -0,0 +1,90 @@ +using System; +using Bit.Core.Models.Data; +using Bit.Core.Enums.Provider; +using Bit.Core.Models.Table.Provider; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api +{ + public class ProviderUserResponseModel : ResponseModel + { + public ProviderUserResponseModel(ProviderUser providerUser, string obj = "providerUser") + : base(obj) + { + if (providerUser == null) + { + throw new ArgumentNullException(nameof(providerUser)); + } + + Id = providerUser.Id.ToString(); + UserId = providerUser.UserId?.ToString(); + Type = providerUser.Type; + Status = providerUser.Status; + Permissions = CoreHelpers.LoadClassFromJsonData(providerUser.Permissions); + } + + public ProviderUserResponseModel(ProviderUserUserDetails providerUser, string obj = "providerUser") + : base(obj) + { + if (providerUser == null) + { + throw new ArgumentNullException(nameof(providerUser)); + } + + Id = providerUser.Id.ToString(); + UserId = providerUser.UserId?.ToString(); + Type = providerUser.Type; + Status = providerUser.Status; + Permissions = CoreHelpers.LoadClassFromJsonData(providerUser.Permissions); + } + + public string Id { get; set; } + public string UserId { get; set; } + public ProviderUserType Type { get; set; } + public ProviderUserStatusType Status { get; set; } + public Permissions Permissions { get; set; } + } + + public class ProviderUserUserDetailsResponseModel : ProviderUserResponseModel + { + public ProviderUserUserDetailsResponseModel(ProviderUserUserDetails providerUser, + string obj = "providerUserUserDetails") : base(providerUser, obj) + { + if (providerUser == null) + { + throw new ArgumentNullException(nameof(providerUser)); + } + + Name = providerUser.Name; + Email = providerUser.Email; + } + + public string Name { get; set; } + public string Email { get; set; } + } + + public class ProviderUserPublicKeyResponseModel : ResponseModel + { + public ProviderUserPublicKeyResponseModel(Guid id, string key, + string obj = "providerUserPublicKeyResponseModel") : base(obj) + { + Id = id; + Key = key; + } + + public Guid Id { get; set; } + public string Key { get; set; } + } + + public class ProviderUserBulkResponseModel : ResponseModel + { + public ProviderUserBulkResponseModel(Guid id, string error, + string obj = "providerBulkConfirmResponseModel") : base(obj) + { + Id = id; + Error = error; + } + public Guid Id { get; set; } + public string Error { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/SyncResponseModel.cs b/src/Core/Models/Api/Response/SyncResponseModel.cs index c4d93729a..38a92d42c 100644 --- a/src/Core/Models/Api/Response/SyncResponseModel.cs +++ b/src/Core/Models/Api/Response/SyncResponseModel.cs @@ -15,6 +15,7 @@ namespace Bit.Core.Models.Api User user, bool userTwoFactorEnabled, IEnumerable organizationUserDetails, + IEnumerable providerUserDetails, IEnumerable folders, IEnumerable collections, IEnumerable ciphers, @@ -24,7 +25,7 @@ namespace Bit.Core.Models.Api IEnumerable sends) : base("sync") { - Profile = new ProfileResponseModel(user, organizationUserDetails, userTwoFactorEnabled); + Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, userTwoFactorEnabled); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Collections = collections?.Select( diff --git a/src/Core/Models/Business/Provider/ProviderUserInvite.cs b/src/Core/Models/Business/Provider/ProviderUserInvite.cs index 99f71e709..39bc43348 100644 --- a/src/Core/Models/Business/Provider/ProviderUserInvite.cs +++ b/src/Core/Models/Business/Provider/ProviderUserInvite.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Bit.Core.Enums.Provider; +using Bit.Core.Models.Api; using Bit.Core.Models.Data; namespace Bit.Core.Models.Business.Provider @@ -8,8 +9,11 @@ namespace Bit.Core.Models.Business.Provider { public IEnumerable Emails { get; set; } public ProviderUserType Type { get; set; } - public Permissions Permissions { get; set; } - public ProviderUserInvite() {} + public ProviderUserInvite(ProviderUserInviteRequestModel requestModel) + { + Emails = requestModel.Emails; + Type = requestModel.Type.Value; + } } } diff --git a/src/Core/Models/Data/EventMessage.cs b/src/Core/Models/Data/EventMessage.cs index e6f979e2d..584ca7241 100644 --- a/src/Core/Models/Data/EventMessage.cs +++ b/src/Core/Models/Data/EventMessage.cs @@ -20,11 +20,13 @@ namespace Bit.Core.Models.Data public EventType Type { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? ProviderId { get; set; } public Guid? CipherId { get; set; } public Guid? CollectionId { get; set; } public Guid? GroupId { get; set; } public Guid? PolicyId { get; set; } public Guid? OrganizationUserId { get; set; } + public Guid? ProviderUserId { get; set; } public Guid? ActingUserId { get; set; } public DeviceType? DeviceType { get; set; } public string IpAddress { get; set; } diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs index 1811be029..4e6748ce1 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs @@ -32,5 +32,7 @@ namespace Bit.Core.Models.Data public string ResetPasswordKey { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } + public Guid? ProviderId { get; set; } + public string ProviderName { get; set; } } } diff --git a/src/Core/Models/Data/Provider/ProviderAbility.cs b/src/Core/Models/Data/Provider/ProviderAbility.cs new file mode 100644 index 000000000..f4f1e9802 --- /dev/null +++ b/src/Core/Models/Data/Provider/ProviderAbility.cs @@ -0,0 +1,22 @@ +using System; +using Bit.Core.Models.Table; +using Bit.Core.Models.Table.Provider; + +namespace Bit.Core.Models.Data +{ + public class ProviderAbility + { + public ProviderAbility() { } + + public ProviderAbility(Provider provider) + { + Id = provider.Id; + UseEvents = provider.UseEvents; + Enabled = provider.Enabled; + } + + public Guid Id { get; set; } + public bool UseEvents { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/src/Core/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs b/src/Core/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs new file mode 100644 index 000000000..7c30553b3 --- /dev/null +++ b/src/Core/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs @@ -0,0 +1,17 @@ +using System; +using Bit.Core.Enums.Provider; + +namespace Bit.Core.Models.Data +{ + public class ProviderOrganizationOrganizationDetails + { + public Guid Id { get; set; } + public Guid ProviderId { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string Key { get; set; } + public string Settings { get; set; } + public DateTime CreationDate { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Core/Models/Data/Provider/ProviderUserProviderDetails.cs b/src/Core/Models/Data/Provider/ProviderUserProviderDetails.cs new file mode 100644 index 000000000..b1ef3d45a --- /dev/null +++ b/src/Core/Models/Data/Provider/ProviderUserProviderDetails.cs @@ -0,0 +1,17 @@ +using System; +using Bit.Core.Enums.Provider; + +namespace Bit.Core.Models.Data +{ + public class ProviderUserProviderDetails + { + public Guid ProviderId { get; set; } + public Guid? UserId { get; set; } + public string Name { get; set; } + public string Key { get; set; } + public ProviderUserStatusType Status { get; set; } + public ProviderUserType Type { get; set; } + public bool Enabled { get; set; } + public string Permissions { get; set; } + } +} diff --git a/src/Core/Models/Data/Provider/ProviderUserPublicKey.cs b/src/Core/Models/Data/Provider/ProviderUserPublicKey.cs new file mode 100644 index 000000000..29bc30520 --- /dev/null +++ b/src/Core/Models/Data/Provider/ProviderUserPublicKey.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.Data +{ + public class ProviderUserPublicKey + { + public Guid Id { get; set; } + public string PublicKey { get; set; } + } +} diff --git a/src/Core/Models/Data/Provider/ProviderUserUserDetails.cs b/src/Core/Models/Data/Provider/ProviderUserUserDetails.cs new file mode 100644 index 000000000..dd082b1f7 --- /dev/null +++ b/src/Core/Models/Data/Provider/ProviderUserUserDetails.cs @@ -0,0 +1,17 @@ +using System; +using Bit.Core.Enums.Provider; + +namespace Bit.Core.Models.Data +{ + public class ProviderUserUserDetails + { + public Guid Id { get; set; } + public Guid ProviderId { get; set; } + public Guid? UserId { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public ProviderUserStatusType Status { get; set; } + public ProviderUserType Type { get; set; } + public string Permissions { get; set; } + } +} diff --git a/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs b/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs index ddb44b5bb..daaba8a49 100644 --- a/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs @@ -5,7 +5,7 @@ public string ProviderId { get; set; } public string Email { get; set; } public string Token { get; set; } - public string Url => string.Format("{0}/setup-provider?providerId={1}&email={2}&token={3}", + public string Url => string.Format("{0}/providers/setup-provider?providerId={1}&email={2}&token={3}", WebVaultUrl, ProviderId, Email, diff --git a/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs b/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs index 86231fb7b..964c51759 100644 --- a/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs @@ -8,7 +8,7 @@ public string Email { get; set; } public string ProviderNameUrlEncoded { get; set; } public string Token { get; set; } - public string Url => string.Format("{0}/accept-provider?providerId={1}&" + + public string Url => string.Format("{0}/providers/accept-provider?providerId={1}&" + "providerUserId={2}&email={3}&providerName={4}&token={5}", WebVaultUrl, ProviderId, diff --git a/src/Core/Models/Table/Provider/Provider.cs b/src/Core/Models/Table/Provider/Provider.cs index d6befd833..439156ca0 100644 --- a/src/Core/Models/Table/Provider/Provider.cs +++ b/src/Core/Models/Table/Provider/Provider.cs @@ -16,6 +16,7 @@ namespace Bit.Core.Models.Table.Provider public string BusinessTaxNumber { get; set; } public string BillingEmail { get; set; } public ProviderStatusType Status { get; set; } + public bool UseEvents { get; set; } public bool Enabled { get; set; } = true; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Models/Table/Provider/ProviderUser.cs b/src/Core/Models/Table/Provider/ProviderUser.cs index 1b3d33035..58b95146c 100644 --- a/src/Core/Models/Table/Provider/ProviderUser.cs +++ b/src/Core/Models/Table/Provider/ProviderUser.cs @@ -14,8 +14,8 @@ namespace Bit.Core.Models.Table.Provider public ProviderUserStatusType Status { get; set; } public ProviderUserType Type { get; set; } public string Permissions { get; set; } - public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; - public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; public void SetNewId() { diff --git a/src/Core/Repositories/IProviderOrganizationRepository.cs b/src/Core/Repositories/IProviderOrganizationRepository.cs index 24c548bf7..71ff4cee9 100644 --- a/src/Core/Repositories/IProviderOrganizationRepository.cs +++ b/src/Core/Repositories/IProviderOrganizationRepository.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; namespace Bit.Core.Repositories { - public interface IProviderOrganizationRepository : IRepository + public interface IProviderOrganizationRepository : IRepository { + Task> GetManyDetailsByProviderAsync(Guid providerId); } } diff --git a/src/Core/Repositories/IProviderRepository.cs b/src/Core/Repositories/IProviderRepository.cs index 8eae0dc91..169cfd7ec 100644 --- a/src/Core/Repositories/IProviderRepository.cs +++ b/src/Core/Repositories/IProviderRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; namespace Bit.Core.Repositories @@ -8,5 +9,6 @@ namespace Bit.Core.Repositories public interface IProviderRepository : IRepository { Task> SearchAsync(string name, string userEmail, int skip, int take); + Task> GetManyAbilitiesAsync(); } } diff --git a/src/Core/Repositories/IProviderUserRepository.cs b/src/Core/Repositories/IProviderUserRepository.cs index c1ca180b9..586edbae0 100644 --- a/src/Core/Repositories/IProviderUserRepository.cs +++ b/src/Core/Repositories/IProviderUserRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; namespace Bit.Core.Repositories @@ -10,7 +11,13 @@ namespace Bit.Core.Repositories { Task GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers); Task> GetManyAsync(IEnumerable ids); + Task> GetManyByUserAsync(Guid userId); + Task GetByProviderUserAsync(Guid providerId, Guid userId); Task> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null); + Task> GetManyDetailsByProviderAsync(Guid providerId); + Task> GetManyDetailsByUserAsync(Guid userId, + ProviderUserStatusType? status = null); Task DeleteManyAsync(IEnumerable userIds); + Task> GetManyPublicKeysByProviderUserAsync(Guid providerId, IEnumerable Ids); } } diff --git a/src/Core/Repositories/SqlServer/ProviderOrganizationRepository.cs b/src/Core/Repositories/SqlServer/ProviderOrganizationRepository.cs index 8109c0c79..28464da14 100644 --- a/src/Core/Repositories/SqlServer/ProviderOrganizationRepository.cs +++ b/src/Core/Repositories/SqlServer/ProviderOrganizationRepository.cs @@ -1,10 +1,17 @@ using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; using Bit.Core.Settings; +using Dapper; +using Microsoft.Data.SqlClient; namespace Bit.Core.Repositories.SqlServer { - public class ProviderOrganizationRepository : Repository, IProviderOrganizationRepository + public class ProviderOrganizationRepository : Repository, IProviderOrganizationRepository { public ProviderOrganizationRepository(GlobalSettings globalSettings) : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) @@ -13,5 +20,18 @@ namespace Bit.Core.Repositories.SqlServer public ProviderOrganizationRepository(string connectionString, string readOnlyConnectionString) : base(connectionString, readOnlyConnectionString) { } + + public async Task> GetManyDetailsByProviderAsync(Guid providerId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId]", + new { ProviderId = providerId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } } diff --git a/src/Core/Repositories/SqlServer/ProviderRepository.cs b/src/Core/Repositories/SqlServer/ProviderRepository.cs index 7c4832546..3d27363c1 100644 --- a/src/Core/Repositories/SqlServer/ProviderRepository.cs +++ b/src/Core/Repositories/SqlServer/ProviderRepository.cs @@ -6,6 +6,7 @@ using System.Data; using Dapper; using System.Linq; using System.Collections.Generic; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; using Bit.Core.Settings; @@ -34,5 +35,17 @@ namespace Bit.Core.Repositories.SqlServer return results.ToList(); } } + + public async Task> GetManyAbilitiesAsync() + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[Provider_ReadAbilities]", + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } } diff --git a/src/Core/Repositories/SqlServer/ProviderUserRepository.cs b/src/Core/Repositories/SqlServer/ProviderUserRepository.cs index 16fecc613..077b84601 100644 --- a/src/Core/Repositories/SqlServer/ProviderUserRepository.cs +++ b/src/Core/Repositories/SqlServer/ProviderUserRepository.cs @@ -4,6 +4,7 @@ using System.Data; using System.Linq; using System.Threading.Tasks; using Bit.Core.Enums.Provider; +using Bit.Core.Models.Data; using Bit.Core.Models.Table.Provider; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -48,6 +49,32 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyByUserAsync(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[ProviderUser_ReadByUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + + public async Task GetByProviderUserAsync(Guid providerId, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[ProviderUser_ReadByProviderIdUserId]", + new { ProviderId = providerId, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + public async Task> GetManyByProviderAsync(Guid providerId, ProviderUserType? type) { using (var connection = new SqlConnection(ConnectionString)) @@ -61,6 +88,33 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyDetailsByProviderAsync(Guid providerId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[ProviderUserUserDetails_ReadByProviderId]", + new { ProviderId = providerId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + + public async Task> GetManyDetailsByUserAsync(Guid userId, + ProviderUserStatusType? status = null) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[ProviderUserProviderDetails_ReadByUserIdStatus]", + new { UserId = userId, Status = status }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task DeleteManyAsync(IEnumerable providerUserIds) { using (var connection = new SqlConnection(ConnectionString)) @@ -69,5 +123,19 @@ namespace Bit.Core.Repositories.SqlServer new { Ids = providerUserIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure); } } + + public async Task> GetManyPublicKeysByProviderUserAsync( + Guid providerId, IEnumerable Ids) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[User_ReadPublicKeysByProviderUserIds]", + new { ProviderId = providerId, ProviderUserIds = Ids.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } } diff --git a/src/Core/Services/IApplicationCacheService.cs b/src/Core/Services/IApplicationCacheService.cs index 6fa37d411..39b8c2528 100644 --- a/src/Core/Services/IApplicationCacheService.cs +++ b/src/Core/Services/IApplicationCacheService.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Services public interface IApplicationCacheService { Task> GetOrganizationAbilitiesAsync(); + Task> GetProviderAbilitiesAsync(); Task UpsertOrganizationAbilityAsync(Organization organization); Task DeleteOrganizationAbilityAsync(Guid organizationId); } diff --git a/src/Core/Services/IProviderService.cs b/src/Core/Services/IProviderService.cs index 897ac026c..3df32455c 100644 --- a/src/Core/Services/IProviderService.cs +++ b/src/Core/Services/IProviderService.cs @@ -10,7 +10,7 @@ namespace Bit.Core.Services public interface IProviderService { Task CreateAsync(string ownerEmail); - Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key); + Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key); Task UpdateAsync(Provider provider, bool updateBilling = false); Task> InviteUserAsync(Guid providerId, Guid invitingUserId, ProviderUserInvite providerUserInvite); diff --git a/src/Core/Services/Implementations/EventService.cs b/src/Core/Services/Implementations/EventService.cs index ce3763fc3..18c870fba 100644 --- a/src/Core/Services/Implementations/EventService.cs +++ b/src/Core/Services/Implementations/EventService.cs @@ -223,16 +223,45 @@ namespace Bit.Core.Services await _eventWriteService.CreateAsync(e); } - // TODO: Implement this - public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null) => throw new NotImplementedException(); + public async Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null) + { + await LogProviderUsersEventAsync(new[] { (providerUser, type, date) }); + } - // TODO: Implement this - public Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events) => throw new NotImplementedException(); + public async Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events) + { + var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync(); + var eventMessages = new List(); + foreach (var (providerUser, type, date) in events) + { + if (!CanUseProviderEvents(providerAbilities, providerUser.ProviderId)) + { + continue; + } + eventMessages.Add(new EventMessage + { + ProviderId = providerUser.ProviderId, + UserId = providerUser.UserId, + ProviderUserId = providerUser.Id, + Type = type, + ActingUserId = _currentContext?.UserId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }); + } + + await _eventWriteService.CreateManyAsync(eventMessages); + } private bool CanUseEvents(IDictionary orgAbilities, Guid orgId) { return orgAbilities != null && orgAbilities.ContainsKey(orgId) && orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents; } + + private bool CanUseProviderEvents(IDictionary providerAbilities, Guid providerId) + { + return providerAbilities != null && providerAbilities.ContainsKey(providerId) && + providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents; + } } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index c52ad8163..92b5e678c 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -675,6 +675,7 @@ namespace Bit.Core.Services ProviderId = providerUser.ProviderId.ToString(), ProviderUserId = providerUser.Id.ToString(), ProviderNameUrlEncoded = WebUtility.UrlEncode(providerName), + Token = token, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, }; diff --git a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs index 0c61b1fb4..f90f1d566 100644 --- a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs @@ -11,14 +11,18 @@ namespace Bit.Core.Services public class InMemoryApplicationCacheService : IApplicationCacheService { private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderRepository _providerRepository; private DateTime _lastOrgAbilityRefresh = DateTime.MinValue; private IDictionary _orgAbilities; private TimeSpan _orgAbilitiesRefreshInterval = TimeSpan.FromMinutes(10); + private IDictionary _providerAbilities; + public InMemoryApplicationCacheService( - IOrganizationRepository organizationRepository) + IOrganizationRepository organizationRepository, IProviderRepository providerRepository) { _organizationRepository = organizationRepository; + _providerRepository = providerRepository; } public virtual async Task> GetOrganizationAbilitiesAsync() @@ -27,6 +31,12 @@ namespace Bit.Core.Services return _orgAbilities; } + public virtual async Task> GetProviderAbilitiesAsync() + { + await InitProviderAbilitiesAsync(); + return _providerAbilities; + } + public virtual async Task UpsertOrganizationAbilityAsync(Organization organization) { await InitOrganizationAbilitiesAsync(); @@ -62,5 +72,16 @@ namespace Bit.Core.Services _lastOrgAbilityRefresh = now; } } + + private async Task InitProviderAbilitiesAsync() + { + var now = DateTime.UtcNow; + if (_providerAbilities == null || (now - _lastOrgAbilityRefresh) > _orgAbilitiesRefreshInterval) + { + var abilities = await _providerRepository.GetManyAbilitiesAsync(); + _providerAbilities = abilities.ToDictionary(a => a.Id); + _lastOrgAbilityRefresh = now; + } + } } } diff --git a/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs index 5df132d26..6a28a9077 100644 --- a/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs @@ -16,8 +16,9 @@ namespace Bit.Core.Services public InMemoryServiceBusApplicationCacheService( IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, GlobalSettings globalSettings) - : base(organizationRepository) + : base(organizationRepository, providerRepository) { _subName = CoreHelpers.GetApplicationCacheServiceBusSubcriptionName(globalSettings); _topicClient = new TopicClient(globalSettings.ServiceBus.ConnectionString, diff --git a/src/Core/Services/NoopImplementations/NoopProviderService.cs b/src/Core/Services/NoopImplementations/NoopProviderService.cs new file mode 100644 index 000000000..1fce11708 --- /dev/null +++ b/src/Core/Services/NoopImplementations/NoopProviderService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Business.Provider; +using Bit.Core.Models.Table; +using Bit.Core.Models.Table.Provider; + +namespace Bit.Core.Services +{ + public class NoopProviderService : IProviderService + { + public Task CreateAsync(string ownerEmail) => throw new NotImplementedException(); + + public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key) => throw new NotImplementedException(); + + public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException(); + + public Task> InviteUserAsync(Guid providerId, Guid invitingUserId, ProviderUserInvite providerUserInvite) => throw new NotImplementedException(); + + public Task>> ResendInvitesAsync(Guid providerId, Guid invitingUserId, IEnumerable providerUsersId) => throw new NotImplementedException(); + + public Task AcceptUserAsync(Guid providerUserId, User user, string token) => throw new NotImplementedException(); + + public Task>> ConfirmUsersAsync(Guid providerId, Dictionary keys, Guid confirmingUserId) => throw new NotImplementedException(); + + public Task SaveUserAsync(ProviderUser user, Guid savingUserId) => throw new NotImplementedException(); + + public Task>> DeleteUsersAsync(Guid providerId, IEnumerable providerUserIds, Guid deletingUserId) => throw new NotImplementedException(); + + public Task AddOrganization(Guid providerId, Guid organizationId, Guid addingUserId, string key) => throw new NotImplementedException(); + + public Task RemoveOrganization(Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException(); + } +} diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 99db9894c..c00703702 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -23,6 +23,7 @@ using Microsoft.Azure.Storage.Blob; using Bit.Core.Models.Table; using IdentityModel; using System.Text.Json; +using Bit.Core.Enums.Provider; namespace Bit.Core.Utilities { @@ -737,7 +738,8 @@ namespace Bit.Core.Utilities return configDict; } - public static List> BuildIdentityClaims(User user, ICollection orgs, bool isPremium) + public static List> BuildIdentityClaims(User user, ICollection orgs, + ICollection providers, bool isPremium) { var claims = new List>() { @@ -849,6 +851,29 @@ namespace Bit.Core.Utilities } } } + + if (providers.Any()) + { + foreach (var group in providers.GroupBy(o => o.Type)) + { + switch (group.Key) + { + case ProviderUserType.ProviderAdmin: + foreach (var provider in group) + { + claims.Add(new KeyValuePair("providerprovideradmin", provider.Id.ToString())); + } + break; + case ProviderUserType.ServiceUser: + foreach (var provider in group) + { + claims.Add(new KeyValuePair("providerserviceuser", provider.Id.ToString())); + } + break; + } + } + } + return claims; } diff --git a/src/Core/Utilities/EmailAddressListAttribute.cs b/src/Core/Utilities/EmailAddressListAttribute.cs new file mode 100644 index 000000000..62f260302 --- /dev/null +++ b/src/Core/Utilities/EmailAddressListAttribute.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Bit.Core.Utilities +{ + public class EmailAddressListAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var emailAttribute = new EmailAddressAttribute(); + var emails = value as IList; + + if (!emails?.Any() ?? true) + { + return new ValidationResult("An email is required."); + } + + if (emails.Count() > 20) + { + return new ValidationResult("You can only submit up to 20 emails at a time."); + } + + for (var i = 0; i < emails.Count(); i++) + { + var email = emails.ElementAt(i); + if (!emailAttribute.IsValid(email) || email.Contains(" ") || email.Contains("<")) + { + return new ValidationResult($"Email #{i + 1} is not valid."); + } + + if (email.Length > 256) + { + return new ValidationResult($"Email #{i + 1} is longer than 256 characters."); + } + } + + return ValidationResult.Success; + } + } +} diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 4c22b2bc1..c5b9bd717 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -126,7 +126,6 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddScoped(); - services.AddScoped(); } public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) @@ -265,6 +264,11 @@ namespace Bit.Core.Utilities } } + public static void AddOosServices(this IServiceCollection services) + { + services.AddScoped(); + } + public static void AddNoopServices(this IServiceCollection services) { services.AddSingleton(); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 174fbbc2e..55834732f 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -74,6 +74,7 @@ + @@ -90,6 +91,8 @@ + + @@ -332,6 +335,7 @@ + @@ -343,12 +347,17 @@ + + + + + diff --git a/src/Sql/dbo/Stored Procedures/ProviderOrganizationOrganizationDetails_ReadByProviderId.sql b/src/Sql/dbo/Stored Procedures/ProviderOrganizationOrganizationDetails_ReadByProviderId.sql new file mode 100644 index 000000000..02b6e22e1 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ProviderOrganizationOrganizationDetails_ReadByProviderId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId] + @ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderOrganizationOrganizationDetailsView] + WHERE + [ProviderId] = @ProviderId +END diff --git a/src/Sql/dbo/Stored Procedures/ProviderUserProviderDetails_ReadByUserIdStatus.sql b/src/Sql/dbo/Stored Procedures/ProviderUserProviderDetails_ReadByUserIdStatus.sql new file mode 100644 index 000000000..605445e93 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ProviderUserProviderDetails_ReadByUserIdStatus.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[ProviderUserProviderDetails_ReadByUserIdStatus] + @UserId UNIQUEIDENTIFIER, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserProviderDetailsView] + WHERE + [UserId] = @UserId + AND (@Status IS NULL OR [Status] = @Status) +END diff --git a/src/Sql/dbo/Stored Procedures/ProviderUserUserDetails_ReadByProviderId.sql b/src/Sql/dbo/Stored Procedures/ProviderUserUserDetails_ReadByProviderId.sql new file mode 100644 index 000000000..cb9ce1da6 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ProviderUserUserDetails_ReadByProviderId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId] + @ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserUserDetailsView] + WHERE + [ProviderId] = @ProviderId +END diff --git a/src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql index bea9d5c12..71c5bf93b 100644 --- a/src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql +++ b/src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql @@ -28,7 +28,7 @@ BEGIN BEGIN BEGIN TRANSACTION ProviderUser_DeleteMany_PUs - DELETE TOP(@BatchSize) OU + DELETE TOP(@BatchSize) PU FROM [dbo].[ProviderUser] PU INNER JOIN diff --git a/src/Sql/dbo/Stored Procedures/ProviderUser_ReadByProviderIdUserId.sql b/src/Sql/dbo/Stored Procedures/ProviderUser_ReadByProviderIdUserId.sql new file mode 100644 index 000000000..977e75e3e --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ProviderUser_ReadByProviderIdUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderIdUserId] + @ProviderId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserView] + WHERE + [ProviderId] = @ProviderId + AND [UserId] = @UserId +END diff --git a/src/Sql/dbo/Stored Procedures/Provider_Create.sql b/src/Sql/dbo/Stored Procedures/Provider_Create.sql index 0ea1cad6f..6cf46cae1 100644 --- a/src/Sql/dbo/Stored Procedures/Provider_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Provider_Create.sql @@ -9,6 +9,7 @@ @BusinessTaxNumber NVARCHAR(30), @BillingEmail NVARCHAR(256), @Status TINYINT, + @UseEvents BIT, @Enabled BIT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) @@ -28,6 +29,7 @@ BEGIN [BusinessTaxNumber], [BillingEmail], [Status], + [UseEvents], [Enabled], [CreationDate], [RevisionDate] @@ -44,6 +46,7 @@ BEGIN @BusinessTaxNumber, @BillingEmail, @Status, + @UseEvents, @Enabled, @CreationDate, @RevisionDate diff --git a/src/Sql/dbo/Stored Procedures/Provider_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Provider_ReadAbilities.sql new file mode 100644 index 000000000..cbb978cb5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Provider_ReadAbilities.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[Provider_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Enabled] + FROM + [dbo].[Provider] +END diff --git a/src/Sql/dbo/Stored Procedures/Provider_Update.sql b/src/Sql/dbo/Stored Procedures/Provider_Update.sql index 3b2551a63..309c09f08 100644 --- a/src/Sql/dbo/Stored Procedures/Provider_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Provider_Update.sql @@ -9,6 +9,7 @@ @BusinessTaxNumber NVARCHAR(30), @BillingEmail NVARCHAR(256), @Status TINYINT, + @UseEvents BIT, @Enabled BIT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) @@ -28,6 +29,7 @@ BEGIN [BusinessTaxNumber] = @BusinessTaxNumber, [BillingEmail] = @BillingEmail, [Status] = @Status, + [UseEvents] = @UseEvents, [Enabled] = @Enabled, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate diff --git a/src/Sql/dbo/Stored Procedures/User_ReadPublicKeysByProviderUserIds.sql b/src/Sql/dbo/Stored Procedures/User_ReadPublicKeysByProviderUserIds.sql new file mode 100644 index 000000000..b276d053e --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/User_ReadPublicKeysByProviderUserIds.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[User_ReadPublicKeysByProviderUserIds] + @ProviderId UNIQUEIDENTIFIER, + @ProviderUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + PU.[Id], + U.[PublicKey] + FROM + @ProviderUserIds PUIDs + INNER JOIN + [dbo].[ProviderUser] PU ON PUIDs.Id = PU.Id AND PU.[Status] = 1 -- Accepted + INNER JOIN + [dbo].[User] U ON PU.UserId = U.Id + WHERE + PU.ProviderId = @ProviderId +END diff --git a/src/Sql/dbo/Tables/Provider.sql b/src/Sql/dbo/Tables/Provider.sql index 5f60cfb6c..8185971bd 100644 --- a/src/Sql/dbo/Tables/Provider.sql +++ b/src/Sql/dbo/Tables/Provider.sql @@ -9,6 +9,7 @@ [BusinessTaxNumber] NVARCHAR (30) NULL, [BillingEmail] NVARCHAR (256) NULL, [Status] TINYINT NOT NULL, + [UseEvents] BIT NOT NULL, [Enabled] BIT NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index e4354fb5d..3f27be2a4 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -27,10 +27,16 @@ SELECT OU.[Status], OU.[Type], SU.[ExternalId] SsoExternalId, - OU.[Permissions] + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName FROM [dbo].[OrganizationUser] OU INNER JOIN [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] LEFT JOIN [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] diff --git a/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql new file mode 100644 index 000000000..8f9dd88fe --- /dev/null +++ b/src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql @@ -0,0 +1,15 @@ +CREATE VIEW [dbo].[ProviderOrganizationOrganizationDetailsView] +AS +SELECT + PO.[Id], + PO.[ProviderId], + PO.[OrganizationId], + O.[Name] OrganizationName, + PO.[Key], + PO.[Settings], + PO.[CreationDate], + PO.[RevisionDate] +FROM + [dbo].[ProviderOrganization] PO +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] diff --git a/src/Sql/dbo/Views/ProviderUserProviderDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderDetailsView.sql new file mode 100644 index 000000000..4d4a6e499 --- /dev/null +++ b/src/Sql/dbo/Views/ProviderUserProviderDetailsView.sql @@ -0,0 +1,15 @@ +CREATE VIEW [dbo].[ProviderUserProviderDetailsView] +AS +SELECT + PU.[UserId], + PU.[ProviderId], + P.[Name], + PU.[Key], + PU.[Status], + PU.[Type], + P.[Enabled], + PU.[Permissions] +FROM + [dbo].[ProviderUser] PU +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] diff --git a/src/Sql/dbo/Views/ProviderUserUserDetailsView.sql b/src/Sql/dbo/Views/ProviderUserUserDetailsView.sql new file mode 100644 index 000000000..4aa375b60 --- /dev/null +++ b/src/Sql/dbo/Views/ProviderUserUserDetailsView.sql @@ -0,0 +1,15 @@ +CREATE VIEW [dbo].[ProviderUserUserDetailsView] +AS +SELECT + PU.[Id], + PU.[UserId], + PU.[ProviderId], + U.[Name], + ISNULL(U.[Email], PU.[Email]) Email, + PU.[Status], + PU.[Type], + PU.[Permissions] +FROM + [dbo].[ProviderUser] PU +LEFT JOIN + [dbo].[User] U ON U.[Id] = PU.[UserId] diff --git a/test/Api.Test/Controllers/AccountsControllerTests.cs b/test/Api.Test/Controllers/AccountsControllerTests.cs index ff75fd51e..2d12bc3ba 100644 --- a/test/Api.Test/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Controllers/AccountsControllerTests.cs @@ -30,6 +30,7 @@ namespace Bit.Api.Test.Controllers private readonly ISsoUserRepository _ssoUserRepository; private readonly IUserRepository _userRepository; private readonly IUserService _userService; + private readonly IProviderUserRepository _providerUserRepository; public AccountsControllerTests() { @@ -39,6 +40,7 @@ namespace Bit.Api.Test.Controllers _folderRepository = Substitute.For(); _organizationService = Substitute.For(); _organizationUserRepository = Substitute.For(); + _providerUserRepository = Substitute.For(); _paymentService = Substitute.For(); _globalSettings = new GlobalSettings(); _sut = new AccountsController( @@ -47,6 +49,7 @@ namespace Bit.Api.Test.Controllers _folderRepository, _organizationService, _organizationUserRepository, + _providerUserRepository, _paymentService, _ssoUserRepository, _userRepository, diff --git a/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs b/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs index 66a8c1741..ac2e81c1a 100644 --- a/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs +++ b/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs @@ -5,7 +5,7 @@ using AutoFixture.Xunit2; namespace Bit.Core.Test.AutoFixture.Attributes { - internal class CustomAutoDataAttribute : AutoDataAttribute + public class CustomAutoDataAttribute : AutoDataAttribute { public CustomAutoDataAttribute(params Type[] iCustomizationTypes) : this(iCustomizationTypes .Select(t => (ICustomization)Activator.CreateInstance(t)).ToArray()) diff --git a/test/Core.Test/Services/InMemoryApplicationCacheServiceTests.cs b/test/Core.Test/Services/InMemoryApplicationCacheServiceTests.cs index 63ba7a128..97c609d2a 100644 --- a/test/Core.Test/Services/InMemoryApplicationCacheServiceTests.cs +++ b/test/Core.Test/Services/InMemoryApplicationCacheServiceTests.cs @@ -11,12 +11,14 @@ namespace Bit.Core.Test.Services private readonly InMemoryApplicationCacheService _sut; private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderRepository _providerRepository; public InMemoryApplicationCacheServiceTests() { _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); - _sut = new InMemoryApplicationCacheService(_organizationRepository); + _sut = new InMemoryApplicationCacheService(_organizationRepository, _providerRepository); } // Remove this test when we add actual tests. It only proves that diff --git a/test/Core.Test/Services/InMemoryServiceBusApplicationCacheServiceTests.cs b/test/Core.Test/Services/InMemoryServiceBusApplicationCacheServiceTests.cs index 090802d8e..17ff55c03 100644 --- a/test/Core.Test/Services/InMemoryServiceBusApplicationCacheServiceTests.cs +++ b/test/Core.Test/Services/InMemoryServiceBusApplicationCacheServiceTests.cs @@ -12,15 +12,18 @@ namespace Bit.Core.Test.Services private readonly InMemoryServiceBusApplicationCacheService _sut; private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderRepository _providerRepository; private readonly GlobalSettings _globalSettings; public InMemoryServiceBusApplicationCacheServiceTests() { _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); _globalSettings = new GlobalSettings(); _sut = new InMemoryServiceBusApplicationCacheService( _organizationRepository, + _providerRepository, _globalSettings ); } diff --git a/util/Migrator/DbScripts/2021-05-26_00_Provider.sql b/util/Migrator/DbScripts/2021-06-04_00_Provider.sql similarity index 78% rename from util/Migrator/DbScripts/2021-05-26_00_Provider.sql rename to util/Migrator/DbScripts/2021-06-04_00_Provider.sql index 97277e6d7..239a603a0 100644 --- a/util/Migrator/DbScripts/2021-05-26_00_Provider.sql +++ b/util/Migrator/DbScripts/2021-06-04_00_Provider.sql @@ -87,6 +87,7 @@ BEGIN [BusinessTaxNumber] NVARCHAR (30) NULL, [BillingEmail] NVARCHAR (256) NOT NULL, [Status] TINYINT NOT NULL, + [UseEvents] BIT NOT NULL, [Enabled] BIT NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, @@ -101,6 +102,29 @@ GO ALTER TABLE [dbo].[Provider] ALTER COLUMN [BillingEmail] NVARCHAR (256) NULL; GO +IF COL_LENGTH('[dbo].[Provider]', 'UseEvents') IS NULL + BEGIN + ALTER TABLE + [dbo].[Provider] + ADD + [UseEvents] BIT NULL + END +GO + +UPDATE + [dbo].[Provider] +SET + [UseEvents] = 0 +WHERE + [UseEvents] IS NULL +GO + +ALTER TABLE + [dbo].[Provider] +ALTER COLUMN + [UseEvents] BIT NOT NULL +GO + IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'ProviderView') BEGIN DROP VIEW [dbo].[ProviderView]; @@ -132,6 +156,7 @@ CREATE PROCEDURE [dbo].[Provider_Create] @BusinessTaxNumber NVARCHAR(30), @BillingEmail NVARCHAR(256), @Status TINYINT, + @UseEvents BIT, @Enabled BIT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) @@ -151,6 +176,7 @@ BEGIN [BusinessTaxNumber], [BillingEmail], [Status], + [UseEvents], [Enabled], [CreationDate], [RevisionDate] @@ -167,6 +193,7 @@ BEGIN @BusinessTaxNumber, @BillingEmail, @Status, + @UseEvents, @Enabled, @CreationDate, @RevisionDate @@ -191,6 +218,7 @@ CREATE PROCEDURE [dbo].[Provider_Update] @BusinessTaxNumber NVARCHAR(30), @BillingEmail NVARCHAR(256), @Status TINYINT, + @UseEvents BIT, @Enabled BIT, @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) @@ -210,6 +238,7 @@ BEGIN [BusinessTaxNumber] = @BusinessTaxNumber, [BillingEmail] = @BillingEmail, [Status] = @Status, + [UseEvents] = @UseEvents, [Enabled] = @Enabled, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate @@ -923,7 +952,7 @@ BEGIN BEGIN BEGIN TRANSACTION ProviderUser_DeleteMany_PUs - DELETE TOP(@BatchSize) OU + DELETE TOP(@BatchSize) PU FROM [dbo].[ProviderUser] PU INNER JOIN @@ -984,3 +1013,256 @@ BEGIN END END GO + +IF OBJECT_ID('[dbo].[ProviderUser_ReadByProviderIdUserId]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[ProviderUser_ReadByProviderIdUserId] + END +GO + +CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderIdUserId] + @ProviderId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserView] + WHERE + [ProviderId] = @ProviderId + AND [UserId] = @UserId +END +GO + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'ProviderUserUserDetailsView') + BEGIN + DROP VIEW [dbo].[ProviderUserUserDetailsView]; + END +GO + +CREATE VIEW [dbo].[ProviderUserUserDetailsView] +AS +SELECT + PU.[Id], + PU.[UserId], + PU.[ProviderId], + U.[Name], + ISNULL(U.[Email], PU.[Email]) Email, + PU.[Status], + PU.[Type], + PU.[Permissions] +FROM + [dbo].[ProviderUser] PU +LEFT JOIN + [dbo].[User] U ON U.[Id] = PU.[UserId] +GO + +IF OBJECT_ID('[dbo].[ProviderUserUserDetails_ReadByProviderId]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId] + END +GO + +CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId] +@ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserUserDetailsView] + WHERE + [ProviderId] = @ProviderId +END +GO + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'ProviderUserProviderDetailsView') + BEGIN + DROP VIEW [dbo].[ProviderUserProviderDetailsView]; + END +GO + +CREATE VIEW [dbo].[ProviderUserProviderDetailsView] +AS +SELECT + PU.[UserId], + PU.[ProviderId], + P.[Name], + PU.[Key], + PU.[Status], + PU.[Type], + P.[Enabled], + PU.[Permissions] +FROM + [dbo].[ProviderUser] PU +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +GO + +IF OBJECT_ID('[dbo].[ProviderUserProviderDetails_ReadByUserIdStatus]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[ProviderUserProviderDetails_ReadByUserIdStatus] + END +GO + +CREATE PROCEDURE [dbo].[ProviderUserProviderDetails_ReadByUserIdStatus] + @UserId UNIQUEIDENTIFIER, + @Status TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderUserProviderDetailsView] + WHERE + [UserId] = @UserId + AND (@Status IS NULL OR [Status] = @Status) +END +GO + +IF OBJECT_ID('[dbo].[Provider_ReadAbilities]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[Provider_ReadAbilities] + END +GO + +CREATE PROCEDURE [dbo].[Provider_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Enabled] + FROM + [dbo].[Provider] +END +GO + +IF OBJECT_ID('[dbo].[User_ReadPublicKeysByProviderUserIds]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[User_ReadPublicKeysByProviderUserIds] + END +GO + +CREATE PROCEDURE [dbo].[User_ReadPublicKeysByProviderUserIds] + @ProviderId UNIQUEIDENTIFIER, + @ProviderUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + PU.[Id], + U.[PublicKey] + FROM + @ProviderUserIds PUIDs + INNER JOIN + [dbo].[ProviderUser] PU ON PUIDs.Id = PU.Id AND PU.[Status] = 1 -- Accepted + INNER JOIN + [dbo].[User] U ON PU.UserId = U.Id + WHERE + PU.ProviderId = @ProviderId +END +GO + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'ProviderOrganizationOrganizationDetailsView') + BEGIN + DROP VIEW [dbo].[ProviderOrganizationOrganizationDetailsView]; + END +GO + +CREATE VIEW [dbo].[ProviderOrganizationOrganizationDetailsView] +AS +SELECT + PO.[Id], + PO.[ProviderId], + PO.[OrganizationId], + O.[Name] OrganizationName, + PO.[Key], + PO.[Settings], + PO.[CreationDate], + PO.[RevisionDate] +FROM + [dbo].[ProviderOrganization] PO +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +GO + +IF OBJECT_ID('[dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId] + END +GO + +CREATE PROCEDURE [dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId] + @ProviderId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[ProviderOrganizationOrganizationDetailsView] + WHERE + [ProviderId] = @ProviderId +END +GO + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView') + BEGIN + DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView]; + END +GO + +CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName +FROM + [dbo].[OrganizationUser] OU +INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId]