Add nda signature middleware
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -60,20 +60,6 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes("UNAUTHORIZED")) {
|
||||
return (
|
||||
<div className={classNames.wrapper}>
|
||||
<h1 className={classNames.title}>
|
||||
<IconPageCross size={26} />
|
||||
{__("Access denied")}
|
||||
</h1>
|
||||
<p className={classNames.description}>
|
||||
{__("You don't have permission to access this organization")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.wrapper}>
|
||||
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
|
||||
|
||||
@@ -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<TrustGraphCurrentQuery>;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
21
pkg/esign/errors.go
Normal file
21
pkg/esign/errors.go
Normal 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")
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
102
pkg/server/api/compliancepage/nda_middleware.go
Normal file
102
pkg/server/api/compliancepage/nda_middleware.go
Normal 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user