From 320d5ee03c7527c380257b4ff107b4cfb7b4f919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 30 Jul 2026 12:27:20 +0200 Subject: [PATCH] Skip NDA gate for public document exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-in users exporting a PUBLIC doc, report, or file should not be forced through the NDA. Keep the gate on private targets and always on bulk requestAccesses. Signed-off-by: Émile Ré --- .../api/complianceportal/v1/nda_directive.go | 153 +++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/pkg/server/api/complianceportal/v1/nda_directive.go b/pkg/server/api/complianceportal/v1/nda_directive.go index 08f19007b..9cf4e65a1 100644 --- a/pkg/server/api/complianceportal/v1/nda_directive.go +++ b/pkg/server/api/complianceportal/v1/nda_directive.go @@ -22,14 +22,18 @@ package complianceportal_v1 import ( "context" + "errors" + "fmt" "github.com/99designs/gqlgen/graphql" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/complianceportal/visitor" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/esign" + "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/complianceportal" + "go.probo.inc/probo/pkg/server/api/complianceportal/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" ) @@ -50,6 +54,22 @@ func newNDADirective( return nil, gqlutils.Internal(ctx) } + scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID) + + skip, err := shouldSkipNDAForPublic( + ctx, + visitorSvc, + scope, + compliancePage.OrganizationID, + ) + if err != nil { + logger.ErrorCtx(ctx, "cannot check target visibility for NDA gate", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if skip { + return next(ctx) + } + membership, err := visitorSvc.GetPortalMembership(ctx, compliancePage.ID, identity.ID) if err != nil { logger.ErrorCtx(ctx, "cannot get compliance page membership", log.Error(err)) @@ -60,8 +80,6 @@ func newNDADirective( return next(ctx) } - scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID) - sig, err := esignSvc.GetSignatureByID(ctx, scope, *membership.ElectronicSignatureID) if err != nil { logger.ErrorCtx(ctx, "cannot get NDA signature", log.Error(err)) @@ -80,3 +98,134 @@ func newNDADirective( return next(ctx) } } + +// shouldSkipNDAForPublic reports whether the @nda field targets a single PUBLIC +// document / report / file and can therefore skip the signature gate. Bulk +// requestAccesses always returns false. Not-found / not-visible targets also +// skip so the resolver can return its usual NotFound/Invalid. +func shouldSkipNDAForPublic( + ctx context.Context, + visitorSvc *visitor.Service, + scope coredata.Scoper, + organizationID gid.GID, +) (bool, error) { + fc := graphql.GetFieldContext(ctx) + if fc == nil { + return false, nil + } + + switch fc.Field.Name { + case "requestAccesses": + return false, nil + + case "exportDocumentPDF": + input, ok := fc.Args["input"].(types.ExportDocumentPDFInput) + if !ok { + return false, nil + } + return isPublicDocument(ctx, visitorSvc, scope, organizationID, input.DocumentID) + + case "requestDocumentAccess": + input, ok := fc.Args["input"].(types.RequestDocumentAccessInput) + if !ok { + return false, nil + } + return isPublicDocument(ctx, visitorSvc, scope, organizationID, input.DocumentID) + + case "exportReportPDF": + input, ok := fc.Args["input"].(types.ExportReportPDFInput) + if !ok { + return false, nil + } + return isPublicReport(ctx, visitorSvc, scope, input.ReportID) + + case "requestReportAccess": + input, ok := fc.Args["input"].(types.RequestReportAccessInput) + if !ok { + return false, nil + } + return isPublicReport(ctx, visitorSvc, scope, input.ReportID) + + case "exportCompliancePortalFile": + input, ok := fc.Args["input"].(types.ExportCompliancePortalFileInput) + if !ok { + return false, nil + } + return isPublicPortalFile(ctx, visitorSvc, scope, organizationID, input.CompliancePortalFileID) + + case "requestCompliancePortalFileAccess": + input, ok := fc.Args["input"].(types.RequestCompliancePortalFileAccessInput) + if !ok { + return false, nil + } + return isPublicPortalFile(ctx, visitorSvc, scope, organizationID, input.CompliancePortalFileID) + + default: + return false, nil + } +} + +func isPublicDocument( + ctx context.Context, + visitorSvc *visitor.Service, + scope coredata.Scoper, + organizationID gid.GID, + documentID gid.GID, +) (bool, error) { + document, err := visitorSvc.GetDocument(ctx, scope, organizationID, documentID) + if err != nil { + if isMissingDocument(err) { + return true, nil + } + return false, fmt.Errorf("cannot load document: %w", err) + } + + return document.CompliancePortalVisibility == coredata.CompliancePortalVisibilityPublic, nil +} + +func isPublicReport( + ctx context.Context, + visitorSvc *visitor.Service, + scope coredata.Scoper, + reportID gid.GID, +) (bool, error) { + audit, err := visitorSvc.GetAuditByReportFileID(ctx, scope, reportID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return true, nil + } + return false, fmt.Errorf("cannot load audit: %w", err) + } + + return audit.CompliancePortalVisibility == coredata.CompliancePortalVisibilityPublic, nil +} + +func isPublicPortalFile( + ctx context.Context, + visitorSvc *visitor.Service, + scope coredata.Scoper, + organizationID gid.GID, + fileID gid.GID, +) (bool, error) { + file, err := visitorSvc.GetPortalFile(ctx, scope, organizationID, fileID) + if err != nil { + if errors.Is(err, visitor.ErrPortalFileNotFound) || + errors.Is(err, visitor.ErrPortalFileNotVisible) || + errors.Is(err, coredata.ErrResourceNotFound) { + return true, nil + } + return false, fmt.Errorf("cannot load portal file: %w", err) + } + + return file.CompliancePortalVisibility == coredata.CompliancePortalVisibilityPublic, nil +} + +func isMissingDocument(err error) bool { + if errors.Is(err, visitor.ErrDocumentNotFound) || + errors.Is(err, visitor.ErrDocumentNotVisible) || + errors.Is(err, coredata.ErrResourceNotFound) { + return true + } + _, ok := errors.AsType[*visitor.ErrDocumentArchived](err) + return ok +}