mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Merge branch 'main' into pm-21265-remove-flags
This commit is contained in:
@ -8,7 +8,8 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -16,7 +16,9 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
|
@ -1,13 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public MaxProjectsQuery(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProjectRepository projectRepository,
|
||||
IGlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_licensingService = licensingService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)
|
||||
{
|
||||
// "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted.
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (planType, maxProjects) = await GetPlanTypeAndMaxProjectsAsync(org);
|
||||
var plan = await _pricingClient.GetPlan(org.PlanType);
|
||||
|
||||
if (planType != PlanType.Free)
|
||||
if (plan is not { SecretsManager: not null, Type: PlanType.Free })
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (maxProjects, false));
|
||||
}
|
||||
|
||||
private async Task<(PlanType planType, int maxProjects)> GetPlanTypeAndMaxProjectsAsync(Organization organization)
|
||||
{
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
var license = await _licensingService.ReadOrganizationLicenseAsync(organization);
|
||||
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("License not found.");
|
||||
}
|
||||
|
||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
var maxProjects = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmMaxProjects);
|
||||
|
||||
if (!maxProjects.HasValue)
|
||||
{
|
||||
throw new BadRequestException("License does not contain a value for max Secrets Manager projects");
|
||||
}
|
||||
|
||||
var planType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType);
|
||||
return (planType, maxProjects.Value);
|
||||
}
|
||||
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
|
||||
if (plan is { SupportsSecretsManager: true })
|
||||
{
|
||||
return (plan.Type, plan.SecretsManager.MaxProjects);
|
||||
}
|
||||
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -17,6 +17,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
@ -1,14 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
||||
[SutProviderCustomize]
|
||||
public class MaxProjectsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
|
||||
|
||||
Assert.Null(max);
|
||||
Assert.Null(overMax);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
|
||||
@ -35,54 +46,6 @@ public class MaxProjectsQueryTests
|
||||
.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||
[BitAutoData(PlanType.Custom)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest(
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var license = new OrganizationLicense();
|
||||
var claimsPrincipal = new ClaimsPrincipal();
|
||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||
@ -97,57 +60,16 @@ public class MaxProjectsQueryTests
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
||||
|
||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||
|
||||
Assert.Null(limit);
|
||||
Assert.Null(overLimit);
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2020)]
|
||||
[BitAutoData(PlanType.TeamsMonthly)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2020)]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
[BitAutoData(PlanType.TeamsStarter)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2020)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2020)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var license = new OrganizationLicense();
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
|
||||
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
|
||||
var claimsPrincipal = new ClaimsPrincipal(identity);
|
||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
|
||||
|
||||
@ -183,7 +105,7 @@ public class MaxProjectsQueryTests
|
||||
[BitAutoData(PlanType.Free, 3, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 4, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 40, 4, true)]
|
||||
public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
||||
public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
@ -191,66 +113,8 @@ public class MaxProjectsQueryTests
|
||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(projects);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType).Returns(plan);
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||
|
||||
Assert.NotNull(max);
|
||||
Assert.NotNull(overMax);
|
||||
Assert.Equal(3, max.Value);
|
||||
Assert.Equal(expectedOverMax, overMax);
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().Received(1)
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.Free, 0, 1, false)]
|
||||
[BitAutoData(PlanType.Free, 1, 1, false)]
|
||||
[BitAutoData(PlanType.Free, 2, 1, false)]
|
||||
[BitAutoData(PlanType.Free, 3, 1, true)]
|
||||
[BitAutoData(PlanType.Free, 4, 1, true)]
|
||||
[BitAutoData(PlanType.Free, 40, 1, true)]
|
||||
[BitAutoData(PlanType.Free, 0, 2, false)]
|
||||
[BitAutoData(PlanType.Free, 1, 2, false)]
|
||||
[BitAutoData(PlanType.Free, 2, 2, true)]
|
||||
[BitAutoData(PlanType.Free, 3, 2, true)]
|
||||
[BitAutoData(PlanType.Free, 4, 2, true)]
|
||||
[BitAutoData(PlanType.Free, 40, 2, true)]
|
||||
[BitAutoData(PlanType.Free, 0, 3, false)]
|
||||
[BitAutoData(PlanType.Free, 1, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 2, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 3, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 4, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 40, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 0, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 1, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 2, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 3, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 4, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 40, 4, true)]
|
||||
public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(projects);
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var license = new OrganizationLicense();
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()),
|
||||
new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString())
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, "TestAuthenticationType");
|
||||
var claimsPrincipal = new ClaimsPrincipal(identity);
|
||||
sutProvider.GetDependency<ILicensingService>().ReadOrganizationLicenseAsync(organization).Returns(license);
|
||||
sutProvider.GetDependency<ILicensingService>().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
"rollForward": "latestFeature"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "4.1.0"
|
||||
"Microsoft.Build.Traversal": "4.1.0",
|
||||
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||
}
|
||||
|
||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
|
||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
@ -178,12 +177,4 @@ public class UsersController : Controller
|
||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
// TODO: Feature flag to be removed in PM-14207
|
||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||
{
|
||||
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller
|
||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
@ -760,11 +758,6 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||
}
|
||||
|
||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||
return usersOrganizationClaimedStatus;
|
||||
}
|
||||
|
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
{
|
||||
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg)
|
||||
if (policy.Type is PolicyType.SingleOrg)
|
||||
{
|
||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||
}
|
||||
|
@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
|
||||
public string InitiationPath { get; set; }
|
||||
|
||||
public bool SkipTrial { get; set; }
|
||||
|
||||
public virtual OrganizationSignup ToOrganizationSignup(User user)
|
||||
{
|
||||
var orgSignup = new OrganizationSignup
|
||||
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
BillingAddressCountry = BillingAddressCountry,
|
||||
},
|
||||
InitiationPath = InitiationPath,
|
||||
SkipTrial = SkipTrial
|
||||
};
|
||||
|
||||
Keys?.ToOrganizationSignup(orgSignup);
|
||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
@ -135,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
/// Obsolete.
|
||||
///
|
||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||
/// </summary>
|
||||
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
||||
@ -145,16 +145,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
set => UserIsClaimedByOrganization = value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates if the organization claims the user.
|
||||
/// Indicates if the user is claimed by the organization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it.
|
||||
/// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -518,9 +518,8 @@ public class AccountsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -9,6 +9,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -207,7 +207,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[HttpDelete("{sponsoringOrganizationId}")]
|
||||
[HttpPost("{sponsoringOrganizationId}/delete")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
|
||||
public async Task RevokeSponsorship(Guid sponsoringOrganizationId, [FromQuery] bool isAdminInitiated = false)
|
||||
{
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);
|
||||
@ -217,7 +217,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
var existingOrgSponsorship = await _organizationSponsorshipRepository
|
||||
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id);
|
||||
.GetBySponsoringOrganizationUserIdAsync(orgUser.Id, isAdminInitiated);
|
||||
|
||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||
}
|
||||
@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
|
||||
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||
sponsorships
|
||||
.Where(s => s.IsAdminInitiated)
|
||||
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Services;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
36
src/Api/Billing/Controllers/TaxController.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Core.Billing.Tax.Commands;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("tax")]
|
||||
public class TaxController(
|
||||
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
|
||||
{
|
||||
[HttpPost("preview-amount/organization-trial")]
|
||||
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
|
||||
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
|
||||
{
|
||||
var parameters = new OrganizationTrialParameters
|
||||
{
|
||||
PlanType = requestBody.PlanType,
|
||||
ProductType = requestBody.ProductType,
|
||||
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
|
||||
{
|
||||
Country = requestBody.TaxInformation.Country,
|
||||
PostalCode = requestBody.TaxInformation.PostalCode,
|
||||
TaxId = requestBody.TaxInformation.TaxId
|
||||
}
|
||||
};
|
||||
|
||||
var result = await previewTaxAmountCommand.Run(parameters);
|
||||
|
||||
return result.Match<IResult>(
|
||||
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
|
||||
badRequest => Error.BadRequest(badRequest.TranslationKey),
|
||||
unhandled => Error.ServerError(unhandled.TranslationKey));
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class PreviewTaxAmountForOrganizationTrialRequestBody
|
||||
{
|
||||
[Required]
|
||||
public PlanType PlanType { get; set; }
|
||||
|
||||
[Required]
|
||||
public ProductType ProductType { get; set; }
|
||||
|
||||
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
|
||||
|
||||
public class TaxInformationDTO
|
||||
{
|
||||
[Required]
|
||||
public string Country { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public string PostalCode { get; set; } = null!;
|
||||
|
||||
public string? TaxId { get; set; }
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
|
@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response;
|
||||
|
||||
public class MemberCipherDetailsResponseModel
|
||||
{
|
||||
public Guid? UserGuid { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// the organization member
|
||||
/// </summary>
|
||||
public IEnumerable<string> CipherIds { get; set; }
|
||||
|
||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||
{
|
||||
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||
this.UserName = memberAccessCipherDetails.UserName;
|
||||
this.Email = memberAccessCipherDetails.Email;
|
||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
||||
|
@ -1086,9 +1086,8 @@ public class CiphersController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
@ -63,6 +63,12 @@ public class FreshdeskController : Controller
|
||||
note += $"<li>Region: {_billingSettings.FreshDesk.Region}</li>";
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user == null)
|
||||
{
|
||||
note += $"<li>No user found: {ticketContactEmail}</li>";
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
@ -121,18 +127,7 @@ public class FreshdeskController : Controller
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
await CreateNote(ticketId, note);
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
@ -208,6 +203,21 @@ public class FreshdeskController : Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task CreateNote(string ticketId, string note)
|
||||
{
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
}
|
||||
|
||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
|
@ -4,8 +4,8 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IMailService mailService,
|
||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
||||
|
||||
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await SendVerifiedDomainUserEmailAsync(domain);
|
||||
}
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await SendVerifiedDomainUserEmailAsync(domain);
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
||||
|
@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null)
|
||||
if (deletingUserId.HasValue && eventSystemUser == null)
|
||||
{
|
||||
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
|
||||
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
|
||||
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null
|
||||
var claimedStatus = deletingUserId.HasValue && eventSystemUser == null
|
||||
? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
|
||||
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
||||
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
||||
|
@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
// Remove non-compliant users
|
||||
var savingUserId = _currentContext.UserId;
|
||||
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId
|
||||
).ToList();
|
||||
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
return validateDecryptionErrorMessage;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
|
||||
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_organizationRepository = organizationRepository;
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var savingUserId = _currentContext.UserId;
|
||||
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
|
||||
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
|
||||
.twoFactorIsEnabled;
|
||||
if (!userTwoFactorEnabled)
|
||||
{
|
||||
if (!orgUser.HasMasterPassword)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||
}
|
||||
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org!.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MembersWithNoMasterPasswordWillLoseAccess(
|
||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||
|
@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
//Send email to administrators
|
||||
if (adminEmails.Count > 0)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);
|
||||
|
@ -4,7 +4,9 @@ using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Commands;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
namespace Bit.Core.Billing.Extensions;
|
||||
|
||||
@ -24,5 +26,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IAutomaticTaxFactory, AutomaticTaxFactory>();
|
||||
services.AddLicenseServices();
|
||||
services.AddPricingClient();
|
||||
services.AddTransient<IPreviewTaxAmountCommand, PreviewTaxAmountCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ public static class OrganizationLicenseConstants
|
||||
public const string UseSecretsManager = nameof(UseSecretsManager);
|
||||
public const string SmSeats = nameof(SmSeats);
|
||||
public const string SmServiceAccounts = nameof(SmServiceAccounts);
|
||||
public const string SmMaxProjects = nameof(SmMaxProjects);
|
||||
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);
|
||||
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);
|
||||
public const string UseRiskInsights = nameof(UseRiskInsights);
|
||||
|
@ -7,5 +7,4 @@ public class LicenseContext
|
||||
{
|
||||
public Guid? InstallationId { get; init; }
|
||||
public required SubscriptionInfo SubscriptionInfo { get; init; }
|
||||
public int? SmMaxProjects { get; set; }
|
||||
}
|
||||
|
@ -112,11 +112,6 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
||||
}
|
||||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
|
||||
|
||||
if (licenseContext.SmMaxProjects.HasValue)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmMaxProjects), licenseContext.SmMaxProjects.ToString()));
|
||||
}
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
|
||||
|
36
src/Core/Billing/Models/BillingCommandResult.cs
Normal file
36
src/Core/Billing/Models/BillingCommandResult.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record BadRequest(string TranslationKey)
|
||||
{
|
||||
public static BadRequest TaxIdNumberInvalid => new(BillingErrorTranslationKeys.TaxIdInvalid);
|
||||
public static BadRequest TaxLocationInvalid => new(BillingErrorTranslationKeys.CustomerTaxLocationInvalid);
|
||||
public static BadRequest UnknownTaxIdType => new(BillingErrorTranslationKeys.UnknownTaxIdType);
|
||||
}
|
||||
|
||||
public record Unhandled(string TranslationKey = BillingErrorTranslationKeys.UnhandledError);
|
||||
|
||||
public class BillingCommandResult<T> : OneOfBase<T, BadRequest, Unhandled>
|
||||
{
|
||||
private BillingCommandResult(OneOf<T, BadRequest, Unhandled> input) : base(input) { }
|
||||
|
||||
public static implicit operator BillingCommandResult<T>(T output) => new(output);
|
||||
public static implicit operator BillingCommandResult<T>(BadRequest badRequest) => new(badRequest);
|
||||
public static implicit operator BillingCommandResult<T>(Unhandled unhandled) => new(unhandled);
|
||||
}
|
||||
|
||||
public static class BillingErrorTranslationKeys
|
||||
{
|
||||
// "The tax ID number you provided was invalid. Please try again or contact support."
|
||||
public const string TaxIdInvalid = "taxIdInvalid";
|
||||
|
||||
// "Your location wasn't recognized. Please ensure your country and postal code are valid and try again."
|
||||
public const string CustomerTaxLocationInvalid = "customerTaxLocationInvalid";
|
||||
|
||||
// "Something went wrong with your request. Please contact support."
|
||||
public const string UnhandledError = "unhandledBillingError";
|
||||
|
||||
// "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support."
|
||||
public const string UnknownTaxIdType = "unknownTaxIdType";
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record PaymentMethod(
|
||||
long AccountCredit,
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
@ -26,12 +27,21 @@ public class OrganizationSale
|
||||
|
||||
public static OrganizationSale From(
|
||||
Organization organization,
|
||||
OrganizationSignup signup) => new()
|
||||
OrganizationSignup signup)
|
||||
{
|
||||
var customerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null;
|
||||
|
||||
var subscriptionSetup = GetSubscriptionSetup(signup);
|
||||
|
||||
subscriptionSetup.SkipTrial = signup.SkipTrial;
|
||||
|
||||
return new OrganizationSale
|
||||
{
|
||||
Organization = organization,
|
||||
CustomerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null,
|
||||
SubscriptionSetup = GetSubscriptionSetup(signup)
|
||||
CustomerSetup = customerSetup,
|
||||
SubscriptionSetup = subscriptionSetup
|
||||
};
|
||||
}
|
||||
|
||||
public static OrganizationSale From(
|
||||
Organization organization,
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
|
@ -6,6 +6,8 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -2,7 +2,9 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -2,6 +2,8 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
147
src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
Normal file
147
src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
Normal file
@ -0,0 +1,147 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.Commands;
|
||||
|
||||
public interface IPreviewTaxAmountCommand
|
||||
{
|
||||
Task<BillingCommandResult<decimal>> Run(OrganizationTrialParameters parameters);
|
||||
}
|
||||
|
||||
public class PreviewTaxAmountCommand(
|
||||
ILogger<PreviewTaxAmountCommand> logger,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ITaxService taxService) : IPreviewTaxAmountCommand
|
||||
{
|
||||
public async Task<BillingCommandResult<decimal>> Run(OrganizationTrialParameters parameters)
|
||||
{
|
||||
var (planType, productType, taxInformation) = parameters;
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(planType);
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
Currency = "usd",
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInformation.Country,
|
||||
PostalCode = taxInformation.PostalCode
|
||||
}
|
||||
},
|
||||
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||
{
|
||||
Items = [
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = plan.HasNonSeatBasedPasswordManagerPlan() ? plan.PasswordManager.StripePlanId : plan.PasswordManager.StripeSeatPlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (productType == ProductType.SecretsManager)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = plan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(taxInformation.TaxId))
|
||||
{
|
||||
var taxIdType = taxService.GetStripeTaxCode(
|
||||
taxInformation.Country,
|
||||
taxInformation.TaxId);
|
||||
|
||||
if (string.IsNullOrEmpty(taxIdType))
|
||||
{
|
||||
return BadRequest.UnknownTaxIdType;
|
||||
}
|
||||
|
||||
options.CustomerDetails.TaxIds = [
|
||||
new InvoiceCustomerDetailsTaxIdOptions
|
||||
{
|
||||
Type = taxIdType,
|
||||
Value = taxInformation.TaxId
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (planType.GetProductTier() == ProductTierType.Families)
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions
|
||||
{
|
||||
Enabled = options.CustomerDetails.Address.Country == "US" ||
|
||||
options.CustomerDetails.TaxIds is [_, ..]
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
return Convert.ToDecimal(invoice.Tax) / 100;
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError.Code ==
|
||||
StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
|
||||
{
|
||||
return BadRequest.TaxLocationInvalid;
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError.Code ==
|
||||
StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
return BadRequest.TaxIdNumberInvalid;
|
||||
}
|
||||
catch (StripeException stripeException)
|
||||
{
|
||||
logger.LogError(stripeException, "Stripe responded with an error during {Operation}. Code: {Code}", nameof(PreviewTaxAmountCommand), stripeException.StripeError.Code);
|
||||
return new Unhandled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Command Parameters
|
||||
|
||||
public record OrganizationTrialParameters
|
||||
{
|
||||
public required PlanType PlanType { get; set; }
|
||||
public required ProductType ProductType { get; set; }
|
||||
public required TaxInformationDTO TaxInformation { get; set; }
|
||||
|
||||
public void Deconstruct(
|
||||
out PlanType planType,
|
||||
out ProductType productType,
|
||||
out TaxInformationDTO taxInformation)
|
||||
{
|
||||
planType = PlanType;
|
||||
productType = ProductType;
|
||||
taxInformation = TaxInformation;
|
||||
}
|
||||
|
||||
public record TaxInformationDTO
|
||||
{
|
||||
public required string Country { get; set; }
|
||||
public required string PostalCode { get; set; }
|
||||
public string? TaxId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
namespace Bit.Core.Billing.Tax.Models;
|
||||
|
||||
public class TaxIdType
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
namespace Bit.Core.Billing.Tax.Models;
|
||||
|
||||
public record TaxInformation(
|
||||
string Country,
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class PreviewIndividualInvoiceRequestBody
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class PreviewOrganizationInvoiceRequestBody
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class TaxInformationRequestModel
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Models.Api.Responses;
|
||||
namespace Bit.Core.Billing.Tax.Responses;
|
||||
|
||||
public record PreviewInvoiceResponseModel(
|
||||
decimal EffectiveTaxRate,
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for defining the correct automatic tax strategy for either personal use of business use.
|
@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
public interface IAutomaticTaxStrategy
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
public interface ITaxService
|
||||
{
|
@ -5,7 +5,7 @@ using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class AutomaticTaxFactory(
|
||||
IFeatureService featureService,
|
@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
|
||||
{
|
@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class TaxService : ITaxService
|
||||
{
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
|
@ -75,4 +75,8 @@
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Infrastructure.IntegrationTest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,27 +0,0 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top">
|
||||
The domain {{DomainName}} in your Bitwarden organization could not be verified.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
The domain will be removed from your organization in 7 days if it is not verified.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Manage Domains
|
||||
</a>
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -1,10 +0,0 @@
|
||||
{{#>BasicTextLayout}}
|
||||
The domain {{DomainName}} in your Bitwarden organization could not be verified.
|
||||
|
||||
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
|
||||
|
||||
The domain will be removed from your organization in 7 days if it is not verified.
|
||||
|
||||
{{Url}}
|
||||
|
||||
{{/BasicTextLayout}}
|
@ -1,9 +0,0 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you are a part of another organization. The {{OrganizationName}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -1,5 +0,0 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been removed from the {{OrganizationName}} organization because you are a part of another
|
||||
organization. The {{OrganizationName}} has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations, or join with a
|
||||
new account.
|
||||
{{/BasicTextLayout}}
|
@ -1,15 +0,0 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you do not have two-step login configured. Before you can re-join this organization you need to set up two-step login on your user account.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Learn how to enable two-step login on your user account at
|
||||
<a target="_blank" href="https://help.bitwarden.com/article/setup-two-step-login/" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">https://help.bitwarden.com/article/setup-two-step-login/</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -1,7 +0,0 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login
|
||||
configured. Before you can re-join this organization you need to set up two-step login on your user account.
|
||||
|
||||
Learn how to enable two-step login on your user account at
|
||||
<https://help.bitwarden.com/article/setup-two-step-login/>
|
||||
{{/BasicTextLayout}}
|
@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel
|
||||
public bool ToDelete { get; set; }
|
||||
|
||||
public bool CloudSponsorshipRemoved { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
|
||||
public OrganizationSponsorshipResponseModel() { }
|
||||
|
||||
@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel
|
||||
ValidUntil = sponsorshipData.ValidUntil;
|
||||
ToDelete = sponsorshipData.ToDelete;
|
||||
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
|
||||
IsAdminInitiated = sponsorshipData.IsAdminInitiated;
|
||||
}
|
||||
|
||||
public OrganizationSponsorshipData ToOrganizationSponsorship()
|
||||
@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel
|
||||
LastSyncDate = LastSyncDate,
|
||||
ValidUntil = ValidUntil,
|
||||
ToDelete = ToDelete,
|
||||
CloudSponsorshipRemoved = CloudSponsorshipRemoved
|
||||
CloudSponsorshipRemoved = CloudSponsorshipRemoved,
|
||||
IsAdminInitiated = IsAdminInitiated,
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -16,4 +16,5 @@ public class OrganizationSignup : OrganizationUpgrade
|
||||
public string InitiationPath { get; set; }
|
||||
public bool IsFromSecretsManagerTrial { get; set; }
|
||||
public bool IsFromProvider { get; set; }
|
||||
public bool SkipTrial { get; set; }
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -17,22 +16,19 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public CloudGetOrganizationLicenseQuery(
|
||||
IInstallationRepository installationRepository,
|
||||
IPaymentService paymentService,
|
||||
ILicensingService licensingService,
|
||||
IProviderRepository providerRepository,
|
||||
IFeatureService featureService,
|
||||
IPricingClient pricingClient)
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
_paymentService = paymentService;
|
||||
_licensingService = licensingService;
|
||||
_providerRepository = providerRepository;
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
|
||||
@ -46,11 +42,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
int? smMaxProjects = plan?.SupportsSecretsManager ?? false
|
||||
? plan.SecretsManager.MaxProjects
|
||||
: null;
|
||||
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo, smMaxProjects);
|
||||
license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo);
|
||||
|
||||
return license;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ public interface IOrganizationSponsorshipRepository : IRepository<OrganizationSp
|
||||
Task UpsertManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> organizationSponsorshipIds);
|
||||
Task<ICollection<OrganizationSponsorship>> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId);
|
||||
Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId);
|
||||
Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false);
|
||||
Task<OrganizationSponsorship?> GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId);
|
||||
Task<DateTime?> GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId);
|
||||
}
|
||||
|
@ -21,8 +21,7 @@ public interface ILicensingService
|
||||
Task<string?> CreateOrganizationTokenAsync(
|
||||
Organization organization,
|
||||
Guid installationId,
|
||||
SubscriptionInfo subscriptionInfo,
|
||||
int? smMaxProjects);
|
||||
SubscriptionInfo subscriptionInfo);
|
||||
|
||||
Task<string?> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ public interface IMailService
|
||||
Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);
|
||||
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
|
||||
@ -61,7 +60,6 @@ public interface IMailService
|
||||
Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null);
|
||||
Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip);
|
||||
Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip);
|
||||
Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email);
|
||||
Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token);
|
||||
Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email);
|
||||
Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email);
|
||||
@ -88,7 +86,6 @@ public interface IMailService
|
||||
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
||||
Task SendOTPEmailAsync(string email, string token);
|
||||
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
|
@ -1,9 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.Api.Responses;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
@ -133,16 +133,11 @@ public interface IUserService
|
||||
/// verified domains of that organization, and the user is a member of it.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Verify whether the new email domain meets the requirements for managed users.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// IdentityResult
|
||||
/// </returns>
|
||||
@ -151,9 +146,6 @@ public interface IUserService
|
||||
/// <summary>
|
||||
/// Gets the organizations that manage the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An empty collection if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
/// <inheritdoc cref="IsClaimedByAnyOrganizationAsync"/>
|
||||
Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId);
|
||||
}
|
||||
|
@ -301,20 +301,6 @@ public class HandlebarsMailService : IMailService
|
||||
await EnqueueMailAsync(messageModels);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
|
||||
var model = new OrganizationUserRemovedForPolicyTwoStepViewModel
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyTwoStep", model);
|
||||
message.Category = "OrganizationUserRemovedForPolicyTwoStep";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
@ -532,20 +518,6 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
|
||||
var model = new OrganizationUserRemovedForPolicySingleOrgViewModel
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicySingleOrg", model);
|
||||
message.Category = "OrganizationUserRemovedForPolicySingleOrg";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
@ -1137,19 +1109,6 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Domain not verified", adminEmails);
|
||||
var model = new OrganizationDomainUnverifiedViewModel
|
||||
{
|
||||
Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification",
|
||||
DomainName = domainName
|
||||
};
|
||||
await AddMessageContentAsync(message, "OrganizationDomainUnverified", model);
|
||||
message.Category = "UnverifiedOrganizationDomain";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Domain not claimed", adminEmails);
|
||||
|
@ -339,13 +339,12 @@ public class LicensingService : ILicensingService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects)
|
||||
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
var licenseContext = new LicenseContext
|
||||
{
|
||||
InstallationId = installationId,
|
||||
SubscriptionInfo = subscriptionInfo,
|
||||
SmMaxProjects = smMaxProjects
|
||||
};
|
||||
|
||||
var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);
|
||||
|
@ -3,14 +3,15 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.Api.Responses;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -1336,11 +1337,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
public async Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return Enumerable.Empty<Organization>();
|
||||
}
|
||||
|
||||
// Get all organizations that have verified the user's email domain.
|
||||
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
|
||||
|
||||
@ -1405,22 +1401,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
p.OrganizationId,
|
||||
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
|
||||
new SystemUser(EventSystemUser.TwoFactorDisabled)));
|
||||
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
organization.DisplayName(), user.Email);
|
||||
}
|
||||
|
||||
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
p.OrganizationId,
|
||||
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
|
||||
new SystemUser(EventSystemUser.TwoFactorDisabled)));
|
||||
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
|
||||
}).ToArray();
|
||||
|
||||
await Task.WhenAll(removeOrgUserTasks);
|
||||
|
@ -62,7 +62,7 @@ public class NoopLicensingService : ILicensingService
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<string?> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects)
|
||||
public Task<string?> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
@ -80,11 +80,6 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
@ -155,11 +150,6 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
@ -268,11 +258,6 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.Utilities;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
@ -24,7 +25,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
|
||||
private UserDecryptionOptions _options = new UserDecryptionOptions();
|
||||
private User? _user;
|
||||
private Core.Auth.Entities.SsoConfig? _ssoConfig;
|
||||
private SsoConfig? _ssoConfig;
|
||||
private Device? _device;
|
||||
|
||||
public UserDecryptionOptionsBuilder(
|
||||
@ -45,7 +46,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
public IUserDecryptionOptionsBuilder WithSso(Core.Auth.Entities.SsoConfig ssoConfig)
|
||||
public IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig)
|
||||
{
|
||||
_ssoConfig = ssoConfig;
|
||||
return this;
|
||||
@ -119,8 +120,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
// their current device.
|
||||
// NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting.
|
||||
hasLoginApprovingDevice = allDevices
|
||||
.Where(d => d.Identifier != _device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
|
||||
.Any();
|
||||
.Any(d => d.Identifier != _device.Identifier && LoginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type)));
|
||||
}
|
||||
|
||||
// Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP
|
||||
|
22
src/Identity/Utilities/LoginApprovingClientTypes.cs
Normal file
22
src/Identity/Utilities/LoginApprovingClientTypes.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Identity.Utilities;
|
||||
|
||||
public static class LoginApprovingClientTypes
|
||||
{
|
||||
private static readonly IReadOnlyCollection<ClientType> _clientTypesThatCanApprove;
|
||||
|
||||
static LoginApprovingClientTypes()
|
||||
{
|
||||
var clientTypes = new List<ClientType>
|
||||
{
|
||||
ClientType.Desktop,
|
||||
ClientType.Mobile,
|
||||
ClientType.Web,
|
||||
ClientType.Browser,
|
||||
};
|
||||
_clientTypesThatCanApprove = clientTypes.AsReadOnly();
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<ClientType> TypesThatCanApprove => _clientTypesThatCanApprove;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Identity.Utilities;
|
||||
|
||||
public static class LoginApprovingDeviceTypes
|
||||
{
|
||||
private static readonly IReadOnlyCollection<DeviceType> _deviceTypes;
|
||||
|
||||
static LoginApprovingDeviceTypes()
|
||||
{
|
||||
var deviceTypes = new List<DeviceType>();
|
||||
deviceTypes.AddRange(DeviceTypes.DesktopTypes);
|
||||
deviceTypes.AddRange(DeviceTypes.MobileTypes);
|
||||
deviceTypes.AddRange(DeviceTypes.BrowserTypes);
|
||||
_deviceTypes = deviceTypes.AsReadOnly();
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<DeviceType> Types => _deviceTypes;
|
||||
}
|
@ -89,7 +89,7 @@ public class OrganizationSponsorshipRepository : Repository<OrganizationSponsors
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId)
|
||||
public async Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@ -97,7 +97,8 @@ public class OrganizationSponsorshipRepository : Repository<OrganizationSponsors
|
||||
"[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]",
|
||||
new
|
||||
{
|
||||
SponsoringOrganizationUserId = sponsoringOrganizationUserId
|
||||
SponsoringOrganizationUserId = sponsoringOrganizationUserId,
|
||||
isAdminInitiated = isAdminInitiated
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
|
@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
public IQueryable<OrganizationUserOrganizationDetails> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization
|
||||
from o in outerOrganization.DefaultIfEmpty()
|
||||
join o in dbContext.Organizations on ou.OrganizationId equals o.Id
|
||||
join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g
|
||||
from su in su_g.DefaultIfEmpty()
|
||||
join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g
|
||||
@ -68,10 +67,11 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
SmServiceAccounts = o.SmServiceAccounts,
|
||||
LimitCollectionCreation = o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = o.LimitCollectionDeletion,
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
||||
UseRiskInsights = o.UseRiskInsights,
|
||||
UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies,
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
IsAdminInitiated = os.IsAdminInitiated
|
||||
};
|
||||
return query;
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0108</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="linq2db" Version="5.4.1" />
|
||||
|
@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Platform;
|
||||
|
||||
public class Installation : C.Installation
|
||||
{
|
||||
// Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129
|
||||
// This isn't a value or entity used by self hosted servers, but it's
|
||||
// being added for synchronicity between database provider options.
|
||||
public DateTime? LastActivityDate { get; set; }
|
||||
}
|
||||
public class Installation : C.Installation;
|
||||
|
||||
public class InstallationMapperProfile : Profile
|
||||
{
|
||||
public InstallationMapperProfile()
|
||||
{
|
||||
CreateMap<C.Installation, Installation>()
|
||||
// Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129
|
||||
.ForMember(i => i.LastActivityDate, opt => opt.Ignore())
|
||||
.ReverseMap();
|
||||
CreateMap<C.Installation, Installation>().ReverseMap();
|
||||
}
|
||||
}
|
||||
|
@ -104,12 +104,13 @@ public class OrganizationSponsorshipRepository : Repository<Core.Entities.Organi
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Core.Entities.OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId)
|
||||
public async Task<Core.Entities.OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId)
|
||||
var orgSponsorship = await GetDbSet(dbContext)
|
||||
.Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId && e.IsAdminInitiated == isAdminInitiated)
|
||||
.FirstOrDefaultAsync();
|
||||
return orgSponsorship;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build">
|
||||
<Sdk Name="Microsoft.Build.Sql" Version="0.1.9-preview" />
|
||||
<Sdk Name="Microsoft.Build.Sql"/>
|
||||
<PropertyGroup>
|
||||
<Name>Sql</Name>
|
||||
<ProjectGuid>{58554e52-fdec-4832-aff9-302b01e08dca}</ProjectGuid>
|
||||
|
@ -3,9 +3,9 @@ CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
|
||||
DECLARE @Domain NVARCHAR(256)
|
||||
|
||||
|
||||
SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))
|
||||
|
||||
SELECT
|
||||
@ -19,8 +19,8 @@ BEGIN
|
||||
[dbo].[OrganizationView] O
|
||||
INNER JOIN [dbo].[OrganizationDomainView] OD
|
||||
ON O.Id = OD.OrganizationId
|
||||
LEFT JOIN [dbo].[Ssoconfig] S
|
||||
LEFT JOIN [dbo].[SsoConfig] S
|
||||
ON O.Id = S.OrganizationId
|
||||
WHERE OD.DomainName = @Domain
|
||||
AND O.Enabled = 1
|
||||
END
|
||||
END
|
||||
|
@ -1,14 +0,0 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]
|
||||
@SponsoringOrganizationUserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OrganizationSponsorshipView]
|
||||
WHERE
|
||||
[SponsoringOrganizationUserId] = @SponsoringOrganizationUserId
|
||||
END
|
||||
GO
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user