From 0e4d73bb0fabedb22bb5d9b0351d9bb0144fc50e Mon Sep 17 00:00:00 2001 From: Ludovic Vielle Date: Wed, 3 Jun 2026 22:19:17 +0200 Subject: [PATCH] Migrate audit reports to the files table Signed-off-by: Ludovic Vielle --- e2e/console/audit_test.go | 58 ++-- pkg/coredata/audit.go | 82 +++-- pkg/coredata/audit_log_resource_type.go | 2 - pkg/coredata/entity_type_reg.go | 4 +- pkg/coredata/file.go | 57 ++++ pkg/coredata/report.go | 295 ------------------ pkg/coredata/trust_center_document_access.go | 102 +++--- pkg/probo/audit_service.go | 127 ++++---- pkg/probo/report_service.go | 131 -------- pkg/probo/service.go | 2 - pkg/probo/trust_center_access_service.go | 4 +- pkg/server/api/console/v1/audit_resolvers.go | 78 +---- pkg/server/api/console/v1/base_resolvers.go | 10 - .../api/console/v1/dataloader/dataloader.go | 18 -- .../api/console/v1/graphql/audit.graphql | 17 +- .../console/v1/graphql/trust_center.graphql | 3 +- .../api/console/v1/trust_center_resolvers.go | 45 ++- pkg/server/api/console/v1/types/audit.go | 6 +- pkg/server/api/console/v1/types/report.go | 31 -- .../v1/types/trust_center_document_access.go | 12 +- pkg/server/api/mcp/v1/schema.resolvers.go | 20 +- pkg/server/api/mcp/v1/types/audit.go | 10 +- pkg/server/api/slack/v1/slack_handler.go | 2 +- pkg/server/api/trust/v1/base_resolvers.go | 8 +- .../api/trust/v1/graphql/trust_center.graphql | 6 +- .../api/trust/v1/trust_center_resolvers.go | 193 ++++++------ .../v1/types/{report.go => audit_report.go} | 8 +- pkg/slack/slack_message_service.go | 11 +- pkg/trust/audit_service.go | 7 +- pkg/trust/report_service.go | 55 ++-- pkg/trust/trust_center_access_service.go | 60 ++-- 31 files changed, 517 insertions(+), 947 deletions(-) delete mode 100644 pkg/coredata/report.go delete mode 100644 pkg/probo/report_service.go delete mode 100644 pkg/server/api/console/v1/types/report.go rename pkg/server/api/trust/v1/types/{report.go => audit_report.go} (88%) diff --git a/e2e/console/audit_test.go b/e2e/console/audit_test.go index 7f447f198..a39e7b695 100644 --- a/e2e/console/audit_test.go +++ b/e2e/console/audit_test.go @@ -1456,9 +1456,9 @@ func TestAudit_UploadReport(t *testing.T) { uploadAuditReport(input: $input) { audit { id - report { + reportFile { id - filename + fileName size } } @@ -1472,12 +1472,12 @@ func TestAudit_UploadReport(t *testing.T) { var result struct { UploadAuditReport struct { Audit struct { - ID string `json:"id"` - Report *struct { + ID string `json:"id"` + ReportFile *struct { ID string `json:"id"` - Filename string `json:"filename"` - Size int `json:"size"` - } `json:"report"` + FileName string `json:"fileName"` + Size int64 `json:"size"` + } `json:"reportFile"` } `json:"audit"` } `json:"uploadAuditReport"` } @@ -1495,9 +1495,9 @@ func TestAudit_UploadReport(t *testing.T) { require.NoError(t, err) assert.Equal(t, auditID, result.UploadAuditReport.Audit.ID) - require.NotNil(t, result.UploadAuditReport.Audit.Report) - assert.Equal(t, "audit-report.pdf", result.UploadAuditReport.Audit.Report.Filename) - assert.Equal(t, len(pdfContent), result.UploadAuditReport.Audit.Report.Size) + require.NotNil(t, result.UploadAuditReport.Audit.ReportFile) + assert.Equal(t, "audit-report.pdf", result.UploadAuditReport.Audit.ReportFile.FileName) + assert.Equal(t, int64(len(pdfContent)), result.UploadAuditReport.Audit.ReportFile.Size) }) t.Run("upload replaces existing report", func(t *testing.T) { @@ -1508,9 +1508,9 @@ func TestAudit_UploadReport(t *testing.T) { uploadAuditReport(input: $input) { audit { id - report { + reportFile { id - filename + fileName } } } @@ -1523,11 +1523,11 @@ func TestAudit_UploadReport(t *testing.T) { var result1 struct { UploadAuditReport struct { Audit struct { - ID string `json:"id"` - Report *struct { + ID string `json:"id"` + ReportFile *struct { ID string `json:"id"` - Filename string `json:"filename"` - } `json:"report"` + FileName string `json:"fileName"` + } `json:"reportFile"` } `json:"audit"` } `json:"uploadAuditReport"` } @@ -1544,7 +1544,7 @@ func TestAudit_UploadReport(t *testing.T) { }, &result1) require.NoError(t, err) - firstReportID := result1.UploadAuditReport.Audit.Report.ID + firstReportID := result1.UploadAuditReport.Audit.ReportFile.ID // Upload second report (should replace) pdfContent2 := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Version /1.4 >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF") @@ -1552,11 +1552,11 @@ func TestAudit_UploadReport(t *testing.T) { var result2 struct { UploadAuditReport struct { Audit struct { - ID string `json:"id"` - Report *struct { + ID string `json:"id"` + ReportFile *struct { ID string `json:"id"` - Filename string `json:"filename"` - } `json:"report"` + FileName string `json:"fileName"` + } `json:"reportFile"` } `json:"audit"` } `json:"uploadAuditReport"` } @@ -1573,8 +1573,8 @@ func TestAudit_UploadReport(t *testing.T) { }, &result2) require.NoError(t, err) - assert.Equal(t, "second-report.pdf", result2.UploadAuditReport.Audit.Report.Filename) - assert.NotEqual(t, firstReportID, result2.UploadAuditReport.Audit.Report.ID, "Report ID should change when replaced") + assert.Equal(t, "second-report.pdf", result2.UploadAuditReport.Audit.ReportFile.FileName) + assert.NotEqual(t, firstReportID, result2.UploadAuditReport.Audit.ReportFile.ID, "Report file ID should change when replaced") }) } @@ -1782,7 +1782,7 @@ func TestAudit_DeleteReport(t *testing.T) { uploadAuditReport(input: $input) { audit { id - report { + reportFile { id } } @@ -1810,7 +1810,7 @@ func TestAudit_DeleteReport(t *testing.T) { deleteAuditReport(input: $input) { audit { id - report { + reportFile { id } } @@ -1821,10 +1821,10 @@ func TestAudit_DeleteReport(t *testing.T) { var deleteResult struct { DeleteAuditReport struct { Audit struct { - ID string `json:"id"` - Report *struct { + ID string `json:"id"` + ReportFile *struct { ID string `json:"id"` - } `json:"report"` + } `json:"reportFile"` } `json:"audit"` } `json:"deleteAuditReport"` } @@ -1836,7 +1836,7 @@ func TestAudit_DeleteReport(t *testing.T) { }, &deleteResult) require.NoError(t, err) assert.Equal(t, auditID, deleteResult.DeleteAuditReport.Audit.ID) - assert.Nil(t, deleteResult.DeleteAuditReport.Audit.Report, "Report should be nil after deletion") + assert.Nil(t, deleteResult.DeleteAuditReport.Audit.ReportFile, "Report file should be nil after deletion") }) } diff --git a/pkg/coredata/audit.go b/pkg/coredata/audit.go index a98606aa1..d3dd4b284 100644 --- a/pkg/coredata/audit.go +++ b/pkg/coredata/audit.go @@ -34,7 +34,7 @@ type ( Name *string `db:"name"` OrganizationID gid.GID `db:"organization_id"` FrameworkID gid.GID `db:"framework_id"` - ReportID *gid.GID `db:"report_id"` + ReportFileID *gid.GID `db:"report_file_id"` ValidFrom *time.Time `db:"valid_from"` ValidUntil *time.Time `db:"valid_until"` State AuditState `db:"state"` @@ -113,7 +113,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -199,7 +199,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -250,7 +250,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -299,7 +299,7 @@ INSERT INTO audits ( tenant_id, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -312,7 +312,7 @@ INSERT INTO audits ( @tenant_id, @organization_id, @framework_id, - @report_id, + @report_file_id, @valid_from, @valid_until, @state, @@ -328,7 +328,7 @@ INSERT INTO audits ( "tenant_id": scope.GetTenantID(), "organization_id": a.OrganizationID, "framework_id": a.FrameworkID, - "report_id": a.ReportID, + "report_file_id": a.ReportFileID, "valid_from": a.ValidFrom, "valid_until": a.ValidUntil, "state": a.State, @@ -354,7 +354,7 @@ func (a *Audit) Update( UPDATE audits SET name = @name, - report_id = @report_id, + report_file_id = @report_file_id, valid_from = @valid_from, valid_until = @valid_until, state = @state, @@ -370,7 +370,7 @@ WHERE args := pgx.StrictNamedArgs{ "id": a.ID, "name": a.Name, - "report_id": a.ReportID, + "report_file_id": a.ReportFileID, "valid_from": a.ValidFrom, "valid_until": a.ValidUntil, "state": a.State, @@ -427,7 +427,7 @@ WITH audits_by_control AS ( a.name, a.organization_id, a.framework_id, - a.report_id, + a.report_file_id, a.valid_from, a.valid_until, a.state, @@ -446,7 +446,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -494,7 +494,7 @@ WITH audits_by_finding AS ( a.name, a.organization_id, a.framework_id, - a.report_id, + a.report_file_id, a.valid_from, a.valid_until, a.state, @@ -513,7 +513,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -632,11 +632,11 @@ WHERE return count, nil } -func (a *Audit) LoadByReportID( +func (a *Audit) LoadByReportFileID( ctx context.Context, conn pg.Querier, scope Scoper, - reportID gid.GID, + fileID gid.GID, ) error { q := ` SELECT @@ -644,7 +644,7 @@ SELECT name, organization_id, framework_id, - report_id, + report_file_id, valid_from, valid_until, state, @@ -654,12 +654,12 @@ SELECT FROM audits WHERE %s - AND report_id = @report_id + AND report_file_id = @report_file_id LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) - args := pgx.StrictNamedArgs{"report_id": reportID} + args := pgx.StrictNamedArgs{"report_file_id": fileID} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) @@ -681,6 +681,52 @@ LIMIT 1; return nil } +func (as *Audits) LoadByReportFileIDs( + ctx context.Context, + conn pg.Querier, + scope Scoper, + fileIDs []gid.GID, +) error { + q := ` +SELECT + id, + name, + organization_id, + framework_id, + report_file_id, + valid_from, + valid_until, + state, + trust_center_visibility, + created_at, + updated_at +FROM + audits +WHERE + %s + AND report_file_id = ANY(@file_ids) +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"file_ids": fileIDs} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query audits by report file IDs: %w", err) + } + + audits, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Audit]) + if err != nil { + return fmt.Errorf("cannot collect audits by report file IDs: %w", err) + } + + *as = audits + + return nil +} + func (as *Audits) LoadByReportIDs( ctx context.Context, conn pg.Querier, diff --git a/pkg/coredata/audit_log_resource_type.go b/pkg/coredata/audit_log_resource_type.go index 02d7888ef..1108683fd 100644 --- a/pkg/coredata/audit_log_resource_type.go +++ b/pkg/coredata/audit_log_resource_type.go @@ -53,8 +53,6 @@ func ResourceTypeName(entityType uint16) string { return "Datum" case AuditEntityType: return "Audit" - case ReportEntityType: - return "Report" case TrustCenterEntityType: return "TrustCenter" case TrustCenterAccessEntityType: diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index a4340e699..25922f639 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -44,7 +44,7 @@ const ( AssetEntityType uint16 = 18 DatumEntityType uint16 = 19 AuditEntityType uint16 = 20 - ReportEntityType uint16 = 21 + _ uint16 = 21 // ReportEntityType - removed TrustCenterEntityType uint16 = 22 TrustCenterAccessEntityType uint16 = 23 ThirdPartyBusinessAssociateAgreementEntityType uint16 = 24 @@ -168,8 +168,6 @@ func NewEntityFromID(id gid.GID) (any, bool) { return &Datum{ID: id}, true case AuditEntityType: return &Audit{ID: id}, true - case ReportEntityType: - return &Report{ID: id}, true case TrustCenterEntityType: return &TrustCenter{ID: id}, true case TrustCenterAccessEntityType: diff --git a/pkg/coredata/file.go b/pkg/coredata/file.go index 842c2897d..c0b4b7cdc 100644 --- a/pkg/coredata/file.go +++ b/pkg/coredata/file.go @@ -157,6 +157,10 @@ LIMIT 1; return nil } +// LoadByIDs Loads every given files, whether they are active or not. See +// Files.LoadActiveByIDs for a safer option. +// +// DISCLAIMER: use with caution on user-facing features. func (f *Files) LoadByIDs( ctx context.Context, conn pg.Querier, @@ -269,6 +273,59 @@ VALUES ( return nil } +func (f *File) LoadActiveByID( + ctx context.Context, + conn pg.Querier, + scope Scoper, + fileID gid.GID, +) error { + q := ` +SELECT + id, + organization_id, + bucket_name, + mime_type, + file_name, + file_key, + file_size, + visibility, + created_at, + updated_at, + deleted_at +FROM + files +WHERE + %s + AND id = @file_id + AND deleted_at IS NULL +LIMIT 1; +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"file_id": fileID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query file: %w", err) + } + defer rows.Close() + + file, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[File]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + + return fmt.Errorf("cannot collect file: %w", err) + } + + *f = file + + return nil +} + func (f *File) LoadPublicByID( ctx context.Context, conn pg.Querier, diff --git a/pkg/coredata/report.go b/pkg/coredata/report.go deleted file mode 100644 index 4a4430d25..000000000 --- a/pkg/coredata/report.go +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "context" - "errors" - "fmt" - "maps" - "time" - - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/iam/policy" - "go.probo.inc/probo/pkg/page" -) - -type ( - Report struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - ObjectKey string `db:"object_key"` - MimeType string `db:"mime_type"` - Filename string `db:"filename"` - Size int64 `db:"size"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - } - - Reports []*Report -) - -func (r *Report) AuthorizationAttributes( - ctx context.Context, - conn pg.Querier, - resourceIDs []gid.GID, -) (policy.AttributesByID, error) { - q := `SELECT id, organization_id FROM reports WHERE id = ANY(@resource_ids::text[])` - - args := pgx.StrictNamedArgs{ - "resource_ids": resourceIDs, - } - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return nil, fmt.Errorf("cannot query authorization attributes: %w", err) - } - - defer rows.Close() - - attrsByID := make(policy.AttributesByID) - - for rows.Next() { - var id, organizationID gid.GID - - if err := rows.Scan(&id, &organizationID); err != nil { - return nil, fmt.Errorf("cannot scan authorization attributes: %w", err) - } - - attrsByID[id] = policy.Attributes{ - "organization_id": organizationID.String(), - } - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("cannot iterate authorization attributes: %w", err) - } - - return attrsByID, nil -} - -func (r *Report) LoadByID( - ctx context.Context, - conn pg.Querier, - scope Scoper, - reportID gid.GID, -) error { - q := ` -SELECT - id, - organization_id, - object_key, - mime_type, - filename, - size, - created_at, - updated_at -FROM - reports -WHERE - %s - AND id = @report_id -LIMIT 1; -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"report_id": reportID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query report: %w", err) - } - - report, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Report]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrResourceNotFound - } - - return fmt.Errorf("cannot collect report: %w", err) - } - - *r = report - - return nil -} - -func (r *Reports) LoadByIDs( - ctx context.Context, - conn pg.Querier, - scope Scoper, - reportIDs []gid.GID, -) error { - q := ` -SELECT - id, - organization_id, - object_key, - mime_type, - filename, - size, - created_at, - updated_at -FROM - reports -WHERE - %s - AND id = ANY(@report_ids); -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"report_ids": reportIDs} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query report: %w", err) - } - - reports, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Report]) - if err != nil { - return fmt.Errorf("cannot collect reports: %w", err) - } - - *r = reports - - return nil -} - -func (r *Report) Insert( - ctx context.Context, - conn pg.Tx, - scope Scoper, -) error { - q := ` -INSERT INTO reports ( - id, - tenant_id, - organization_id, - object_key, - mime_type, - filename, - size, - created_at, - updated_at -) VALUES ( - @id, - @tenant_id, - @organization_id, - @object_key, - @mime_type, - @filename, - @size, - @created_at, - @updated_at -) -` - - args := pgx.StrictNamedArgs{ - "id": r.ID, - "tenant_id": scope.GetTenantID(), - "organization_id": r.OrganizationID, - "object_key": r.ObjectKey, - "mime_type": r.MimeType, - "filename": r.Filename, - "size": r.Size, - "created_at": r.CreatedAt, - "updated_at": r.UpdatedAt, - } - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot insert report: %w", err) - } - - return nil -} - -func (r *Report) Update( - ctx context.Context, - conn pg.Tx, - scope Scoper, -) error { - q := ` -UPDATE reports -SET - object_key = @object_key, - mime_type = @mime_type, - filename = @filename, - size = @size, - updated_at = @updated_at -WHERE - %s - AND id = @id -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{ - "id": r.ID, - "object_key": r.ObjectKey, - "mime_type": r.MimeType, - "filename": r.Filename, - "size": r.Size, - "updated_at": r.UpdatedAt, - } - maps.Copy(args, scope.SQLArguments()) - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot update report: %w", err) - } - - return nil -} - -func (r *Report) Delete( - ctx context.Context, - conn pg.Tx, - scope Scoper, -) error { - q := ` -DELETE FROM reports -WHERE - %s - AND id = @id -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"id": r.ID} - maps.Copy(args, scope.SQLArguments()) - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot delete report: %w", err) - } - - return nil -} - -func (r *Report) CursorKey(orderBy ReportOrderField) page.CursorKey { - switch orderBy { - case ReportOrderFieldID: - return page.NewCursorKey(r.ID, r.ID) - } - - panic(fmt.Sprintf("unsupported order by: %s", orderBy)) -} diff --git a/pkg/coredata/trust_center_document_access.go b/pkg/coredata/trust_center_document_access.go index 5c876d5e3..4fe8cad58 100644 --- a/pkg/coredata/trust_center_document_access.go +++ b/pkg/coredata/trust_center_document_access.go @@ -35,7 +35,7 @@ type ( OrganizationID gid.GID `db:"organization_id"` TrustCenterAccessID gid.GID `db:"trust_center_access_id"` DocumentID *gid.GID `db:"document_id"` - ReportID *gid.GID `db:"report_id"` + ReportFileID *gid.GID `db:"report_file_id"` TrustCenterFileID *gid.GID `db:"trust_center_file_id"` Status TrustCenterDocumentAccessStatus `db:"status"` CreatedAt time.Time `db:"created_at"` @@ -105,7 +105,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -155,7 +155,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -196,12 +196,12 @@ LIMIT 1; return nil } -func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndReportID( +func (tcda *TrustCenterDocumentAccess) LoadByTrustCenterAccessIDAndReportFileID( ctx context.Context, conn pg.Querier, scope Scoper, trustCenterAccessID gid.GID, - reportID gid.GID, + reportFileID gid.GID, ) error { q := ` SELECT @@ -209,7 +209,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -219,7 +219,7 @@ FROM WHERE %s AND trust_center_access_id = @trust_center_access_id - AND report_id = @report_id + AND report_file_id = @report_file_id LIMIT 1; ` @@ -227,7 +227,7 @@ LIMIT 1; args := pgx.StrictNamedArgs{ "trust_center_access_id": trustCenterAccessID, - "report_id": reportID, + "report_file_id": reportFileID, } maps.Copy(args, scope.SQLArguments()) @@ -262,7 +262,7 @@ INSERT INTO trust_center_document_accesses ( organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -273,7 +273,7 @@ INSERT INTO trust_center_document_accesses ( @organization_id, @trust_center_access_id, @document_id, - @report_id, + @report_file_id, @trust_center_file_id, @status::trust_center_document_access_status, @created_at, @@ -287,7 +287,7 @@ INSERT INTO trust_center_document_accesses ( "organization_id": tcda.OrganizationID, "trust_center_access_id": tcda.TrustCenterAccessID, "document_id": tcda.DocumentID, - "report_id": tcda.ReportID, + "report_file_id": tcda.ReportFileID, "trust_center_file_id": tcda.TrustCenterFileID, "status": tcda.Status, "created_at": tcda.CreatedAt, @@ -300,7 +300,7 @@ INSERT INTO trust_center_document_accesses ( if pgErr.Code == "23505" { switch pgErr.ConstraintName { case "trust_center_document_accesse_trust_center_access_id_docume_key", - "trust_center_document_accesse_trust_center_access_id_report_key", + "trust_center_document_accesses_trust_center_access_id_report_file_key", "trust_center_document_accesses_trust_center_file_id_key": return ErrResourceAlreadyExists } @@ -496,7 +496,7 @@ all_items AS ( SELECT d.id AS item_id, d.id AS document_id, - NULL::text AS report_id, + NULL::text AS report_file_id, NULL::text AS trust_center_file_id, d.created_at AS item_created_at, d.updated_at AS item_updated_at @@ -508,23 +508,23 @@ all_items AS ( UNION ALL SELECT - r.report_id AS item_id, + r.report_file_id AS item_id, NULL::text AS document_id, - r.report_id AS report_id, + r.report_file_id AS report_file_id, NULL::text AS trust_center_file_id, r.created_at AS item_created_at, r.updated_at AS item_updated_at FROM audits r, tenant_organization o WHERE r.organization_id = o.organization_id AND r.trust_center_visibility = 'PRIVATE'::trust_center_visibility - AND r.report_id IS NOT NULL + AND r.report_file_id IS NOT NULL UNION ALL SELECT tcf.id AS item_id, NULL::text AS document_id, - NULL::text AS report_id, + NULL::text AS report_file_id, tcf.id AS trust_center_file_id, tcf.created_at AS item_created_at, tcf.updated_at AS item_updated_at @@ -542,7 +542,7 @@ final_items AS ( (SELECT organization_id FROM organization) AS organization_id, @trust_center_access_id AS trust_center_access_id, ai.document_id, - ai.report_id, + ai.report_file_id, ai.trust_center_file_id, COALESCE(tcda.status, 'REQUESTED'::trust_center_document_access_status) AS status, COALESCE(tcda.created_at, ai.item_created_at) AS created_at, @@ -552,7 +552,7 @@ final_items AS ( tcda.trust_center_access_id = @trust_center_access_id AND ( (tcda.document_id = ai.document_id AND ai.document_id IS NOT NULL) - OR (tcda.report_id = ai.report_id AND ai.report_id IS NOT NULL) + OR (tcda.report_file_id = ai.report_file_id AND ai.report_file_id IS NOT NULL) OR (tcda.trust_center_file_id = ai.trust_center_file_id AND ai.trust_center_file_id IS NOT NULL) ) ) @@ -562,7 +562,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -606,7 +606,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -714,12 +714,12 @@ WHERE return nil } -func GrantByReportIDs( +func GrantByReportFileIDs( ctx context.Context, conn pg.Querier, scope Scoper, trustCenterAccessID gid.GID, - reportIDs []gid.GID, + reportFileIDs []gid.GID, updatedAt time.Time, ) error { q := ` @@ -728,32 +728,32 @@ SET status = 'GRANTED'::trust_center_document_access_status, updated_at = @updat WHERE %s AND trust_center_access_id = @trust_center_access_id - AND report_id = ANY(@report_ids) + AND report_file_id = ANY(@report_file_ids) ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "trust_center_access_id": trustCenterAccessID, - "report_ids": reportIDs, + "report_file_ids": reportFileIDs, "updated_at": updatedAt, } maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) if err != nil { - return fmt.Errorf("cannot grant trust center document accesses by report IDs: %w", err) + return fmt.Errorf("cannot grant trust center document accesses by report file IDs: %w", err) } return nil } -func RejectOrRevokeByReportIDs( +func RejectOrRevokeByReportFileIDs( ctx context.Context, conn pg.Querier, scope Scoper, trustCenterAccessID gid.GID, - reportIDs []gid.GID, + reportFileIDs []gid.GID, updatedAt time.Time, ) error { q := ` @@ -767,21 +767,21 @@ SET WHERE %s AND trust_center_access_id = @trust_center_access_id - AND report_id = ANY(@report_ids) + AND report_file_id = ANY(@report_file_ids) ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "trust_center_access_id": trustCenterAccessID, - "report_ids": reportIDs, + "report_file_ids": reportFileIDs, "updated_at": updatedAt, } maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) if err != nil { - return fmt.Errorf("cannot reject trust center document accesses by report IDs: %w", err) + return fmt.Errorf("cannot reject trust center document accesses by report file IDs: %w", err) } return nil @@ -829,7 +829,7 @@ WHEN NOT MATCHED organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -887,7 +887,7 @@ WITH document_access_data AS ( @organization_id AS organization_id, @trust_center_access_id AS trust_center_access_id, unnest(@document_ids::text[]) AS document_id, - null::text AS report_id, + null::text AS report_file_id, null::text AS trust_center_file_id, @status::trust_center_document_access_status AS status, @created_at::timestamptz AS created_at, @@ -899,7 +899,7 @@ INSERT INTO trust_center_document_accesses ( organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -927,7 +927,7 @@ ON CONFLICT DO NOTHING return nil } -func (tcdas TrustCenterDocumentAccesses) MergeReportAccesses( +func (tcdas TrustCenterDocumentAccesses) MergeReportFileAccesses( ctx context.Context, conn pg.Querier, scope Scoper, @@ -947,7 +947,7 @@ WITH data AS ( ) MERGE INTO trust_center_document_accesses AS tcda USING data - ON data.id = tcda.report_id + ON data.id = tcda.report_file_id AND tcda.tenant_id = @tenant_id AND tcda.trust_center_access_id = @trust_center_access_id WHEN MATCHED @@ -955,7 +955,7 @@ WHEN MATCHED WHEN NOT MATCHED BY SOURCE AND tcda.tenant_id = @tenant_id AND tcda.trust_center_access_id = @trust_center_access_id - AND tcda.report_id IS NOT NULL + AND tcda.report_file_id IS NOT NULL THEN DELETE WHEN NOT MATCHED THEN INSERT ( @@ -964,7 +964,7 @@ WHEN NOT MATCHED organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -1000,29 +1000,29 @@ WHEN NOT MATCHED return nil } -func (tcdas TrustCenterDocumentAccesses) BulkInsertReportAccesses( +func (tcdas TrustCenterDocumentAccesses) BulkInsertReportFileAccesses( ctx context.Context, conn pg.Querier, scope Scoper, trustCenterAccessID gid.GID, organizationID gid.GID, - reportIDs []gid.GID, + reportFileIDs []gid.GID, status TrustCenterDocumentAccessStatus, createdAt time.Time, ) error { - if len(reportIDs) == 0 { + if len(reportFileIDs) == 0 { return nil } q := ` -WITH report_access_data AS ( +WITH report_file_access_data AS ( SELECT generate_gid(decode_base64_unpadded(@tenant_id), @trust_center_document_access_entity_type) AS id, @tenant_id AS tenant_id, @organization_id AS organization_id, @trust_center_access_id AS trust_center_access_id, null::text AS document_id, - unnest(@report_ids::text[]) AS report_id, + unnest(@report_file_ids::text[]) AS report_file_id, null::text AS trust_center_file_id, @status::trust_center_document_access_status AS status, @created_at::timestamptz AS created_at, @@ -1034,13 +1034,13 @@ INSERT INTO trust_center_document_accesses ( organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, updated_at ) -SELECT * FROM report_access_data +SELECT * FROM report_file_access_data ON CONFLICT DO NOTHING ` @@ -1049,14 +1049,14 @@ ON CONFLICT DO NOTHING "organization_id": organizationID, "trust_center_document_access_entity_type": TrustCenterDocumentAccessEntityType, "trust_center_access_id": trustCenterAccessID, - "report_ids": reportIDs, + "report_file_ids": reportFileIDs, "status": status, "created_at": createdAt, "updated_at": createdAt, } if _, err := conn.Exec(ctx, q, args); err != nil { - return fmt.Errorf("cannot bulk insert trust center report accesses: %w", err) + return fmt.Errorf("cannot bulk insert trust center report file accesses: %w", err) } return nil @@ -1075,7 +1075,7 @@ SELECT organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -1226,7 +1226,7 @@ WHEN NOT MATCHED organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, @@ -1280,7 +1280,7 @@ WITH trust_center_file_access_data AS ( @organization_id AS organization_id, @trust_center_access_id AS trust_center_access_id, null::text AS document_id, - null::text AS report_id, + null::text AS report_file_id, unnest(@trust_center_file_ids::text[]) AS trust_center_file_id, @status::trust_center_document_access_status AS status, @created_at::timestamptz AS created_at, @@ -1292,7 +1292,7 @@ INSERT INTO trust_center_document_accesses ( organization_id, trust_center_access_id, document_id, - report_id, + report_file_id, trust_center_file_id, status, created_at, diff --git a/pkg/probo/audit_service.go b/pkg/probo/audit_service.go index 9f08f0cd2..8e91616cf 100644 --- a/pkg/probo/audit_service.go +++ b/pkg/probo/audit_service.go @@ -19,8 +19,6 @@ import ( "fmt" "time" - "github.com/aws/aws-sdk-go-v2/service/s3" - "go.gearno.de/crypto/uuid" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/filevalidation" @@ -113,7 +111,11 @@ func (s AuditService) Get( err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - return audit.LoadByID(ctx, conn, scope, auditID) + if err := audit.LoadByID(ctx, conn, scope, auditID); err != nil { + return fmt.Errorf("cannot load audit: %w", err) + } + + return nil }, ) if err != nil { @@ -123,16 +125,20 @@ func (s AuditService) Get( return audit, nil } -func (s AuditService) GetByReportID( +func (s AuditService) GetByReportFileID( ctx context.Context, scope coredata.Scoper, - reportID gid.GID, + fileID gid.GID, ) (*coredata.Audit, error) { audit := &coredata.Audit{} err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - return audit.LoadByReportID(ctx, conn, scope, reportID) + if err := audit.LoadByReportFileID(ctx, conn, scope, fileID); err != nil { + return fmt.Errorf("cannot load report file: %w", err) + } + + return nil }, ) if err != nil { @@ -326,7 +332,7 @@ func (s AuditService) CountForOrganizationID( func (s AuditService) UploadReport( ctx context.Context, scope coredata.Scoper, - req UploadAuditReportRequest, + req *UploadAuditReportRequest, ) (*coredata.Audit, error) { if err := req.Validate(); err != nil { return nil, fmt.Errorf("invalid request: %w", err) @@ -334,60 +340,52 @@ func (s AuditService) UploadReport( audit := &coredata.Audit{} - err := s.svc.pg.WithTx( + err := s.svc.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := audit.LoadByID(ctx, conn, scope, req.AuditID) + if err != nil { + return fmt.Errorf("cannot load audit: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + fv := filevalidation.NewValidator( + filevalidation.WithCategories(filevalidation.CategoryDocument), + filevalidation.WithMaxFileSize(25*1024*1024), + ) + + file, err := s.svc.Files.UploadAndSaveFile( + ctx, scope, + fv, + map[string]string{"organization-id": audit.OrganizationID.String()}, + &FileUpload{ + Content: req.File.Content, + Filename: req.File.Filename, + Size: req.File.Size, + ContentType: req.File.ContentType, + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot upload file: %w", err) + } + + err = s.svc.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { if err := audit.LoadByID(ctx, conn, scope, req.AuditID); err != nil { return fmt.Errorf("cannot load audit: %w", err) } - reportID := gid.New(scope.GetTenantID(), coredata.ReportEntityType) - now := time.Now() - - objectKey, err := uuid.NewV7() - if err != nil { - return fmt.Errorf("cannot generate object key: %w", err) - } - - _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(objectKey.String()), - Body: req.File.Content, - ContentType: new(req.File.ContentType), - CacheControl: new("private, max-age=3600"), - Metadata: map[string]string{ - "type": "report", - "report-id": reportID.String(), - "organization-id": audit.OrganizationID.String(), - }, - }) - if err != nil { - return fmt.Errorf("cannot upload report to S3: %w", err) - } - - report := &coredata.Report{ - ID: reportID, - OrganizationID: audit.OrganizationID, - ObjectKey: objectKey.String(), - MimeType: req.File.ContentType, - Filename: req.File.Filename, - Size: req.File.Size, - CreatedAt: now, - UpdatedAt: now, - } - - if err := report.Insert(ctx, conn, scope); err != nil { - return fmt.Errorf("cannot insert report: %w", err) - } - - audit.ReportID = &report.ID + audit.ReportFileID = &file.ID audit.UpdatedAt = time.Now() - if err := audit.Update(ctx, conn, scope); err != nil { - return fmt.Errorf("cannot update audit: %w", err) - } - - return nil + return audit.Update(ctx, conn, scope) }, ) if err != nil { @@ -407,16 +405,16 @@ func (s AuditService) GenerateReportURL( return nil, fmt.Errorf("cannot get audit: %w", err) } - if audit.ReportID == nil { + if audit.ReportFileID == nil { return nil, fmt.Errorf("audit has no report") } - url, err := s.svc.Reports.GenerateDownloadURL(ctx, scope, *audit.ReportID, expiresIn) + url, err := s.svc.Files.GenerateFileTempURL(ctx, scope, *audit.ReportFileID, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate report download URL: %w", err) } - return url, nil + return &url, nil } func (s AuditService) DeleteReport( @@ -425,21 +423,21 @@ func (s AuditService) DeleteReport( ) (*coredata.Audit, error) { audit := &coredata.Audit{} - err := s.svc.pg.WithTx( + return audit, s.svc.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { if err := audit.LoadByID(ctx, conn, scope, auditID); err != nil { return fmt.Errorf("cannot load audit: %w", err) } - if audit.ReportID != nil { - report := &coredata.Report{ID: *audit.ReportID} + if audit.ReportFileID != nil { + file := coredata.File{ID: *audit.ReportFileID} - if err := report.Delete(ctx, conn, scope); err != nil { - return fmt.Errorf("cannot delete report: %w", err) + if err := file.SoftDelete(ctx, conn, scope); err != nil { + return fmt.Errorf("cannot soft-delete report file: %w", err) } - audit.ReportID = nil + audit.ReportFileID = nil audit.UpdatedAt = time.Now() if err := audit.Update(ctx, conn, scope); err != nil { @@ -450,11 +448,6 @@ func (s AuditService) DeleteReport( return nil }, ) - if err != nil { - return nil, err - } - - return audit, nil } func (s AuditService) ListForControlID( diff --git a/pkg/probo/report_service.go b/pkg/probo/report_service.go deleted file mode 100644 index f662ad249..000000000 --- a/pkg/probo/report_service.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package probo - -import ( - "context" - "fmt" - "time" - - "github.com/aws/aws-sdk-go-v2/service/s3" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/gid" -) - -type ReportService struct { - svc *Service -} - -func (s ReportService) Get( - ctx context.Context, scope coredata.Scoper, - reportID gid.GID, -) (*coredata.Report, error) { - report := &coredata.Report{} - - err := s.svc.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - err := report.LoadByID(ctx, conn, scope, reportID) - if err != nil { - return fmt.Errorf("cannot load report: %w", err) - } - - return nil - }, - ) - if err != nil { - return nil, err - } - - return report, nil -} - -func (s ReportService) GetByIDs( - ctx context.Context, scope coredata.Scoper, - reportIDs ...gid.GID, -) (coredata.Reports, error) { - var reports coredata.Reports - - err := s.svc.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - if err := reports.LoadByIDs( - ctx, - conn, - scope, - reportIDs, - ); err != nil { - return fmt.Errorf("cannot load reports by ids: %w", err) - } - - return nil - }, - ) - if err != nil { - return nil, err - } - - return reports, nil -} - -func (s ReportService) Delete( - ctx context.Context, scope coredata.Scoper, - reportID gid.GID, -) error { - return s.svc.pg.WithTx(ctx, func(ctx context.Context, conn pg.Tx) error { - report := &coredata.Report{} - - err := report.LoadByID(ctx, conn, scope, reportID) - if err != nil { - return fmt.Errorf("cannot get report: %w", err) - } - - err = report.Delete(ctx, conn, scope) - if err != nil { - return fmt.Errorf("cannot delete report: %w", err) - } - - return nil - }) -} - -func (s ReportService) GenerateDownloadURL( - ctx context.Context, scope coredata.Scoper, - reportID gid.GID, - expiresIn time.Duration, -) (*string, error) { - report, err := s.Get(ctx, scope, reportID) - if err != nil { - return nil, fmt.Errorf("cannot get report: %w", err) - } - - presignClient := s3.NewPresignClient(s.svc.s3) - - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(report.ObjectKey), - ResponseCacheControl: new("max-age=3600, public"), - ResponseContentType: new(report.MimeType), - ResponseContentDisposition: new(fmt.Sprintf("attachment; filename=\"%s\"", report.Filename)), - }, func(opts *s3.PresignOptions) { - opts.Expires = expiresIn - }) - if err != nil { - return nil, fmt.Errorf("cannot presign GetObject request: %w", err) - } - - return &presignedReq.URL, nil -} diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 936a4045c..3f4086e2a 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -106,7 +106,6 @@ type ( Data *DatumService Audits *AuditService WebhookSubscriptions *WebhookSubscriptionService - Reports *ReportService TrustCenters *TrustCenterService TrustCenterAccesses *TrustCenterAccessService TrustCenterReferences *TrustCenterReferenceService @@ -230,7 +229,6 @@ func NewService( svc.Data = &DatumService{svc: svc} svc.Audits = &AuditService{svc: svc} svc.WebhookSubscriptions = &WebhookSubscriptionService{svc: svc} - svc.Reports = &ReportService{svc: svc} svc.TrustCenters = &TrustCenterService{svc: svc} svc.TrustCenterAccesses = &TrustCenterAccessService{svc: svc} svc.TrustCenterReferences = &TrustCenterReferenceService{svc: svc} diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index fc681d1ba..266ed52a3 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -68,7 +68,7 @@ func (utcar *UpdateTrustCenterAccessRequest) Validate() error { } for i, reportAccess := range utcar.ReportAccesses { - v.Check(reportAccess.ID, fmt.Sprintf("reportAccesses[%d].ID", i), validator.Required(), validator.GID(coredata.ReportEntityType)) + v.Check(reportAccess.ID, fmt.Sprintf("reportAccesses[%d].ID", i), validator.Required(), validator.GID(coredata.FileEntityType)) } for i, reportAccess := range utcar.TrustCenterFileAccesses { @@ -263,7 +263,7 @@ func (s TrustCenterAccessService) Update( }) } - if err := tcdas.MergeReportAccesses(ctx, tx, scope, access.OrganizationID, access.ID, reportData); err != nil { + if err := tcdas.MergeReportFileAccesses(ctx, tx, scope, access.OrganizationID, access.ID, reportData); err != nil { return fmt.Errorf("cannot merge report accesses: %w", err) } } diff --git a/pkg/server/api/console/v1/audit_resolvers.go b/pkg/server/api/console/v1/audit_resolvers.go index a808234bb..afb4032e4 100644 --- a/pkg/server/api/console/v1/audit_resolvers.go +++ b/pkg/server/api/console/v1/audit_resolvers.go @@ -8,7 +8,6 @@ package console_v1 import ( "context" "errors" - "time" "github.com/vikstrous/dataloadgen" "go.gearno.de/kit/log" @@ -68,50 +67,30 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types return types.NewFramework(framework), nil } -// Report is the resolver for the report field. -func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Report, error) { +// ReportFile is the resolver for the reportFile field. +func (r *auditResolver) ReportFile(ctx context.Context, obj *types.Audit) (*types.File, error) { if _, err := r.authorize(ctx, obj.ID, probo.ActionReportGet); err != nil { return nil, err } - if obj.Report == nil { + if obj.ReportFile == nil { return nil, nil } loaders := dataloader.FromContext(ctx) - report, err := loaders.Report.Load(ctx, obj.Report.ID) + file, err := loaders.File.Load(ctx, obj.ReportFile.ID) if err != nil { if errors.Is(err, dataloadgen.ErrNotFound) { return nil, gqlutils.NotFound(ctx, err) } - r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err)) + r.logger.ErrorCtx(ctx, "cannot load report file", log.Error(err)) return nil, gqlutils.Internal(ctx) } - return types.NewReport(report), nil -} - -// ReportURL is the resolver for the reportUrl field. -func (r *auditResolver) ReportURL(ctx context.Context, obj *types.Audit) (*string, error) { - scope, err := r.authorize(ctx, obj.ID, probo.ActionReportGetReportUrl) - if err != nil { - return nil, err - } - - if obj.Report == nil { - return nil, nil - } - - url, err := r.probo.Audits.GenerateReportURL(ctx, scope, obj.ID, 15*time.Minute) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot generate report URL", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return url, nil + return types.NewFile(file), nil } // Controls is the resolver for the controls field. @@ -431,7 +410,7 @@ func (r *mutationResolver) CreateAudit(ctx context.Context, input types.CreateAu }, } - audit, err = r.probo.Audits.UploadReport(ctx, scope, uploadReq) + audit, err = r.probo.Audits.UploadReport(ctx, scope, &uploadReq) if err != nil { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) @@ -514,7 +493,7 @@ func (r *mutationResolver) UploadAuditReport(ctx context.Context, input types.Up }, } - audit, err := r.probo.Audits.UploadReport(ctx, scope, req) + audit, err := r.probo.Audits.UploadReport(ctx, scope, &req) if err != nil { if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok { return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors) @@ -704,43 +683,6 @@ func (r *mutationResolver) PublishFindingList(ctx context.Context, input types.P }, nil } -// DownloadURL is the resolver for the downloadUrl field. -func (r *reportResolver) DownloadURL(ctx context.Context, obj *types.Report) (*string, error) { - scope, err := r.authorize(ctx, obj.ID, probo.ActionReportDownloadUrlGet) - if err != nil { - return nil, err - } - - url, err := r.probo.Reports.GenerateDownloadURL(ctx, scope, obj.ID, 15*time.Minute) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot generate download URL", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return url, nil -} - -// Audit is the resolver for the audit field. -func (r *reportResolver) Audit(ctx context.Context, obj *types.Report) (*types.Audit, error) { - scope, err := r.authorize(ctx, obj.ID, probo.ActionAuditGet) - if err != nil { - return nil, err - } - - audit, err := r.probo.Audits.GetByReportID(ctx, scope, obj.ID) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot load audit for report", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewAudit(audit), nil -} - -// Permission is the resolver for the permission field. -func (r *reportResolver) Permission(ctx context.Context, obj *types.Report, action string) (bool, error) { - return r.Resolver.Permission(ctx, obj, action) -} - // Audit returns schema.AuditResolver implementation. func (r *Resolver) Audit() schema.AuditResolver { return &auditResolver{r} } @@ -757,11 +699,7 @@ func (r *Resolver) FindingConnection() schema.FindingConnectionResolver { return &findingConnectionResolver{r} } -// Report returns schema.ReportResolver implementation. -func (r *Resolver) Report() schema.ReportResolver { return &reportResolver{r} } - type auditResolver struct{ *Resolver } type auditConnectionResolver struct{ *Resolver } type findingResolver struct{ *Resolver } type findingConnectionResolver struct{ *Resolver } -type reportResolver struct{ *Resolver } diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go index 1e94ce399..a150f0075 100644 --- a/pkg/server/api/console/v1/base_resolvers.go +++ b/pkg/server/api/console/v1/base_resolvers.go @@ -279,16 +279,6 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewObligation(obligation), nil } - case coredata.ReportEntityType: - action = probo.ActionReportGet - loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) { - report, err := r.probo.Reports.Get(ctx, scope, id) - if err != nil { - return nil, err - } - - return types.NewReport(report), nil - } case coredata.ProcessingActivityEntityType: action = probo.ActionProcessingActivityList loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) { diff --git a/pkg/server/api/console/v1/dataloader/dataloader.go b/pkg/server/api/console/v1/dataloader/dataloader.go index be4b22d7f..3f474ca0f 100644 --- a/pkg/server/api/console/v1/dataloader/dataloader.go +++ b/pkg/server/api/console/v1/dataloader/dataloader.go @@ -62,7 +62,6 @@ type ( Measure *dataloadgen.Loader[gid.GID, *coredata.Measure] Task *dataloadgen.Loader[gid.GID, *coredata.Task] File *dataloadgen.Loader[gid.GID, *coredata.File] - Report *dataloadgen.Loader[gid.GID, *coredata.Report] CookieBanner *dataloadgen.Loader[gid.GID, *coredata.CookieBanner] CookieCategory *dataloadgen.Loader[gid.GID, *coredata.CookieCategory] CommonTrackerPattern *dataloadgen.Loader[gid.GID, *coredata.CommonTrackerPattern] @@ -114,7 +113,6 @@ func (f *batchFetcher) newLoaders() *Loaders { Measure: dataloadgen.NewMappedLoader(f.fetchMeasures), Task: dataloadgen.NewMappedLoader(f.fetchTasks), File: dataloadgen.NewMappedLoader(f.fetchFiles), - Report: dataloadgen.NewMappedLoader(f.fetchReports), CookieBanner: dataloadgen.NewMappedLoader(f.fetchCookieBanners), CookieCategory: dataloadgen.NewMappedLoader(f.fetchCookieCategories), CommonTrackerPattern: dataloadgen.NewMappedLoader(f.fetchCommonTrackerPatterns), @@ -286,22 +284,6 @@ func (f *batchFetcher) fetchFiles(ctx context.Context, keys []gid.GID) (map[gid. return result, nil } -func (f *batchFetcher) fetchReports(ctx context.Context, keys []gid.GID) (map[gid.GID]*coredata.Report, error) { - scope := coredata.NewScopeFromObjectID(keys[0]) - - reports, err := f.probo.Reports.GetByIDs(ctx, scope, keys...) - if err != nil { - return nil, fmt.Errorf("cannot batch load reports: %w", err) - } - - result := make(map[gid.GID]*coredata.Report, len(reports)) - for _, v := range reports { - result[v.ID] = v - } - - return result, nil -} - func (f *batchFetcher) fetchCookieBanners(ctx context.Context, keys []gid.GID) (map[gid.GID]*coredata.CookieBanner, error) { scope := coredata.NewScopeFromObjectID(keys[0]) diff --git a/pkg/server/api/console/v1/graphql/audit.graphql b/pkg/server/api/console/v1/graphql/audit.graphql index 41cb764a8..ad49a3000 100644 --- a/pkg/server/api/console/v1/graphql/audit.graphql +++ b/pkg/server/api/console/v1/graphql/audit.graphql @@ -151,8 +151,7 @@ type Audit implements Node { framework: Framework @goField(forceResolver: true) validFrom: Datetime validUntil: Datetime - report: Report @goField(forceResolver: true) - reportUrl: String @goField(forceResolver: true) + reportFile: File @goField(forceResolver: true) state: AuditState! controls( @@ -208,20 +207,6 @@ type Finding implements Node { permission(action: String!): Boolean! @goField(forceResolver: true) } -type Report implements Node { - id: ID! - objectKey: String! - mimeType: String! - filename: String! - size: Int! - downloadUrl: String @goField(forceResolver: true) - createdAt: Datetime! - updatedAt: Datetime! - audit: Audit @goField(forceResolver: true) - - permission(action: String!): Boolean! @goField(forceResolver: true) -} - type AuditConnection @goModel( model: "go.probo.inc/probo/pkg/server/api/console/v1/types.AuditConnection" diff --git a/pkg/server/api/console/v1/graphql/trust_center.graphql b/pkg/server/api/console/v1/graphql/trust_center.graphql index acebb6c9d..ba0d6110b 100644 --- a/pkg/server/api/console/v1/graphql/trust_center.graphql +++ b/pkg/server/api/console/v1/graphql/trust_center.graphql @@ -334,7 +334,8 @@ type TrustCenterDocumentAccess id: ID! status: TrustCenterDocumentAccessStatus! document: Document @goField(forceResolver: true) - report: Report @goField(forceResolver: true) + reportFile: File @goField(forceResolver: true) + audit: Audit @goField(forceResolver: true) trustCenterFile: TrustCenterFile @goField(forceResolver: true) } diff --git a/pkg/server/api/console/v1/trust_center_resolvers.go b/pkg/server/api/console/v1/trust_center_resolvers.go index 81345b06e..04c414468 100644 --- a/pkg/server/api/console/v1/trust_center_resolvers.go +++ b/pkg/server/api/console/v1/trust_center_resolvers.go @@ -1056,24 +1056,55 @@ func (r *trustCenterDocumentAccessResolver) Document(ctx context.Context, obj *t return types.NewDocument(document), nil } -// Report is the resolver for the report field. -func (r *trustCenterDocumentAccessResolver) Report(ctx context.Context, obj *types.TrustCenterDocumentAccess) (*types.Report, error) { - scope, err := r.authorize(ctx, obj.TrustCenterAccessID, probo.ActionReportGet) +// ReportFile is the resolver for the reportFile field. +func (r *trustCenterDocumentAccessResolver) ReportFile(ctx context.Context, obj *types.TrustCenterDocumentAccess) (*types.File, error) { + if _, err := r.authorize(ctx, obj.ID, probo.ActionFileGet); err != nil { + return nil, err + } + + if obj.ReportFile == nil { + return nil, nil + } + + loaders := dataloader.FromContext(ctx) + + file, err := loaders.File.Load(ctx, obj.ReportFile.ID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) { + return nil, gqlutils.NotFound(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot load report file", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return types.NewFile(file), nil +} + +// Audit is the resolver for the audit field. +func (r *trustCenterDocumentAccessResolver) Audit(ctx context.Context, obj *types.TrustCenterDocumentAccess) (*types.Audit, error) { + scope, err := r.authorize(ctx, obj.ID, probo.ActionAuditGet) if err != nil { return nil, err } - if obj.ReportID == nil { + if obj.ReportFileID == nil { return nil, nil } - report, err := r.probo.Reports.Get(ctx, scope, *obj.ReportID) + audit, err := r.probo.Audits.GetByReportFileID(ctx, scope, *obj.ReportFileID) if err != nil { - r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err)) + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, nil + } + + r.logger.ErrorCtx(ctx, "cannot load audit for report file", log.Error(err)) + return nil, gqlutils.Internal(ctx) } - return types.NewReport(report), nil + return types.NewAudit(audit), nil } // TrustCenterFile is the resolver for the trustCenterFile field. diff --git a/pkg/server/api/console/v1/types/audit.go b/pkg/server/api/console/v1/types/audit.go index 91ad6f549..99e28f19e 100644 --- a/pkg/server/api/console/v1/types/audit.go +++ b/pkg/server/api/console/v1/types/audit.go @@ -77,9 +77,9 @@ func NewAudit(a *coredata.Audit) *Audit { UpdatedAt: a.UpdatedAt, } - if a.ReportID != nil { - node.Report = &Report{ - ID: *a.ReportID, + if a.ReportFileID != nil { + node.ReportFile = &File{ + ID: *a.ReportFileID, } } diff --git a/pkg/server/api/console/v1/types/report.go b/pkg/server/api/console/v1/types/report.go deleted file mode 100644 index 429ed9e81..000000000 --- a/pkg/server/api/console/v1/types/report.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package types - -import ( - "go.probo.inc/probo/pkg/coredata" -) - -func NewReport(r *coredata.Report) *Report { - return &Report{ - ID: r.ID, - ObjectKey: r.ObjectKey, - MimeType: r.MimeType, - Filename: r.Filename, - Size: int(r.Size), - CreatedAt: r.CreatedAt, - UpdatedAt: r.UpdatedAt, - } -} diff --git a/pkg/server/api/console/v1/types/trust_center_document_access.go b/pkg/server/api/console/v1/types/trust_center_document_access.go index 50deb7a2c..bff9fed77 100644 --- a/pkg/server/api/console/v1/types/trust_center_document_access.go +++ b/pkg/server/api/console/v1/types/trust_center_document_access.go @@ -42,13 +42,13 @@ type ( UpdatedAt time.Time `json:"updatedAt"` TrustCenterAccess *TrustCenterAccess `json:"trustCenterAccess"` Document *Document `json:"document,omitempty"` - Report *Report `json:"report,omitempty"` + ReportFile *File `json:"reportFile,omitempty"` TrustCenterFile *TrustCenterFile `json:"trustCenterFile,omitempty"` // Internal fields used by resolvers TrustCenterAccessID gid.GID `json:"-"` DocumentID *gid.GID `json:"-"` - ReportID *gid.GID `json:"-"` + ReportFileID *gid.GID `json:"-"` TrustCenterFileID *gid.GID `json:"-"` } ) @@ -62,7 +62,7 @@ func NewTrustCenterDocumentAccess(tcda *coredata.TrustCenterDocumentAccess) *Tru UpdatedAt: tcda.UpdatedAt, TrustCenterAccessID: tcda.TrustCenterAccessID, DocumentID: tcda.DocumentID, - ReportID: tcda.ReportID, + ReportFileID: tcda.ReportFileID, TrustCenterFileID: tcda.TrustCenterFileID, } @@ -72,9 +72,9 @@ func NewTrustCenterDocumentAccess(tcda *coredata.TrustCenterDocumentAccess) *Tru } } - if tcda.ReportID != nil { - object.Report = &Report{ - ID: *tcda.ReportID, + if tcda.ReportFileID != nil { + object.ReportFile = &File{ + ID: *tcda.ReportFileID, } } diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index e0cecd90b..20f70fe47 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -1468,16 +1468,16 @@ func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, i return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit: %w", err) } - var report *coredata.Report - if audit.ReportID != nil { - report, err = prb.Reports.Get(ctx, scope, *audit.ReportID) + var file *coredata.File + if audit.ReportFileID != nil { + file, err = prb.Files.Get(ctx, scope, *audit.ReportFileID) if err != nil { - return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit report file: %w", err) } } return nil, types.GetAuditOutput{ - Audit: types.NewAudit(audit, report), + Audit: types.NewAudit(audit, file), }, nil } @@ -1532,16 +1532,16 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot update audit: %w", err) } - var report *coredata.Report - if audit.ReportID != nil { - report, err = svc.Reports.Get(ctx, scope, *audit.ReportID) + var file *coredata.File + if audit.ReportFileID != nil { + file, err = svc.Files.Get(ctx, scope, *audit.ReportFileID) if err != nil { - return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot get audit report file: %w", err) } } return nil, types.UpdateAuditOutput{ - Audit: types.NewAudit(audit, report), + Audit: types.NewAudit(audit, file), }, nil } diff --git a/pkg/server/api/mcp/v1/types/audit.go b/pkg/server/api/mcp/v1/types/audit.go index 263803e07..14cb04c31 100644 --- a/pkg/server/api/mcp/v1/types/audit.go +++ b/pkg/server/api/mcp/v1/types/audit.go @@ -19,7 +19,7 @@ import ( "go.probo.inc/probo/pkg/page" ) -func NewAudit(a *coredata.Audit, report *coredata.Report) *Audit { +func NewAudit(a *coredata.Audit, file *coredata.File) *Audit { audit := &Audit{ ID: a.ID, Name: a.Name, @@ -27,16 +27,16 @@ func NewAudit(a *coredata.Audit, report *coredata.Report) *Audit { FrameworkID: a.FrameworkID, State: a.State, TrustCenterVisibility: a.TrustCenterVisibility, - HasReport: a.ReportID != nil, + HasReport: a.ReportFileID != nil, ValidFrom: a.ValidFrom, ValidUntil: a.ValidUntil, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } - if report != nil { - audit.ReportFilename = &report.Filename - audit.ReportMimeType = &report.MimeType + if file != nil { + audit.ReportFilename = &file.FileName + audit.ReportMimeType = &file.MimeType } return audit diff --git a/pkg/server/api/slack/v1/slack_handler.go b/pkg/server/api/slack/v1/slack_handler.go index 62309e922..09c40fb19 100644 --- a/pkg/server/api/slack/v1/slack_handler.go +++ b/pkg/server/api/slack/v1/slack_handler.go @@ -227,7 +227,7 @@ func SlackHandler(slackSvc *slack.Service, slackSigningSecret string, logger *lo switch gID.EntityType() { case coredata.DocumentEntityType: documentIDs = []gid.GID{gID} - case coredata.ReportEntityType: + case coredata.FileEntityType: reportIDs = []gid.GID{gID} case coredata.TrustCenterFileEntityType: fileIDs = []gid.GID{gID} diff --git a/pkg/server/api/trust/v1/base_resolvers.go b/pkg/server/api/trust/v1/base_resolvers.go index 24922b718..6517de618 100644 --- a/pkg/server/api/trust/v1/base_resolvers.go +++ b/pkg/server/api/trust/v1/base_resolvers.go @@ -83,21 +83,21 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewFramework(framework), nil - case coredata.ReportEntityType: + case coredata.FileEntityType: trustCenter := compliancepage.CompliancePageFromContext(ctx) - report, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id) + file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id) if err != nil { if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } - r.logger.ErrorCtx(ctx, "cannot get report", log.Error(err)) + r.logger.ErrorCtx(ctx, "cannot get audit report file", log.Error(err)) return nil, gqlutils.Internal(ctx) } - return types.NewReport(report), nil + return types.NewAuditReport(file), nil case coredata.AuditEntityType: audit, err := trustService.Audits.Get(ctx, scope, id) diff --git a/pkg/server/api/trust/v1/graphql/trust_center.graphql b/pkg/server/api/trust/v1/graphql/trust_center.graphql index 5a7c6110e..760cca8dc 100644 --- a/pkg/server/api/trust/v1/graphql/trust_center.graphql +++ b/pkg/server/api/trust/v1/graphql/trust_center.graphql @@ -114,9 +114,9 @@ type Framework implements Node @nda { darkLogoURL: String @goField(forceResolver: true) } -type Report implements Node @nda { +type AuditReport implements Node @nda { id: ID! - filename: String! + fileName: String! isUserAuthorized: Boolean! @goField(forceResolver: true) access: DocumentAccess @goField(forceResolver: true) } @@ -125,7 +125,7 @@ type Audit implements Node @nda { id: ID! name: String framework: Framework! @goField(forceResolver: true) - report: Report @goField(forceResolver: true) + reportFile: AuditReport @goField(forceResolver: true) } type AuditConnection @nda { diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go index 4f83d3db7..c1e969413 100644 --- a/pkg/server/api/trust/v1/trust_center_resolvers.go +++ b/pkg/server/api/trust/v1/trust_center_resolvers.go @@ -44,8 +44,8 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types return types.NewFramework(framework), nil } -// Report is the resolver for the report field. -func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Report, error) { +// ReportFile is the resolver for the reportFile field. +func (r *auditResolver) ReportFile(ctx context.Context, obj *types.Audit) (*types.AuditReport, error) { scope := coredata.NewScopeFromObjectID(obj.ID) trustService := r.trust @@ -55,19 +55,105 @@ func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Re return nil, gqlutils.Internal(ctx) } - if audit.ReportID == nil { + if audit.ReportFileID == nil { return nil, nil } trustCenter := compliancepage.CompliancePageFromContext(ctx) - report, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, *audit.ReportID) + file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, *audit.ReportFileID) if err != nil { - r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err)) + r.logger.ErrorCtx(ctx, "cannot load report file", log.Error(err)) return nil, gqlutils.Internal(ctx) } - return types.NewReport(report), nil + return types.NewAuditReport(file), nil +} + +// IsUserAuthorized is the resolver for the isUserAuthorized field. +func (r *auditReportResolver) IsUserAuthorized(ctx context.Context, obj *types.AuditReport) (bool, error) { + scope := coredata.NewScopeFromObjectID(obj.ID) + trustService := r.trust + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + audit, err := trustService.Audits.GetByReportFileID(ctx, scope, obj.ID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return false, nil + } + + r.logger.ErrorCtx(ctx, "cannot load audit for report file", log.Error(err)) + + return false, gqlutils.Internal(ctx) + } + + if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return true, nil + } + + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil + } + + reportAccess, err := trustService.TrustCenterAccesses.GetReportFileAccess(ctx, scope, + trustCenter.ID, + identity.ID, + obj.ID, + ) + if err != nil { + if errors.Is(err, trust.ErrMembershipNotFound) || + errors.Is(err, trust.ErrUserNotFound) || + errors.Is(err, trust.ErrUserInactive) || + errors.Is(err, trust.ErrDocumentAccessNotFound) { + return false, nil + } + + r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) + + return false, gqlutils.Internal(ctx) + } + + return reportAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil +} + +// Access is the resolver for the access field. +func (r *auditReportResolver) Access(ctx context.Context, obj *types.AuditReport) (*types.DocumentAccess, error) { + scope := coredata.NewScopeFromObjectID(obj.ID) + trustService := r.trust + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, nil + } + + access, err := trustService.TrustCenterAccesses.GetReportFileAccess( + ctx, scope, + trustCenter.ID, + identity.ID, + obj.ID, + ) + if err != nil { + if errors.Is(err, trust.ErrMembershipNotFound) || + errors.Is(err, trust.ErrUserNotFound) || + errors.Is(err, trust.ErrDocumentAccessNotFound) { + return nil, nil + } + + if errors.Is(err, trust.ErrUserInactive) { + return nil, gqlutils.Forbidden(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot get audit report access", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return &types.DocumentAccess{ + ID: access.ID, + Status: access.Status, + }, nil } // Framework is the resolver for the framework field on ComplianceFramework. @@ -294,7 +380,7 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo trustService := r.trust trustCenter := compliancepage.CompliancePageFromContext(ctx) - audit, err := trustService.Audits.GetByReportID(ctx, scope, input.ReportID) + audit, err := trustService.Audits.GetByReportFileID(ctx, scope, input.ReportID) if err != nil { r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) return nil, gqlutils.Internal(ctx) @@ -317,7 +403,7 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") } - reportAccess, err := trustService.TrustCenterAccesses.GetReportAccess( + reportAccess, err := trustService.TrustCenterAccesses.GetReportFileAccess( ctx, scope, trustCenter.ID, identity.ID, @@ -458,7 +544,7 @@ func (r *mutationResolver) RequestReportAccess(ctx context.Context, input types. scope := coredata.NewScopeFromObjectID(trustCenter.ID) trustService := r.trust - audit, err := trustService.Audits.GetByReportID(ctx, scope, input.ReportID) + audit, err := trustService.Audits.GetByReportFileID(ctx, scope, input.ReportID) if err != nil { r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) return nil, gqlutils.Internal(ctx) @@ -543,87 +629,6 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp }, nil } -// IsUserAuthorized is the resolver for the isUserAuthorized field. -func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) { - scope := coredata.NewScopeFromObjectID(obj.ID) - trustService := r.trust - trustCenter := compliancepage.CompliancePageFromContext(ctx) - - audit, err := trustService.Audits.GetByReportID(ctx, scope, obj.ID) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) - return false, gqlutils.Internal(ctx) - } - - if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - return true, nil - } - - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return false, nil - } - - reportAccess, err := trustService.TrustCenterAccesses.GetReportAccess(ctx, scope, - trustCenter.ID, - identity.ID, - obj.ID, - ) - if err != nil { - if errors.Is(err, trust.ErrMembershipNotFound) || - errors.Is(err, trust.ErrUserNotFound) || - errors.Is(err, trust.ErrUserInactive) || - errors.Is(err, trust.ErrDocumentAccessNotFound) { - return false, nil - } - - r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) - - return false, gqlutils.Internal(ctx) - } - - return reportAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil -} - -// Access is the resolver for the access field. -func (r *reportResolver) Access(ctx context.Context, obj *types.Report) (*types.DocumentAccess, error) { - scope := coredata.NewScopeFromObjectID(obj.ID) - trustService := r.trust - trustCenter := compliancepage.CompliancePageFromContext(ctx) - - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return nil, nil // User is not authenticated, so no access requested - } - - access, err := trustService.TrustCenterAccesses.GetReportAccess( - ctx, scope, - trustCenter.ID, - identity.ID, - obj.ID, - ) - if err != nil { - if errors.Is(err, trust.ErrMembershipNotFound) || - errors.Is(err, trust.ErrUserNotFound) || - errors.Is(err, trust.ErrDocumentAccessNotFound) { - return nil, nil - } - - if errors.Is(err, trust.ErrUserInactive) { - return nil, gqlutils.Forbidden(ctx, err) - } - - r.logger.ErrorCtx(ctx, "cannot get audit report access", log.Error(err)) - - return nil, gqlutils.Internal(ctx) - } - - return &types.DocumentAccess{ - ID: access.ID, - Status: access.Status, - }, nil -} - // TotalCount is the resolver for the totalCount field. func (r *subprocessorConnectionResolver) TotalCount(ctx context.Context, obj *types.SubprocessorConnection) (int, error) { scope := coredata.NewScopeFromObjectID(obj.ParentID) @@ -987,6 +992,9 @@ func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.T // Audit returns schema.AuditResolver implementation. func (r *Resolver) Audit() schema.AuditResolver { return &auditResolver{r} } +// AuditReport returns schema.AuditReportResolver implementation. +func (r *Resolver) AuditReport() schema.AuditReportResolver { return &auditReportResolver{r} } + // ComplianceFramework returns schema.ComplianceFrameworkResolver implementation. func (r *Resolver) ComplianceFramework() schema.ComplianceFrameworkResolver { return &complianceFrameworkResolver{r} @@ -998,9 +1006,6 @@ func (r *Resolver) Document() schema.DocumentResolver { return &documentResolver // Framework returns schema.FrameworkResolver implementation. func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} } -// Report returns schema.ReportResolver implementation. -func (r *Resolver) Report() schema.ReportResolver { return &reportResolver{r} } - // SubprocessorConnection returns schema.SubprocessorConnectionResolver implementation. func (r *Resolver) SubprocessorConnection() schema.SubprocessorConnectionResolver { return &subprocessorConnectionResolver{r} @@ -1020,10 +1025,10 @@ func (r *Resolver) TrustCenterReference() schema.TrustCenterReferenceResolver { } type auditResolver struct{ *Resolver } +type auditReportResolver struct{ *Resolver } type complianceFrameworkResolver struct{ *Resolver } type documentResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver } -type reportResolver struct{ *Resolver } type subprocessorConnectionResolver struct{ *Resolver } type trustCenterResolver struct{ *Resolver } type trustCenterFileResolver struct{ *Resolver } diff --git a/pkg/server/api/trust/v1/types/report.go b/pkg/server/api/trust/v1/types/audit_report.go similarity index 88% rename from pkg/server/api/trust/v1/types/report.go rename to pkg/server/api/trust/v1/types/audit_report.go index 6416e78c4..0e9b4e818 100644 --- a/pkg/server/api/trust/v1/types/report.go +++ b/pkg/server/api/trust/v1/types/audit_report.go @@ -18,9 +18,9 @@ import ( "go.probo.inc/probo/pkg/coredata" ) -func NewReport(r *coredata.Report) *Report { - return &Report{ - ID: r.ID, - Filename: r.Filename, +func NewAuditReport(f *coredata.File) *AuditReport { + return &AuditReport{ + ID: f.ID, + FileName: f.FileName, } } diff --git a/pkg/slack/slack_message_service.go b/pkg/slack/slack_message_service.go index b81818345..a520fbbd6 100644 --- a/pkg/slack/slack_message_service.go +++ b/pkg/slack/slack_message_service.go @@ -349,14 +349,9 @@ func (s *Service) loadDocumentsReportsAndFilesFromAccesses( ) } - if access.ReportID != nil { - rep := &coredata.Report{} - if err := rep.LoadByID(ctx, conn, scope, *access.ReportID); err != nil { - return nil, nil, nil, fmt.Errorf("cannot load report: %w", err) - } - + if access.ReportFileID != nil { audit := &coredata.Audit{} - if err := audit.LoadByReportID(ctx, conn, scope, *access.ReportID); err != nil { + if err := audit.LoadByReportFileID(ctx, conn, scope, *access.ReportFileID); err != nil { return nil, nil, nil, fmt.Errorf("cannot load audit: %w", err) } @@ -373,7 +368,7 @@ func (s *Service) loadDocumentsReportsAndFilesFromAccesses( reports = append( reports, SlackMessageReport{ - ID: access.ReportID.String(), + ID: access.ReportFileID.String(), Title: label, AuditID: audit.ID.String(), Status: access.Status.String(), diff --git a/pkg/trust/audit_service.go b/pkg/trust/audit_service.go index b94022dce..490bd27fd 100644 --- a/pkg/trust/audit_service.go +++ b/pkg/trust/audit_service.go @@ -53,18 +53,17 @@ func (s AuditService) Get( return audit, nil } -func (s AuditService) GetByReportID( +func (s AuditService) GetByReportFileID( ctx context.Context, scope coredata.Scoper, - reportID gid.GID, + fileID gid.GID, ) (*coredata.Audit, error) { audit := &coredata.Audit{} err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - err := audit.LoadByReportID(ctx, conn, scope, reportID) - if err != nil { + if err := audit.LoadByReportFileID(ctx, conn, scope, fileID); err != nil { return fmt.Errorf("cannot load audit: %w", err) } diff --git a/pkg/trust/report_service.go b/pkg/trust/report_service.go index 38a6a9159..ebe29ad2e 100644 --- a/pkg/trust/report_service.go +++ b/pkg/trust/report_service.go @@ -16,6 +16,7 @@ package trust import ( "context" + "errors" "fmt" "io" "time" @@ -36,33 +37,43 @@ func (s ReportService) Get( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, - reportID gid.GID, -) (*coredata.Report, error) { - report, err := s.loadByID(ctx, scope, reportID) + fileID gid.GID, +) (*coredata.File, error) { + file, err := s.loadByID(ctx, scope, fileID) if err != nil { return nil, err } - if report.OrganizationID != organizationID { + if file.OrganizationID != organizationID { return nil, ErrReportNotFound } - return report, nil + // check the given report file ID is linked to an audit in order to avoid + // being able to get any file from the report request. + _, err = s.svc.Audits.GetByReportFileID(ctx, scope, fileID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, ErrReportNotFound + } + + return nil, fmt.Errorf("cannot verify report file: %w", err) + } + + return file, nil } func (s ReportService) loadByID( ctx context.Context, scope coredata.Scoper, - reportID gid.GID, -) (*coredata.Report, error) { - report := &coredata.Report{} + fileID gid.GID, +) (*coredata.File, error) { + file := &coredata.File{} err := s.svc.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - err := report.LoadByID(ctx, conn, scope, reportID) - if err != nil { - return fmt.Errorf("cannot load report: %w", err) + if err := file.LoadActiveByID(ctx, conn, scope, fileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) } return nil @@ -72,28 +83,28 @@ func (s ReportService) loadByID( return nil, err } - return report, nil + return file, nil } func (s ReportService) GenerateDownloadURL( ctx context.Context, scope coredata.Scoper, - reportID gid.GID, + fileID gid.GID, expiresIn time.Duration, ) (*string, error) { - report, err := s.loadByID(ctx, scope, reportID) + file, err := s.loadByID(ctx, scope, fileID) if err != nil { - return nil, fmt.Errorf("cannot get report: %w", err) + return nil, fmt.Errorf("cannot get file: %w", err) } presignClient := s3.NewPresignClient(s.svc.s3) presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ Bucket: new(s.svc.bucket), - Key: new(report.ObjectKey), + Key: new(file.FileKey), ResponseCacheControl: new("max-age=3600, public"), - ResponseContentType: new(report.MimeType), - ResponseContentDisposition: new(fmt.Sprintf("attachment; filename=\"%s\"", report.Filename)), + ResponseContentType: new(file.MimeType), + ResponseContentDisposition: new(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)), }, func(opts *s3.PresignOptions) { opts.Expires = expiresIn }) @@ -134,16 +145,16 @@ func (s ReportService) ExportPDFWithoutWatermark( func (s ReportService) exportPDFData( ctx context.Context, scope coredata.Scoper, - reportID gid.GID, + fileID gid.GID, ) ([]byte, error) { - report, err := s.loadByID(ctx, scope, reportID) + file, err := s.loadByID(ctx, scope, fileID) if err != nil { - return nil, fmt.Errorf("cannot get report: %w", err) + return nil, fmt.Errorf("cannot get file: %w", err) } result, err := s.svc.s3.GetObject(ctx, &s3.GetObjectInput{ Bucket: new(s.svc.bucket), - Key: new(report.ObjectKey), + Key: new(file.FileKey), }) if err != nil { return nil, fmt.Errorf("cannot download PDF from S3: %w", err) diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 24f1f4fe0..39d9fbd30 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -100,8 +100,8 @@ func (s TrustCenterAccessService) Request( } for _, audit := range allAudits { - if audit.ReportID != nil { - reportIDs = append(reportIDs, *audit.ReportID) + if audit.ReportFileID != nil { + reportIDs = append(reportIDs, *audit.ReportFileID) } } } @@ -148,7 +148,7 @@ func (s TrustCenterAccessService) Request( return fmt.Errorf("cannot bulk insert trust center document accesses: %w", err) } - if err := accesses.BulkInsertReportAccesses( + if err := accesses.BulkInsertReportFileAccesses( ctx, tx, scope, @@ -255,12 +255,12 @@ func (s TrustCenterAccessService) GetDocumentAccess( return documentAccess, nil } -func (s TrustCenterAccessService) GetReportAccess( +func (s TrustCenterAccessService) GetReportFileAccess( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, identityID gid.GID, - reportID gid.GID, + reportFileID gid.GID, ) (*coredata.TrustCenterDocumentAccess, error) { var reportAccess *coredata.TrustCenterDocumentAccess @@ -289,7 +289,7 @@ func (s TrustCenterAccessService) GetReportAccess( reportAccess = &coredata.TrustCenterDocumentAccess{} - err = reportAccess.LoadByTrustCenterAccessIDAndReportID(ctx, conn, scope, access.ID, reportID) + err = reportAccess.LoadByTrustCenterAccessIDAndReportFileID(ctx, conn, scope, access.ID, reportFileID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return ErrDocumentAccessNotFound @@ -405,7 +405,7 @@ func (s *TrustCenterAccessService) GrantByIDs( } if len(reportIDs) > 0 { - if err := coredata.GrantByReportIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { + if err := coredata.GrantByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { return fmt.Errorf("cannot grant report accesses: %w", err) } } @@ -526,7 +526,7 @@ func (s *TrustCenterAccessService) RejectOrRevokeByIDs( if len(reportIDs) > 0 { shouldSendEmail = true - if err := coredata.RejectOrRevokeByReportIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { + if err := coredata.RejectOrRevokeByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { return fmt.Errorf("cannot reject/revoke report accesses: %w", err) } } @@ -645,8 +645,8 @@ func extractExistingIDs(accesses coredata.TrustCenterDocumentAccesses) ([]gid.GI documentIDs = append(documentIDs, *access.DocumentID) } - if access.ReportID != nil { - reportIDs = append(reportIDs, *access.ReportID) + if access.ReportFileID != nil { + reportIDs = append(reportIDs, *access.ReportFileID) } if access.TrustCenterFileID != nil { @@ -678,36 +678,36 @@ func reportAccessLabels( ctx context.Context, conn pg.Querier, scope coredata.Scoper, - reportIDs []gid.GID, + reportFileIDs []gid.GID, ) ([]string, error) { - var reports coredata.Reports - if err := reports.LoadByIDs(ctx, conn, scope, reportIDs); err != nil { - return nil, fmt.Errorf("cannot load reports by IDs: %w", err) + var reportFiles coredata.Files + if err := reportFiles.LoadByIDs(ctx, conn, scope, reportFileIDs); err != nil { + return nil, fmt.Errorf("cannot load report files by IDs: %w", err) } - reportByID := make(map[gid.GID]*coredata.Report, len(reports)) - for _, report := range reports { - reportByID[report.ID] = report + fileByID := make(map[gid.GID]*coredata.File, len(reportFiles)) + for _, f := range reportFiles { + fileByID[f.ID] = f } var audits coredata.Audits - if err := audits.LoadByReportIDs(ctx, conn, scope, reportIDs); err != nil { - return nil, fmt.Errorf("cannot load audits by report IDs: %w", err) + if err := audits.LoadByReportFileIDs(ctx, conn, scope, reportFileIDs); err != nil { + return nil, fmt.Errorf("cannot load audits by report file IDs: %w", err) } - auditByReportID := make(map[gid.GID]*coredata.Audit, len(audits)) + auditByFileID := make(map[gid.GID]*coredata.Audit, len(audits)) frameworkIDSet := make(map[gid.GID]struct{}) for _, audit := range audits { - if audit.ReportID == nil { + if audit.ReportFileID == nil { continue } - if _, exists := auditByReportID[*audit.ReportID]; exists { + if _, exists := auditByFileID[*audit.ReportFileID]; exists { continue } - auditByReportID[*audit.ReportID] = audit + auditByFileID[*audit.ReportFileID] = audit frameworkIDSet[audit.FrameworkID] = struct{}{} } @@ -729,23 +729,23 @@ func reportAccessLabels( } } - labels := make([]string, 0, len(reportIDs)) + labels := make([]string, 0, len(reportFileIDs)) - for _, reportID := range reportIDs { - report, ok := reportByID[reportID] + for _, fileID := range reportFileIDs { + file, ok := fileByID[fileID] if !ok { - return nil, fmt.Errorf("cannot load report %q: %w", reportID, coredata.ErrResourceNotFound) + return nil, fmt.Errorf("cannot load report file %q: %w", fileID, coredata.ErrResourceNotFound) } - audit, ok := auditByReportID[reportID] + audit, ok := auditByFileID[fileID] if !ok { - labels = append(labels, report.Filename) + labels = append(labels, file.FileName) continue } framework, ok := frameworkByID[audit.FrameworkID] if !ok { - labels = append(labels, report.Filename) + labels = append(labels, file.FileName) continue }