1
0
mirror of https://github.com/bitwarden/server synced 2025-12-18 17:23:28 +00:00

Merge branch 'master' into feature/sm-billing

This commit is contained in:
Thomas Rittson
2023-07-19 09:59:00 +10:00
committed by GitHub
12 changed files with 230 additions and 3 deletions

View File

@@ -293,6 +293,16 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return policy == null ? (false, false) : (policy.Read, policy.Write); return policy == null ? (false, false) : (policy.Read, policy.Write);
} }
public async Task EmptyTrash(DateTime currentDate, uint deleteAfterThisNumberOfDays)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
await dbContext.Secret.Where(s => s.DeletedDate != null && s.DeletedDate < currentDate.AddDays(-deleteAfterThisNumberOfDays)).ExecuteDeleteAsync();
await dbContext.SaveChangesAsync();
}
private IQueryable<SecretPermissionDetails> SecretToPermissionDetails(IQueryable<Secret> query, Guid userId, AccessClientType accessType) private IQueryable<SecretPermissionDetails> SecretToPermissionDetails(IQueryable<Secret> query, Guid userId, AccessClientType accessType)
{ {
var secrets = accessType switch var secrets = accessType switch

View File

@@ -41,6 +41,11 @@ public class JobsHostedService : BaseJobsHostedService
.StartNow() .StartNow()
.WithCronSchedule("0 30 */12 * * ?") .WithCronSchedule("0 30 */12 * * ?")
.Build(); .Build();
var smTrashCleanupTrigger = TriggerBuilder.Create()
.WithIdentity("SMTrashCleanupTrigger")
.StartNow()
.WithCronSchedule("0 0 22 * * ?")
.Build();
var randomDailySponsorshipSyncTrigger = TriggerBuilder.Create() var randomDailySponsorshipSyncTrigger = TriggerBuilder.Create()
.WithIdentity("RandomDailySponsorshipSyncTrigger") .WithIdentity("RandomDailySponsorshipSyncTrigger")
.StartAt(DateBuilder.FutureDate(new Random().Next(24), IntervalUnit.Hour)) .StartAt(DateBuilder.FutureDate(new Random().Next(24), IntervalUnit.Hour))
@@ -70,6 +75,10 @@ public class JobsHostedService : BaseJobsHostedService
jobs.Add(new Tuple<Type, ITrigger>(typeof(SelfHostedSponsorshipSyncJob), randomDailySponsorshipSyncTrigger)); jobs.Add(new Tuple<Type, ITrigger>(typeof(SelfHostedSponsorshipSyncJob), randomDailySponsorshipSyncTrigger));
} }
#if !OSS
jobs.Add(new Tuple<Type, ITrigger>(typeof(EmptySecretsManagerTrashJob), smTrashCleanupTrigger));
#endif
Jobs = jobs; Jobs = jobs;
await base.StartAsync(cancellationToken); await base.StartAsync(cancellationToken);
@@ -88,4 +97,9 @@ public class JobsHostedService : BaseJobsHostedService
services.AddTransient<ValidateOrganizationsJob>(); services.AddTransient<ValidateOrganizationsJob>();
services.AddTransient<ValidateOrganizationDomainJob>(); services.AddTransient<ValidateOrganizationDomainJob>();
} }
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
{
services.AddTransient<EmptySecretsManagerTrashJob>();
}
} }

View File

@@ -0,0 +1,23 @@
using Bit.Core.Jobs;
using Bit.Core.SecretsManager.Repositories;
using Quartz;
namespace Bit.Api.Jobs;
public class EmptySecretsManagerTrashJob : BaseJob
{
private ISecretRepository _secretRepository;
private const uint DeleteAfterThisNumberOfDays = 30;
public EmptySecretsManagerTrashJob(ISecretRepository secretRepository, ILogger<EmptySecretsManagerTrashJob> logger) : base(logger)
{
_secretRepository = secretRepository;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
_logger.LogInformation("Execute job task: EmptySecretsManagerTrashJob: Start");
await _secretRepository.EmptyTrash(DateTime.UtcNow, DeleteAfterThisNumberOfDays);
_logger.LogInformation("Execute job task: EmptySecretsManagerTrashJob: End");
}
}

View File

@@ -149,6 +149,7 @@ public class Startup
services.AddCommercialCoreServices(); services.AddCommercialCoreServices();
services.AddCommercialSecretsManagerServices(); services.AddCommercialSecretsManagerServices();
services.AddSecretsManagerEfRepositories(); services.AddSecretsManagerEfRepositories();
Jobs.JobsHostedService.AddCommercialSecretsManagerJobServices(services);
#endif #endif
// MVC // MVC

View File

@@ -63,9 +63,16 @@ public class SsoConfigService : ISsoConfigService
throw new BadRequestException("Key Connector cannot be disabled at this moment."); throw new BadRequestException("Key Connector cannot be disabled at this moment.");
} }
// Automatically enable reset password policy if trusted device encryption is selected // Automatically enable account recovery and single org policies if trusted device encryption is selected
if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption) if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
{ {
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.SingleOrg };
singleOrgPolicy.Enabled = true;
await _policyService.SaveAsync(singleOrgPolicy, _userService, _organizationService, null);
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };

View File

@@ -20,4 +20,5 @@ public interface ISecretRepository
Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets); Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);
Task UpdateRevisionDates(IEnumerable<Guid> ids); Task UpdateRevisionDates(IEnumerable<Guid> ids);
Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType); Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
} }

View File

