From 02153ca0dcd69277e97c03a2f9abf6d653b805e0 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Mon, 22 Jun 2026 08:39:54 +0200 Subject: [PATCH] Add alias fields and alias-based node resolution in trust API Node lookup now accepts an alias slug in addition to a GID, resolving it against the organization's alias table before dispatching. Adds alias fields to Document, AuditReport, and TrustCenterFile. Signed-off-by: Bryan Frimin --- pkg/server/api/trust/v1/base_resolvers.go | 119 ++------------ pkg/server/api/trust/v1/graphql/base.graphql | 2 +- .../api/trust/v1/graphql/trust_center.graphql | 3 + pkg/server/api/trust/v1/node_resolver.go | 148 ++++++++++++++++++ .../trust/v1/trust_center_alias_resolvers.go | 42 +++++ .../api/trust/v1/trust_center_resolvers.go | 15 ++ 6 files changed, 223 insertions(+), 106 deletions(-) create mode 100644 pkg/server/api/trust/v1/node_resolver.go create mode 100644 pkg/server/api/trust/v1/trust_center_alias_resolvers.go diff --git a/pkg/server/api/trust/v1/base_resolvers.go b/pkg/server/api/trust/v1/base_resolvers.go index 6517de618..f4f8f9d20 100644 --- a/pkg/server/api/trust/v1/base_resolvers.go +++ b/pkg/server/api/trust/v1/base_resolvers.go @@ -18,7 +18,6 @@ import ( "go.probo.inc/probo/pkg/server/api/trust/v1/schema" "go.probo.inc/probo/pkg/server/api/trust/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" - "go.probo.inc/probo/pkg/trust" ) // Viewer is the resolver for the viewer field. @@ -40,120 +39,30 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) { } // Node is the resolver for the node field. -func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) { - scope := coredata.NewScopeFromObjectID(id) - trustService := r.trust - - switch id.EntityType() { - case coredata.OrganizationEntityType: - organization, err := trustService.Organizations.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewOrganization(organization), nil - - case coredata.DocumentEntityType: +func (r *queryResolver) Node(ctx context.Context, id string) (types.Node, error) { + resourceID, err := gid.ParseGID(id) + if err != nil { trustCenter := compliancepage.CompliancePageFromContext(ctx) + scope := coredata.NewScopeFromObjectID(trustCenter.ID) - document, err := trustService.Documents.Get(ctx, scope, trustCenter.OrganizationID, id) + resourceID, err = r.trust.TrustCenterAliases.ResolveAlias( + ctx, + scope, + trustCenter.OrganizationID, + id, + ) if err != nil { - if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { + if errors.Is(err, coredata.ErrResourceNotFound) { return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } - if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { - return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) - } - - r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err)) + r.logger.ErrorCtx(ctx, "cannot resolve trust center alias", log.Error(err)) return nil, gqlutils.Internal(ctx) } - - return types.NewDocument(document), nil - - case coredata.FrameworkEntityType: - framework, err := trustService.Frameworks.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewFramework(framework), nil - - case coredata.FileEntityType: - trustCenter := compliancepage.CompliancePageFromContext(ctx) - - file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id) - if err != nil { - if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) { - return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) - } - - r.logger.ErrorCtx(ctx, "cannot get audit report file", log.Error(err)) - - return nil, gqlutils.Internal(ctx) - } - - return types.NewAuditReport(file), nil - - case coredata.AuditEntityType: - audit, err := trustService.Audits.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewAudit(audit), nil - - case coredata.ThirdPartyEntityType: - thirdParty, err := trustService.ThirdParties.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewSubprocessor(thirdParty), nil - - case coredata.TrustCenterEntityType: - trustCenter, err := trustService.TrustCenters.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewTrustCenter(trustCenter), nil - - case coredata.TrustCenterReferenceEntityType: - reference, err := trustService.TrustCenterReferences.Get(ctx, scope, id) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewTrustCenterReference(reference), nil - - case coredata.TrustCenterFileEntityType: - trustCenter := compliancepage.CompliancePageFromContext(ctx) - - trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, scope, trustCenter.OrganizationID, id) - if err != nil { - if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { - return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) - } - - r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err)) - - return nil, gqlutils.Internal(ctx) - } - - return types.NewTrustCenterFile(trustCenterFile), nil - - default: - return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } + + return r.nodeByGID(ctx, resourceID, id) } // CurrentTrustCenter is the resolver for the currentTrustCenter field. diff --git a/pkg/server/api/trust/v1/graphql/base.graphql b/pkg/server/api/trust/v1/graphql/base.graphql index 309d8ff65..c69e94524 100644 --- a/pkg/server/api/trust/v1/graphql/base.graphql +++ b/pkg/server/api/trust/v1/graphql/base.graphql @@ -24,7 +24,7 @@ interface Node { type Query { viewer: Identity - node(id: ID!): Node + node(id: String!): Node currentTrustCenter: TrustCenter oidcProviders: [OIDCProviderInfo!]! @goField(forceResolver: true) diff --git a/pkg/server/api/trust/v1/graphql/trust_center.graphql b/pkg/server/api/trust/v1/graphql/trust_center.graphql index 0193b29b5..fb91ee142 100644 --- a/pkg/server/api/trust/v1/graphql/trust_center.graphql +++ b/pkg/server/api/trust/v1/graphql/trust_center.graphql @@ -93,6 +93,7 @@ type Document implements Node @nda { id: ID! title: String! documentType: DocumentType! + alias: String @goField(forceResolver: true) isUserAuthorized: Boolean! @goField(forceResolver: true) access: DocumentAccess @goField(forceResolver: true) } @@ -117,6 +118,7 @@ type Framework implements Node @nda { type AuditReport implements Node @nda { id: ID! fileName: String! + alias: String @goField(forceResolver: true) isUserAuthorized: Boolean! @goField(forceResolver: true) access: DocumentAccess @goField(forceResolver: true) } @@ -279,6 +281,7 @@ type TrustCenterFile implements Node @nda { id: ID! name: String! category: String! + alias: String @goField(forceResolver: true) isUserAuthorized: Boolean! @goField(forceResolver: true) access: DocumentAccess @goField(forceResolver: true) } diff --git a/pkg/server/api/trust/v1/node_resolver.go b/pkg/server/api/trust/v1/node_resolver.go new file mode 100644 index 000000000..008b55964 --- /dev/null +++ b/pkg/server/api/trust/v1/node_resolver.go @@ -0,0 +1,148 @@ +// Copyright (c) 2025-2026 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 trust_v1 + +import ( + "context" + "errors" + + "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/server/api/compliancepage" + "go.probo.inc/probo/pkg/server/api/trust/v1/types" + "go.probo.inc/probo/pkg/server/gqlutils" + "go.probo.inc/probo/pkg/trust" +) + +func (r *queryResolver) nodeByGID( + ctx context.Context, + id gid.GID, + notFoundLabel string, +) (types.Node, error) { + scope := coredata.NewScopeFromObjectID(id) + trustService := r.trust + + switch id.EntityType() { + case coredata.OrganizationEntityType: + organization, err := trustService.Organizations.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewOrganization(organization), nil + + case coredata.DocumentEntityType: + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + document, err := trustService.Documents.Get(ctx, scope, trustCenter.OrganizationID, id) + if err != nil { + if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) { + return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel) + } + + if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok { + return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel) + } + + r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return types.NewDocument(document), nil + + case coredata.FrameworkEntityType: + framework, err := trustService.Frameworks.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewFramework(framework), nil + + case coredata.FileEntityType: + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id) + if err != nil { + if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) { + return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel) + } + + r.logger.ErrorCtx(ctx, "cannot get audit report file", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return types.NewAuditReport(file), nil + + case coredata.AuditEntityType: + audit, err := trustService.Audits.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewAudit(audit), nil + + case coredata.ThirdPartyEntityType: + thirdParty, err := trustService.ThirdParties.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewSubprocessor(thirdParty), nil + + case coredata.TrustCenterEntityType: + trustCenter, err := trustService.TrustCenters.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewTrustCenter(trustCenter), nil + + case coredata.TrustCenterReferenceEntityType: + reference, err := trustService.TrustCenterReferences.Get(ctx, scope, id) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewTrustCenterReference(reference), nil + + case coredata.TrustCenterFileEntityType: + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, scope, trustCenter.OrganizationID, id) + if err != nil { + if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) { + return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel) + } + + r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return types.NewTrustCenterFile(trustCenterFile), nil + + default: + return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel) + } +} diff --git a/pkg/server/api/trust/v1/trust_center_alias_resolvers.go b/pkg/server/api/trust/v1/trust_center_alias_resolvers.go new file mode 100644 index 000000000..b34579d70 --- /dev/null +++ b/pkg/server/api/trust/v1/trust_center_alias_resolvers.go @@ -0,0 +1,42 @@ +// Copyright (c) 2025-2026 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 trust_v1 + +import ( + "context" + + "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/server/api/compliancepage" + "go.probo.inc/probo/pkg/server/gqlutils" +) + +func (r *Resolver) trustCenterAliasForStorageResource( + ctx context.Context, + storageResourceID gid.GID, +) (*string, error) { + trustCenter := compliancepage.CompliancePageFromContext(ctx) + scope := coredata.NewScopeFromObjectID(trustCenter.ID) + + alias, err := r.trust.TrustCenterAliases.GetByStorageResourceID(ctx, scope, storageResourceID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load trust center alias", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return alias, nil +} diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go index 33fecca0c..32c5440ef 100644 --- a/pkg/server/api/trust/v1/trust_center_resolvers.go +++ b/pkg/server/api/trust/v1/trust_center_resolvers.go @@ -69,6 +69,11 @@ func (r *auditResolver) ReportFile(ctx context.Context, obj *types.Audit) (*type return types.NewAuditReport(file), nil } +// Alias is the resolver for the alias field. +func (r *auditReportResolver) Alias(ctx context.Context, obj *types.AuditReport) (*string, error) { + return r.trustCenterAliasForStorageResource(ctx, obj.ID) +} + // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *auditReportResolver) IsUserAuthorized(ctx context.Context, obj *types.AuditReport) (bool, error) { scope := coredata.NewScopeFromObjectID(obj.ID) @@ -169,6 +174,11 @@ func (r *complianceFrameworkResolver) Framework(ctx context.Context, obj *types. return types.NewFramework(framework), nil } +// Alias is the resolver for the alias field. +func (r *documentResolver) Alias(ctx context.Context, obj *types.Document) (*string, error) { + return r.trustCenterAliasForStorageResource(ctx, obj.ID) +} + // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Document) (bool, error) { scope := coredata.NewScopeFromObjectID(obj.ID) @@ -908,6 +918,11 @@ func (r *trustCenterResolver) Updates(ctx context.Context, obj *types.TrustCente return types.NewMailingListUpdateConnection(result), nil } +// Alias is the resolver for the alias field. +func (r *trustCenterFileResolver) Alias(ctx context.Context, obj *types.TrustCenterFile) (*string, error) { + return r.trustCenterAliasForStorageResource(ctx, obj.ID) +} + // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error) { scope := coredata.NewScopeFromObjectID(obj.ID)