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 (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<div className={classNames.wrapper}>
|
||||||
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
|
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { OrganizationSidebar } from "#/components/OrganizationSidebar";
|
|||||||
import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback";
|
import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback";
|
||||||
import { TrustCenterProvider } from "#/providers/TrustCenterProvider";
|
import { TrustCenterProvider } from "#/providers/TrustCenterProvider";
|
||||||
import { Viewer } from "#/providers/Viewer";
|
import { Viewer } from "#/providers/Viewer";
|
||||||
import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql";
|
|
||||||
import { currentTrustGraphQuery } from "#/queries/TrustGraph";
|
import { currentTrustGraphQuery } from "#/queries/TrustGraph";
|
||||||
|
import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryRef: PreloadedQuery<TrustGraphCurrentQuery>;
|
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 {
|
export class ForbiddenError extends Error {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super(message || "FORBIDDEN");
|
super(message || "FORBIDDEN");
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { type FetchFunction } from "relay-runtime";
|
|||||||
import {
|
import {
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
UnAuthenticatedError,
|
UnAuthenticatedError,
|
||||||
UnauthorizedError,
|
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
AssumptionRequiredError,
|
AssumptionRequiredError,
|
||||||
|
NDASignatureRequiredError,
|
||||||
} from "./errors";
|
} from "./errors";
|
||||||
import { GraphQLError } from "graphql";
|
import { GraphQLError } from "graphql";
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ const hasUnauthenticatedError = (error: GraphQLError) =>
|
|||||||
const hasAssumptionRequiredError = (error: GraphQLError) =>
|
const hasAssumptionRequiredError = (error: GraphQLError) =>
|
||||||
error.extensions?.code == "ASSUMPTION_REQUIRED";
|
error.extensions?.code == "ASSUMPTION_REQUIRED";
|
||||||
|
|
||||||
const hasUnauthorizedError = (error: GraphQLError) =>
|
const hasNDASignatureRequiredError = (error: GraphQLError) =>
|
||||||
error.extensions?.code == "UNAUTHORIZED";
|
error.extensions?.code == "NDA_SIGNATURE_REQUIRED";
|
||||||
|
|
||||||
const hasForbiddenError = (error: GraphQLError) =>
|
const hasForbiddenError = (error: GraphQLError) =>
|
||||||
error.extensions?.code == "FORBIDDEN";
|
error.extensions?.code == "FORBIDDEN";
|
||||||
@@ -88,9 +88,9 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => {
|
|||||||
throw new AssumptionRequiredError(assumptionRequiredError.message)
|
throw new AssumptionRequiredError(assumptionRequiredError.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unauthorizedError = errors.find(hasUnauthorizedError);
|
const ndaSignatureRequiredError = errors.find(hasNDASignatureRequiredError);
|
||||||
if (unauthorizedError) {
|
if (ndaSignatureRequiredError) {
|
||||||
throw new UnauthorizedError(unauthorizedError.message);
|
throw new NDASignatureRequiredError(ndaSignatureRequiredError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const forbiddenError = errors.find(hasForbiddenError);
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -338,6 +339,10 @@ func (s *Service) GetSignatureByID(ctx context.Context, id gid.GID) (*coredata.E
|
|||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(conn pg.Conn) error {
|
||||||
if err := signature.LoadByID(ctx, conn, scope, id); err != nil {
|
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)
|
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(compliancepage.NewCompliancePagePresenceMiddleware())
|
||||||
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
||||||
r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger))
|
r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger))
|
||||||
|
r.Use(compliancepage.NewNDAMiddleware(trustSvc, esignSvc, logger))
|
||||||
|
|
||||||
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig)
|
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")
|
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)
|
pdf, err := trustService.Documents.ExportPDF(ctx, input.DocumentID, identity.EmailAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.ErrorCtx(ctx, "cannot export document PDF", log.Error(err))
|
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")
|
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)
|
pdf, err := trustService.Reports.ExportPDF(ctx, input.ReportID, identity.EmailAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.ErrorCtx(ctx, "cannot export report PDF", log.Error(err))
|
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")
|
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)
|
fileData, err := trustService.TrustCenterFiles.ExportFile(ctx, input.TrustCenterFileID, identity.EmailAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.ErrorCtx(ctx, "cannot export trust center file", log.Error(err))
|
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...))
|
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 {
|
func Forbidden(ctx context.Context, err error) *gqlerror.Error {
|
||||||
return &gqlerror.Error{
|
return &gqlerror.Error{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ var (
|
|||||||
ErrMembershipNotFound = errors.New("membership not found")
|
ErrMembershipNotFound = errors.New("membership not found")
|
||||||
ErrMembershipInactive = errors.New("membership inactive")
|
ErrMembershipInactive = errors.New("membership inactive")
|
||||||
ErrDocumentAccessNotFound = errors.New("document access not found")
|
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
|
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