Add nda signature middleware

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-20 19:17:11 +04:00
parent bb8d477495
commit 9da8cc2e3d
12 changed files with 191 additions and 41 deletions

21
pkg/esign/errors.go Normal file
View File

@@ -0,0 +1,21 @@
// Copyright (c) 2025 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 esign
import "errors"
var (
ErrElectronicSignatureNotFound = errors.New("electronic signature not found")
)

View File

@@ -17,6 +17,7 @@ package esign
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"time"
@@ -338,6 +339,10 @@ func (s *Service) GetSignatureByID(ctx context.Context, id gid.GID) (*coredata.E
ctx,
func(conn pg.Conn) error {
if err := signature.LoadByID(ctx, conn, scope, id); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrElectronicSignatureNotFound
}
return fmt.Errorf("cannot load electronic signature: %w", err)
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2025 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 compliancepage
import (
"errors"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/trust"
)
func NewNDAMiddleware(trustSvc *trust.Service, esignSvc *esign.Service, logger *log.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
membership := ComplianceMembershipFromContext(ctx)
if membership == nil {
next.ServeHTTP(w, r)
return
}
compliancePage := CompliancePageFromContext(ctx)
if _, err := trustSvc.GetNDAFile(ctx, compliancePage.ID); err != nil {
if errors.Is(err, trust.ErrNDAFileNotFound) {
next.ServeHTTP(w, r)
return
}
logger.ErrorCtx(ctx, "cannot get NDA file", log.Error(err))
httpserver.RenderJSON(
w,
http.StatusInternalServerError,
&graphql.Response{
Errors: gqlerror.List{
gqlutils.Internal(ctx),
},
},
)
return
}
if membership.ElectronicSignatureID == nil {
next.ServeHTTP(w, r)
return
}
sig, err := esignSvc.GetSignatureByID(ctx, *membership.ElectronicSignatureID)
if err != nil {
logger.ErrorCtx(ctx, "cannot get NDA signature", log.Error(err))
httpserver.RenderJSON(
w,
http.StatusInternalServerError,
&graphql.Response{
Errors: gqlerror.List{
gqlutils.Internal(ctx),
},
},
)
return
}
if sig.Status != coredata.ElectronicSignatureStatusCompleted {
httpserver.RenderJSON(
w,
http.StatusForbidden,
&graphql.Response{
Errors: gqlerror.List{
gqlutils.NDASignatureRequiredf(ctx, "NDA signature required"),
},
},
)
return
}
next.ServeHTTP(w, r)
},
)
}
}

View File

@@ -88,6 +88,7 @@ func NewMux(
r.Use(compliancepage.NewCompliancePagePresenceMiddleware())
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger))
r.Use(compliancepage.NewNDAMiddleware(trustSvc, esignSvc, logger))
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig)

View File

@@ -289,10 +289,6 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this document")
}
if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
return nil, err
}
pdf, err := trustService.Documents.ExportPDF(ctx, input.DocumentID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export document PDF", log.Error(err))
@@ -347,10 +343,6 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this report")
}
if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
return nil, err
}
pdf, err := trustService.Reports.ExportPDF(ctx, input.ReportID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export report PDF", log.Error(err))
@@ -403,10 +395,6 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this file")
}
if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
return nil, err
}
fileData, err := trustService.TrustCenterFiles.ExportFile(ctx, input.TrustCenterFileID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export trust center file", log.Error(err))

View File

@@ -66,6 +66,20 @@ func AssumptionRequiredf(ctx context.Context, format string, a ...any) *gqlerror
return AssumptionRequired(ctx, fmt.Errorf(format, a...))
}
func NDASignatureRequired(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Path: graphql.GetPath(ctx),
Extensions: map[string]any{
"code": "NDA_SIGNATURE_REQUIRED",
},
}
}
func NDASignatureRequiredf(ctx context.Context, format string, a ...any) *gqlerror.Error {
return NDASignatureRequired(ctx, fmt.Errorf(format, a...))
}
func Forbidden(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),

View File

@@ -22,4 +22,5 @@ var (
ErrMembershipNotFound = errors.New("membership not found")
ErrMembershipInactive = errors.New("membership inactive")
ErrDocumentAccessNotFound = errors.New("document access not found")
ErrNDAFileNotFound = errors.New("NDA file not found")
)

View File

@@ -300,3 +300,43 @@ func (s *Service) GetMembershipByCompliancePageIDAndEmail(ctx context.Context, c
return membership, nil
}
func (s *Service) GetNDAFile(
ctx context.Context,
compliancePageID gid.GID,
) (*coredata.File, error) {
var (
file *coredata.File
scope = coredata.NewScopeFromObjectID(compliancePageID)
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
trustCenter := &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
if trustCenter.NonDisclosureAgreementFileID == nil {
return nil
}
file = &coredata.File{}
if err := file.LoadByID(ctx, conn, scope, *trustCenter.NonDisclosureAgreementFileID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrNDAFileNotFound
}
return fmt.Errorf("cannot load file: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return file, nil
}