@@ -61,6 +61,7 @@ public class PolicyService : IPolicyService
await RequiredBySsoAsync(org); await RequiredBySsoAsync(org);
await RequiredByVaultTimeoutAsync(org); await RequiredByVaultTimeoutAsync(org);
await RequiredByKeyConnectorAsync(org); await RequiredByKeyConnectorAsync(org);
await RequiredByAccountRecoveryAsync(org);
} }
break; break;
@@ -80,6 +81,11 @@ public class PolicyService : IPolicyService
{ {
await RequiredBySsoTrustedDeviceEncryptionAsync(org); await RequiredBySsoTrustedDeviceEncryptionAsync(org);
} }
if (policy.Enabled)
{
await DependsOnSingleOrgAsync(org);
}
break; break;
case PolicyType.MaximumVaultTimeout: case PolicyType.MaximumVaultTimeout:
@@ -244,6 +250,15 @@ public class PolicyService : IPolicyService
} }
} }
private async Task RequiredByAccountRecoveryAsync(Organization org)
{
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword);
if (requireSso?.Enabled == true)
{
throw new BadRequestException("Account recovery policy is enabled.");
}
}
private async Task RequiredByVaultTimeoutAsync(Organization org) private async Task RequiredByVaultTimeoutAsync(Organization org)
{ {
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout); var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);

View File

@@ -6,11 +6,13 @@ BEGIN
SET NOCOUNT ON SET NOCOUNT ON
SELECT SELECT
ar.*, ou.[Email], ou.[Id] AS [OrganizationUserId] ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]
FROM FROM
[dbo].[AuthRequestView] ar [dbo].[AuthRequestView] ar
INNER JOIN INNER JOIN
[dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId] [dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]
INNER JOIN
[dbo].[User] u ON u.[Id] = ar.[UserId]
WHERE WHERE
ar.[OrganizationId] = @OrganizationId ar.[OrganizationId] = @OrganizationId
AND AND

View File

@@ -5,11 +5,13 @@ BEGIN
SET NOCOUNT ON SET NOCOUNT ON
SELECT SELECT
ar.*, ou.[Email], ou.[OrganizationId], ou.[Id] AS [OrganizationUserId] ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]
FROM FROM
[dbo].[AuthRequestView] ar [dbo].[AuthRequestView] ar
INNER JOIN INNER JOIN
[dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId] [dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]
INNER JOIN
[dbo].[User] u ON u.[Id] = ar.[UserId]
WHERE WHERE
ar.[OrganizationId] = @OrganizationId ar.[OrganizationId] = @OrganizationId
AND AND

View File

@@ -6,7 +6,9 @@ using Bit.Core.Auth.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@@ -317,4 +319,40 @@ public class SsoConfigServiceTests
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs() await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()
.UpsertAsync(default); .UpsertAsync(default);
} }
[Theory, BitAutoData]
public async Task SaveAsync_Tde_Enable_Required_Policies(SutProvider<SsoConfigService> sutProvider, Organization organization)
{
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}.Serialize(),
Enabled = true,
OrganizationId = organization.Id,
};
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
await sutProvider.GetDependency<IPolicyService>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == Enums.PolicyType.SingleOrg),
Arg.Any<IUserService>(),
Arg.Any<IOrganizationService>(),
null
);
await sutProvider.GetDependency<IPolicyService>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == Enums.PolicyType.ResetPassword && t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled),
Arg.Any<IUserService>(),
Arg.Any<IOrganizationService>(),
null
);
await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()
.UpsertAsync(default);
}
} }

View File

@@ -217,6 +217,10 @@ public class PolicyServiceTests
UsePolicies = true, UsePolicies = true,
}); });
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = true }));
var utcNow = DateTime.UtcNow; var utcNow = DateTime.UtcNow;
await sutProvider.Sut.SaveAsync(policy, Substitute.For<IUserService>(), Substitute.For<IOrganizationService>(), Guid.NewGuid()); await sutProvider.Sut.SaveAsync(policy, Substitute.For<IUserService>(), Substitute.For<IOrganizationService>(), Guid.NewGuid());
@@ -444,6 +448,70 @@ public class PolicyServiceTests
.LogPolicyEventAsync(default, default, default); .LogPolicyEventAsync(default, default, default);
} }
[Theory, BitAutoData]
public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync(
[PolicyFixtures.Policy(Enums.PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = true;
policy.SetDataModel(new ResetPasswordDataModel());
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg)
.Returns(Task.FromResult(new Policy { Enabled = false }));
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
Assert.Contains("Single Organization policy not enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_SingleOrg_AccountRecoveryEnabled_ThrowsBadRequest(
[PolicyFixtures.Policy(Enums.PolicyType.SingleOrg)] Policy policy, SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.ResetPassword)
.Returns(new Policy { Enabled = true });
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
Assert.Contains("Account recovery policy is enabled.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
}
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider) public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider)
{ {

View File

@@ -0,0 +1,46 @@
CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_ReadAdminApprovalsByIds]
@OrganizationId UNIQUEIDENTIFIER,
@Ids AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
SELECT
ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]
FROM
[dbo].[AuthRequestView] ar
INNER JOIN
[dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]
INNER JOIN
[dbo].[User] u ON u.[Id] = ar.[UserId]
WHERE
ar.[OrganizationId] = @OrganizationId
AND
ar.[Type] = 2 -- AdminApproval
AND
ar.[Id] IN (SELECT [Id] FROM @Ids)
END
GO
CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_ReadPendingByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]
FROM
[dbo].[AuthRequestView] ar
INNER JOIN
[dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]
INNER JOIN
[dbo].[User] u ON u.[Id] = ar.[UserId]
WHERE
ar.[OrganizationId] = @OrganizationId
AND
ar.[ResponseDate] IS NULL
AND
ar.[Type] = 2 -- AdminApproval
END
GO