Migrate audit reports to the files table

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-06-03 22:19:17 +02:00
parent b0a0f0efc9
commit 0e4d73bb0f
31 changed files with 517 additions and 947 deletions

View File

@@ -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")
})
}

View File

@@ -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,

View File

@@ -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:

View File

@@ -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:

View File

@@ -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,

View File

@@ -1,295 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// 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))
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -1,131 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// 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
}

View File

@@ -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}

View File

@@ -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)
}
}

View File

@@ -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 }

View File

@@ -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) {

View File

@@ -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])

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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,
}
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// 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,
}
}

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 }

View File

@@ -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,
}
}

View File

@@ -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(),

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
}