mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 09:32:48 -05:00
[AC-1287] AC Team code ownership moves: Policies (1/2) (#3383)
* note: IPolicyData and EntityFramework Policy.cs are moved without any changes to namespace or content in order to preserve git history.
This commit is contained in:
32
src/Core/AdminConsole/Entities/Policy.cs
Normal file
32
src/Core/AdminConsole/Entities/Policy.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Entities;
|
||||
|
||||
public class Policy : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public PolicyType Type { get; set; }
|
||||
public string Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
return CoreHelpers.LoadClassFromJsonData<T>(Data);
|
||||
}
|
||||
|
||||
public void SetDataModel<T>(T dataModel) where T : IPolicyDataModel, new()
|
||||
{
|
||||
Data = CoreHelpers.ClassToJsonData(dataModel);
|
||||
}
|
||||
}
|
17
src/Core/AdminConsole/Enums/PolicyType.cs
Normal file
17
src/Core/AdminConsole/Enums/PolicyType.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Bit.Core.AdminConsole.Enums;
|
||||
|
||||
public enum PolicyType : byte
|
||||
{
|
||||
TwoFactorAuthentication = 0,
|
||||
MasterPassword = 1,
|
||||
PasswordGenerator = 2,
|
||||
SingleOrg = 3,
|
||||
RequireSso = 4,
|
||||
PersonalOwnership = 5,
|
||||
DisableSend = 6,
|
||||
SendOptions = 7,
|
||||
ResetPassword = 8,
|
||||
MaximumVaultTimeout = 9,
|
||||
DisablePersonalVaultExport = 10,
|
||||
ActivateAutofill = 11,
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Api.Response;
|
||||
|
||||
public class PolicyResponseModel : ResponseModel
|
||||
{
|
||||
public PolicyResponseModel(Policy policy, string obj = "policy")
|
||||
: base(obj)
|
||||
{
|
||||
if (policy == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(policy));
|
||||
}
|
||||
|
||||
Id = policy.Id;
|
||||
OrganizationId = policy.OrganizationId;
|
||||
Type = policy.Type;
|
||||
Enabled = policy.Enabled;
|
||||
if (!string.IsNullOrWhiteSpace(policy.Data))
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public PolicyType Type { get; set; }
|
||||
public Dictionary<string, object> Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
public interface IPolicyDataModel
|
||||
{
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class MasterPasswordPolicyData : IPolicyDataModel
|
||||
{
|
||||
public int? MinComplexity { get; set; }
|
||||
public int? MinLength { get; set; }
|
||||
public bool? RequireLower { get; set; }
|
||||
public bool? RequireUpper { get; set; }
|
||||
public bool? RequireNumbers { get; set; }
|
||||
public bool? RequireSpecial { get; set; }
|
||||
public bool? EnforceOnLogin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combine the other policy data with this instance, taking the most secure options
|
||||
/// </summary>
|
||||
/// <param name="other">The other policy instance to combine with this</param>
|
||||
public void CombineWith(MasterPasswordPolicyData other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (other.MinComplexity.HasValue && (!MinComplexity.HasValue || other.MinComplexity > MinComplexity))
|
||||
{
|
||||
MinComplexity = other.MinComplexity;
|
||||
}
|
||||
|
||||
if (other.MinLength.HasValue && (!MinLength.HasValue || other.MinLength > MinLength))
|
||||
{
|
||||
MinLength = other.MinLength;
|
||||
}
|
||||
|
||||
RequireLower = (other.RequireLower ?? false) || (RequireLower ?? false);
|
||||
RequireUpper = (other.RequireUpper ?? false) || (RequireUpper ?? false);
|
||||
RequireNumbers = (other.RequireNumbers ?? false) || (RequireNumbers ?? false);
|
||||
RequireSpecial = (other.RequireSpecial ?? false) || (RequireSpecial ?? false);
|
||||
EnforceOnLogin = (other.EnforceOnLogin ?? false) || (EnforceOnLogin ?? false);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class ResetPasswordDataModel : IPolicyDataModel
|
||||
{
|
||||
[Display(Name = "ResetPasswordAutoEnrollCheckbox")]
|
||||
public bool AutoEnrollEnabled { get; set; }
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class SendOptionsPolicyData : IPolicyDataModel
|
||||
{
|
||||
[Display(Name = "DisableHideEmail")]
|
||||
public bool DisableHideEmail { get; set; }
|
||||
}
|
12
src/Core/AdminConsole/Repositories/IPolicyRepository.cs
Normal file
12
src/Core/AdminConsole/Repositories/IPolicyRepository.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
public interface IPolicyRepository : IRepository<Policy, Guid>
|
||||
{
|
||||
Task<Policy> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);
|
||||
Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);
|
||||
}
|
22
src/Core/AdminConsole/Services/IPolicyService.cs
Normal file
22
src/Core/AdminConsole/Services/IPolicyService.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public interface IPolicyService
|
||||
{
|
||||
Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
|
||||
Guid? savingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined master password policy options for the specified user.
|
||||
/// </summary>
|
||||
Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user);
|
||||
Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted);
|
||||
Task<bool> AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted);
|
||||
}
|
281
src/Core/AdminConsole/Services/Implementations/PolicyService.cs
Normal file
281
src/Core/AdminConsole/Services/Implementations/PolicyService.cs
Normal file
@ -0,0 +1,281 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services.Implementations;
|
||||
|
||||
public class PolicyService : IPolicyService
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_mailService = mailService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
|
||||
Guid? savingUserId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
if (!org.UsePolicies)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use policies.");
|
||||
}
|
||||
|
||||
// Handle dependent policy checks
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.SingleOrg:
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await RequiredBySsoAsync(org);
|
||||
await RequiredByVaultTimeoutAsync(org);
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredByAccountRecoveryAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.RequireSso:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.ResetPassword:
|
||||
if (!policy.Enabled || policy.GetDataModel<ResetPasswordDataModel>()?.AutoEnrollEnabled == false)
|
||||
{
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.MaximumVaultTimeout:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (policy.Id == default(Guid))
|
||||
{
|
||||
policy.CreationDate = now;
|
||||
}
|
||||
|
||||
if (policy.Enabled)
|
||||
{
|
||||
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||
if (!currentPolicy?.Enabled ?? true)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
|
||||
policy.OrganizationId);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.TwoFactorAuthentication:
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
|
||||
{
|
||||
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org.Name, orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PolicyType.SingleOrg:
|
||||
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 organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.Name, orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
policy.RevisionDate = now;
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
{
|
||||
var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))
|
||||
.Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)
|
||||
.ToList();
|
||||
|
||||
if (!policies.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var enforcedOptions = new MasterPasswordPolicyData();
|
||||
|
||||
foreach (var policy in policies)
|
||||
{
|
||||
enforcedOptions.CombineWith(policy.GetDataModel<MasterPasswordPolicyData>());
|
||||
}
|
||||
|
||||
return enforcedOptions;
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
||||
{
|
||||
var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus);
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
||||
{
|
||||
var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus);
|
||||
return result.Any();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<OrganizationUserPolicyDetails>> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
|
||||
{
|
||||
var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);
|
||||
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
return organizationUserPolicyDetails.Where(o =>
|
||||
(!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) &&
|
||||
o.PolicyEnabled &&
|
||||
!excludedUserTypes.Contains(o.OrganizationUserType) &&
|
||||
o.OrganizationUserStatus >= minStatus &&
|
||||
!o.IsProvider);
|
||||
}
|
||||
|
||||
private OrganizationUserType[] GetUserTypesExcludedFromPolicy(PolicyType policyType)
|
||||
{
|
||||
switch (policyType)
|
||||
{
|
||||
case PolicyType.MasterPassword:
|
||||
return Array.Empty<OrganizationUserType>();
|
||||
case PolicyType.RequireSso:
|
||||
// If 'EnforceSsoPolicyForAllUsers' is set to true then SSO policy applies to all user types otherwise it does not apply to Owner or Admin
|
||||
if (_globalSettings.Sso.EnforceSsoPolicyForAllUsers)
|
||||
{
|
||||
return Array.Empty<OrganizationUserType>();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||
}
|
||||
|
||||
private async Task DependsOnSingleOrgAsync(Organization org)
|
||||
{
|
||||
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
|
||||
if (singleOrg?.Enabled != true)
|
||||
{
|
||||
throw new BadRequestException("Single Organization policy not enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoAsync(Organization org)
|
||||
{
|
||||
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
|
||||
if (requireSso?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByKeyConnectorAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
|
||||
{
|
||||
throw new BadRequestException("Key Connector is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
|
||||
if (vaultTimeout?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
|
||||
{
|
||||
throw new BadRequestException("Trusted device encryption is on and requires this policy.");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user