mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'main' into bre-385-create-seeded-database
This commit is contained in:
commit
52b7953150
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@ -21,6 +21,8 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- check-run
|
||||
steps:
|
||||
- name: Debug
|
||||
run: |
|
||||
@ -41,7 +43,8 @@ jobs:
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-22.04
|
||||
needs: lint
|
||||
needs:
|
||||
- lint
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -135,7 +138,6 @@ jobs:
|
||||
security-events: write
|
||||
needs:
|
||||
- build-artifacts
|
||||
- check-run
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -481,7 +483,8 @@ jobs:
|
||||
build-mssqlmigratorutility:
|
||||
name: Build MSSQL migrator utility
|
||||
runs-on: ubuntu-22.04
|
||||
needs: lint
|
||||
needs:
|
||||
- lint
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@ -534,7 +537,8 @@ jobs:
|
||||
name: Trigger self-host build
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -567,7 +571,8 @@ jobs:
|
||||
name: Trigger k8s deploy
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-docker
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -603,7 +608,8 @@ jobs:
|
||||
github.event_name == 'pull_request_target'
|
||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
|
@ -1,7 +1,11 @@
|
||||
using Bit.Api.AdminConsole.Models.Request;
|
||||
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.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -16,7 +20,6 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
@ -32,6 +35,8 @@ public class PoliciesController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IDataProtector _organizationServiceDataProtector;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public PoliciesController(
|
||||
IPolicyRepository policyRepository,
|
||||
@ -41,7 +46,9 @@ public class PoliciesController : Controller
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
@ -53,10 +60,12 @@ public class PoliciesController : Controller
|
||||
"OrganizationServiceDataProtector");
|
||||
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{type}")]
|
||||
public async Task<PolicyResponseModel> Get(Guid orgId, int type)
|
||||
public async Task<PolicyDetailResponseModel> Get(Guid orgId, int type)
|
||||
{
|
||||
if (!await _currentContext.ManagePolicies(orgId))
|
||||
{
|
||||
@ -65,10 +74,15 @@ public class PoliciesController : Controller
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
||||
if (policy == null)
|
||||
{
|
||||
return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
|
||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||
}
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg)
|
||||
{
|
||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||
}
|
||||
|
||||
return new PolicyDetailResponseModel(policy);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -81,8 +95,8 @@ public class PoliciesController : Controller
|
||||
}
|
||||
|
||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid);
|
||||
var responses = policies.Select(p => new PolicyResponseModel(p));
|
||||
return new ListResponseModel<PolicyResponseModel>(responses);
|
||||
|
||||
return new ListResponseModel<PolicyResponseModel>(policies.Select(p => new PolicyResponseModel(p)));
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -0,0 +1,19 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
|
||||
public static class PolicyDetailResponses
|
||||
{
|
||||
public static async Task<PolicyDetailResponseModel> GetSingleOrgPolicyDetailResponseAsync(this Policy policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)
|
||||
{
|
||||
if (policy.Type is not PolicyType.SingleOrg)
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
||||
}
|
||||
|
||||
return new PolicyDetailResponseModel(policy, !await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId));
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class PolicyDetailResponseModel : PolicyResponseModel
|
||||
{
|
||||
public PolicyDetailResponseModel(Policy policy, string obj = "policy") : base(policy, obj)
|
||||
{
|
||||
}
|
||||
|
||||
public PolicyDetailResponseModel(Policy policy, bool canToggleState) : base(policy)
|
||||
{
|
||||
CanToggleState = canToggleState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the Policy can be enabled/disabled
|
||||
/// </summary>
|
||||
public bool CanToggleState { get; set; } = true;
|
||||
}
|
@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Api.Response;
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class PolicyResponseModel : ResponseModel
|
||||
{
|
@ -41,14 +41,13 @@ public class PoliciesController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(PolicyType type)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(
|
||||
_currentContext.OrganizationId.Value, type);
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(_currentContext.OrganizationId.Value, type);
|
||||
if (policy == null)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new PolicyResponseModel(policy);
|
||||
return new JsonResult(response);
|
||||
|
||||
return new JsonResult(new PolicyResponseModel(policy));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -62,9 +61,8 @@ public class PoliciesController : Controller
|
||||
public async Task<IActionResult> List()
|
||||
{
|
||||
var policies = await _policyRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value);
|
||||
var policyResponses = policies.Select(p => new PolicyResponseModel(p));
|
||||
var response = new ListResponseModel<PolicyResponseModel>(policyResponses);
|
||||
return new JsonResult(response);
|
||||
|
||||
return new JsonResult(new ListResponseModel<PolicyResponseModel>(policies.Select(p => new PolicyResponseModel(p))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -18,7 +18,6 @@ public abstract class MemberBaseModel
|
||||
|
||||
Type = user.Type;
|
||||
ExternalId = user.ExternalId;
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
|
||||
if (Type == OrganizationUserType.Custom)
|
||||
{
|
||||
@ -35,7 +34,6 @@ public abstract class MemberBaseModel
|
||||
|
||||
Type = user.Type;
|
||||
ExternalId = user.ExternalId;
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
|
||||
if (Type == OrganizationUserType.Custom)
|
||||
{
|
||||
@ -55,11 +53,7 @@ public abstract class MemberBaseModel
|
||||
/// <example>external_id_123456</example>
|
||||
[StringLength(300)]
|
||||
public string ExternalId { get; set; }
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the member has enrolled in Password Reset assistance within the organization
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool ResetPasswordEnrolled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The member's custom permissions if the member has a Custom role. If not supplied, all custom permissions will
|
||||
/// default to false.
|
||||
|
@ -28,6 +28,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
Email = user.Email;
|
||||
Status = user.Status;
|
||||
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
}
|
||||
|
||||
public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled,
|
||||
@ -45,6 +46,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
Status = user.Status;
|
||||
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -93,4 +95,10 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
/// The associated collections that this member can access.
|
||||
/// </summary>
|
||||
public IEnumerable<AssociationWithPermissionsResponseModel> Collections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the member has enrolled in Password Reset assistance within the organization
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool ResetPasswordEnrolled { get; }
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -32,6 +32,8 @@ using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
|
||||
|
||||
#if !OSS
|
||||
@ -176,6 +178,7 @@ public class Startup
|
||||
services.AddOrganizationSubscriptionServices();
|
||||
services.AddCoreLocalizationServices();
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
|
||||
// Authorization Handlers
|
||||
services.AddAuthorizationHandlers();
|
||||
|
@ -1,13 +1,13 @@
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Api.Tools.Models;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Core.Tools.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -17,33 +17,55 @@ namespace Bit.Api.Tools.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ReportsController : Controller
|
||||
{
|
||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
|
||||
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
|
||||
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
|
||||
|
||||
public ReportsController(
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
|
||||
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery,
|
||||
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
|
||||
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery
|
||||
)
|
||||
{
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
_currentContext = currentContext;
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
|
||||
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
|
||||
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Organization member information containing a list of cipher ids
|
||||
/// assigned
|
||||
/// </summary>
|
||||
/// <param name="orgId">Organzation Id</param>
|
||||
/// <returns>IEnumerable of MemberCipherDetailsResponseModel</returns>
|
||||
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
||||
[HttpGet("member-cipher-details/{orgId}")]
|
||||
public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId)
|
||||
{
|
||||
// Using the AccessReports permission here until new permissions
|
||||
// are needed for more control over reports
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
|
||||
|
||||
var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x));
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access details for an organization member. Includes the member information,
|
||||
/// group collection assignment, and item counts
|
||||
/// </summary>
|
||||
/// <param name="orgId">Organization Id</param>
|
||||
/// <returns>IEnumerable of MemberAccessReportResponseModel</returns>
|
||||
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
||||
[HttpGet("member-access/{orgId}")]
|
||||
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
||||
{
|
||||
@ -52,26 +74,91 @@ public class ReportsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
||||
new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
IncludeCollections = true,
|
||||
IncludeGroups = true
|
||||
});
|
||||
var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
|
||||
|
||||
var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
|
||||
var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(orgId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x));
|
||||
|
||||
var reports = MemberAccessReportResponseModel.CreateReport(
|
||||
orgGroups,
|
||||
orgCollectionsWithAccess,
|
||||
orgItems,
|
||||
organizationUsersTwoFactorEnabled,
|
||||
orgAbility);
|
||||
return reports;
|
||||
return responses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the organization member info, the cipher ids associated with the member,
|
||||
/// and details on their collections, groups, and permissions
|
||||
/// </summary>
|
||||
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>
|
||||
/// <returns>IEnumerable of MemberAccessCipherDetails</returns>
|
||||
private async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request)
|
||||
{
|
||||
var memberCipherDetails =
|
||||
await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request);
|
||||
return memberCipherDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the password health report applications for an organization
|
||||
/// </summary>
|
||||
/// <param name="orgId">A valid Organization Id</param>
|
||||
/// <returns>An Enumerable of PasswordHealthReportApplication </returns>
|
||||
/// <exception cref="NotFoundException">If the user lacks access</exception>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
[HttpGet("password-health-report-applications/{orgId}")]
|
||||
public async Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplications(Guid orgId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return await _getPwdHealthReportAppQuery.GetPasswordHealthReportApplicationAsync(orgId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new record into PasswordHealthReportApplication
|
||||
/// </summary>
|
||||
/// <param name="request">A single instance of PasswordHealthReportApplication Model</param>
|
||||
/// <returns>A single instance of PasswordHealthReportApplication</returns>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
/// <exception cref="NotFoundException">If the user lacks access</exception>
|
||||
[HttpPost("password-health-report-application")]
|
||||
public async Task<PasswordHealthReportApplication> AddPasswordHealthReportApplication(
|
||||
[FromBody] PasswordHealthReportApplicationModel request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(request.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var commandRequest = new AddPasswordHealthReportApplicationRequest
|
||||
{
|
||||
OrganizationId = request.OrganizationId,
|
||||
Url = request.Url
|
||||
};
|
||||
|
||||
return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple records into PasswordHealthReportApplication
|
||||
/// </summary>
|
||||
/// <param name="request">A enumerable of PasswordHealthReportApplicationModel</param>
|
||||
/// <returns>An Enumerable of PasswordHealthReportApplication</returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the OrganizationId</exception>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
[HttpPost("password-health-report-applications")]
|
||||
public async Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplications(
|
||||
[FromBody] IEnumerable<PasswordHealthReportApplicationModel> request)
|
||||
{
|
||||
if (request.Any(_ => _currentContext.AccessReports(_.OrganizationId).Result == false))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var commandRequests = request.Select(request => new AddPasswordHealthReportApplicationRequest
|
||||
{
|
||||
OrganizationId = request.OrganizationId,
|
||||
Url = request.Url
|
||||
}).ToList();
|
||||
|
||||
return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Api.Tools.Models;
|
||||
|
||||
public class PasswordHealthReportApplicationModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
@ -1,30 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Api.Tools.Models.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Member access details. The individual item for the detailed member access
|
||||
/// report. A collection can be assigned directly to a user without a group or
|
||||
/// the user can be assigned to a collection through a group. Group level permissions
|
||||
/// can override collection level permissions.
|
||||
/// </summary>
|
||||
public class MemberAccessReportAccessDetails
|
||||
{
|
||||
public Guid? CollectionId { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public string GroupName { get; set; }
|
||||
public string CollectionName { get; set; }
|
||||
public int ItemCount { get; set; }
|
||||
public bool? ReadOnly { get; set; }
|
||||
public bool? HidePasswords { get; set; }
|
||||
public bool? Manage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the collections and group collections a user has access to including
|
||||
/// the permission level for the collection and group collection.
|
||||
@ -40,134 +17,18 @@ public class MemberAccessReportResponseModel
|
||||
public int TotalItemCount { get; set; }
|
||||
public Guid? UserGuid { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
public IEnumerable<MemberAccessReportAccessDetails> AccessDetails { get; set; }
|
||||
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a report for all members of an organization. Containing summary information
|
||||
/// such as item, collection, and group counts. As well as detailed information on the
|
||||
/// user and group collections along with their permissions
|
||||
/// </summary>
|
||||
/// <param name="orgGroups">Organization groups collection</param>
|
||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
||||
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
||||
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
||||
/// <returns>List of the MemberAccessReportResponseModel</returns>;
|
||||
public static IEnumerable<MemberAccessReportResponseModel> CreateReport(
|
||||
ICollection<Group> orgGroups,
|
||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
||||
OrganizationAbility orgAbility)
|
||||
public MemberAccessReportResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||
{
|
||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
|
||||
// Create a dictionary to lookup the group names later.
|
||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
||||
|
||||
// Get collections grouped and into a dictionary for counts
|
||||
var collectionItems = orgItems
|
||||
.SelectMany(x => x.CollectionIds,
|
||||
(x, b) => new { CipherId = x.Id, CollectionId = b })
|
||||
.GroupBy(y => y.CollectionId,
|
||||
(key, g) => new { CollectionId = key, Ciphers = g });
|
||||
var collectionItemCounts = collectionItems.ToDictionary(x => x.CollectionId, x => x.Ciphers.Count());
|
||||
|
||||
|
||||
// Loop through the org users and populate report and access data
|
||||
var memberAccessReport = new List<MemberAccessReportResponseModel>();
|
||||
foreach (var user in orgUsers)
|
||||
{
|
||||
// Take the collections/groups and create the access details items
|
||||
var groupAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
var userCollectionAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
foreach (var tCollect in orgCollectionsWithAccess)
|
||||
{
|
||||
var itemCounts = collectionItemCounts.TryGetValue(tCollect.Item1.Id, out var itemCount) ? itemCount : 0;
|
||||
if (tCollect.Item2.Groups.Count() > 0)
|
||||
{
|
||||
var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x =>
|
||||
new MemberAccessReportAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
GroupId = x.Id,
|
||||
GroupName = groupNameDictionary[x.Id],
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
});
|
||||
groupAccessDetails.AddRange(groupDetails);
|
||||
}
|
||||
|
||||
// All collections assigned to users and their permissions
|
||||
if (tCollect.Item2.Users.Count() > 0)
|
||||
{
|
||||
var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x =>
|
||||
new MemberAccessReportAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
});
|
||||
userCollectionAccessDetails.AddRange(userCollectionDetails);
|
||||
}
|
||||
}
|
||||
|
||||
var report = new MemberAccessReportResponseModel
|
||||
{
|
||||
UserName = user.Name,
|
||||
Email = user.Email,
|
||||
TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
|
||||
// Both the user's ResetPasswordKey must be set and the organization can UseResetPassword
|
||||
AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||
UserGuid = user.Id,
|
||||
UsesKeyConnector = user.UsesKeyConnector
|
||||
};
|
||||
|
||||
var userAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
if (user.Groups.Any())
|
||||
{
|
||||
var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
|
||||
userAccessDetails.AddRange(userGroups);
|
||||
}
|
||||
|
||||
// There can be edge cases where groups don't have a collection
|
||||
var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
|
||||
if (groupsWithoutCollections.Count() > 0)
|
||||
{
|
||||
var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessReportAccessDetails
|
||||
{
|
||||
GroupId = x,
|
||||
GroupName = groupNameDictionary[x],
|
||||
ItemCount = 0
|
||||
});
|
||||
userAccessDetails.AddRange(emptyGroups);
|
||||
}
|
||||
|
||||
if (user.Collections.Any())
|
||||
{
|
||||
var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
|
||||
userAccessDetails.AddRange(userCollections);
|
||||
}
|
||||
report.AccessDetails = userAccessDetails;
|
||||
|
||||
report.TotalItemCount = collectionItems
|
||||
.Where(x => report.AccessDetails.Any(y => x.CollectionId == y.CollectionId))
|
||||
.SelectMany(x => x.Ciphers)
|
||||
.GroupBy(g => g.CipherId).Select(grp => grp.FirstOrDefault())
|
||||
.Count();
|
||||
|
||||
// Distinct items only
|
||||
var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
|
||||
report.CollectionsCount = distinctItems.Count();
|
||||
report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
|
||||
memberAccessReport.Add(report);
|
||||
}
|
||||
return memberAccessReport;
|
||||
this.UserName = memberAccessCipherDetails.UserName;
|
||||
this.Email = memberAccessCipherDetails.Email;
|
||||
this.TwoFactorEnabled = memberAccessCipherDetails.TwoFactorEnabled;
|
||||
this.AccountRecoveryEnabled = memberAccessCipherDetails.AccountRecoveryEnabled;
|
||||
this.GroupsCount = memberAccessCipherDetails.GroupsCount;
|
||||
this.CollectionsCount = memberAccessCipherDetails.CollectionsCount;
|
||||
this.TotalItemCount = memberAccessCipherDetails.TotalItemCount;
|
||||
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
||||
this.AccessDetails = memberAccessCipherDetails.AccessDetails;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
namespace Bit.Api.Tools.Models.Response;
|
||||
|
||||
public class MemberCipherDetailsResponseModel
|
||||
{
|
||||
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
|
||||
/// the organization member
|
||||
/// </summary>
|
||||
public IEnumerable<string> CipherIds { get; set; }
|
||||
|
||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
||||
{
|
||||
this.UserName = memberAccessCipherDetails.UserName;
|
||||
this.Email = memberAccessCipherDetails.Email;
|
||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
||||
this.CipherIds = memberAccessCipherDetails.CipherIds;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
@ -94,20 +94,17 @@ public class ProviderEventService(
|
||||
|
||||
var unassignedEnterpriseSeats = enterpriseProviderPlan.SeatMinimum - enterpriseClientSeats ?? 0;
|
||||
|
||||
if (unassignedEnterpriseSeats > 0)
|
||||
invoiceItems.Add(new ProviderInvoiceItem
|
||||
{
|
||||
invoiceItems.Add(new ProviderInvoiceItem
|
||||
{
|
||||
ProviderId = parsedProviderId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceNumber = invoice.Number,
|
||||
ClientName = "Unassigned seats",
|
||||
PlanName = enterprisePlan.Name,
|
||||
AssignedSeats = unassignedEnterpriseSeats,
|
||||
UsedSeats = 0,
|
||||
Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice
|
||||
});
|
||||
}
|
||||
ProviderId = parsedProviderId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceNumber = invoice.Number,
|
||||
ClientName = "Unassigned seats",
|
||||
PlanName = enterprisePlan.Name,
|
||||
AssignedSeats = unassignedEnterpriseSeats,
|
||||
UsedSeats = 0,
|
||||
Total = unassignedEnterpriseSeats * discountedEnterpriseSeatPrice
|
||||
});
|
||||
}
|
||||
|
||||
if (teamsProviderPlan.PurchasedSeats is null or 0)
|
||||
@ -118,20 +115,17 @@ public class ProviderEventService(
|
||||
|
||||
var unassignedTeamsSeats = teamsProviderPlan.SeatMinimum - teamsClientSeats ?? 0;
|
||||
|
||||
if (unassignedTeamsSeats > 0)
|
||||
invoiceItems.Add(new ProviderInvoiceItem
|
||||
{
|
||||
invoiceItems.Add(new ProviderInvoiceItem
|
||||
{
|
||||
ProviderId = parsedProviderId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceNumber = invoice.Number,
|
||||
ClientName = "Unassigned seats",
|
||||
PlanName = teamsPlan.Name,
|
||||
AssignedSeats = unassignedTeamsSeats,
|
||||
UsedSeats = 0,
|
||||
Total = unassignedTeamsSeats * discountedTeamsSeatPrice
|
||||
});
|
||||
}
|
||||
ProviderId = parsedProviderId,
|
||||
InvoiceId = invoice.Id,
|
||||
InvoiceNumber = invoice.Number,
|
||||
ClientName = "Unassigned seats",
|
||||
PlanName = teamsPlan.Name,
|
||||
AssignedSeats = unassignedTeamsSeats,
|
||||
UsedSeats = 0,
|
||||
Total = unassignedTeamsSeats * discountedTeamsSeatPrice
|
||||
});
|
||||
}
|
||||
|
||||
await Task.WhenAll(invoiceItems.Select(providerInvoiceItemRepository.CreateAsync));
|
||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
@ -30,7 +29,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationService organizationService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
@ -39,7 +37,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_organizationService = organizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
@ -87,8 +87,7 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled)
|
||||
{
|
||||
var missingRequiredPolicyTypes = validator.RequiredPolicies
|
||||
.Where(requiredPolicyType =>
|
||||
savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
||||
.Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
|
||||
.ToList();
|
||||
|
||||
if (missingRequiredPolicyTypes.Count != 0)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -23,7 +24,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public SingleOrgPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -31,14 +34,18 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ICurrentContext currentContext,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@ -93,9 +100,21 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
|
||||
return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
|
||||
var validateDecryptionErrorMessage = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(validateDecryptionErrorMessage))
|
||||
{
|
||||
return validateDecryptionErrorMessage;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return "The Single organization policy is required for organizations that have enabled domain verification.";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ public class PolicyService : IPolicyService
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
|
||||
{
|
||||
throw new BadRequestException("Organization has verified domains.");
|
||||
throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,6 +154,7 @@ public static class FeatureFlagKeys
|
||||
public const string IntegrationPage = "pm-14505-admin-console-integration-page";
|
||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.35" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.45" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.37" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.47" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
|
43
src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs
Normal file
43
src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
public class MemberAccessDetails
|
||||
{
|
||||
public Guid? CollectionId { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public string GroupName { get; set; }
|
||||
public string CollectionName { get; set; }
|
||||
public int ItemCount { get; set; }
|
||||
public bool? ReadOnly { get; set; }
|
||||
public bool? HidePasswords { get; set; }
|
||||
public bool? Manage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CipherIds associated with the group/collection access
|
||||
/// </summary>
|
||||
public IEnumerable<string> CollectionCipherIds { get; set; }
|
||||
}
|
||||
|
||||
public class MemberAccessCipherDetails
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public bool AccountRecoveryEnabled { get; set; }
|
||||
public int GroupsCount { get; set; }
|
||||
public int CollectionsCount { get; set; }
|
||||
public int TotalItemCount { get; set; }
|
||||
public Guid? UserGuid { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The details for the member's collection access depending
|
||||
/// on the collections and groups they are assigned to
|
||||
/// </summary>
|
||||
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A distinct list of the cipher ids associated with
|
||||
/// the organization member
|
||||
/// </summary>
|
||||
public IEnumerable<string> CipherIds { get; set; }
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Requests;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand
|
||||
{
|
||||
private IOrganizationRepository _organizationRepo;
|
||||
private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo;
|
||||
|
||||
public AddPasswordHealthReportApplicationCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository)
|
||||
{
|
||||
_organizationRepo = organizationRepository;
|
||||
_passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository;
|
||||
}
|
||||
|
||||
public async Task<PasswordHealthReportApplication> AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request)
|
||||
{
|
||||
var (req, IsValid, errorMessage) = await ValidateRequestAsync(request);
|
||||
if (!IsValid)
|
||||
{
|
||||
throw new BadRequestException(errorMessage);
|
||||
}
|
||||
|
||||
var passwordHealthReportApplication = new PasswordHealthReportApplication
|
||||
{
|
||||
OrganizationId = request.OrganizationId,
|
||||
Uri = request.Url,
|
||||
};
|
||||
|
||||
passwordHealthReportApplication.SetNewId();
|
||||
|
||||
var data = await _passwordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplicationAsync(IEnumerable<AddPasswordHealthReportApplicationRequest> requests)
|
||||
{
|
||||
var requestsList = requests.ToList();
|
||||
|
||||
// create tasks to validate each request
|
||||
var tasks = requestsList.Select(async request =>
|
||||
{
|
||||
var (req, IsValid, errorMessage) = await ValidateRequestAsync(request);
|
||||
if (!IsValid)
|
||||
{
|
||||
throw new BadRequestException(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// run validations and allow exceptions to bubble
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// create PasswordHealthReportApplication entities
|
||||
var passwordHealthReportApplications = requestsList.Select(request =>
|
||||
{
|
||||
var pwdHealthReportApplication = new PasswordHealthReportApplication
|
||||
{
|
||||
OrganizationId = request.OrganizationId,
|
||||
Uri = request.Url,
|
||||
};
|
||||
pwdHealthReportApplication.SetNewId();
|
||||
return pwdHealthReportApplication;
|
||||
});
|
||||
|
||||
// create and return the entities
|
||||
var response = new List<PasswordHealthReportApplication>();
|
||||
foreach (var record in passwordHealthReportApplications)
|
||||
{
|
||||
var data = await _passwordHealthReportApplicationRepo.CreateAsync(record);
|
||||
response.Add(data);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task<Tuple<AddPasswordHealthReportApplicationRequest, bool, string>> ValidateRequestAsync(
|
||||
AddPasswordHealthReportApplicationRequest request)
|
||||
{
|
||||
// verify that the organization exists
|
||||
var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, false, "Invalid Organization");
|
||||
}
|
||||
|
||||
// ensure that we have a URL
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
{
|
||||
return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, false, "URL is required");
|
||||
}
|
||||
|
||||
return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, true, string.Empty);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery
|
||||
{
|
||||
private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo;
|
||||
|
||||
public GetPasswordHealthReportApplicationQuery(
|
||||
IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepo)
|
||||
{
|
||||
_passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepo;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplicationAsync(Guid organizationId)
|
||||
{
|
||||
if (organizationId == Guid.Empty)
|
||||
{
|
||||
throw new BadRequestException("OrganizationId is required.");
|
||||
}
|
||||
|
||||
return await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(organizationId);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Requests;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
|
||||
public interface IAddPasswordHealthReportApplicationCommand
|
||||
{
|
||||
Task<PasswordHealthReportApplication> AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request);
|
||||
Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplicationAsync(IEnumerable<AddPasswordHealthReportApplicationRequest> requests);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
|
||||
public interface IGetPasswordHealthReportApplicationQuery
|
||||
{
|
||||
Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplicationAsync(Guid organizationId);
|
||||
}
|
208
src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs
Normal file
208
src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs
Normal file
@ -0,0 +1,208 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
|
||||
{
|
||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public MemberAccessCipherDetailsQuery(
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
|
||||
)
|
||||
{
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
|
||||
{
|
||||
var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
||||
new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = request.OrganizationId,
|
||||
IncludeCollections = true,
|
||||
IncludeGroups = true
|
||||
});
|
||||
|
||||
var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId);
|
||||
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);
|
||||
var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId);
|
||||
var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
|
||||
var memberAccessCipherDetails = GenerateAccessData(
|
||||
orgGroups,
|
||||
orgCollectionsWithAccess,
|
||||
orgItems,
|
||||
organizationUsersTwoFactorEnabled,
|
||||
orgAbility
|
||||
);
|
||||
|
||||
return memberAccessCipherDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a report for all members of an organization. Containing summary information
|
||||
/// such as item, collection, and group counts. Including the cipherIds a member is assigned.
|
||||
/// Child collection includes detailed information on the user and group collections along
|
||||
/// with their permissions.
|
||||
/// </summary>
|
||||
/// <param name="orgGroups">Organization groups collection</param>
|
||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
||||
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
||||
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
||||
/// <returns>List of the MemberAccessCipherDetailsModel</returns>;
|
||||
private IEnumerable<MemberAccessCipherDetails> GenerateAccessData(
|
||||
ICollection<Group> orgGroups,
|
||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
||||
OrganizationAbility orgAbility)
|
||||
{
|
||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
|
||||
// Create a dictionary to lookup the group names later.
|
||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
||||
|
||||
// Get collections grouped and into a dictionary for counts
|
||||
var collectionItems = orgItems
|
||||
.SelectMany(x => x.CollectionIds,
|
||||
(cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
|
||||
.GroupBy(y => y.CollectionId,
|
||||
(key, ciphers) => new { CollectionId = key, Ciphers = ciphers });
|
||||
var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()));
|
||||
|
||||
// Loop through the org users and populate report and access data
|
||||
var memberAccessCipherDetails = new List<MemberAccessCipherDetails>();
|
||||
foreach (var user in orgUsers)
|
||||
{
|
||||
var groupAccessDetails = new List<MemberAccessDetails>();
|
||||
var userCollectionAccessDetails = new List<MemberAccessDetails>();
|
||||
foreach (var tCollect in orgCollectionsWithAccess)
|
||||
{
|
||||
var hasItems = itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items);
|
||||
var collectionCiphers = hasItems ? items.Select(x => x) : null;
|
||||
|
||||
var itemCounts = hasItems ? collectionCiphers.Count() : 0;
|
||||
if (tCollect.Item2.Groups.Count() > 0)
|
||||
{
|
||||
|
||||
var groupDetails = tCollect.Item2.Groups.Where((tCollectGroups) => user.Groups.Contains(tCollectGroups.Id)).Select(x =>
|
||||
new MemberAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
GroupId = x.Id,
|
||||
GroupName = groupNameDictionary[x.Id],
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
CollectionCipherIds = items
|
||||
});
|
||||
|
||||
groupAccessDetails.AddRange(groupDetails);
|
||||
}
|
||||
|
||||
// All collections assigned to users and their permissions
|
||||
if (tCollect.Item2.Users.Count() > 0)
|
||||
{
|
||||
var userCollectionDetails = tCollect.Item2.Users.Where((tCollectUser) => tCollectUser.Id == user.Id).Select(x =>
|
||||
new MemberAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
CollectionCipherIds = items
|
||||
});
|
||||
userCollectionAccessDetails.AddRange(userCollectionDetails);
|
||||
}
|
||||
}
|
||||
|
||||
var report = new MemberAccessCipherDetails
|
||||
{
|
||||
UserName = user.Name,
|
||||
Email = user.Email,
|
||||
TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
|
||||
// Both the user's ResetPasswordKey must be set and the organization can UseResetPassword
|
||||
AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||
UserGuid = user.Id,
|
||||
UsesKeyConnector = user.UsesKeyConnector
|
||||
};
|
||||
|
||||
var userAccessDetails = new List<MemberAccessDetails>();
|
||||
if (user.Groups.Any())
|
||||
{
|
||||
var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
|
||||
userAccessDetails.AddRange(userGroups);
|
||||
}
|
||||
|
||||
// There can be edge cases where groups don't have a collection
|
||||
var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
|
||||
if (groupsWithoutCollections.Count() > 0)
|
||||
{
|
||||
var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails
|
||||
{
|
||||
GroupId = x,
|
||||
GroupName = groupNameDictionary[x],
|
||||
ItemCount = 0
|
||||
});
|
||||
userAccessDetails.AddRange(emptyGroups);
|
||||
}
|
||||
|
||||
if (user.Collections.Any())
|
||||
{
|
||||
var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
|
||||
userAccessDetails.AddRange(userCollections);
|
||||
}
|
||||
report.AccessDetails = userAccessDetails;
|
||||
|
||||
var userCiphers =
|
||||
report.AccessDetails
|
||||
.Where(x => x.ItemCount > 0)
|
||||
.SelectMany(y => y.CollectionCipherIds)
|
||||
.Distinct();
|
||||
report.CipherIds = userCiphers;
|
||||
report.TotalItemCount = userCiphers.Count();
|
||||
|
||||
// Distinct items only
|
||||
var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
|
||||
report.CollectionsCount = distinctItems.Count();
|
||||
report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
|
||||
memberAccessCipherDetails.Add(report);
|
||||
}
|
||||
return memberAccessCipherDetails;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
|
||||
public interface IMemberAccessCipherDetailsQuery
|
||||
{
|
||||
Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
public static class ReportingServiceCollectionExtensions
|
||||
{
|
||||
public static void AddReportingServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>();
|
||||
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
|
||||
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Tools.ReportFeatures.Requests;
|
||||
|
||||
public class MemberAccessCipherDetailsRequest
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Tools.Requests;
|
||||
|
||||
public class AddPasswordHealthReportApplicationRequest
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
@ -95,6 +95,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
||||
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();
|
||||
services
|
||||
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
|
||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||
|
||||
if (selfHosted)
|
||||
{
|
||||
|
@ -34,6 +34,7 @@ using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault;
|
||||
@ -116,6 +117,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddLoginServices();
|
||||
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
|
||||
services.AddVaultServices();
|
||||
services.AddReportingServices();
|
||||
}
|
||||
|
||||
public static void AddTokenizers(this IServiceCollection services)
|
||||
|
@ -0,0 +1,69 @@
|
||||
using AutoFixture;
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;
|
||||
|
||||
public class PolicyDetailResponsesTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndHasVerifiedDomains_ThenShouldNotBeAbleToToggle()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.SingleOrg)
|
||||
.Create();
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
|
||||
Assert.False(result.CanToggleState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.TwoFactorAuthentication)
|
||||
.Create();
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
var action = async () => await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>("policy", action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.SingleOrg)
|
||||
.Create();
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
|
||||
Assert.True(result.CanToggleState);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Public.Models.Response;
|
||||
|
||||
|
||||
public class MemberResponseModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResetPasswordEnrolled_ShouldBeTrue_WhenUserHasResetPasswordKey()
|
||||
{
|
||||
// Arrange
|
||||
var user = Substitute.For<OrganizationUser>();
|
||||
var collections = Substitute.For<IEnumerable<CollectionAccessSelection>>();
|
||||
user.ResetPasswordKey = "none-empty";
|
||||
|
||||
|
||||
// Act
|
||||
var sut = new MemberResponseModel(user, collections);
|
||||
|
||||
// Assert
|
||||
Assert.True(sut.ResetPasswordEnrolled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetPasswordEnrolled_ShouldBeFalse_WhenUserDoesNotHaveResetPasswordKey()
|
||||
{
|
||||
// Arrange
|
||||
var user = Substitute.For<OrganizationUser>();
|
||||
var collections = Substitute.For<IEnumerable<CollectionAccessSelection>>();
|
||||
|
||||
// Act
|
||||
var sut = new MemberResponseModel(user, collections);
|
||||
|
||||
// Assert
|
||||
Assert.False(sut.ResetPasswordEnrolled);
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@ -157,7 +157,7 @@ public class PoliciesControllerTests
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyResponseModel>(result);
|
||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
||||
Assert.Equal(policy.Id, result.Id);
|
||||
Assert.Equal(policy.Type, result.Type);
|
||||
Assert.Equal(policy.Enabled, result.Enabled);
|
||||
@ -182,7 +182,7 @@ public class PoliciesControllerTests
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyResponseModel>(result);
|
||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
||||
Assert.Equal(result.Type, (PolicyType)type);
|
||||
Assert.False(result.Enabled);
|
||||
}
|
||||
|
49
test/Api.Test/Tools/Controllers/ReportsControllerTests.cs
Normal file
49
test/Api.Test/Tools/Controllers/ReportsControllerTests.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Bit.Api.Tools.Controllers;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Tools.Controllers;
|
||||
|
||||
|
||||
[ControllerCustomize(typeof(ReportsController))]
|
||||
[SutProviderCustomize]
|
||||
public class ReportsControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPasswordHealthReportApplicationAsync_Success(SutProvider<ReportsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);
|
||||
|
||||
// Act
|
||||
var orgId = Guid.NewGuid();
|
||||
var result = await sutProvider.Sut.GetPasswordHealthReportApplications(orgId);
|
||||
|
||||
// Assert
|
||||
_ = sutProvider.GetDependency<IGetPasswordHealthReportApplicationQuery>()
|
||||
.Received(1)
|
||||
.GetPasswordHealthReportApplicationAsync(Arg.Is<Guid>(_ => _ == orgId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
var orgId = Guid.NewGuid();
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetPasswordHealthReportApplications(orgId));
|
||||
|
||||
// Assert
|
||||
_ = sutProvider.GetDependency<IGetPasswordHealthReportApplicationQuery>()
|
||||
.Received(0);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -842,6 +842,6 @@ public class PolicyServiceTests
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policy, null));
|
||||
|
||||
Assert.Equal("Organization has verified domains.", badRequestException.Message);
|
||||
Assert.Equal("The Single organization policy is required for organizations that have enabled domain verification.", badRequestException.Message);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.Requests;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AddPasswordHealthReportApplicationCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_WithValidRequest_ShouldReturnPasswordHealthReportApplication(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<AddPasswordHealthReportApplicationRequest>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.Create<Organization>());
|
||||
|
||||
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
|
||||
.CreateAsync(Arg.Any<PasswordHealthReportApplication>())
|
||||
.Returns(c => c.Arg<PasswordHealthReportApplication>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldThrowError(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Create<AddPasswordHealthReportApplicationRequest>();
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(null as Organization);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_WithInvalidUrl_ShouldThrowError(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()
|
||||
.Without(_ => _.Url)
|
||||
.Create();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.Create<Organization>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));
|
||||
Assert.Equal("URL is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()
|
||||
.Without(_ => _.OrganizationId)
|
||||
.CreateMany(2).ToList();
|
||||
|
||||
request[0].OrganizationId = Guid.NewGuid();
|
||||
request[1].OrganizationId = Guid.Empty;
|
||||
|
||||
// only return an organization for the first request and null for the second
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Is<Guid>(x => x == request[0].OrganizationId))
|
||||
.Returns(fixture.Create<Organization>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));
|
||||
Assert.Equal("Invalid Organization", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidUrl_ShouldThrowError(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()
|
||||
.CreateMany(2).ToList();
|
||||
|
||||
request[1].Url = string.Empty;
|
||||
|
||||
// return an organization for both requests
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.Create<Organization>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));
|
||||
Assert.Equal("URL is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithValidRequest_ShouldBeSuccessful(
|
||||
SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var request = fixture.CreateMany<AddPasswordHealthReportApplicationRequest>(2);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.Create<Organization>());
|
||||
|
||||
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
|
||||
.CreateAsync(Arg.Any<PasswordHealthReportApplication>())
|
||||
.Returns(c => c.Arg<PasswordHealthReportApplication>());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Count() == 2);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().Received(2);
|
||||
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>().Received(2);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.ReportFeatures;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetPasswordHealthReportApplicationQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPasswordHealthReportApplicationAsync_WithValidOrganizationId_ShouldReturnPasswordHealthReportApplication(
|
||||
SutProvider<GetPasswordHealthReportApplicationQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
var organizationId = fixture.Create<Guid>();
|
||||
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Any<Guid>())
|
||||
.Returns(fixture.CreateMany<PasswordHealthReportApplication>(2).ToList());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(organizationId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldFail(
|
||||
SutProvider<GetPasswordHealthReportApplicationQuery> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var fixture = new Fixture();
|
||||
sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()
|
||||
.GetByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))
|
||||
.Returns(new List<PasswordHealthReportApplication>());
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(Guid.Empty));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("OrganizationId is required.", exception.Message);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user