-
Notifications
You must be signed in to change notification settings - Fork 1.5k
PM-31920 adding the whole report endpoints v2 #7079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
99bc641
8002621
d9b1bb7
5d3f7b7
65092cb
b6def67
ffa2e80
5852cba
0abca22
ed8116d
a41cbf5
afe7c61
e5e3b68
f821b55
a2f6db2
6b6f857
f0e0b51
d9d3e1b
c65a75e
afe4df2
5326782
0e48567
ad38c67
1c4e37f
37b79c6
4bb40d0
adaf501
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using Bit.Core.Enums; | ||
|
|
||
| namespace Bit.Api.Dirt.Models.Response; | ||
|
|
||
| public class OrganizationReportFileResponseModel | ||
| { | ||
| public OrganizationReportFileResponseModel() { } | ||
|
|
||
| public string ReportFileUploadUrl { get; set; } = string.Empty; | ||
| public OrganizationReportResponseModel ReportResponse { get; set; } = null!; | ||
| public FileUploadType FileUploadType { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| using Bit.Core.Dirt.Entities; | ||
| using Bit.Core.Dirt.Models.Data; | ||
|
|
||
| namespace Bit.Api.Dirt.Models.Response; | ||
|
|
||
|
|
@@ -10,11 +11,10 @@ public class OrganizationReportResponseModel | |
| public string? ContentEncryptionKey { get; set; } | ||
| public string? SummaryData { get; set; } | ||
| public string? ApplicationData { get; set; } | ||
| public int? PasswordCount { get; set; } | ||
| public int? PasswordAtRiskCount { get; set; } | ||
| public int? MemberCount { get; set; } | ||
| public DateTime? CreationDate { get; set; } = null; | ||
| public DateTime? RevisionDate { get; set; } = null; | ||
| public ReportFile? ReportFile { get; set; } | ||
| public string? ReportFileDownloadUrl { get; set; } | ||
| public DateTime? CreationDate { get; set; } | ||
| public DateTime? RevisionDate { get; set; } | ||
|
|
||
| public OrganizationReportResponseModel(OrganizationReport organizationReport) | ||
| { | ||
|
|
@@ -29,9 +29,7 @@ public OrganizationReportResponseModel(OrganizationReport organizationReport) | |
| ContentEncryptionKey = organizationReport.ContentEncryptionKey; | ||
| SummaryData = organizationReport.SummaryData; | ||
| ApplicationData = organizationReport.ApplicationData; | ||
| PasswordCount = organizationReport.PasswordCount; | ||
| PasswordAtRiskCount = organizationReport.PasswordAtRiskCount; | ||
| MemberCount = organizationReport.MemberCount; | ||
| ReportFile = organizationReport.GetReportFile(); | ||
| CreationDate = organizationReport.CreationDate; | ||
| RevisionDate = organizationReport.RevisionDate; | ||
| } | ||
|
Comment on lines
19
to
35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The response model only maps a subset of the entity's metric fields ( |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| using Bit.Core.Dirt.Entities; | ||
| using Bit.Core.Dirt.Models.Data; | ||
| using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; | ||
| using Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
| using Bit.Core.Dirt.Repositories; | ||
| using Bit.Core.Exceptions; | ||
| using Bit.Core.Repositories; | ||
| using Bit.Core.Utilities; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures; | ||
|
|
||
| public class CreateOrganizationReportCommand : ICreateOrganizationReportCommand | ||
| { | ||
| private readonly IOrganizationRepository _organizationRepo; | ||
| private readonly IOrganizationReportRepository _organizationReportRepo; | ||
| private readonly ILogger<CreateOrganizationReportCommand> _logger; | ||
|
|
||
| public CreateOrganizationReportCommand( | ||
| IOrganizationRepository organizationRepository, | ||
| IOrganizationReportRepository organizationReportRepository, | ||
| ILogger<CreateOrganizationReportCommand> logger) | ||
| { | ||
| _organizationRepo = organizationRepository; | ||
| _organizationReportRepo = organizationReportRepository; | ||
| _logger = logger; | ||
| } | ||
|
|
||
| public async Task<OrganizationReport> CreateAsync(AddOrganizationReportRequest request) | ||
| { | ||
| _logger.LogInformation(Constants.BypassFiltersEventId, | ||
| "Creating organization report for organization {organizationId}", request.OrganizationId); | ||
|
|
||
| var (isValid, errorMessage) = await ValidateRequestAsync(request); | ||
| if (!isValid) | ||
| { | ||
| _logger.LogInformation(Constants.BypassFiltersEventId, | ||
| "Failed to create organization {organizationId} report: {errorMessage}", | ||
| request.OrganizationId, errorMessage); | ||
| throw new BadRequestException(errorMessage); | ||
| } | ||
|
|
||
| var fileData = new ReportFile | ||
| { | ||
| Id = CoreHelpers.SecureRandomString(32, upper: false, special: false), | ||
| FileName = "report-data.json", | ||
| Size = request.FileSize ?? 0, | ||
| Validated = false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a fail-open default for a security-sensitive field. If The safe default should be |
||
| }; | ||
|
|
||
| var organizationReport = new OrganizationReport | ||
| { | ||
| OrganizationId = request.OrganizationId, | ||
| CreationDate = DateTime.UtcNow, | ||
| ContentEncryptionKey = request.ContentEncryptionKey ?? string.Empty, | ||
| SummaryData = request.SummaryData, | ||
| ApplicationData = request.ApplicationData, | ||
| ApplicationCount = request.ReportMetrics?.ApplicationCount, | ||
| ApplicationAtRiskCount = request.ReportMetrics?.ApplicationAtRiskCount, | ||
| CriticalApplicationCount = request.ReportMetrics?.CriticalApplicationCount, | ||
| CriticalApplicationAtRiskCount = request.ReportMetrics?.CriticalApplicationAtRiskCount, | ||
| MemberCount = request.ReportMetrics?.MemberCount, | ||
| MemberAtRiskCount = request.ReportMetrics?.MemberAtRiskCount, | ||
| CriticalMemberCount = request.ReportMetrics?.CriticalMemberCount, | ||
| CriticalMemberAtRiskCount = request.ReportMetrics?.CriticalMemberAtRiskCount, | ||
| PasswordCount = request.ReportMetrics?.PasswordCount, | ||
| PasswordAtRiskCount = request.ReportMetrics?.PasswordAtRiskCount, | ||
| CriticalPasswordCount = request.ReportMetrics?.CriticalPasswordCount, | ||
| CriticalPasswordAtRiskCount = request.ReportMetrics?.CriticalPasswordAtRiskCount, | ||
| RevisionDate = DateTime.UtcNow | ||
| }; | ||
| organizationReport.SetReportFile(fileData); | ||
|
|
||
| var data = await _organizationReportRepo.CreateAsync(organizationReport); | ||
|
|
||
| _logger.LogInformation(Constants.BypassFiltersEventId, | ||
| "Successfully created organization report for organization {organizationId}, {organizationReportId}", | ||
| request.OrganizationId, data.Id); | ||
|
|
||
| return data; | ||
| } | ||
|
|
||
| private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync( | ||
| AddOrganizationReportRequest request) | ||
| { | ||
| var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); | ||
| if (organization == null) | ||
| { | ||
| return (false, "Invalid Organization"); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey)) | ||
| { | ||
| return (false, "Content Encryption Key is required"); | ||
| } | ||
|
|
||
| return (true, string.Empty); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| using Bit.Core.Dirt.Entities; | ||
| using Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; | ||
|
|
||
| public interface ICreateOrganizationReportCommand | ||
| { | ||
| Task<OrganizationReport> CreateAsync(AddOrganizationReportRequest request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| using Bit.Core.Dirt.Entities; | ||
| using Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; | ||
|
|
||
| public interface IUpdateOrganizationReportV2Command | ||
| { | ||
| Task<OrganizationReport> UpdateAsync(UpdateOrganizationReportV2Request request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| using Bit.Core.Dirt.Entities; | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; | ||
|
|
||
| public interface IValidateOrganizationReportFileCommand | ||
| { | ||
| Task<bool> ValidateAsync(OrganizationReport report, string reportFileId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
|
|
||
| public class OrganizationReportMetrics | ||
| { | ||
| [JsonPropertyName("totalApplicationCount")] | ||
| public int? ApplicationCount { get; set; } = null; | ||
| [JsonPropertyName("totalAtRiskApplicationCount")] | ||
| public int? ApplicationAtRiskCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalApplicationCount")] | ||
| public int? CriticalApplicationCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalAtRiskApplicationCount")] | ||
| public int? CriticalApplicationAtRiskCount { get; set; } = null; | ||
| [JsonPropertyName("totalMemberCount")] | ||
| public int? MemberCount { get; set; } = null; | ||
| [JsonPropertyName("totalAtRiskMemberCount")] | ||
| public int? MemberAtRiskCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalMemberCount")] | ||
| public int? CriticalMemberCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalAtRiskMemberCount")] | ||
| public int? CriticalMemberAtRiskCount { get; set; } = null; | ||
| [JsonPropertyName("totalPasswordCount")] | ||
| public int? PasswordCount { get; set; } = null; | ||
| [JsonPropertyName("totalAtRiskPasswordCount")] | ||
| public int? PasswordAtRiskCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalPasswordCount")] | ||
| public int? CriticalPasswordCount { get; set; } = null; | ||
| [JsonPropertyName("totalCriticalAtRiskPasswordCount")] | ||
| public int? CriticalPasswordAtRiskCount { get; set; } = null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,8 @@ | ||
| // FIXME: Update this file to be null safe and then delete the line below | ||
| #nullable disable | ||
|
|
||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
|
|
||
| public class UpdateOrganizationReportDataRequest | ||
| { | ||
| public Guid OrganizationId { get; set; } | ||
| public Guid ReportId { get; set; } | ||
| public string ReportData { get; set; } | ||
| public string? ReportData { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; | ||
|
|
||
| public class UpdateOrganizationReportV2Request | ||
| { | ||
| public Guid ReportId { get; set; } | ||
| public Guid OrganizationId { get; set; } | ||
| public string? ReportData { get; set; } | ||
| public string? ContentEncryptionKey { get; set; } | ||
| public string? SummaryData { get; set; } | ||
| public string? ApplicationData { get; set; } | ||
| public OrganizationReportMetrics? ReportMetrics { get; set; } | ||
| public bool RequiresNewFileUpload { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Estimated size of the report file in bytes. Required when RequiresNewFileUpload is true. | ||
| /// </summary> | ||
| public long? FileSize { get; set; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Information leak: The
ReportFileobject (containing internal fields likeId,Size,Validated,FileName) is exposed directly to API consumers via the response model. TheIdis aCoreHelpers.SecureRandomStringthat's used as a path component for blob storage — leaking it gives clients knowledge of internal storage paths.Consider either removing this property from the response model (the download URL is already exposed via
ReportFileDownloadUrl), or creating a slimmed-down DTO that only exposes what the client needs (e.g.,FileNameandSize).