diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx index 1c0675c34..3142552b4 100644 --- a/apps/console/src/components/PageError.tsx +++ b/apps/console/src/components/PageError.tsx @@ -60,20 +60,6 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) { ); } - if (error instanceof Error && error.message.includes("UNAUTHORIZED")) { - return ( -
-

- - {__("Access denied")} -

-

- {__("You don't have permission to access this organization")} -

-
- ); - } - return (

{__("Unexpected error :(")}

diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx index 140a15003..bf12e4626 100644 --- a/apps/trust/src/layouts/MainLayout.tsx +++ b/apps/trust/src/layouts/MainLayout.tsx @@ -8,8 +8,8 @@ import { OrganizationSidebar } from "#/components/OrganizationSidebar"; import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback"; import { TrustCenterProvider } from "#/providers/TrustCenterProvider"; import { Viewer } from "#/providers/Viewer"; -import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql"; import { currentTrustGraphQuery } from "#/queries/TrustGraph"; +import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql"; type Props = { queryRef: PreloadedQuery; diff --git a/packages/relay/src/errors.ts b/packages/relay/src/errors.ts index ee15363a5..450979eb8 100644 --- a/packages/relay/src/errors.ts +++ b/packages/relay/src/errors.ts @@ -30,14 +30,6 @@ export class AssumptionRequiredError extends Error { } } -export class UnauthorizedError extends Error { - constructor(message?: string) { - super(message || "UNAUTHORIZED"); - this.name = "UnauthorizedError"; - Object.setPrototypeOf(this, UnauthorizedError.prototype); - } -} - export class ForbiddenError extends Error { constructor(message?: string) { super(message || "FORBIDDEN"); diff --git a/packages/relay/src/fetch.ts b/packages/relay/src/fetch.ts index 1b1d9a4b5..95e86b37f 100644 --- a/packages/relay/src/fetch.ts +++ b/packages/relay/src/fetch.ts @@ -2,9 +2,9 @@ import { type FetchFunction } from "relay-runtime"; import { InternalServerError, UnAuthenticatedError, - UnauthorizedError, ForbiddenError, AssumptionRequiredError, + NDASignatureRequiredError, } from "./errors"; import { GraphQLError } from "graphql"; @@ -14,8 +14,8 @@ const hasUnauthenticatedError = (error: GraphQLError) => const hasAssumptionRequiredError = (error: GraphQLError) => error.extensions?.code == "ASSUMPTION_REQUIRED"; -const hasUnauthorizedError = (error: GraphQLError) => - error.extensions?.code == "UNAUTHORIZED"; +const hasNDASignatureRequiredError = (error: GraphQLError) => + error.extensions?.code == "NDA_SIGNATURE_REQUIRED"; const hasForbiddenError = (error: GraphQLError) => error.extensions?.code == "FORBIDDEN"; @@ -88,9 +88,9 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => { throw new AssumptionRequiredError(assumptionRequiredError.message) } - const unauthorizedError = errors.find(hasUnauthorizedError); - if (unauthorizedError) { - throw new UnauthorizedError(unauthorizedError.message); + const ndaSignatureRequiredError = errors.find(hasNDASignatureRequiredError); + if (ndaSignatureRequiredError) { + throw new NDASignatureRequiredError(ndaSignatureRequiredError.message); } const forbiddenError = errors.find(hasForbiddenError); diff --git a/pkg/esign/errors.go b/pkg/esign/errors.go new file mode 100644 index 000000000..65b7d13be --- /dev/null +++ b/pkg/esign/errors.go @@ -0,0 +1,21 @@ +// Copyright (c) 2025 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 esign + +import "errors" + +var ( + ErrElectronicSignatureNotFound = errors.New("electronic signature not found") +) diff --git a/pkg/esign/service.go b/pkg/esign/service.go index 14c866fc3..2437a6779 100644 --- a/pkg/esign/service.go +++ b/pkg/esign/service.go @@ -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) } diff --git a/pkg/server/api/compliancepage/nda_middleware.go b/pkg/server/api/compliancepage/nda_middleware.go new file mode 100644 index 000000000..9d15a1ba6 --- /dev/null +++ b/pkg/server/api/compliancepage/nda_middleware.go @@ -0,0 +1,102 @@ +// Copyright (c) 2025 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 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) + }, + ) + } +} diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index dcdd058b6..4d1145233 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -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) diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 48834649d..c6f7dbe38 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -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)) diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 4f7307efd..5dce8027c 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -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(), diff --git a/pkg/trust/errors.go b/pkg/trust/errors.go index 3a73989e5..135fb906d 100644 --- a/pkg/trust/errors.go +++ b/pkg/trust/errors.go @@ -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") ) diff --git a/pkg/trust/service.go b/pkg/trust/service.go index ef1dccff9..53c651b41 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -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 +}