From 44ad34561ea5a74856247ed12257d9104998e55e Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 10 Jul 2026 15:12:51 +0200 Subject: [PATCH] Extract complianceportal package from trust and probo Split the compliance portal into an admin-facing management side and a public-facing visitor side under pkg/complianceportal. Trust-center CRUD, domains, custom links, frameworks, files and accesses move out of pkg/probo, and the visitor read logic moves out of pkg/trust. IAM actions migrate onto compliance-portal scopes. Signed-off-by: Bryan Frimin --- pkg/complianceportal/actions.go | 81 +++ pkg/complianceportal/domain_resolver.go | 51 ++ pkg/complianceportal/errors.go | 31 ++ .../management/access_service.go} | 94 ++-- .../management/custom_link_service.go} | 92 ++-- .../management/domain_service.go | 327 ++++++++++++ .../management/file_service.go} | 139 ++--- .../management/framework_service.go} | 64 +-- .../management/portal_service.go} | 224 +++++---- .../management/reference_service.go} | 332 ++++++------ pkg/complianceportal/management/service.go | 105 ++++ pkg/complianceportal/oauth2_scopes.go | 93 ++++ pkg/complianceportal/policies.go | 65 +++ pkg/complianceportal/resolver/resolver.go | 146 ++++++ .../visitor}/audit_service.go | 18 +- .../visitor}/compliance.md.tmpl | 4 +- .../visitor}/compliance_framework_service.go | 10 +- .../visitor}/compliance_page_service.go | 128 +++-- ...pliance_portal_commitment_group_service.go | 29 +- .../compliance_portal_commitment_service.go | 29 +- .../visitor/custom_link_service.go} | 22 +- .../visitor}/document_service.go | 52 +- .../visitor}/errors.go | 3 +- .../visitor}/framework_service.go | 37 +- .../visitor}/organization_service.go | 71 +-- .../visitor/portal_access_service.go} | 473 +++++++++--------- .../visitor/portal_file_service.go} | 60 +-- .../visitor/portal_reference_service.go} | 46 +- .../visitor/portal_service.go} | 94 ++-- .../visitor}/report_service.go | 66 +-- .../visitor}/rights_request_service.go | 36 +- .../visitor}/rights_request_service_test.go | 6 +- .../visitor}/robots.txt.tmpl | 0 .../visitor}/service.go | 163 +++--- .../visitor}/sitemap.xml.tmpl | 0 .../visitor}/third_party_service.go | 39 +- pkg/probo/actions.go | 99 +--- pkg/probo/custom_domain_service.go | 165 ------ pkg/probo/oauth2_scopes.go | 76 --- pkg/probo/policies.go | 11 - pkg/probo/service.go | 65 --- 41 files changed, 2102 insertions(+), 1544 deletions(-) create mode 100644 pkg/complianceportal/actions.go create mode 100644 pkg/complianceportal/domain_resolver.go create mode 100644 pkg/complianceportal/errors.go rename pkg/{probo/trust_center_access_service.go => complianceportal/management/access_service.go} (84%) rename pkg/{probo/compliance_external_url_service.go => complianceportal/management/custom_link_service.go} (63%) create mode 100644 pkg/complianceportal/management/domain_service.go rename pkg/{probo/trust_center_file_service.go => complianceportal/management/file_service.go} (79%) rename pkg/{probo/compliance_framework_service.go => complianceportal/management/framework_service.go} (75%) rename pkg/{probo/trust_center_service.go => complianceportal/management/portal_service.go} (79%) rename pkg/{probo/trust_center_reference_service.go => complianceportal/management/reference_service.go} (53%) create mode 100644 pkg/complianceportal/management/service.go create mode 100644 pkg/complianceportal/oauth2_scopes.go create mode 100644 pkg/complianceportal/policies.go create mode 100644 pkg/complianceportal/resolver/resolver.go rename pkg/{trust => complianceportal/visitor}/audit_service.go (91%) rename pkg/{trust => complianceportal/visitor}/compliance.md.tmpl (91%) rename pkg/{trust => complianceportal/visitor}/compliance_framework_service.go (92%) rename pkg/{trust => complianceportal/visitor}/compliance_page_service.go (79%) rename pkg/{trust => complianceportal/visitor}/compliance_portal_commitment_group_service.go (80%) rename pkg/{trust => complianceportal/visitor}/compliance_portal_commitment_service.go (81%) rename pkg/{trust/compliance_external_url_service.go => complianceportal/visitor/custom_link_service.go} (73%) rename pkg/{trust => complianceportal/visitor}/document_service.go (89%) rename pkg/{trust => complianceportal/visitor}/errors.go (95%) rename pkg/{trust => complianceportal/visitor}/framework_service.go (82%) rename pkg/{trust => complianceportal/visitor}/organization_service.go (64%) rename pkg/{trust/trust_center_access_service.go => complianceportal/visitor/portal_access_service.go} (63%) rename pkg/{trust/trust_center_file_service.go => complianceportal/visitor/portal_file_service.go} (78%) rename pkg/{trust/trust_center_reference_service.go => complianceportal/visitor/portal_reference_service.go} (75%) rename pkg/{trust/trust_center_service.go => complianceportal/visitor/portal_service.go} (79%) rename pkg/{trust => complianceportal/visitor}/report_service.go (74%) rename pkg/{trust => complianceportal/visitor}/rights_request_service.go (86%) rename pkg/{trust => complianceportal/visitor}/rights_request_service_test.go (97%) rename pkg/{trust => complianceportal/visitor}/robots.txt.tmpl (100%) rename pkg/{trust => complianceportal/visitor}/service.go (71%) rename pkg/{trust => complianceportal/visitor}/sitemap.xml.tmpl (100%) rename pkg/{trust => complianceportal/visitor}/third_party_service.go (84%) delete mode 100644 pkg/probo/custom_domain_service.go diff --git a/pkg/complianceportal/actions.go b/pkg/complianceportal/actions.go new file mode 100644 index 000000000..1def1d104 --- /dev/null +++ b/pkg/complianceportal/actions.go @@ -0,0 +1,81 @@ +// 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 complianceportal + +const ( + // Custom domain actions. + ActionCustomDomainGet = "compliance-portal:custom-domain:get" + ActionCustomDomainCreate = "compliance-portal:custom-domain:create" + ActionCustomDomainDelete = "compliance-portal:custom-domain:delete" + + // Compliance portal actions. + ActionCompliancePortalGet = "compliance-portal:portal:get" + ActionCompliancePortalUpdate = "compliance-portal:portal:update" + ActionCompliancePortalGetNda = "compliance-portal:portal:get-nda" + ActionCompliancePortalNonDisclosureAgreementUpload = "compliance-portal:portal:upload-nda" + ActionCompliancePortalNonDisclosureAgreementDelete = "compliance-portal:portal:delete-nda" + + // Compliance portal access actions. + ActionCompliancePortalAccessGet = "compliance-portal:portal-access:get" + ActionCompliancePortalAccessList = "compliance-portal:portal-access:list" + ActionCompliancePortalAccessCreate = "compliance-portal:portal-access:create" + ActionCompliancePortalAccessUpdate = "compliance-portal:portal-access:update" + ActionCompliancePortalAccessDelete = "compliance-portal:portal-access:delete" + + // Compliance portal reference actions. + ActionCompliancePortalReferenceList = "compliance-portal:portal-reference:list" + ActionCompliancePortalReferenceGetLogoUrl = "compliance-portal:portal-reference:get-logo-url" + ActionCompliancePortalReferenceCreate = "compliance-portal:portal-reference:create" + ActionCompliancePortalReferenceUpdate = "compliance-portal:portal-reference:update" + ActionCompliancePortalReferenceDelete = "compliance-portal:portal-reference:delete" + + // Compliance portal file actions. + ActionCompliancePortalFileGet = "compliance-portal:portal-file:get" + ActionCompliancePortalFileList = "compliance-portal:portal-file:list" + ActionCompliancePortalFileGetFileUrl = "compliance-portal:portal-file:get-file-url" + ActionCompliancePortalFileUpdate = "compliance-portal:portal-file:update" + ActionCompliancePortalFileDelete = "compliance-portal:portal-file:delete" + ActionCompliancePortalFileCreate = "compliance-portal:portal-file:create" + + // Compliance portal document access actions. + ActionCompliancePortalDocumentAccessList = "compliance-portal:portal-document-access:list" + + // MailingListUpdate actions. + ActionMailingListUpdateList = "compliance-portal:mailing-list-update:list" + ActionMailingListUpdateCreate = "compliance-portal:mailing-list-update:create" + ActionMailingListUpdateUpdate = "compliance-portal:mailing-list-update:update" + ActionMailingListUpdateSend = "compliance-portal:mailing-list-update:send" + ActionMailingListUpdateDelete = "compliance-portal:mailing-list-update:delete" + + // MailingList actions. + ActionMailingListUpdate = "compliance-portal:mailing-list:update" + + // MailingListSubscriber actions. + ActionMailingListSubscriberList = "compliance-portal:mailing-list-subscriber:list" + ActionMailingListSubscriberCreate = "compliance-portal:mailing-list-subscriber:create" + ActionMailingListSubscriberDelete = "compliance-portal:mailing-list-subscriber:delete" + + // ComplianceFramework actions. + ActionComplianceFrameworkList = "compliance-portal:compliance-framework:list" + ActionComplianceFrameworkCreate = "compliance-portal:compliance-framework:create" + ActionComplianceFrameworkDelete = "compliance-portal:compliance-framework:delete" + ActionComplianceFrameworkUpdateRank = "compliance-portal:compliance-framework:update-rank" + + // ComplianceCustomLink actions. + ActionComplianceCustomLinkList = "compliance-portal:compliance-custom-link:list" + ActionComplianceCustomLinkCreate = "compliance-portal:compliance-custom-link:create" + ActionComplianceCustomLinkUpdate = "compliance-portal:compliance-custom-link:update" + ActionComplianceCustomLinkDelete = "compliance-portal:compliance-custom-link:delete" +) diff --git a/pkg/complianceportal/domain_resolver.go b/pkg/complianceportal/domain_resolver.go new file mode 100644 index 000000000..23e5d4396 --- /dev/null +++ b/pkg/complianceportal/domain_resolver.go @@ -0,0 +1,51 @@ +// 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 complianceportal + +import ( + "context" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/complianceportal/resolver" + "go.probo.inc/probo/pkg/coredata" +) + +// EffectiveDomainForTrustCenter returns the domain a compliance page is served +// under: the custom domain when it has an active certificate, otherwise the +// default subdomain when its certificate is active. It returns nil when no +// serving domain is available yet. +// +// It is the single certificate-status-aware domain resolver shared by the +// management and visitor sub-packages. +func EffectiveDomainForTrustCenter( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + trustCenter *coredata.TrustCenter, +) (*coredata.CustomDomain, error) { + return resolver.EffectiveDomainForTrustCenter(ctx, conn, scope, trustCenter) +} + +// PublicURLForTrustCenter returns the canonical public URL of a compliance +// page on its dedicated domain. +func PublicURLForTrustCenter( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + trustCenter *coredata.TrustCenter, + baseDomain string, +) (string, error) { + return resolver.PublicURLForTrustCenter(ctx, conn, scope, trustCenter, baseDomain) +} diff --git a/pkg/complianceportal/errors.go b/pkg/complianceportal/errors.go new file mode 100644 index 000000000..6b6cc01d7 --- /dev/null +++ b/pkg/complianceportal/errors.go @@ -0,0 +1,31 @@ +// 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 complianceportal + +import "errors" + +var ( + // ErrCustomDomainNotActive is returned when a domain is set as primary + // while its SSL certificate is not yet active. + ErrCustomDomainNotActive = errors.New("custom domain SSL certificate is not active") + + // ErrCustomDomainManaged is returned when an operation is attempted on the + // managed probopage subdomain that is only allowed on customer domains. + ErrCustomDomainManaged = errors.New("managed custom domain cannot be modified") + + // ErrCustomDomainNotFound is returned when no custom domain exists for the + // requested resource. + ErrCustomDomainNotFound = errors.New("custom domain not found") +) diff --git a/pkg/probo/trust_center_access_service.go b/pkg/complianceportal/management/access_service.go similarity index 84% rename from pkg/probo/trust_center_access_service.go rename to pkg/complianceportal/management/access_service.go index 100184cfe..ce769d96b 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/complianceportal/management/access_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "context" @@ -37,34 +37,30 @@ import ( ) type ( - TrustCenterAccessService struct { - svc *Service - } - - CreateTrustCenterAccessRequest struct { + CreateAccessRequest struct { TrustCenterID gid.GID IdentityID gid.GID } - UpdateTrustCenterDocumentAccessRequest struct { + UpdateDocumentAccessRequest struct { ID gid.GID Status coredata.TrustCenterDocumentAccessStatus } - UpdateTrustCenterAccessRequest struct { + UpdateAccessRequest struct { ID gid.GID - DocumentAccesses []UpdateTrustCenterDocumentAccessRequest - ReportAccesses []UpdateTrustCenterDocumentAccessRequest - TrustCenterFileAccesses []UpdateTrustCenterDocumentAccessRequest + DocumentAccesses []UpdateDocumentAccessRequest + ReportAccesses []UpdateDocumentAccessRequest + TrustCenterFileAccesses []UpdateDocumentAccessRequest } - TrustCenterAccessData struct { + AccessData struct { TrustCenterID gid.GID `json:"trust_center_id"` Email mail.Addr `json:"email"` } ) -func (utcar *UpdateTrustCenterAccessRequest) Validate() error { +func (utcar *UpdateAccessRequest) Validate() error { v := validator.New() v.Check(utcar.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterAccessEntityType)) @@ -84,14 +80,15 @@ func (utcar *UpdateTrustCenterAccessRequest) Validate() error { return v.Error() } -func (s TrustCenterAccessService) ListForTrustCenterID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListAccesses( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, cursor *page.Cursor[coredata.TrustCenterAccessOrderField], ) (*page.Page[*coredata.TrustCenterAccess, coredata.TrustCenterAccessOrderField], error) { var accesses coredata.TrustCenterAccesses - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { return accesses.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) @@ -104,14 +101,15 @@ func (s TrustCenterAccessService) ListForTrustCenterID( return page.NewPage(accesses, cursor), nil } -func (s TrustCenterAccessService) ListAvailableDocumentAccesses( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListAvailableDocumentAccesses( + ctx context.Context, + scope coredata.Scoper, trustCenterAccessID gid.GID, cursor *page.Cursor[coredata.TrustCenterDocumentAccessOrderField], ) (*page.Page[*coredata.TrustCenterDocumentAccess, coredata.TrustCenterDocumentAccessOrderField], error) { var documentAccesses coredata.TrustCenterDocumentAccesses - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { return documentAccesses.LoadAvailableByTrustCenterAccessID(ctx, conn, scope, trustCenterAccessID, cursor) @@ -124,13 +122,14 @@ func (s TrustCenterAccessService) ListAvailableDocumentAccesses( return page.NewPage(documentAccesses, cursor), nil } -func (s TrustCenterAccessService) Get( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GetAccess( + ctx context.Context, + scope coredata.Scoper, accessID gid.GID, ) (*coredata.TrustCenterAccess, error) { var access coredata.TrustCenterAccess - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { return access.LoadByID(ctx, conn, scope, accessID) @@ -143,13 +142,14 @@ func (s TrustCenterAccessService) Get( return &access, nil } -func (s TrustCenterAccessService) CountDocumentAccesses( - ctx context.Context, scope coredata.Scoper, +func (s *Service) CountDocumentAccesses( + ctx context.Context, + scope coredata.Scoper, trustCenterAccessID gid.GID, ) (int, error) { var count int - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var ( @@ -169,13 +169,14 @@ func (s TrustCenterAccessService) CountDocumentAccesses( return count, nil } -func (s TrustCenterAccessService) CountPendingRequestDocumentAccesses( - ctx context.Context, scope coredata.Scoper, +func (s *Service) CountPendingRequestDocumentAccesses( + ctx context.Context, + scope coredata.Scoper, trustCenterAccessID gid.GID, ) (int, error) { var count int - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var ( @@ -195,13 +196,14 @@ func (s TrustCenterAccessService) CountPendingRequestDocumentAccesses( return count, nil } -func (s TrustCenterAccessService) CountActiveDocumentAccesses( - ctx context.Context, scope coredata.Scoper, +func (s *Service) CountActiveDocumentAccesses( + ctx context.Context, + scope coredata.Scoper, trustCenterAccessID gid.GID, ) (int, error) { var count int - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var ( @@ -221,9 +223,10 @@ func (s TrustCenterAccessService) CountActiveDocumentAccesses( return count, nil } -func (s TrustCenterAccessService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateTrustCenterAccessRequest, +func (s *Service) UpdateAccess( + ctx context.Context, + scope coredata.Scoper, + req *UpdateAccessRequest, ) (*coredata.TrustCenterAccess, error) { if err := req.Validate(); err != nil { return nil, err @@ -235,7 +238,7 @@ func (s TrustCenterAccessService) Update( shouldUpdateSlackMessage bool ) - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { access = &coredata.TrustCenterAccess{} @@ -255,6 +258,7 @@ func (s TrustCenterAccessService) Update( ID: d.ID, Status: d.Status, }) + documentIDs = append(documentIDs, d.ID) } @@ -277,6 +281,7 @@ func (s TrustCenterAccessService) Update( ID: d.ID, Status: d.Status, }) + reportIDs = append(reportIDs, d.ID) } @@ -299,6 +304,7 @@ func (s TrustCenterAccessService) Update( ID: d.ID, Status: d.Status, }) + trustCenterFileIDs = append(trustCenterFileIDs, d.ID) } @@ -331,7 +337,7 @@ func (s TrustCenterAccessService) Update( } if shouldUpdateSlackMessage { - if err := s.svc.SlackMessages.QueueSlackNotification(ctx, scope, access.IdentityID, access.TrustCenterID); err != nil { + if err := s.SlackMessages.QueueSlackNotification(ctx, scope, access.IdentityID, access.TrustCenterID); err != nil { if !errors.Is(err, slack.ErrNoSlackConnector) { return nil, fmt.Errorf("cannot queue slack notification: %w", err) } @@ -341,11 +347,12 @@ func (s TrustCenterAccessService) Update( return access, nil } -func (s TrustCenterAccessService) Delete( - ctx context.Context, scope coredata.Scoper, +func (s *Service) DeleteAccess( + ctx context.Context, + scope coredata.Scoper, trustCenterAccessID gid.GID, ) error { - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { access := &coredata.TrustCenterAccess{} @@ -365,7 +372,12 @@ func (s TrustCenterAccessService) Delete( return err } -func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, scope coredata.Scoper, tx pg.Tx, access *coredata.TrustCenterAccess) error { +func (s *Service) sendAccessEmail( + ctx context.Context, + scope coredata.Scoper, + tx pg.Tx, + access *coredata.TrustCenterAccess, +) error { organization := &coredata.Organization{} if err := organization.LoadByID(ctx, tx, scope, access.OrganizationID); err != nil { return fmt.Errorf("cannot load organization: %w", err) @@ -389,7 +401,7 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, scope cor return fmt.Errorf("cannot load profile: %w", err) } - emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, scope, access.TrustCenterID) + emailPresenterCfg, err := s.EmailPresenterConfig(ctx, scope, access.TrustCenterID) if err != nil { return fmt.Errorf("cannot get compliance page email presenter config: %w", err) } diff --git a/pkg/probo/compliance_external_url_service.go b/pkg/complianceportal/management/custom_link_service.go similarity index 63% rename from pkg/probo/compliance_external_url_service.go rename to pkg/complianceportal/management/custom_link_service.go index 1993e09ad..e7046d97d 100644 --- a/pkg/probo/compliance_external_url_service.go +++ b/pkg/complianceportal/management/custom_link_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "context" @@ -33,29 +33,25 @@ import ( ) type ( - ComplianceExternalURLService struct { - svc *Service - } - - CreateComplianceExternalURLRequest struct { + CreateCustomLinkRequest struct { TrustCenterID gid.GID Name string URL string } - UpdateComplianceExternalURLRequest struct { + UpdateCustomLinkRequest struct { ID gid.GID Name string URL string Rank *int } - DeleteComplianceExternalURLRequest struct { + DeleteCustomLinkRequest struct { ID gid.GID } ) -func (r *CreateComplianceExternalURLRequest) Validate() error { +func (r *CreateCustomLinkRequest) Validate() error { v := validator.New() v.Check(r.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) v.Check(r.URL, "url", validator.Required(), validator.URL()) @@ -63,34 +59,35 @@ func (r *CreateComplianceExternalURLRequest) Validate() error { return v.Error() } -func (r *UpdateComplianceExternalURLRequest) Validate() error { +func (r *UpdateCustomLinkRequest) Validate() error { v := validator.New() - v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceExternalURLEntityType)) + v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceCustomLinkEntityType)) v.Check(r.URL, "url", validator.Required(), validator.URL()) v.Check(r.Rank, "rank", validator.Min(1)) return v.Error() } -func (r *DeleteComplianceExternalURLRequest) Validate() error { +func (r *DeleteCustomLinkRequest) Validate() error { v := validator.New() - v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceExternalURLEntityType)) + v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceCustomLinkEntityType)) return v.Error() } -func (s ComplianceExternalURLService) List( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListCustomLinks( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, - cursor *page.Cursor[coredata.ComplianceExternalURLOrderField], -) (*page.Page[*coredata.ComplianceExternalURL, coredata.ComplianceExternalURLOrderField], error) { - var items coredata.ComplianceExternalURLs + cursor *page.Cursor[coredata.ComplianceCustomLinkOrderField], +) (*page.Page[*coredata.ComplianceCustomLink, coredata.ComplianceCustomLinkOrderField], error) { + var items coredata.ComplianceCustomLinks - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := items.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor); err != nil { - return fmt.Errorf("cannot load compliance external URLs: %w", err) + return fmt.Errorf("cannot load custom links: %w", err) } return nil @@ -103,20 +100,21 @@ func (s ComplianceExternalURLService) List( return page.NewPage(items, cursor), nil } -func (s ComplianceExternalURLService) Create( - ctx context.Context, scope coredata.Scoper, - req *CreateComplianceExternalURLRequest, -) (*coredata.ComplianceExternalURL, error) { +func (s *Service) CreateCustomLink( + ctx context.Context, + scope coredata.Scoper, + req *CreateCustomLinkRequest, +) (*coredata.ComplianceCustomLink, error) { if err := req.Validate(); err != nil { return nil, err } now := time.Now() - id := gid.New(scope.GetTenantID(), coredata.ComplianceExternalURLEntityType) + id := gid.New(scope.GetTenantID(), coredata.ComplianceCustomLinkEntityType) - var item *coredata.ComplianceExternalURL + var item *coredata.ComplianceCustomLink - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { trustCenter := &coredata.TrustCenter{} @@ -124,7 +122,7 @@ func (s ComplianceExternalURLService) Create( return fmt.Errorf("cannot load trust center: %w", err) } - item = &coredata.ComplianceExternalURL{ + item = &coredata.ComplianceCustomLink{ ID: id, OrganizationID: trustCenter.OrganizationID, TrustCenterID: req.TrustCenterID, @@ -135,7 +133,7 @@ func (s ComplianceExternalURLService) Create( } if err := item.Insert(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot insert compliance external URL: %w", err) + return fmt.Errorf("cannot insert custom link: %w", err) } return nil @@ -148,23 +146,24 @@ func (s ComplianceExternalURLService) Create( return item, nil } -func (s ComplianceExternalURLService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateComplianceExternalURLRequest, -) (*coredata.ComplianceExternalURL, error) { +func (s *Service) UpdateCustomLink( + ctx context.Context, + scope coredata.Scoper, + req *UpdateCustomLinkRequest, +) (*coredata.ComplianceCustomLink, error) { if err := req.Validate(); err != nil { return nil, err } - var item *coredata.ComplianceExternalURL + var item *coredata.ComplianceCustomLink - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { - item = &coredata.ComplianceExternalURL{} + item = &coredata.ComplianceCustomLink{} if err := item.LoadByID(ctx, tx, scope, req.ID); err != nil { - return fmt.Errorf("cannot load compliance external URL: %w", err) + return fmt.Errorf("cannot load custom link: %w", err) } item.Name = req.Name @@ -174,12 +173,12 @@ func (s ComplianceExternalURLService) Update( if req.Rank != nil { item.Rank = *req.Rank if err := item.UpdateRank(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update compliance external URL rank: %w", err) + return fmt.Errorf("cannot update custom link rank: %w", err) } } if err := item.Update(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update compliance external URL: %w", err) + return fmt.Errorf("cannot update custom link: %w", err) } return nil @@ -192,25 +191,26 @@ func (s ComplianceExternalURLService) Update( return item, nil } -func (s ComplianceExternalURLService) Delete( - ctx context.Context, scope coredata.Scoper, - req *DeleteComplianceExternalURLRequest, +func (s *Service) DeleteCustomLink( + ctx context.Context, + scope coredata.Scoper, + req *DeleteCustomLinkRequest, ) error { if err := req.Validate(); err != nil { return err } - return s.svc.pg.WithTx( + return s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { - item := &coredata.ComplianceExternalURL{} + item := &coredata.ComplianceCustomLink{} if err := item.LoadByID(ctx, tx, scope, req.ID); err != nil { - return fmt.Errorf("cannot load compliance external URL: %w", err) + return fmt.Errorf("cannot load custom link: %w", err) } if err := item.Delete(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot delete compliance external URL: %w", err) + return fmt.Errorf("cannot delete custom link: %w", err) } return nil diff --git a/pkg/complianceportal/management/domain_service.go b/pkg/complianceportal/management/domain_service.go new file mode 100644 index 000000000..6ca8d0309 --- /dev/null +++ b/pkg/complianceportal/management/domain_service.go @@ -0,0 +1,327 @@ +// 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 management + +import ( + "context" + "errors" + "fmt" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/complianceportal" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/validator" +) + +// The compliance portal service owns the relationship between a compliance +// page (trust center) and its domains. A page has two slots stored on the +// trust center row: a default {slug}.probopage.com domain provided by Probo and +// an optional custom domain. It provisions each domain's TLS certificate +// through the generic certmanager service within the trust center's transaction +// so slot changes stay atomic with the page. + +// ErrCustomDomainSlotTaken is returned when a compliance page already has a +// custom domain and another one is added. +var ErrCustomDomainSlotTaken = errors.New("compliance page already has a custom domain") + +// AddCustomDomain provisions the compliance page's custom domain. It fails +// when the page already has one. The default probopage subdomain, provisioned +// at page creation, keeps serving as a fallback while the new certificate +// provisions. +func (s *Service) AddCustomDomain( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, + domain string, +) (*coredata.CustomDomain, error) { + v := validator.New() + v.Check(compliancePageID, "compliance_page_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) + v.Check(domain, "domain", validator.Required(), validator.NotEmpty(), validator.Domain()) + if err := v.Error(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + var customDomain *coredata.CustomDomain + + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, tx, scope, compliancePageID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + if trustCenter.CustomDomainID != nil { + return ErrCustomDomainSlotTaken + } + + certificate, err := s.certManager.EnsureCertificate(ctx, tx, scope, domain) + if err != nil { + return fmt.Errorf("cannot ensure certificate: %w", err) + } + + customDomain = coredata.NewCustomDomain( + scope.GetTenantID(), + trustCenter.OrganizationID, + domain, + false, + ) + customDomain.CertificateID = &certificate.ID + + if err := customDomain.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert custom domain: %w", err) + } + + trustCenter.CustomDomainID = &customDomain.ID + trustCenter.UpdatedAt = time.Now() + + if err := trustCenter.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update trust center: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return customDomain, nil +} + +// RemoveCustomDomain clears the compliance page's custom domain and deletes +// the underlying domain together with its certificate. The default domain +// cannot be removed. +func (s *Service) RemoveCustomDomain( + ctx context.Context, + scope coredata.Scoper, + customDomainID gid.GID, +) error { + return s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + domain := &coredata.CustomDomain{} + if err := domain.LoadByID(ctx, tx, scope, customDomainID); err != nil { + return fmt.Errorf("cannot load custom domain: %w", err) + } + + if domain.Managed { + return complianceportal.ErrCustomDomainManaged + } + + trustCenter := &coredata.TrustCenter{} + err := trustCenter.LoadByDomainID(ctx, tx, customDomainID) + switch { + case err == nil: + if trustCenter.CustomDomainID != nil && *trustCenter.CustomDomainID == customDomainID { + trustCenter.CustomDomainID = nil + trustCenter.UpdatedAt = time.Now() + + if err := trustCenter.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update trust center: %w", err) + } + } + case errors.Is(err, coredata.ErrResourceNotFound): + default: + return fmt.Errorf("cannot load trust center by domain id: %w", err) + } + + if err := domain.Delete(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot delete custom domain: %w", err) + } + + if domain.CertificateID != nil { + if err := s.certManager.Delete(ctx, tx, scope, *domain.CertificateID); err != nil { + return fmt.Errorf("cannot delete certificate: %w", err) + } + } + + return nil + }, + ) +} + +// GetCertificate returns the certificate backing a custom domain, or nil when +// the domain has no certificate yet. +func (s *Service) GetCertificate( + ctx context.Context, + scope coredata.Scoper, + domain *coredata.CustomDomain, +) (*coredata.Certificate, error) { + if domain == nil || domain.CertificateID == nil { + return nil, nil + } + + return s.certManager.Get(ctx, scope, *domain.CertificateID) +} + +// GetDefaultDomain returns the compliance page's default probopage subdomain, +// or nil when it has not been provisioned yet. +func (s *Service) GetDefaultDomain( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (*coredata.CustomDomain, error) { + return s.domainSlot(ctx, scope, compliancePageID, func(tc *coredata.TrustCenter) *gid.GID { + return tc.DefaultDomainID + }, + ) +} + +// GetCustomDomain returns the compliance page's custom domain, or nil when +// none is configured. +func (s *Service) GetCustomDomain( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (*coredata.CustomDomain, error) { + return s.domainSlot(ctx, scope, compliancePageID, func(tc *coredata.TrustCenter) *gid.GID { + return tc.CustomDomainID + }, + ) +} + +func (s *Service) domainSlot( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, + slot func(*coredata.TrustCenter) *gid.GID, +) (*coredata.CustomDomain, error) { + var domain *coredata.CustomDomain + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, conn, scope, compliancePageID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + domainID := slot(trustCenter) + if domainID == nil { + return nil + } + + loaded := &coredata.CustomDomain{} + if err := loaded.LoadByID(ctx, conn, scope, *domainID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil + } + + return fmt.Errorf("cannot load custom domain: %w", err) + } + + domain = loaded + + return nil + }, + ) + if err != nil { + return nil, err + } + + return domain, nil +} + +// EffectiveDomain returns the domain a compliance page is served under: the +// custom domain when it has an active certificate, otherwise the default +// subdomain when its certificate is active. It returns nil when no serving +// domain is available yet. +func (s *Service) EffectiveDomain( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (*coredata.CustomDomain, error) { + var effective *coredata.CustomDomain + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, conn, scope, compliancePageID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + d, err := complianceportal.EffectiveDomainForTrustCenter(ctx, conn, scope, trustCenter) + if err != nil { + return err + } + + effective = d + + return nil + }, + ) + if err != nil { + return nil, err + } + + return effective, nil +} + +// EffectiveCanonicalHost returns the host a compliance page should be served +// under, or an empty string when no serving host is available yet. +func (s *Service) EffectiveCanonicalHost( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (string, error) { + domain, err := s.EffectiveDomain(ctx, scope, compliancePageID) + if err != nil { + return "", err + } + + if domain == nil { + return "", nil + } + + return domain.Domain, nil +} + +// PublicURL returns the canonical public URL of a compliance page on its +// dedicated domain. +func (s *Service) PublicURL( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (string, error) { + var publicURL string + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, conn, scope, compliancePageID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + url, err := complianceportal.PublicURLForTrustCenter(ctx, conn, scope, trustCenter, s.baseDomain) + if err != nil { + return err + } + + publicURL = url + + return nil + }, + ) + if err != nil { + return "", err + } + + return publicURL, nil +} diff --git a/pkg/probo/trust_center_file_service.go b/pkg/complianceportal/management/file_service.go similarity index 79% rename from pkg/probo/trust_center_file_service.go rename to pkg/complianceportal/management/file_service.go index b582e4d7d..d2ff8eb70 100644 --- a/pkg/probo/trust_center_file_service.go +++ b/pkg/complianceportal/management/file_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "bytes" @@ -34,19 +34,13 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/filemanager" - "go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/validator" ) type ( - TrustCenterFileService struct { - svc *Service - fileValidator *filevalidation.FileValidator - } - - CreateTrustCenterFileRequest struct { + CreateFileRequest struct { OrganizationID gid.GID Name string Category string @@ -54,7 +48,7 @@ type ( TrustCenterVisibility coredata.TrustCenterVisibility } - UpdateTrustCenterFileRequest struct { + UpdateFileRequest struct { ID gid.GID Name *string Category *string @@ -62,7 +56,7 @@ type ( } ) -func (ctcfr *CreateTrustCenterFileRequest) Validate() error { +func (ctcfr *CreateFileRequest) Validate() error { v := validator.New() v.Check(ctcfr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) @@ -74,7 +68,7 @@ func (ctcfr *CreateTrustCenterFileRequest) Validate() error { return v.Error() } -func (utcfr *UpdateTrustCenterFileRequest) Validate() error { +func (utcfr *UpdateFileRequest) Validate() error { v := validator.New() v.Check(utcfr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterFileEntityType)) @@ -85,15 +79,16 @@ func (utcfr *UpdateTrustCenterFileRequest) Validate() error { return v.Error() } -func (s TrustCenterFileService) ListForOrganizationID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListFilesForOrganizationID( + ctx context.Context, + scope coredata.Scoper, organizationID gid.GID, cursor *page.Cursor[coredata.TrustCenterFileOrderField], filter *coredata.TrustCenterFileFilter, ) (*page.Page[*coredata.TrustCenterFile, coredata.TrustCenterFileOrderField], error) { var files coredata.TrustCenterFiles - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := files.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter); err != nil { @@ -102,6 +97,7 @@ func (s TrustCenterFileService) ListForOrganizationID( return nil }) + if err != nil { return nil, err } @@ -109,13 +105,14 @@ func (s TrustCenterFileService) ListForOrganizationID( return page.NewPage(files, cursor), nil } -func (s TrustCenterFileService) CountForOrganizationID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) CountFilesForOrganizationID( + ctx context.Context, + scope coredata.Scoper, organizationID gid.GID, ) (int, error) { var count int - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { var err error @@ -127,6 +124,7 @@ func (s TrustCenterFileService) CountForOrganizationID( return nil }) + if err != nil { return 0, err } @@ -134,20 +132,24 @@ func (s TrustCenterFileService) CountForOrganizationID( return count, nil } -func (s TrustCenterFileService) Get( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GetFile( + ctx context.Context, + scope coredata.Scoper, id gid.GID, ) (*coredata.TrustCenterFile, error) { var file *coredata.TrustCenterFile - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - file = &coredata.TrustCenterFile{} - if err := file.LoadByID(ctx, conn, scope, id); err != nil { - return fmt.Errorf("cannot load trust center file: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + file = &coredata.TrustCenterFile{} + if err := file.LoadByID(ctx, conn, scope, id); err != nil { + return fmt.Errorf("cannot load trust center file: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -155,9 +157,10 @@ func (s TrustCenterFileService) Get( return file, nil } -func (s TrustCenterFileService) Create( - ctx context.Context, scope coredata.Scoper, - req *CreateTrustCenterFileRequest, +func (s *Service) CreateFile( + ctx context.Context, + scope coredata.Scoper, + req *CreateFileRequest, ) (*coredata.TrustCenterFile, error) { if err := req.Validate(); err != nil { return nil, err @@ -185,7 +188,7 @@ func (s TrustCenterFileService) Create( s3Key string ) - err = s.svc.pg.WithTx( + err = s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { fileID, objectKey, err := s.uploadFile(ctx, scope, tx, req.File, trustCenterFileID, req.OrganizationID, now) @@ -214,16 +217,17 @@ func (s TrustCenterFileService) Create( }, ) if err != nil { - s.cleanupS3Object(ctx, scope, s3Key) + s.cleanupFileS3Object(ctx, scope, s3Key) return nil, err } return file, nil } -func (s TrustCenterFileService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateTrustCenterFileRequest, +func (s *Service) UpdateFile( + ctx context.Context, + scope coredata.Scoper, + req *UpdateFileRequest, ) (*coredata.TrustCenterFile, error) { if err := req.Validate(); err != nil { return nil, err @@ -233,7 +237,7 @@ func (s TrustCenterFileService) Update( var file *coredata.TrustCenterFile - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { file = &coredata.TrustCenterFile{} @@ -270,11 +274,12 @@ func (s TrustCenterFileService) Update( return file, nil } -func (s TrustCenterFileService) Delete( - ctx context.Context, scope coredata.Scoper, +func (s *Service) DeleteFile( + ctx context.Context, + scope coredata.Scoper, trustCenterFileID gid.GID, ) error { - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { file := &coredata.TrustCenterFile{} @@ -293,14 +298,15 @@ func (s TrustCenterFileService) Delete( return err } -func (s TrustCenterFileService) GenerateFileURL( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GenerateFileURL( + ctx context.Context, + scope coredata.Scoper, trustCenterFileID gid.GID, duration time.Duration, ) (string, error) { var storedFile *coredata.File - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { file := &coredata.TrustCenterFile{} @@ -320,7 +326,7 @@ func (s TrustCenterFileService) GenerateFileURL( return "", err } - fileURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, storedFile, duration) + fileURL, err := s.fileManager.GeneratePresignedURL(ctx, storedFile, duration) if err != nil { return "", fmt.Errorf("cannot generate file URL: %w", err) } @@ -328,8 +334,9 @@ func (s TrustCenterFileService) GenerateFileURL( return fileURL, nil } -func (s TrustCenterFileService) uploadFile( - ctx context.Context, scope coredata.Scoper, +func (s *Service) uploadFile( + ctx context.Context, + scope coredata.Scoper, tx pg.Tx, file File, trustCenterFileID gid.GID, @@ -389,18 +396,21 @@ func (s TrustCenterFileService) uploadFile( } } - _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(objectKey.String()), - Body: fileContent, - ContentType: new(contentType), - CacheControl: new("private, max-age=3600"), - Metadata: map[string]string{ - "type": "trust-center-file", - "trust-center-file-id": trustCenterFileID.String(), - "organization-id": organizationID.String(), + _, err = s.s3.PutObject( + ctx, + &s3.PutObjectInput{ + Bucket: new(s.bucket), + Key: new(objectKey.String()), + Body: fileContent, + ContentType: new(contentType), + CacheControl: new("private, max-age=3600"), + Metadata: map[string]string{ + "type": "trust-center-file", + "trust-center-file-id": trustCenterFileID.String(), + "organization-id": organizationID.String(), + }, }, - }) + ) if err != nil { return gid.GID{}, "", fmt.Errorf("cannot upload file to S3: %w", err) } @@ -408,7 +418,7 @@ func (s TrustCenterFileService) uploadFile( fileRecord := &coredata.File{ ID: fileID, OrganizationID: organizationID, - BucketName: s.svc.bucket, + BucketName: s.bucket, MimeType: contentType, FileName: filename, FileKey: objectKey.String(), @@ -425,13 +435,20 @@ func (s TrustCenterFileService) uploadFile( return fileID, objectKey.String(), nil } -func (s TrustCenterFileService) cleanupS3Object(ctx context.Context, scope coredata.Scoper, s3Key string) { +func (s *Service) cleanupFileS3Object( + ctx context.Context, + scope coredata.Scoper, + s3Key string, +) { if s3Key == "" { return } - _, _ = s.svc.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(s3Key), - }) + _, _ = s.s3.DeleteObject( + ctx, + &s3.DeleteObjectInput{ + Bucket: new(s.bucket), + Key: new(s3Key), + }, + ) } diff --git a/pkg/probo/compliance_framework_service.go b/pkg/complianceportal/management/framework_service.go similarity index 75% rename from pkg/probo/compliance_framework_service.go rename to pkg/complianceportal/management/framework_service.go index 5143d3e73..bcd5dc50d 100644 --- a/pkg/probo/compliance_framework_service.go +++ b/pkg/complianceportal/management/framework_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "context" @@ -33,26 +33,22 @@ import ( ) type ( - ComplianceFrameworkService struct { - svc *Service - } - - CreateComplianceFrameworkRequest struct { + CreateFrameworkRequest struct { TrustCenterID gid.GID FrameworkID gid.GID } - UpdateComplianceFrameworkRequest struct { + UpdateFrameworkRequest struct { ID gid.GID Rank int } - DeleteComplianceFrameworkRequest struct { + DeleteFrameworkRequest struct { ID gid.GID } ) -func (r *CreateComplianceFrameworkRequest) Validate() error { +func (r *CreateFrameworkRequest) Validate() error { v := validator.New() v.Check(r.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) @@ -61,7 +57,7 @@ func (r *CreateComplianceFrameworkRequest) Validate() error { return v.Error() } -func (r *UpdateComplianceFrameworkRequest) Validate() error { +func (r *UpdateFrameworkRequest) Validate() error { v := validator.New() v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceFrameworkEntityType)) @@ -69,7 +65,7 @@ func (r *UpdateComplianceFrameworkRequest) Validate() error { return v.Error() } -func (r *DeleteComplianceFrameworkRequest) Validate() error { +func (r *DeleteFrameworkRequest) Validate() error { v := validator.New() v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.ComplianceFrameworkEntityType)) @@ -77,18 +73,19 @@ func (r *DeleteComplianceFrameworkRequest) Validate() error { return v.Error() } -func (s ComplianceFrameworkService) ListWithHiddenForTrustCenterID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListFrameworksWithHidden( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, cursor *page.Cursor[coredata.ComplianceFrameworkOrderField], ) (*page.Page[*coredata.ComplianceFramework, coredata.ComplianceFrameworkOrderField], error) { var cfs coredata.ComplianceFrameworks - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := cfs.LoadWithHiddenByTrustCenterID(ctx, conn, scope, trustCenterID, cursor); err != nil { - return fmt.Errorf("cannot load compliance frameworks with hidden: %w", err) + return fmt.Errorf("cannot load frameworks with hidden: %w", err) } return nil @@ -101,9 +98,10 @@ func (s ComplianceFrameworkService) ListWithHiddenForTrustCenterID( return page.NewPage(cfs, cursor), nil } -func (s ComplianceFrameworkService) Create( - ctx context.Context, scope coredata.Scoper, - req *CreateComplianceFrameworkRequest, +func (s *Service) CreateFramework( + ctx context.Context, + scope coredata.Scoper, + req *CreateFrameworkRequest, ) (*coredata.ComplianceFramework, error) { if err := req.Validate(); err != nil { return nil, err @@ -115,7 +113,7 @@ func (s ComplianceFrameworkService) Create( var cf *coredata.ComplianceFramework - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { trustCenter := &coredata.TrustCenter{} @@ -138,7 +136,7 @@ func (s ComplianceFrameworkService) Create( } if err := cf.Insert(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot insert compliance framework: %w", err) + return fmt.Errorf("cannot insert framework: %w", err) } return nil @@ -151,9 +149,10 @@ func (s ComplianceFrameworkService) Create( return cf, nil } -func (s ComplianceFrameworkService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateComplianceFrameworkRequest, +func (s *Service) UpdateFramework( + ctx context.Context, + scope coredata.Scoper, + req *UpdateFrameworkRequest, ) (*coredata.ComplianceFramework, error) { if err := req.Validate(); err != nil { return nil, err @@ -161,20 +160,20 @@ func (s ComplianceFrameworkService) Update( var cf *coredata.ComplianceFramework - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { cf = &coredata.ComplianceFramework{} if err := cf.LoadByID(ctx, tx, scope, req.ID); err != nil { - return fmt.Errorf("cannot load compliance framework: %w", err) + return fmt.Errorf("cannot load framework: %w", err) } cf.Rank = req.Rank cf.UpdatedAt = time.Now() if err := cf.UpdateRank(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update compliance framework rank: %w", err) + return fmt.Errorf("cannot update framework rank: %w", err) } return nil @@ -187,25 +186,26 @@ func (s ComplianceFrameworkService) Update( return cf, nil } -func (s ComplianceFrameworkService) Delete( - ctx context.Context, scope coredata.Scoper, - req *DeleteComplianceFrameworkRequest, +func (s *Service) DeleteFramework( + ctx context.Context, + scope coredata.Scoper, + req *DeleteFrameworkRequest, ) error { if err := req.Validate(); err != nil { return err } - return s.svc.pg.WithTx( + return s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { cf := &coredata.ComplianceFramework{} if err := cf.LoadByID(ctx, tx, scope, req.ID); err != nil { - return fmt.Errorf("cannot load compliance framework: %w", err) + return fmt.Errorf("cannot load framework: %w", err) } if err := cf.Delete(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot delete compliance framework: %w", err) + return fmt.Errorf("cannot delete framework: %w", err) } return nil diff --git a/pkg/probo/trust_center_service.go b/pkg/complianceportal/management/portal_service.go similarity index 79% rename from pkg/probo/trust_center_service.go rename to pkg/complianceportal/management/portal_service.go index 21aa399eb..2534075df 100644 --- a/pkg/probo/trust_center_service.go +++ b/pkg/complianceportal/management/portal_service.go @@ -18,15 +18,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "context" - "errors" "fmt" "io" "mime" - "net/url" + "net/mail" "path/filepath" "time" @@ -34,6 +33,7 @@ import ( "go.gearno.de/crypto/uuid" "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" + "go.probo.inc/probo/pkg/complianceportal" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/gid" @@ -41,25 +41,25 @@ import ( ) type ( - TrustCenterService struct { - svc *Service - } - - UpdateTrustCenterRequest struct { + UpdateRequest struct { ID gid.GID Active *bool Slug *string SearchEngineIndexing *coredata.SearchEngineIndexing NonDisclosureAgreementFileID *gid.GID + Description **string + WebsiteURL **string + Email **string + HeadquarterAddress **string } - UploadTrustCenterNDARequest struct { + UploadNDARequest struct { TrustCenterID gid.GID File io.Reader FileName string } - UpdateTrustCenterBrandRequest struct { + UpdateBrandRequest struct { TrustCenterID gid.GID LogoFile **FileUpload DarkLogoFile **FileUpload @@ -68,17 +68,33 @@ type ( const maxBrandFileSize = 5 * 1024 * 1024 // 5MB -func (utcr *UpdateTrustCenterRequest) Validate() error { +func (utcr *UpdateRequest) Validate() error { v := validator.New() v.Check(utcr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) v.Check(utcr.Slug, "slug", validator.SafeText(NameMaxLength)) v.Check(utcr.NonDisclosureAgreementFileID, "non_disclosure_agreement_file_id", validator.GID(coredata.FileEntityType)) + if utcr.Description != nil { + v.Check(*utcr.Description, "description", validator.SafeText(ContentMaxLength)) + } + + if utcr.WebsiteURL != nil { + v.Check(*utcr.WebsiteURL, "website_url", validator.SafeText(2048)) + } + + if utcr.Email != nil { + v.Check(*utcr.Email, "email", validator.SafeText(255)) + } + + if utcr.HeadquarterAddress != nil { + v.Check(*utcr.HeadquarterAddress, "headquarter_address", validator.SafeText(2048)) + } + return v.Error() } -func (utcndar *UploadTrustCenterNDARequest) Validate() error { +func (utcndar *UploadNDARequest) Validate() error { v := validator.New() v.Check(utcndar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) @@ -87,7 +103,7 @@ func (utcndar *UploadTrustCenterNDARequest) Validate() error { return v.Error() } -func (req *UpdateTrustCenterBrandRequest) Validate() error { +func (req *UpdateBrandRequest) Validate() error { fv := filevalidation.NewValidator( filevalidation.WithCategories(filevalidation.CategoryImage), filevalidation.WithMaxFileSize(maxBrandFileSize), @@ -110,13 +126,14 @@ func (req *UpdateTrustCenterBrandRequest) Validate() error { return nil } -func (s TrustCenterService) Get( - ctx context.Context, scope coredata.Scoper, +func (s *Service) Get( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.TrustCenter, error) { var trustCenter *coredata.TrustCenter - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter = &coredata.TrustCenter{} @@ -134,13 +151,14 @@ func (s TrustCenterService) Get( return trustCenter, nil } -func (s TrustCenterService) GetByOrganizationID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GetByOrganizationID( + ctx context.Context, + scope coredata.Scoper, organizationID gid.GID, ) (*coredata.TrustCenter, error) { var trustCenter *coredata.TrustCenter - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter = &coredata.TrustCenter{} @@ -158,9 +176,10 @@ func (s TrustCenterService) GetByOrganizationID( return trustCenter, nil } -func (s TrustCenterService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateTrustCenterRequest, +func (s *Service) Update( + ctx context.Context, + scope coredata.Scoper, + req *UpdateRequest, ) (*coredata.TrustCenter, *coredata.File, error) { if err := req.Validate(); err != nil { return nil, nil, err @@ -171,7 +190,7 @@ func (s TrustCenterService) Update( file *coredata.File ) - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { trustCenter = &coredata.TrustCenter{} @@ -191,6 +210,28 @@ func (s TrustCenterService) Update( trustCenter.SearchEngineIndexing = *req.SearchEngineIndexing } + if req.Description != nil { + trustCenter.Description = *req.Description + } + + if req.WebsiteURL != nil { + trustCenter.WebsiteURL = *req.WebsiteURL + } + + if req.Email != nil { + if *req.Email != nil { + if _, err := mail.ParseAddress(**req.Email); err != nil { + return fmt.Errorf("invalid email address: %w", err) + } + } + + trustCenter.Email = *req.Email + } + + if req.HeadquarterAddress != nil { + trustCenter.HeadquarterAddress = *req.HeadquarterAddress + } + trustCenter.UpdatedAt = time.Now() if err := trustCenter.Update(ctx, conn, scope); err != nil { @@ -214,9 +255,10 @@ func (s TrustCenterService) Update( return trustCenter, file, nil } -func (s TrustCenterService) UploadNDA( - ctx context.Context, scope coredata.Scoper, - req *UploadTrustCenterNDARequest, +func (s *Service) UploadNDA( + ctx context.Context, + scope coredata.Scoper, + req *UploadNDARequest, ) (*coredata.TrustCenter, *coredata.File, error) { if err := req.Validate(); err != nil { return nil, nil, err @@ -227,7 +269,7 @@ func (s TrustCenterService) UploadNDA( file *coredata.File ) - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { trustCenter = &coredata.TrustCenter{} @@ -255,7 +297,7 @@ func (s TrustCenterService) UploadNDA( file = &coredata.File{ ID: fileID, OrganizationID: trustCenter.OrganizationID, - BucketName: s.svc.bucket, + BucketName: s.bucket, MimeType: mimeType, FileName: req.FileName, FileKey: objectKey.String(), @@ -264,7 +306,7 @@ func (s TrustCenterService) UploadNDA( UpdatedAt: now, } - fileSize, err := s.svc.fileManager.PutFile( + fileSize, err := s.fileManager.PutFile( ctx, file, req.File, @@ -301,13 +343,14 @@ func (s TrustCenterService) UploadNDA( return trustCenter, file, nil } -func (s TrustCenterService) DeleteNDA( - ctx context.Context, scope coredata.Scoper, +func (s *Service) DeleteNDA( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.TrustCenter, *coredata.File, error) { var trustCenter *coredata.TrustCenter - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { trustCenter = &coredata.TrustCenter{} @@ -332,9 +375,10 @@ func (s TrustCenterService) DeleteNDA( return trustCenter, nil, nil } -func (s TrustCenterService) UpdateTrustCenterBrand( - ctx context.Context, scope coredata.Scoper, - req *UpdateTrustCenterBrandRequest, +func (s *Service) UpdateBrand( + ctx context.Context, + scope coredata.Scoper, + req *UpdateBrandRequest, ) (*coredata.TrustCenter, *coredata.File, error) { if err := req.Validate(); err != nil { return nil, nil, err @@ -345,7 +389,7 @@ func (s TrustCenterService) UpdateTrustCenterBrand( ndaFile *coredata.File ) - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, conn pg.Tx) error { trustCenter = &coredata.TrustCenter{} @@ -359,7 +403,7 @@ func (s TrustCenterService) UpdateTrustCenterBrand( if *req.LogoFile == nil { trustCenter.LogoFileID = nil } else { - file, err := s.uploadFile(ctx, scope, conn, *req.LogoFile, "trust-center-logo", trustCenter) + file, err := s.uploadBrandFile(ctx, scope, conn, *req.LogoFile, "trust-center-logo", trustCenter) if err != nil { return fmt.Errorf("cannot upload logo file: %w", err) } @@ -372,7 +416,7 @@ func (s TrustCenterService) UpdateTrustCenterBrand( if *req.DarkLogoFile == nil { trustCenter.DarkLogoFileID = nil } else { - file, err := s.uploadFile(ctx, scope, conn, *req.DarkLogoFile, "trust-center-dark-logo", trustCenter) + file, err := s.uploadBrandFile(ctx, scope, conn, *req.DarkLogoFile, "trust-center-dark-logo", trustCenter) if err != nil { return fmt.Errorf("cannot upload dark logo file: %w", err) } @@ -404,8 +448,9 @@ func (s TrustCenterService) UpdateTrustCenterBrand( return trustCenter, ndaFile, nil } -func (s TrustCenterService) uploadFile( - ctx context.Context, scope coredata.Scoper, +func (s *Service) uploadBrandFile( + ctx context.Context, + scope coredata.Scoper, conn pg.Tx, fileUpload *FileUpload, fileType string, @@ -421,8 +466,8 @@ func (s TrustCenterService) uploadFile( mimeType = mime.TypeByExtension(filepath.Ext(fileUpload.Filename)) } - _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ - Bucket: &s.svc.bucket, + _, err = s.s3.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &s.bucket, Key: new(objectKey.String()), Body: fileUpload.Content, ContentType: &mimeType, @@ -437,8 +482,8 @@ func (s TrustCenterService) uploadFile( return nil, fmt.Errorf("cannot upload file to S3: %w", err) } - headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{ - Bucket: new(s.svc.bucket), + headOutput, err := s.s3.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: new(s.bucket), Key: new(objectKey.String()), }) if err != nil { @@ -451,7 +496,7 @@ func (s TrustCenterService) uploadFile( file := &coredata.File{ ID: fileID, OrganizationID: trustCenter.OrganizationID, - BucketName: s.svc.bucket, + BucketName: s.bucket, MimeType: mimeType, FileName: fileUpload.Filename, FileKey: objectKey.String(), @@ -468,8 +513,9 @@ func (s TrustCenterService) uploadFile( return file, nil } -func (s TrustCenterService) GenerateNDAFileURL( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GenerateNDAFileURL( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, expiresIn time.Duration, ) (*string, error) { @@ -477,7 +523,7 @@ func (s TrustCenterService) GenerateNDAFileURL( trustCenter := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := trustCenter.LoadByID(ctx, conn, scope, trustCenterID); err != nil { @@ -504,7 +550,7 @@ func (s TrustCenterService) GenerateNDAFileURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -512,15 +558,16 @@ func (s TrustCenterService) GenerateNDAFileURL( return &presignedURL, nil } -func (s TrustCenterService) GenerateLogoURL( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GenerateLogoURL( + ctx context.Context, + scope coredata.Scoper, compliancePageID gid.GID, expiresIn time.Duration, ) (*string, error) { file := &coredata.File{} compliancePage := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -550,7 +597,7 @@ func (s TrustCenterService) GenerateLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -558,15 +605,16 @@ func (s TrustCenterService) GenerateLogoURL( return &presignedURL, nil } -func (s TrustCenterService) GenerateDarkLogoURL( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GenerateDarkLogoURL( + ctx context.Context, + scope coredata.Scoper, compliancePageID gid.GID, expiresIn time.Duration, ) (*string, error) { file := &coredata.File{} compliancePage := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -596,7 +644,7 @@ func (s TrustCenterService) GenerateDarkLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -604,16 +652,20 @@ func (s TrustCenterService) GenerateDarkLogoURL( return &presignedURL, nil } -func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, scope coredata.Scoper, compliancePageID gid.GID) (emails.PresenterConfig, error) { +func (s *Service) EmailPresenterConfig( + ctx context.Context, + scope coredata.Scoper, + compliancePageID gid.GID, +) (emails.PresenterConfig, error) { var ( compliancePage = &coredata.TrustCenter{} organization = &coredata.Organization{} - customDomain *coredata.CustomDomain logoFile = &coredata.File{} - emailPresenterCfg = emails.DefaultPresenterConfig(s.svc.baseURL) + compliancePageURL string + emailPresenterCfg = emails.DefaultPresenterConfig(s.baseURL) ) - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -630,13 +682,19 @@ func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, scope cor return fmt.Errorf("cannot load organization: %w", err) } - customDomain = &coredata.CustomDomain{} - if err := customDomain.LoadByOrganizationID(ctx, conn, scope, organization.ID); err != nil { - if !errors.Is(err, coredata.ErrResourceNotFound) { - return fmt.Errorf("cannot load custom domain: %w", err) - } + publicURL, err := complianceportal.PublicURLForTrustCenter( + ctx, + conn, + scope, + compliancePage, + s.baseDomain, + ) + if err != nil { + return fmt.Errorf("cannot resolve compliance page URL: %w", err) } + compliancePageURL = publicURL + return nil }, ) @@ -644,24 +702,7 @@ func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, scope cor return emailPresenterCfg, err } - parsedBaseURL, err := url.Parse(s.svc.baseURL) - if err != nil { - return emailPresenterCfg, fmt.Errorf("cannot parse base URL: %w", err) - } - - baseURL := url.URL{ - Scheme: parsedBaseURL.Scheme, - Host: parsedBaseURL.Host, - Path: "/trust/" + compliancePage.ID.String(), - } - - if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive { - baseURL.Host = customDomain.Domain - baseURL.Scheme = "https" - baseURL.Path = "" - } - - emailPresenterCfg.BaseURL = baseURL.String() + emailPresenterCfg.BaseURL = compliancePageURL if compliancePage.LogoFileID != nil { if logoFile.FileKey == "" { @@ -671,25 +712,26 @@ func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, scope cor emailPresenterCfg.SenderCompanyLogoPath = filepath.Join("/api/files/v1/public/", logoFile.ID.String()) emailPresenterCfg.SenderCompanyName = organization.Name - if organization.WebsiteURL != nil { - emailPresenterCfg.SenderCompanyWebsiteURL = *organization.WebsiteURL + if compliancePage.WebsiteURL != nil { + emailPresenterCfg.SenderCompanyWebsiteURL = *compliancePage.WebsiteURL } - if organization.HeadquarterAddress != nil { - emailPresenterCfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress + if compliancePage.HeadquarterAddress != nil { + emailPresenterCfg.SenderCompanyHeadquarterAddress = *compliancePage.HeadquarterAddress } } return emailPresenterCfg, nil } -func (s *TrustCenterService) GetMailingList( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GetMailingList( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.MailingList, error) { var mailingList *coredata.MailingList - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter := &coredata.TrustCenter{} diff --git a/pkg/probo/trust_center_reference_service.go b/pkg/complianceportal/management/reference_service.go similarity index 53% rename from pkg/probo/trust_center_reference_service.go rename to pkg/complianceportal/management/reference_service.go index 471806aa7..1ebdd1605 100644 --- a/pkg/probo/trust_center_reference_service.go +++ b/pkg/complianceportal/management/reference_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package probo +package management import ( "bytes" @@ -39,11 +39,7 @@ import ( ) type ( - TrustCenterReferenceService struct { - svc *Service - } - - CreateTrustCenterReferenceRequest struct { + CreateReferenceRequest struct { TrustCenterID gid.GID Name string Description *string @@ -51,7 +47,7 @@ type ( LogoFile File } - UpdateTrustCenterReferenceRequest struct { + UpdateReferenceRequest struct { ID gid.GID Name *string Description **string @@ -61,7 +57,7 @@ type ( } ) -func (ctcrr *CreateTrustCenterReferenceRequest) Validate() error { +func (ctcrr *CreateReferenceRequest) Validate() error { v := validator.New() v.Check(ctcrr.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) @@ -72,7 +68,7 @@ func (ctcrr *CreateTrustCenterReferenceRequest) Validate() error { return v.Error() } -func (utcrr *UpdateTrustCenterReferenceRequest) Validate() error { +func (utcrr *UpdateReferenceRequest) Validate() error { v := validator.New() v.Check(utcrr.ID, "id", validator.Required(), validator.GID(coredata.TrustCenterReferenceEntityType)) @@ -83,21 +79,25 @@ func (utcrr *UpdateTrustCenterReferenceRequest) Validate() error { return v.Error() } -func (s TrustCenterReferenceService) ListForTrustCenterID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) ListReferences( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, cursor *page.Cursor[coredata.TrustCenterReferenceOrderField], ) (*page.Page[*coredata.TrustCenterReference, coredata.TrustCenterReferenceOrderField], error) { var references coredata.TrustCenterReferences - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := references.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) - if err != nil { - return fmt.Errorf("cannot load trust center references: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := references.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) + if err != nil { + return fmt.Errorf("cannot load trust center references: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -105,22 +105,26 @@ func (s TrustCenterReferenceService) ListForTrustCenterID( return page.NewPage(references, cursor), nil } -func (s TrustCenterReferenceService) CountForTrustCenterID( - ctx context.Context, scope coredata.Scoper, +func (s *Service) CountReferences( + ctx context.Context, + scope coredata.Scoper, trustCenterID gid.GID, ) (int, error) { var count int - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) (err error) { - references := coredata.TrustCenterReferences{} + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) (err error) { + references := coredata.TrustCenterReferences{} - count, err = references.CountByTrustCenterID(ctx, conn, scope, trustCenterID) - if err != nil { - return fmt.Errorf("cannot count trust center references: %w", err) - } + count, err = references.CountByTrustCenterID(ctx, conn, scope, trustCenterID) + if err != nil { + return fmt.Errorf("cannot count trust center references: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return 0, err } @@ -128,20 +132,24 @@ func (s TrustCenterReferenceService) CountForTrustCenterID( return count, nil } -func (s TrustCenterReferenceService) Get( - ctx context.Context, scope coredata.Scoper, +func (s *Service) GetReference( + ctx context.Context, + scope coredata.Scoper, referenceID gid.GID, ) (*coredata.TrustCenterReference, error) { var reference coredata.TrustCenterReference - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := reference.LoadByID(ctx, conn, scope, referenceID) - if err != nil { - return fmt.Errorf("cannot load trust center reference: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := reference.LoadByID(ctx, conn, scope, referenceID) + if err != nil { + return fmt.Errorf("cannot load trust center reference: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -149,9 +157,10 @@ func (s TrustCenterReferenceService) Get( return &reference, nil } -func (s TrustCenterReferenceService) Create( - ctx context.Context, scope coredata.Scoper, - req *CreateTrustCenterReferenceRequest, +func (s *Service) CreateReference( + ctx context.Context, + scope coredata.Scoper, + req *CreateReferenceRequest, ) (*coredata.TrustCenterReference, error) { if err := req.Validate(); err != nil { return nil, err @@ -165,48 +174,52 @@ func (s TrustCenterReferenceService) Create( var logoKey string - err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - trustCenter := &coredata.TrustCenter{} - if err := trustCenter.LoadByID(ctx, tx, scope, req.TrustCenterID); err != nil { - return fmt.Errorf("cannot load trust center: %w", err) - } + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, tx, scope, req.TrustCenterID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } - fileID, s3Key, err := s.uploadLogoFile(ctx, scope, tx, req.LogoFile, referenceID, req.TrustCenterID, now) - if err != nil { - return fmt.Errorf("cannot upload logo file: %w", err) - } + fileID, s3Key, err := s.uploadReferenceLogoFile(ctx, scope, tx, req.LogoFile, referenceID, req.TrustCenterID, now) + if err != nil { + return fmt.Errorf("cannot upload logo file: %w", err) + } - logoKey = s3Key + logoKey = s3Key - reference = &coredata.TrustCenterReference{ - ID: referenceID, - OrganizationID: trustCenter.OrganizationID, - TrustCenterID: req.TrustCenterID, - Name: req.Name, - Description: req.Description, - WebsiteURL: req.WebsiteURL, - LogoFileID: fileID, - CreatedAt: now, - UpdatedAt: now, - } + reference = &coredata.TrustCenterReference{ + ID: referenceID, + OrganizationID: trustCenter.OrganizationID, + TrustCenterID: req.TrustCenterID, + Name: req.Name, + Description: req.Description, + WebsiteURL: req.WebsiteURL, + LogoFileID: fileID, + CreatedAt: now, + UpdatedAt: now, + } - if err := reference.Insert(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot insert trust center reference: %w", err) - } + if err := reference.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert trust center reference: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { - s.cleanupS3Object(ctx, scope, logoKey) + s.cleanupReferenceS3Object(ctx, scope, logoKey) return nil, err } return reference, nil } -func (s TrustCenterReferenceService) Update( - ctx context.Context, scope coredata.Scoper, - req *UpdateTrustCenterReferenceRequest, +func (s *Service) UpdateReference( + ctx context.Context, + scope coredata.Scoper, + req *UpdateReferenceRequest, ) (*coredata.TrustCenterReference, error) { if err := req.Validate(); err != nil { return nil, err @@ -220,107 +233,118 @@ func (s TrustCenterReferenceService) Update( logoKey string ) - err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - reference = &coredata.TrustCenterReference{} + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + reference = &coredata.TrustCenterReference{} - if err := reference.LoadByID(ctx, tx, scope, req.ID); err != nil { - return fmt.Errorf("cannot load trust center reference: %w", err) - } - - if req.LogoFile != nil { - fileID, s3Key, err := s.uploadLogoFile(ctx, scope, tx, *req.LogoFile, req.ID, reference.TrustCenterID, now) - if err != nil { - return fmt.Errorf("cannot upload logo file: %w", err) + if err := reference.LoadByID(ctx, tx, scope, req.ID); err != nil { + return fmt.Errorf("cannot load trust center reference: %w", err) } - newFileID = &fileID - logoKey = s3Key - } + if req.LogoFile != nil { + fileID, s3Key, err := s.uploadReferenceLogoFile(ctx, scope, tx, *req.LogoFile, req.ID, reference.TrustCenterID, now) + if err != nil { + return fmt.Errorf("cannot upload logo file: %w", err) + } - if req.Name != nil { - reference.Name = *req.Name - } - - if req.Description != nil { - reference.Description = *req.Description - } - - if req.WebsiteURL != nil { - reference.WebsiteURL = *req.WebsiteURL - } - - if newFileID != nil { - reference.LogoFileID = *newFileID - } - - reference.UpdatedAt = now - - if req.Rank != nil { - reference.Rank = *req.Rank - if err := reference.UpdateRank(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update rank: %w", err) + newFileID = &fileID + logoKey = s3Key } - } - if err := reference.Update(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update trust center reference: %w", err) - } + if req.Name != nil { + reference.Name = *req.Name + } - return nil - }) + if req.Description != nil { + reference.Description = *req.Description + } + + if req.WebsiteURL != nil { + reference.WebsiteURL = *req.WebsiteURL + } + + if newFileID != nil { + reference.LogoFileID = *newFileID + } + + reference.UpdatedAt = now + + if req.Rank != nil { + reference.Rank = *req.Rank + if err := reference.UpdateRank(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update rank: %w", err) + } + } + + if err := reference.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update trust center reference: %w", err) + } + + return nil + }, + ) if err != nil { - s.cleanupS3Object(ctx, scope, logoKey) + s.cleanupReferenceS3Object(ctx, scope, logoKey) return nil, err } return reference, nil } -func (s TrustCenterReferenceService) Delete( - ctx context.Context, scope coredata.Scoper, +func (s *Service) DeleteReference( + ctx context.Context, + scope coredata.Scoper, trustCenterReferenceID gid.GID, ) error { - err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - reference := &coredata.TrustCenterReference{} + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + reference := &coredata.TrustCenterReference{} - if err := reference.LoadByID(ctx, tx, scope, trustCenterReferenceID); err != nil { - return fmt.Errorf("cannot load trust center reference: %w", err) - } + if err := reference.LoadByID(ctx, tx, scope, trustCenterReferenceID); err != nil { + return fmt.Errorf("cannot load trust center reference: %w", err) + } - if err := reference.Delete(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot delete trust center reference: %w", err) - } + if err := reference.Delete(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot delete trust center reference: %w", err) + } - return nil - }) + return nil + }, + ) return err } -func (s TrustCenterReferenceService) GenerateLogoURL( +func (s *Service) GenerateReferenceLogoURL( ctx context.Context, scope coredata.Scoper, referenceID gid.GID, ) (string, error) { reference := &coredata.TrustCenterReference{} - err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return reference.LoadByID(ctx, tx, scope, referenceID) - }) + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + return reference.LoadByID(ctx, tx, scope, referenceID) + }, + ) if err != nil { return "", fmt.Errorf("cannot load trust center reference: %w", err) } - file, err := s.svc.fileManager.GetPublicFile(ctx, reference.LogoFileID) + file, err := s.fileManager.GetPublicFile(ctx, reference.LogoFileID) if err != nil { return "", err } - return s.svc.fileManager.GenerateFileURL(file), nil + return s.fileManager.GenerateFileURL(file), nil } -func (s TrustCenterReferenceService) uploadLogoFile( - ctx context.Context, scope coredata.Scoper, +func (s *Service) uploadReferenceLogoFile( + ctx context.Context, + scope coredata.Scoper, tx pg.Tx, file File, referenceID gid.GID, @@ -385,18 +409,21 @@ func (s TrustCenterReferenceService) uploadLogoFile( } } - _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(objectKey.String()), - Body: fileContent, - ContentType: new(contentType), - CacheControl: new("max-age=3600, public"), - Metadata: map[string]string{ - "type": "trust-center-reference-logo", - "trust-center-reference-id": referenceID.String(), - "organization-id": trustCenter.OrganizationID.String(), + _, err = s.s3.PutObject( + ctx, + &s3.PutObjectInput{ + Bucket: new(s.bucket), + Key: new(objectKey.String()), + Body: fileContent, + ContentType: new(contentType), + CacheControl: new("max-age=3600, public"), + Metadata: map[string]string{ + "type": "trust-center-reference-logo", + "trust-center-reference-id": referenceID.String(), + "organization-id": trustCenter.OrganizationID.String(), + }, }, - }) + ) if err != nil { return gid.GID{}, "", fmt.Errorf("cannot upload logo file to S3: %w", err) } @@ -404,7 +431,7 @@ func (s TrustCenterReferenceService) uploadLogoFile( fileRecord := &coredata.File{ ID: fileID, OrganizationID: trustCenter.OrganizationID, - BucketName: s.svc.bucket, + BucketName: s.bucket, MimeType: contentType, FileName: filename, FileKey: objectKey.String(), @@ -421,13 +448,20 @@ func (s TrustCenterReferenceService) uploadLogoFile( return fileID, objectKey.String(), nil } -func (s TrustCenterReferenceService) cleanupS3Object(ctx context.Context, scope coredata.Scoper, s3Key string) { +func (s *Service) cleanupReferenceS3Object( + ctx context.Context, + scope coredata.Scoper, + s3Key string, +) { if s3Key == "" { return } - _, _ = s.svc.s3.DeleteObject(ctx, &s3.DeleteObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(s3Key), - }) + _, _ = s.s3.DeleteObject( + ctx, + &s3.DeleteObjectInput{ + Bucket: new(s.bucket), + Key: new(s3Key), + }, + ) } diff --git a/pkg/complianceportal/management/service.go b/pkg/complianceportal/management/service.go new file mode 100644 index 000000000..e3f82ca2e --- /dev/null +++ b/pkg/complianceportal/management/service.go @@ -0,0 +1,105 @@ +// 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 management holds the scoped, admin-facing compliance portal services +// (trust center CRUD, domains, frameworks, external URLs, references, files and +// accesses). It is the write side of the compliance portal feature. +package management + +import ( + "io" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/certmanager" + "go.probo.inc/probo/pkg/filemanager" + "go.probo.inc/probo/pkg/filevalidation" + "go.probo.inc/probo/pkg/slack" +) + +const ( + NameMaxLength = 100 + TitleMaxLength = 1000 + ContentMaxLength = 5000 +) + +type ( + // Service is the admin-facing compliance portal service. It exposes the + // scoped CRUD operations for the trust center and its related resources as + // methods on a single type. + Service struct { + pg *pg.Client + s3 *s3.Client + bucket string + baseURL string + baseDomain string + fileManager *filemanager.Service + certManager *certmanager.Service + logger *log.Logger + SlackMessages *slack.Service + fileValidator *filevalidation.FileValidator + } + + // FileUpload is an in-memory file supplied by a caller for upload. + FileUpload struct { + Content io.Reader + Filename string + Size int64 + ContentType string + } + + // File is an in-memory file supplied by a caller for upload. + File struct { + Content io.Reader + Filename string + Size int64 + ContentType string + } +) + +func NewService( + pgClient *pg.Client, + s3Client *s3.Client, + bucket string, + baseURL string, + baseDomain string, + fileManagerService *filemanager.Service, + certManagerService *certmanager.Service, + slackService *slack.Service, + logger *log.Logger, +) *Service { + return &Service{ + pg: pgClient, + s3: s3Client, + bucket: bucket, + baseURL: baseURL, + baseDomain: baseDomain, + fileManager: fileManagerService, + certManager: certManagerService, + logger: logger, + SlackMessages: slackService, + fileValidator: filevalidation.NewValidator( + filevalidation.WithCategories( + filevalidation.CategoryData, + filevalidation.CategoryDocument, + filevalidation.CategoryImage, + filevalidation.CategoryPresentation, + filevalidation.CategorySpreadsheet, + filevalidation.CategoryText, + ), + filevalidation.WithMaxFileSize(10*1024*1024), // 10MB + ), + } +} diff --git a/pkg/complianceportal/oauth2_scopes.go b/pkg/complianceportal/oauth2_scopes.go new file mode 100644 index 000000000..c2e86058f --- /dev/null +++ b/pkg/complianceportal/oauth2_scopes.go @@ -0,0 +1,93 @@ +// Copyright (c) 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 complianceportal + +import ( + "go.probo.inc/probo/pkg/coredata" +) + +// The compliance-page scope string values are part of the external OAuth2 +// contract and are kept stable even though the feature is named "compliance +// portal" on the Go side. +const ( + ScopeV1CompliancePortalRead coredata.OAuth2Scope = "v1:compliance-page:read" + ScopeV1CompliancePortal coredata.OAuth2Scope = "v1:compliance-page" +) + +// OAuth2ScopeMappings maps the compliance portal OAuth2 scopes to the actions +// they grant. +var OAuth2ScopeMappings = map[coredata.OAuth2Scope][]string{ + ScopeV1CompliancePortalRead: { + ActionCompliancePortalGet, + ActionCompliancePortalGetNda, + ActionCompliancePortalAccessGet, + ActionCompliancePortalAccessList, + ActionCompliancePortalFileGet, + ActionCompliancePortalFileList, + ActionCompliancePortalFileGetFileUrl, + ActionCompliancePortalReferenceList, + ActionCompliancePortalReferenceGetLogoUrl, + ActionCompliancePortalDocumentAccessList, + ActionMailingListUpdateList, + ActionMailingListSubscriberList, + ActionComplianceFrameworkList, + ActionComplianceCustomLinkList, + ActionCustomDomainGet, + }, + ScopeV1CompliancePortal: { + ActionCompliancePortalGet, + ActionCompliancePortalGetNda, + ActionCompliancePortalAccessGet, + ActionCompliancePortalAccessList, + ActionCompliancePortalFileGet, + ActionCompliancePortalFileList, + ActionCompliancePortalFileGetFileUrl, + ActionCompliancePortalReferenceList, + ActionCompliancePortalReferenceGetLogoUrl, + ActionCompliancePortalDocumentAccessList, + ActionMailingListUpdateList, + ActionMailingListSubscriberList, + ActionComplianceFrameworkList, + ActionComplianceCustomLinkList, + ActionCustomDomainGet, + ActionCompliancePortalUpdate, + ActionCompliancePortalNonDisclosureAgreementUpload, + ActionCompliancePortalNonDisclosureAgreementDelete, + ActionCompliancePortalAccessCreate, + ActionCompliancePortalAccessUpdate, + ActionCompliancePortalAccessDelete, + ActionCompliancePortalFileUpdate, + ActionCompliancePortalFileDelete, + ActionCompliancePortalFileCreate, + ActionCompliancePortalReferenceCreate, + ActionCompliancePortalReferenceUpdate, + ActionCompliancePortalReferenceDelete, + ActionMailingListUpdateCreate, + ActionMailingListUpdateUpdate, + ActionMailingListUpdateSend, + ActionMailingListUpdateDelete, + ActionMailingListUpdate, + ActionMailingListSubscriberCreate, + ActionMailingListSubscriberDelete, + ActionComplianceFrameworkCreate, + ActionComplianceFrameworkDelete, + ActionComplianceFrameworkUpdateRank, + ActionComplianceCustomLinkCreate, + ActionComplianceCustomLinkUpdate, + ActionComplianceCustomLinkDelete, + ActionCustomDomainCreate, + ActionCustomDomainDelete, + }, +} diff --git a/pkg/complianceportal/policies.go b/pkg/complianceportal/policies.go new file mode 100644 index 000000000..658064958 --- /dev/null +++ b/pkg/complianceportal/policies.go @@ -0,0 +1,65 @@ +// 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 complianceportal + +import ( + "go.probo.inc/probo/pkg/iam" + "go.probo.inc/probo/pkg/iam/policy" +) + +var organizationCondition = policy.Equals("principal.organization_id", "resource.organization_id") + +// FullAccessPolicy grants organization owners and admins complete access to +// every compliance portal capability, including custom domains, portal +// configuration, access grants, files, references, frameworks, external URLs +// and mailing lists. +// +// The managed probopage subdomain is a system-owned resource and can never be +// deleted, so an explicit deny (which takes precedence over any allow) blocks +// deletion of managed domains for every role. +var FullAccessPolicy = policy.NewPolicy( + "compliance-portal:full-access", + "Compliance Portal Full Access", + policy.Allow("compliance-portal:*"). + WithSID("compliance-portal-full-access"). + When(organizationCondition), + policy.Deny(ActionCustomDomainDelete). + WithSID("custom-domain-managed-no-delete"). + When(policy.Equals("resource.managed", "true")), +).WithDescription("Full compliance portal access for organization owners and admins") + +// ViewerPolicy grants organization viewers read-only access to the compliance +// portal. +var ViewerPolicy = policy.NewPolicy( + "compliance-portal:viewer", + "Compliance Portal Viewer", + policy.Allow( + ActionCustomDomainGet, + ActionCompliancePortalGet, + ActionCompliancePortalAccessGet, ActionCompliancePortalAccessList, + ActionCompliancePortalDocumentAccessList, + ActionCompliancePortalFileGet, ActionCompliancePortalFileList, ActionCompliancePortalFileGetFileUrl, + ActionCompliancePortalReferenceList, ActionCompliancePortalReferenceGetLogoUrl, + ActionComplianceFrameworkList, + ).WithSID("compliance-portal-read-access").When(organizationCondition), +).WithDescription("Read-only compliance portal access for organization viewers") + +// PolicySet returns the PolicySet for the compliance portal service. +func PolicySet() *iam.PolicySet { + return iam.NewPolicySet(). + AddRolePolicy("OWNER", FullAccessPolicy). + AddRolePolicy("ADMIN", FullAccessPolicy). + AddRolePolicy("VIEWER", ViewerPolicy) +} diff --git a/pkg/complianceportal/resolver/resolver.go b/pkg/complianceportal/resolver/resolver.go new file mode 100644 index 000000000..382c66242 --- /dev/null +++ b/pkg/complianceportal/resolver/resolver.go @@ -0,0 +1,146 @@ +// 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 resolver + +import ( + "context" + "fmt" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" +) + +// EffectiveDomainForTrustCenter returns the domain a compliance page is served +// under: the custom domain when it has an active certificate, otherwise the +// default subdomain when its certificate is active. It returns nil when no +// serving domain is available yet. +func EffectiveDomainForTrustCenter( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + trustCenter *coredata.TrustCenter, +) (*coredata.CustomDomain, error) { + byID, active, err := loadDomains(ctx, conn, scope, trustCenter) + if err != nil { + return nil, err + } + + if trustCenter.CustomDomainID != nil { + if d := byID[*trustCenter.CustomDomainID]; d != nil && active[d.ID] { + return d, nil + } + } + + if trustCenter.DefaultDomainID != nil { + if d := byID[*trustCenter.DefaultDomainID]; d != nil && active[d.ID] { + return d, nil + } + } + + return nil, nil +} + +// PublicURLForTrustCenter returns the canonical public URL of a compliance +// page. Compliance pages are always served on a dedicated domain: the custom +// domain when its certificate is active, otherwise the default probopage +// subdomain (even while its certificate provisions), and finally the default +// subdomain hostname derived from the page slug when no domain row is loaded +// yet. +func PublicURLForTrustCenter( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + trustCenter *coredata.TrustCenter, + baseDomain string, +) (string, error) { + byID, active, err := loadDomains(ctx, conn, scope, trustCenter) + if err != nil { + return "", err + } + + var host string + + switch { + case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil && active[*trustCenter.CustomDomainID]: + host = byID[*trustCenter.CustomDomainID].Domain + case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil: + host = byID[*trustCenter.DefaultDomainID].Domain + case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil: + host = byID[*trustCenter.CustomDomainID].Domain + } + + if host == "" { + host = trustCenter.Slug + "." + baseDomain + } + + return "https://" + host, nil +} + +func loadDomains( + ctx context.Context, + conn pg.Querier, + scope coredata.Scoper, + trustCenter *coredata.TrustCenter, +) (map[gid.GID]*coredata.CustomDomain, map[gid.GID]bool, error) { + var ids []gid.GID + if trustCenter.CustomDomainID != nil { + ids = append(ids, *trustCenter.CustomDomainID) + } + + if trustCenter.DefaultDomainID != nil { + ids = append(ids, *trustCenter.DefaultDomainID) + } + + byID := make(map[gid.GID]*coredata.CustomDomain) + active := make(map[gid.GID]bool) + + if len(ids) == 0 { + return byID, active, nil + } + + var domains coredata.CustomDomains + if err := domains.LoadByIDs(ctx, conn, scope, ids); err != nil { + return nil, nil, fmt.Errorf("cannot load custom domains: %w", err) + } + + var certificateIDs []gid.GID + domainByCertificate := make(map[gid.GID]gid.GID) + + for _, d := range domains { + byID[d.ID] = d + if d.CertificateID != nil { + certificateIDs = append(certificateIDs, *d.CertificateID) + domainByCertificate[*d.CertificateID] = d.ID + } + } + + if len(certificateIDs) == 0 { + return byID, active, nil + } + + var certificates coredata.Certificates + if err := certificates.LoadByIDs(ctx, conn, scope, certificateIDs); err != nil { + return nil, nil, fmt.Errorf("cannot load certificates: %w", err) + } + + for _, c := range certificates { + if domainID, ok := domainByCertificate[c.ID]; ok { + active[domainID] = c.Status == coredata.CertificateStatusActive + } + } + + return byID, active, nil +} diff --git a/pkg/trust/audit_service.go b/pkg/complianceportal/visitor/audit_service.go similarity index 91% rename from pkg/trust/audit_service.go rename to pkg/complianceportal/visitor/audit_service.go index 47c925a22..de9d954de 100644 --- a/pkg/trust/audit_service.go +++ b/pkg/complianceportal/visitor/audit_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,18 +30,14 @@ import ( "go.probo.inc/probo/pkg/page" ) -type AuditService struct { - svc *Service -} - -func (s AuditService) Get( +func (s *Service) GetAudit( ctx context.Context, scope coredata.Scoper, auditID gid.GID, ) (*coredata.Audit, error) { audit := &coredata.Audit{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := audit.LoadByID(ctx, conn, scope, auditID) @@ -59,14 +55,14 @@ func (s AuditService) Get( return audit, nil } -func (s AuditService) GetByReportFileID( +func (s *Service) GetAuditByReportFileID( ctx context.Context, scope coredata.Scoper, fileID gid.GID, ) (*coredata.Audit, error) { audit := &coredata.Audit{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := audit.LoadByReportFileID(ctx, conn, scope, fileID); err != nil { @@ -83,7 +79,7 @@ func (s AuditService) GetByReportFileID( return audit, nil } -func (s AuditService) ListForOrganizationId( +func (s *Service) ListAuditsForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -96,7 +92,7 @@ func (s AuditService) ListForOrganizationId( filter = coredata.NewAuditTrustCenterFilter() } - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := audits.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter) diff --git a/pkg/trust/compliance.md.tmpl b/pkg/complianceportal/visitor/compliance.md.tmpl similarity index 91% rename from pkg/trust/compliance.md.tmpl rename to pkg/complianceportal/visitor/compliance.md.tmpl index e0d5b51e5..3a2b067ec 100644 --- a/pkg/trust/compliance.md.tmpl +++ b/pkg/complianceportal/visitor/compliance.md.tmpl @@ -31,10 +31,10 @@ | Name | Description | Website | |------|-------------|---------| {{ range .References }}| {{ cell .Name }} | {{ cell .Description }} | {{ cell .Website }} | -{{ end }}{{ end }}{{ if .ExternalLinks }} +{{ end }}{{ end }}{{ if .CustomLinks }} ## External Links | Name | URL | |------|-----| -{{ range .ExternalLinks }}| {{ cell .Name }} | {{ cell .URL }} | +{{ range .CustomLinks }}| {{ cell .Name }} | {{ cell .URL }} | {{ end }}{{ end }} diff --git a/pkg/trust/compliance_framework_service.go b/pkg/complianceportal/visitor/compliance_framework_service.go similarity index 92% rename from pkg/trust/compliance_framework_service.go rename to pkg/complianceportal/visitor/compliance_framework_service.go index 6eaaa0b16..177f8ea1f 100644 --- a/pkg/trust/compliance_framework_service.go +++ b/pkg/complianceportal/visitor/compliance_framework_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,11 +30,7 @@ import ( "go.probo.inc/probo/pkg/page" ) -type ComplianceFrameworkService struct { - svc *Service -} - -func (s ComplianceFrameworkService) ListByTrustCenterID( +func (s *Service) ListComplianceFrameworksByPortalID( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -42,7 +38,7 @@ func (s ComplianceFrameworkService) ListByTrustCenterID( ) (*page.Page[*coredata.ComplianceFramework, coredata.ComplianceFrameworkOrderField], error) { var complianceFrameworks coredata.ComplianceFrameworks - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := complianceFrameworks.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) diff --git a/pkg/trust/compliance_page_service.go b/pkg/complianceportal/visitor/compliance_page_service.go similarity index 79% rename from pkg/trust/compliance_page_service.go rename to pkg/complianceportal/visitor/compliance_page_service.go index 3fb6420c6..e5c69076b 100644 --- a/pkg/trust/compliance_page_service.go +++ b/pkg/complianceportal/visitor/compliance_page_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -67,15 +67,15 @@ var robotsTmpl = template.Must( type ( compliancePageData struct { - OrgName string - Description string - Details []compliancePageDetail - Frameworks []compliancePageFramework - Documents []compliancePageDocument - Audits []compliancePageAudit - ThirdParties []compliancePageThirdParty - References []compliancePageReference - ExternalLinks []compliancePageExternalLink + OrgName string + Description string + Details []compliancePageDetail + Frameworks []compliancePageFramework + Documents []compliancePageDocument + Audits []compliancePageAudit + ThirdParties []compliancePageThirdParty + References []compliancePageReference + CustomLinks []compliancePageCustomLink } compliancePageDetail struct { @@ -113,7 +113,7 @@ type ( Website string } - compliancePageExternalLink struct { + compliancePageCustomLink struct { Name string URL string } @@ -125,29 +125,34 @@ func (s *Service) RenderCompliancePageMarkdown( trustCenterID gid.GID, scope coredata.Scoper, ) error { - org, err := s.GetOrganizationByTrustCenterID(ctx, trustCenterID) + org, err := s.GetPortalOrganization(ctx, trustCenterID) if err != nil { return fmt.Errorf("cannot load organization for compliance page: %w", err) } + trustCenter, err := s.GetPortalByID(ctx, trustCenterID) + if err != nil { + return fmt.Errorf("cannot load trust center for compliance page: %w", err) + } + data := &compliancePageData{ OrgName: org.Name, } - if org.Description != nil && *org.Description != "" { - data.Description = *org.Description + if trustCenter.Description != nil && *trustCenter.Description != "" { + data.Description = *trustCenter.Description } - if org.WebsiteURL != nil && *org.WebsiteURL != "" { - data.Details = append(data.Details, compliancePageDetail{Label: "Website", Value: *org.WebsiteURL}) + if trustCenter.WebsiteURL != nil && *trustCenter.WebsiteURL != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Website", Value: *trustCenter.WebsiteURL}) } - if org.Email != nil && *org.Email != "" { - data.Details = append(data.Details, compliancePageDetail{Label: "Email", Value: *org.Email}) + if trustCenter.Email != nil && *trustCenter.Email != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Email", Value: *trustCenter.Email}) } - if org.HeadquarterAddress != nil && *org.HeadquarterAddress != "" { - data.Details = append(data.Details, compliancePageDetail{Label: "Headquarters", Value: *org.HeadquarterAddress}) + if trustCenter.HeadquarterAddress != nil && *trustCenter.HeadquarterAddress != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Headquarters", Value: *trustCenter.HeadquarterAddress}) } data.Frameworks, err = s.fetchComplianceFrameworks(ctx, scope, trustCenterID) @@ -175,7 +180,7 @@ func (s *Service) RenderCompliancePageMarkdown( return fmt.Errorf("cannot fetch references: %w", err) } - data.ExternalLinks, err = s.fetchExternalLinks(ctx, scope, trustCenterID) + data.CustomLinks, err = s.fetchCustomLinks(ctx, scope, trustCenterID) if err != nil { return fmt.Errorf("cannot fetch external links: %w", err) } @@ -206,7 +211,7 @@ func (s *Service) RenderSitemap( scope coredata.Scoper, baseURL string, ) error { - org, err := s.GetOrganizationByTrustCenterID(ctx, trustCenterID) + org, err := s.GetPortalOrganization(ctx, trustCenterID) if err != nil { return fmt.Errorf("cannot load organization for sitemap: %w", err) } @@ -245,7 +250,11 @@ func (s *Service) RenderRobotsTxt( return nil } -func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, orgID gid.GID) ([]string, error) { +func (s *Service) fetchDocumentIDs( + ctx context.Context, + scope coredata.Scoper, + orgID gid.GID, +) ([]string, error) { seen := make(map[gid.GID]struct{}) var resourceIDs []gid.GID @@ -271,7 +280,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o }, ) - result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil) + result, err := s.ListDocumentsForOrganizationID(ctx, scope, orgID, cursor, nil) if err != nil { return nil, fmt.Errorf("cannot list documents: %w", err) } @@ -305,7 +314,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o }, ) - result, err := s.TrustCenterFiles.ListForOrganizationId( + result, err := s.ListPortalFilesForOrganizationID( ctx, scope, orgID, @@ -345,7 +354,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o }, ) - result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil) + result, err := s.ListAuditsForOrganizationID(ctx, scope, orgID, cursor, nil) if err != nil { return nil, fmt.Errorf("cannot list audits: %w", err) } @@ -389,7 +398,11 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o return paths, nil } -func (s *Service) fetchComplianceFrameworks(ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID) ([]compliancePageFramework, error) { +func (s *Service) fetchComplianceFrameworks( + ctx context.Context, + scope coredata.Scoper, + trustCenterID gid.GID, +) ([]compliancePageFramework, error) { var frameworks []compliancePageFramework var cursorKey *page.CursorKey @@ -404,7 +417,7 @@ func (s *Service) fetchComplianceFrameworks(ctx context.Context, scope coredata. }, ) - result, err := s.ComplianceFrameworks.ListByTrustCenterID(ctx, scope, trustCenterID, cursor) + result, err := s.ListComplianceFrameworksByPortalID(ctx, scope, trustCenterID, cursor) if err != nil { return nil, fmt.Errorf("cannot list compliance frameworks: %w", err) } @@ -414,7 +427,7 @@ func (s *Service) fetchComplianceFrameworks(ctx context.Context, scope coredata. continue } - fw, err := s.Frameworks.Get(ctx, scope, cf.FrameworkID) + fw, err := s.GetFramework(ctx, scope, cf.FrameworkID) if err != nil { return nil, fmt.Errorf("cannot get framework %s: %w", cf.FrameworkID, err) } @@ -439,7 +452,11 @@ func (s *Service) fetchComplianceFrameworks(ctx context.Context, scope coredata. return frameworks, nil } -func (s *Service) fetchDocuments(ctx context.Context, scope coredata.Scoper, orgID gid.GID) ([]compliancePageDocument, error) { +func (s *Service) fetchDocuments( + ctx context.Context, + scope coredata.Scoper, + orgID gid.GID, +) ([]compliancePageDocument, error) { var docs []compliancePageDocument var cursorKey *page.CursorKey @@ -454,7 +471,7 @@ func (s *Service) fetchDocuments(ctx context.Context, scope coredata.Scoper, org }, ) - result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil) + result, err := s.ListDocumentsForOrganizationID(ctx, scope, orgID, cursor, nil) if err != nil { return nil, fmt.Errorf("cannot list documents: %w", err) } @@ -485,7 +502,11 @@ func (s *Service) fetchDocuments(ctx context.Context, scope coredata.Scoper, org return docs, nil } -func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID gid.GID) ([]compliancePageAudit, error) { +func (s *Service) fetchAudits( + ctx context.Context, + scope coredata.Scoper, + orgID gid.GID, +) ([]compliancePageAudit, error) { var audits []compliancePageAudit var cursorKey *page.CursorKey @@ -500,7 +521,7 @@ func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID }, ) - result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil) + result, err := s.ListAuditsForOrganizationID(ctx, scope, orgID, cursor, nil) if err != nil { return nil, fmt.Errorf("cannot list audits: %w", err) } @@ -512,7 +533,7 @@ func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID frameworkName := "" - fw, err := s.Frameworks.Get(ctx, scope, audit.FrameworkID) + fw, err := s.GetFramework(ctx, scope, audit.FrameworkID) if err == nil { frameworkName = fw.Name } @@ -544,7 +565,11 @@ func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID return audits, nil } -func (s *Service) fetchThirdParties(ctx context.Context, scope coredata.Scoper, orgID gid.GID) ([]compliancePageThirdParty, error) { +func (s *Service) fetchThirdParties( + ctx context.Context, + scope coredata.Scoper, + orgID gid.GID, +) ([]compliancePageThirdParty, error) { var thirdParties []compliancePageThirdParty var cursorKey *page.CursorKey @@ -559,10 +584,7 @@ func (s *Service) fetchThirdParties(ctx context.Context, scope coredata.Scoper, }, ) - showOnTrustCenter := true - filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, nil, nil, nil) - - result, err := s.ThirdParties.ListForOrganizationId(ctx, scope, orgID, cursor, filter) + result, err := s.ListThirdPartiesForOrganizationID(ctx, scope, orgID, cursor) if err != nil { return nil, fmt.Errorf("cannot list thirdParties: %w", err) } @@ -596,7 +618,11 @@ func (s *Service) fetchThirdParties(ctx context.Context, scope coredata.Scoper, return thirdParties, nil } -func (s *Service) fetchReferences(ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID) ([]compliancePageReference, error) { +func (s *Service) fetchReferences( + ctx context.Context, + scope coredata.Scoper, + trustCenterID gid.GID, +) ([]compliancePageReference, error) { var refs []compliancePageReference var cursorKey *page.CursorKey @@ -611,7 +637,7 @@ func (s *Service) fetchReferences(ctx context.Context, scope coredata.Scoper, tr }, ) - result, err := s.TrustCenterReferences.ListForTrustCenterID(ctx, scope, trustCenterID, cursor) + result, err := s.ListPortalReferencesForPortalID(ctx, scope, trustCenterID, cursor) if err != nil { return nil, fmt.Errorf("cannot list references: %w", err) } @@ -640,8 +666,12 @@ func (s *Service) fetchReferences(ctx context.Context, scope coredata.Scoper, tr return refs, nil } -func (s *Service) fetchExternalLinks(ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID) ([]compliancePageExternalLink, error) { - var links []compliancePageExternalLink +func (s *Service) fetchCustomLinks( + ctx context.Context, + scope coredata.Scoper, + trustCenterID gid.GID, +) ([]compliancePageCustomLink, error) { + var links []compliancePageCustomLink var cursorKey *page.CursorKey for { @@ -649,21 +679,21 @@ func (s *Service) fetchExternalLinks(ctx context.Context, scope coredata.Scoper, page.MaxCursorSize, cursorKey, page.Head, - page.OrderBy[coredata.ComplianceExternalURLOrderField]{ - Field: coredata.ComplianceExternalURLOrderFieldRank, + page.OrderBy[coredata.ComplianceCustomLinkOrderField]{ + Field: coredata.ComplianceCustomLinkOrderFieldRank, Direction: page.OrderDirectionAsc, }, ) - result, err := s.ComplianceExternalURLs.ListForTrustCenterID(ctx, scope, trustCenterID, cursor) + result, err := s.ListCustomLinksForPortalID(ctx, scope, trustCenterID, cursor) if err != nil { - return nil, fmt.Errorf("cannot list external links: %w", err) + return nil, fmt.Errorf("cannot list custom links: %w", err) } for _, l := range result.Data { links = append( links, - compliancePageExternalLink{ + compliancePageCustomLink{ Name: l.Name, URL: l.URL, }, @@ -675,7 +705,7 @@ func (s *Service) fetchExternalLinks(ctx context.Context, scope coredata.Scoper, } last := result.Data[len(result.Data)-1] - ck := last.CursorKey(coredata.ComplianceExternalURLOrderFieldRank) + ck := last.CursorKey(coredata.ComplianceCustomLinkOrderFieldRank) cursorKey = &ck } diff --git a/pkg/trust/compliance_portal_commitment_group_service.go b/pkg/complianceportal/visitor/compliance_portal_commitment_group_service.go similarity index 80% rename from pkg/trust/compliance_portal_commitment_group_service.go rename to pkg/complianceportal/visitor/compliance_portal_commitment_group_service.go index ce64143b7..31078028f 100644 --- a/pkg/trust/compliance_portal_commitment_group_service.go +++ b/pkg/complianceportal/visitor/compliance_portal_commitment_group_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,11 +30,7 @@ import ( "go.probo.inc/probo/pkg/page" ) -type CompliancePortalCommitmentGroupService struct { - svc *Service -} - -func (s CompliancePortalCommitmentGroupService) ListForTrustCenterID( +func (s *Service) ListCommitmentGroupsForPortalID( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -42,14 +38,17 @@ func (s CompliancePortalCommitmentGroupService) ListForTrustCenterID( ) (*page.Page[*coredata.CompliancePortalCommitmentGroup, coredata.CompliancePortalCommitmentGroupOrderField], error) { var groups coredata.CompliancePortalCommitmentGroups - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := groups.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) - if err != nil { - return fmt.Errorf("cannot load compliance portal commitment groups: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := groups.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) + if err != nil { + return fmt.Errorf("cannot load compliance portal commitment groups: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -57,14 +56,14 @@ func (s CompliancePortalCommitmentGroupService) ListForTrustCenterID( return page.NewPage(groups, cursor), nil } -func (s CompliancePortalCommitmentGroupService) Get( +func (s *Service) GetCommitmentGroup( ctx context.Context, scope coredata.Scoper, groupID gid.GID, ) (*coredata.CompliancePortalCommitmentGroup, error) { group := &coredata.CompliancePortalCommitmentGroup{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := group.LoadByID(ctx, conn, scope, groupID) diff --git a/pkg/trust/compliance_portal_commitment_service.go b/pkg/complianceportal/visitor/compliance_portal_commitment_service.go similarity index 81% rename from pkg/trust/compliance_portal_commitment_service.go rename to pkg/complianceportal/visitor/compliance_portal_commitment_service.go index ad2dd269e..26fc12dc4 100644 --- a/pkg/trust/compliance_portal_commitment_service.go +++ b/pkg/complianceportal/visitor/compliance_portal_commitment_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,11 +30,7 @@ import ( "go.probo.inc/probo/pkg/page" ) -type CompliancePortalCommitmentService struct { - svc *Service -} - -func (s CompliancePortalCommitmentService) ListForGroupID( +func (s *Service) ListCommitmentsForGroupID( ctx context.Context, scope coredata.Scoper, groupID gid.GID, @@ -42,14 +38,17 @@ func (s CompliancePortalCommitmentService) ListForGroupID( ) (*page.Page[*coredata.CompliancePortalCommitment, coredata.CompliancePortalCommitmentOrderField], error) { var commitments coredata.CompliancePortalCommitments - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := commitments.LoadByGroupID(ctx, conn, scope, groupID, cursor) - if err != nil { - return fmt.Errorf("cannot load compliance portal commitments: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := commitments.LoadByGroupID(ctx, conn, scope, groupID, cursor) + if err != nil { + return fmt.Errorf("cannot load compliance portal commitments: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -57,14 +56,14 @@ func (s CompliancePortalCommitmentService) ListForGroupID( return page.NewPage(commitments, cursor), nil } -func (s CompliancePortalCommitmentService) Get( +func (s *Service) GetCommitment( ctx context.Context, scope coredata.Scoper, commitmentID gid.GID, ) (*coredata.CompliancePortalCommitment, error) { commitment := &coredata.CompliancePortalCommitment{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := commitment.LoadByID(ctx, conn, scope, commitmentID) diff --git a/pkg/trust/compliance_external_url_service.go b/pkg/complianceportal/visitor/custom_link_service.go similarity index 73% rename from pkg/trust/compliance_external_url_service.go rename to pkg/complianceportal/visitor/custom_link_service.go index c45caedd9..de8192017 100644 --- a/pkg/trust/compliance_external_url_service.go +++ b/pkg/complianceportal/visitor/custom_link_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,24 +30,20 @@ import ( "go.probo.inc/probo/pkg/page" ) -type ComplianceExternalURLService struct { - svc *Service -} - -func (s ComplianceExternalURLService) ListForTrustCenterID( +func (s *Service) ListCustomLinksForPortalID( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, - cursor *page.Cursor[coredata.ComplianceExternalURLOrderField], -) (*page.Page[*coredata.ComplianceExternalURL, coredata.ComplianceExternalURLOrderField], error) { - var items coredata.ComplianceExternalURLs + cursor *page.Cursor[coredata.ComplianceCustomLinkOrderField], +) (*page.Page[*coredata.ComplianceCustomLink, coredata.ComplianceCustomLinkOrderField], error) { + var links coredata.ComplianceCustomLinks - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { - err := items.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) + err := links.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) if err != nil { - return fmt.Errorf("cannot load compliance external URLs: %w", err) + return fmt.Errorf("cannot load custom links: %w", err) } return nil @@ -57,5 +53,5 @@ func (s ComplianceExternalURLService) ListForTrustCenterID( return nil, err } - return page.NewPage(items, cursor), nil + return page.NewPage(links, cursor), nil } diff --git a/pkg/trust/document_service.go b/pkg/complianceportal/visitor/document_service.go similarity index 89% rename from pkg/trust/document_service.go rename to pkg/complianceportal/visitor/document_service.go index f23fac9a4..6c3dae866 100644 --- a/pkg/trust/document_service.go +++ b/pkg/complianceportal/visitor/document_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -37,20 +37,13 @@ import ( "go.probo.inc/probo/pkg/pdfutils" ) -type ( - DocumentService struct { - svc *Service - html2pdfConverter *html2pdf.Converter - } - - ErrDocumentArchived struct{} -) +type ErrDocumentArchived struct{} func (e ErrDocumentArchived) Error() string { return "cannot access an archived document" } -func (s *DocumentService) ListForOrganizationId( +func (s *Service) ListDocumentsForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -63,7 +56,7 @@ func (s *DocumentService) ListForOrganizationId( filter = coredata.NewDocumentTrustCenterFilter() } - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := documents.LoadPublishedByOrganizationID(ctx, conn, scope, organizationID, cursor, filter); err != nil { @@ -80,13 +73,13 @@ func (s *DocumentService) ListForOrganizationId( return page.NewPage(documents, cursor), nil } -func (s *DocumentService) ExportPDF( +func (s *Service) ExportDocumentPDF( ctx context.Context, scope coredata.Scoper, documentID gid.GID, email mail.Addr, ) ([]byte, error) { - pdfData, err := s.exportPDFData(ctx, scope, documentID) + pdfData, err := s.exportDocumentPDFData(ctx, scope, documentID) if err != nil { return nil, fmt.Errorf("cannot export document PDF: %w", err) } @@ -99,15 +92,15 @@ func (s *DocumentService) ExportPDF( return watermarkedPDF, nil } -func (s *DocumentService) ExportPDFWithoutWatermark( +func (s *Service) ExportDocumentPDFWithoutWatermark( ctx context.Context, scope coredata.Scoper, documentID gid.GID, ) ([]byte, error) { - return s.exportPDFData(ctx, scope, documentID) + return s.exportDocumentPDFData(ctx, scope, documentID) } -func (s DocumentService) Get( +func (s *Service) GetDocument( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -115,7 +108,7 @@ func (s DocumentService) Get( ) (*coredata.Document, error) { document := &coredata.Document{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := document.LoadByID(ctx, conn, scope, documentID) @@ -145,7 +138,7 @@ func (s DocumentService) Get( return document, nil } -func (s *DocumentService) exportPDFData( +func (s *Service) exportDocumentPDFData( ctx context.Context, scope coredata.Scoper, documentID gid.GID, @@ -154,7 +147,7 @@ func (s *DocumentService) exportPDFData( version := &coredata.DocumentVersion{} fileRecord := &coredata.File{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := document.LoadByID(ctx, conn, scope, documentID); err != nil { @@ -189,7 +182,7 @@ func (s *DocumentService) exportPDFData( } if version.FileID != nil { - pdfData, err := s.svc.fileManager.GetFileBytes(ctx, fileRecord) + pdfData, err := s.fileManager.GetFileBytes(ctx, fileRecord) if err != nil { return nil, fmt.Errorf("cannot fetch document PDF file: %w", err) } @@ -198,7 +191,7 @@ func (s *DocumentService) exportPDFData( } // TODO: remove on-the-fly fallback once all published versions have a stored PDF. - pdfData, err := s.generatePDFOnTheFly(ctx, scope, document, version) + pdfData, err := s.generateDocumentPDFOnTheFly(ctx, scope, document, version) if err != nil { return nil, fmt.Errorf("cannot generate PDF on the fly: %w", err) } @@ -209,7 +202,7 @@ func (s *DocumentService) exportPDFData( // generatePDFOnTheFly generates a PDF from scratch for versions that don't have // a stored file yet. Can be removed once all published versions have been // processed by the document PDF worker. -func (s *DocumentService) generatePDFOnTheFly( +func (s *Service) generateDocumentPDFOnTheFly( ctx context.Context, scope coredata.Scoper, document *coredata.Document, @@ -219,7 +212,7 @@ func (s *DocumentService) generatePDFOnTheFly( var approverNames []string - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { lastQuorum := &coredata.DocumentVersionApprovalQuorum{} @@ -296,11 +289,16 @@ func (s *DocumentService) generatePDFOnTheFly( if organization.HorizontalLogoFileID != nil { fileRecord := &coredata.File{} - fileErr := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - return fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID) - }) + fileErr := s.pg.WithConn( + + ctx, + + func(ctx context.Context, conn pg.Querier) error { + return fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID) + }) + if fileErr == nil { - base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord) + base64Data, mimeType, logoErr := s.fileManager.GetFileBase64(ctx, fileRecord) if logoErr == nil { horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) } diff --git a/pkg/trust/errors.go b/pkg/complianceportal/visitor/errors.go similarity index 95% rename from pkg/trust/errors.go rename to pkg/complianceportal/visitor/errors.go index 5832a77c7..dcbcb25fa 100644 --- a/pkg/trust/errors.go +++ b/pkg/complianceportal/visitor/errors.go @@ -18,12 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import "errors" var ( - ErrCustomDomainNotFound = errors.New("custom domain not found") ErrPageNotFound = errors.New("page not found") ErrMembershipNotFound = errors.New("membership not found") ErrUserNotFound = errors.New("user not found") diff --git a/pkg/trust/framework_service.go b/pkg/complianceportal/visitor/framework_service.go similarity index 82% rename from pkg/trust/framework_service.go rename to pkg/complianceportal/visitor/framework_service.go index 094b6d989..12b95843f 100644 --- a/pkg/trust/framework_service.go +++ b/pkg/complianceportal/visitor/framework_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,25 +30,24 @@ import ( "go.probo.inc/probo/pkg/gid" ) -type FrameworkService struct { - svc *Service -} - -func (s FrameworkService) Get( +func (s *Service) GetFramework( ctx context.Context, scope coredata.Scoper, frameworkID gid.GID, ) (*coredata.Framework, error) { framework := &coredata.Framework{} - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := framework.LoadByID(ctx, conn, scope, frameworkID) - if err != nil { - return fmt.Errorf("cannot load framework: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + err := framework.LoadByID(ctx, conn, scope, frameworkID) + if err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, err } @@ -56,7 +55,7 @@ func (s FrameworkService) Get( return framework, nil } -func (s FrameworkService) GenerateLightLogoURL( +func (s *Service) GenerateFrameworkLightLogoURL( ctx context.Context, scope coredata.Scoper, frameworkID gid.GID, @@ -64,7 +63,7 @@ func (s FrameworkService) GenerateLightLogoURL( ) (*string, error) { file := &coredata.File{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { framework := &coredata.Framework{} @@ -91,7 +90,7 @@ func (s FrameworkService) GenerateLightLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -99,7 +98,7 @@ func (s FrameworkService) GenerateLightLogoURL( return &presignedURL, nil } -func (s FrameworkService) GenerateDarkLogoURL( +func (s *Service) GenerateFrameworkDarkLogoURL( ctx context.Context, scope coredata.Scoper, frameworkID gid.GID, @@ -107,7 +106,7 @@ func (s FrameworkService) GenerateDarkLogoURL( ) (*string, error) { file := &coredata.File{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { framework := &coredata.Framework{} @@ -134,7 +133,7 @@ func (s FrameworkService) GenerateDarkLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } diff --git a/pkg/trust/organization_service.go b/pkg/complianceportal/visitor/organization_service.go similarity index 64% rename from pkg/trust/organization_service.go rename to pkg/complianceportal/visitor/organization_service.go index 75281e5e2..7db9c917e 100644 --- a/pkg/trust/organization_service.go +++ b/pkg/complianceportal/visitor/organization_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -32,18 +32,14 @@ import ( "go.probo.inc/probo/pkg/gid" ) -type OrganizationService struct { - svc *Service -} - -func (s OrganizationService) Get( +func (s *Service) GetOrganization( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, ) (*coredata.Organization, error) { organization := &coredata.Organization{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := organization.LoadByID( @@ -66,47 +62,13 @@ func (s OrganizationService) Get( return organization, nil } -func (s OrganizationService) GetOrganizationCustomDomain( - ctx context.Context, - scope coredata.Scoper, - organizationID gid.GID, -) (*coredata.CustomDomain, error) { - var domain *coredata.CustomDomain - - err := s.svc.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - var org coredata.Organization - if err := org.LoadByID(ctx, conn, scope, organizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - if org.CustomDomainID == nil { - return nil - } - - domain = &coredata.CustomDomain{} - if err := domain.LoadByID(ctx, conn, scope, *org.CustomDomainID); err != nil { - return fmt.Errorf("cannot load custom domain: %w", err) - } - - return nil - }, - ) - if err != nil { - return nil, err - } - - return domain, nil -} - -func (s OrganizationService) GenerateLogoURL( +func (s *Service) GenerateOrganizationLogoURL( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, expiresIn time.Duration, ) (*string, error) { - organization, err := s.Get(ctx, scope, organizationID) + organization, err := s.GetOrganization(ctx, scope, organizationID) if err != nil { return nil, fmt.Errorf("cannot get organization: %w", err) } @@ -117,7 +79,7 @@ func (s OrganizationService) GenerateLogoURL( file := &coredata.File{} - err = s.svc.pg.WithConn( + err = s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { return file.LoadByID(ctx, conn, scope, *organization.LogoFileID) @@ -127,20 +89,23 @@ func (s OrganizationService) GenerateLogoURL( return nil, fmt.Errorf("cannot load file: %w", err) } - presignClient := s3.NewPresignClient(s.svc.s3) + presignClient := s3.NewPresignClient(s.s3) encodedFilename := url.QueryEscape(file.FileName) contentDisposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", encodedFilename, encodedFilename) - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - ResponseCacheControl: new("max-age=3600, public"), - ResponseContentDisposition: new(contentDisposition), - }, func(opts *s3.PresignOptions) { - opts.Expires = expiresIn - }) + presignedReq, err := presignClient.PresignGetObject( + ctx, + &s3.GetObjectInput{ + Bucket: new(s.bucket), + Key: new(file.FileKey), + ResponseCacheControl: new("max-age=3600, public"), + ResponseContentDisposition: new(contentDisposition), + }, func(opts *s3.PresignOptions) { + opts.Expires = expiresIn + }, + ) if err != nil { return nil, fmt.Errorf("cannot presign GetObject request: %w", err) } diff --git a/pkg/trust/trust_center_access_service.go b/pkg/complianceportal/visitor/portal_access_service.go similarity index 63% rename from pkg/trust/trust_center_access_service.go rename to pkg/complianceportal/visitor/portal_access_service.go index 37f2cdcdc..1786be118 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/complianceportal/visitor/portal_access_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -31,42 +31,33 @@ import ( "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" ) -type ( - TrustCenterAccessService struct { - svc *Service - iamSvc *iam.Service - logger *log.Logger - } - - TrustCenterAccessRequest struct { - TrustCenterID gid.GID - IdentityID gid.GID - DocumentIDs []gid.GID - ReportIDs []gid.GID - TrustCenterFileIDs []gid.GID - } -) +type PortalAccessRequest struct { + TrustCenterID gid.GID + IdentityID gid.GID + DocumentIDs []gid.GID + ReportIDs []gid.GID + TrustCenterFileIDs []gid.GID +} const ( - TrustCenterAccessURLFormat = "https://%s/organizations/%s/trust-center/access" + PortalAccessURLFormat = "https://%s/organizations/%s/trust-center/access" ) -func (s TrustCenterAccessService) Request( +func (s *Service) RequestPortalAccess( ctx context.Context, scope coredata.Scoper, - req *TrustCenterAccessRequest, + req *PortalAccessRequest, ) (*coredata.TrustCenterAccess, error) { var ( now = time.Now() access *coredata.TrustCenterAccess ) - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { trustCenter := &coredata.TrustCenter{} @@ -241,14 +232,14 @@ func (s TrustCenterAccessService) Request( return nil, err } - if err := s.svc.slack.QueueSlackNotification(ctx, scope, req.IdentityID, req.TrustCenterID); err != nil { + if err := s.slack.QueueSlackNotification(ctx, scope, req.IdentityID, req.TrustCenterID); err != nil { s.logger.ErrorCtx(ctx, "cannot queue slack notification", log.Error(err)) } return access, nil } -func (s TrustCenterAccessService) GetAccess( +func (s *Service) GetPortalAccess( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -256,14 +247,17 @@ func (s TrustCenterAccessService) GetAccess( ) (coredata.TrustCenterAccess, error) { var access coredata.TrustCenterAccess - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - return access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) - }) + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + return access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) + }, + ) return access, err } -func (s TrustCenterAccessService) GetDocumentAccess( +func (s *Service) GetPortalDocumentAccess( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -272,42 +266,45 @@ func (s TrustCenterAccessService) GetDocumentAccess( ) (*coredata.TrustCenterDocumentAccess, error) { var documentAccess *coredata.TrustCenterDocumentAccess - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - access := &coredata.TrustCenterAccess{} + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + access := &coredata.TrustCenterAccess{} - err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrMembershipNotFound + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrMembershipNotFound + } + + return fmt.Errorf("cannot load trust center access: %w", err) } - return fmt.Errorf("cannot load trust center access: %w", err) - } - - profile := &coredata.MembershipProfile{} - if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrUserNotFound - } - } - - if profile.State != coredata.ProfileStateActive { - return ErrUserInactive - } - - documentAccess = &coredata.TrustCenterDocumentAccess{} - - err = documentAccess.LoadByTrustCenterAccessIDAndDocumentID(ctx, conn, scope, access.ID, documentID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrDocumentAccessNotFound + profile := &coredata.MembershipProfile{} + if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrUserNotFound + } } - return fmt.Errorf("cannot load document access: %w", err) - } + if profile.State != coredata.ProfileStateActive { + return ErrUserInactive + } - return nil - }) + documentAccess = &coredata.TrustCenterDocumentAccess{} + + err = documentAccess.LoadByTrustCenterAccessIDAndDocumentID(ctx, conn, scope, access.ID, documentID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrDocumentAccessNotFound + } + + return fmt.Errorf("cannot load document access: %w", err) + } + + return nil + }, + ) if err != nil { return nil, err } @@ -315,7 +312,7 @@ func (s TrustCenterAccessService) GetDocumentAccess( return documentAccess, nil } -func (s TrustCenterAccessService) GetReportFileAccess( +func (s *Service) GetPortalReportFileAccess( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -324,42 +321,45 @@ func (s TrustCenterAccessService) GetReportFileAccess( ) (*coredata.TrustCenterDocumentAccess, error) { var reportAccess *coredata.TrustCenterDocumentAccess - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - access := &coredata.TrustCenterAccess{} + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + access := &coredata.TrustCenterAccess{} - err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrMembershipNotFound + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrMembershipNotFound + } + + return fmt.Errorf("cannot load trust center access: %w", err) } - return fmt.Errorf("cannot load trust center access: %w", err) - } - - profile := &coredata.MembershipProfile{} - if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrUserNotFound - } - } - - if profile.State != coredata.ProfileStateActive { - return ErrUserInactive - } - - reportAccess = &coredata.TrustCenterDocumentAccess{} - - err = reportAccess.LoadByTrustCenterAccessIDAndReportFileID(ctx, conn, scope, access.ID, reportFileID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrDocumentAccessNotFound + profile := &coredata.MembershipProfile{} + if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrUserNotFound + } } - return fmt.Errorf("cannot load report access: %w", err) - } + if profile.State != coredata.ProfileStateActive { + return ErrUserInactive + } - return nil - }) + reportAccess = &coredata.TrustCenterDocumentAccess{} + + err = reportAccess.LoadByTrustCenterAccessIDAndReportFileID(ctx, conn, scope, access.ID, reportFileID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrDocumentAccessNotFound + } + + return fmt.Errorf("cannot load report access: %w", err) + } + + return nil + }, + ) if err != nil { return nil, err } @@ -367,7 +367,7 @@ func (s TrustCenterAccessService) GetReportFileAccess( return reportAccess, nil } -func (s TrustCenterAccessService) GetTrustCenterFileAccess( +func (s *Service) GetPortalFileAccess( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -376,42 +376,45 @@ func (s TrustCenterAccessService) GetTrustCenterFileAccess( ) (*coredata.TrustCenterDocumentAccess, error) { var fileAccess *coredata.TrustCenterDocumentAccess - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - access := &coredata.TrustCenterAccess{} + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + access := &coredata.TrustCenterAccess{} - err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrMembershipNotFound + err := access.LoadByTrustCenterIDAndIdentityID(ctx, conn, scope, trustCenterID, identityID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrMembershipNotFound + } + + return fmt.Errorf("cannot load trust center access: %w", err) } - return fmt.Errorf("cannot load trust center access: %w", err) - } - - profile := &coredata.MembershipProfile{} - if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrUserNotFound - } - } - - if profile.State != coredata.ProfileStateActive { - return ErrUserInactive - } - - fileAccess = &coredata.TrustCenterDocumentAccess{} - - err = fileAccess.LoadByTrustCenterAccessIDAndTrustCenterFileID(ctx, conn, scope, access.ID, trustCenterFileID) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrDocumentAccessNotFound + profile := &coredata.MembershipProfile{} + if err := profile.LoadByIdentityIDAndOrganizationID(ctx, conn, scope, identityID, access.OrganizationID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrUserNotFound + } } - return fmt.Errorf("cannot load trust center file access: %w", err) - } + if profile.State != coredata.ProfileStateActive { + return ErrUserInactive + } - return nil - }) + fileAccess = &coredata.TrustCenterDocumentAccess{} + + err = fileAccess.LoadByTrustCenterAccessIDAndTrustCenterFileID(ctx, conn, scope, access.ID, trustCenterFileID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrDocumentAccessNotFound + } + + return fmt.Errorf("cannot load trust center file access: %w", err) + } + + return nil + }, + ) if err != nil { return nil, err } @@ -419,7 +422,7 @@ func (s TrustCenterAccessService) GetTrustCenterFileAccess( return fileAccess, nil } -func (s *TrustCenterAccessService) GrantByIDs( +func (s *Service) GrantPortalAccessByIDs( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -428,72 +431,75 @@ func (s *TrustCenterAccessService) GrantByIDs( reportIDs []gid.GID, fileIDs []gid.GID, ) error { - return s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - trustCenter := &coredata.TrustCenter{} - if err := trustCenter.LoadByOrganizationID(ctx, tx, scope, organizationID); err != nil { - return fmt.Errorf("cannot load trust center: %w", err) - } - - identity := &coredata.Identity{} - if err := identity.LoadByEmail(ctx, tx, email); err != nil { - return fmt.Errorf("cannot load identity: %w", err) - } - - access := &coredata.TrustCenterAccess{} - if err := access.LoadByTrustCenterIDAndIdentityID(ctx, tx, scope, trustCenter.ID, identity.ID); err != nil { - return fmt.Errorf("cannot load trust center access: %w", err) - } - - profile := &coredata.MembershipProfile{} - if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, access.OrganizationID); err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrUserNotFound - } - } - - if profile.State != coredata.ProfileStateActive { - return ErrUserInactive - } - - shouldSendEmail := profile.State != coredata.ProfileStateActive - now := time.Now() - - if len(documentIDs) > 0 { - if err := coredata.GrantByDocumentIDs(ctx, tx, scope, access.ID, documentIDs, now); err != nil { - return fmt.Errorf("cannot grant document accesses: %w", err) - } - } - - if len(reportIDs) > 0 { - if err := coredata.GrantByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { - return fmt.Errorf("cannot grant report accesses: %w", err) - } - } - - if len(fileIDs) > 0 { - if err := coredata.GrantByTrustCenterFileIDs(ctx, tx, scope, access.ID, fileIDs, now); err != nil { - return fmt.Errorf("cannot grant trust center file accesses: %w", err) - } - } - - if shouldSendEmail { - profile.State = coredata.ProfileStateActive - - profile.UpdatedAt = now - if err := profile.Update(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot update profile: %w", err) + return s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByOrganizationID(ctx, tx, scope, organizationID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) } - if err := s.sendAccessEmail(ctx, tx, scope, access, profile); err != nil { - return fmt.Errorf("cannot send access email: %w", err) + identity := &coredata.Identity{} + if err := identity.LoadByEmail(ctx, tx, email); err != nil { + return fmt.Errorf("cannot load identity: %w", err) } - } - return nil - }) + access := &coredata.TrustCenterAccess{} + if err := access.LoadByTrustCenterIDAndIdentityID(ctx, tx, scope, trustCenter.ID, identity.ID); err != nil { + return fmt.Errorf("cannot load trust center access: %w", err) + } + + profile := &coredata.MembershipProfile{} + if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, access.OrganizationID); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return ErrUserNotFound + } + } + + if profile.State != coredata.ProfileStateActive { + return ErrUserInactive + } + + shouldSendEmail := profile.State != coredata.ProfileStateActive + now := time.Now() + + if len(documentIDs) > 0 { + if err := coredata.GrantByDocumentIDs(ctx, tx, scope, access.ID, documentIDs, now); err != nil { + return fmt.Errorf("cannot grant document accesses: %w", err) + } + } + + if len(reportIDs) > 0 { + if err := coredata.GrantByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { + return fmt.Errorf("cannot grant report accesses: %w", err) + } + } + + if len(fileIDs) > 0 { + if err := coredata.GrantByTrustCenterFileIDs(ctx, tx, scope, access.ID, fileIDs, now); err != nil { + return fmt.Errorf("cannot grant trust center file accesses: %w", err) + } + } + + if shouldSendEmail { + profile.State = coredata.ProfileStateActive + + profile.UpdatedAt = now + if err := profile.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update profile: %w", err) + } + + if err := s.sendPortalAccessEmail(ctx, tx, scope, access, profile); err != nil { + return fmt.Errorf("cannot send access email: %w", err) + } + } + + return nil + }, + ) } -func (s *TrustCenterAccessService) sendAccessEmail( +func (s *Service) sendPortalAccessEmail( ctx context.Context, tx pg.Tx, scope coredata.Scoper, @@ -512,7 +518,7 @@ func (s *TrustCenterAccessService) sendAccessEmail( return fmt.Errorf("cannot update trust center access with expiration: %w", err) } - emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, scope, access.TrustCenterID) + emailPresenterCfg, err := s.GetPortalEmailPresenterConfig(ctx, scope, access.TrustCenterID) if err != nil { return fmt.Errorf("cannot get compliance page email presenter config: %w", err) } @@ -542,7 +548,7 @@ func (s *TrustCenterAccessService) sendAccessEmail( return nil } -func (s *TrustCenterAccessService) RejectOrRevokeByIDs( +func (s *Service) RejectOrRevokePortalAccessByIDs( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -551,65 +557,68 @@ func (s *TrustCenterAccessService) RejectOrRevokeByIDs( reportIDs []gid.GID, fileIDs []gid.GID, ) error { - return s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - trustCenter := &coredata.TrustCenter{} - if err := trustCenter.LoadByOrganizationID(ctx, tx, scope, organizationID); err != nil { - return fmt.Errorf("cannot load trust center: %w", err) - } - - identity := &coredata.Identity{} - if err := identity.LoadByEmail(ctx, tx, email); err != nil { - return fmt.Errorf("cannot load identity: %w", err) - } - - access := &coredata.TrustCenterAccess{} - if err := access.LoadByTrustCenterIDAndIdentityID(ctx, tx, scope, trustCenter.ID, identity.ID); err != nil { - return fmt.Errorf("cannot load trust center access: %w", err) - } - - profile := &coredata.MembershipProfile{} - if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, access.OrganizationID); err != nil { - return fmt.Errorf("cannot load profile: %w", err) - } - - shouldSendEmail := false - now := time.Now() - - if len(documentIDs) > 0 { - shouldSendEmail = true - - if err := coredata.RejectOrRevokeByDocumentIDs(ctx, tx, scope, access.ID, documentIDs, now); err != nil { - return fmt.Errorf("cannot reject/revoke document accesses: %w", err) + return s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByOrganizationID(ctx, tx, scope, organizationID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) } - } - if len(reportIDs) > 0 { - shouldSendEmail = true - - if err := coredata.RejectOrRevokeByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { - return fmt.Errorf("cannot reject/revoke report accesses: %w", err) + identity := &coredata.Identity{} + if err := identity.LoadByEmail(ctx, tx, email); err != nil { + return fmt.Errorf("cannot load identity: %w", err) } - } - if len(fileIDs) > 0 { - shouldSendEmail = true - - if err := coredata.RejectOrRevokeByTrustCenterFileIDs(ctx, tx, scope, access.ID, fileIDs, now); err != nil { - return fmt.Errorf("cannot reject/revoke trust center file accesses: %w", err) + access := &coredata.TrustCenterAccess{} + if err := access.LoadByTrustCenterIDAndIdentityID(ctx, tx, scope, trustCenter.ID, identity.ID); err != nil { + return fmt.Errorf("cannot load trust center access: %w", err) } - } - if shouldSendEmail { - if err := s.sendDocumentAccessRejectedEmail(ctx, tx, scope, access, profile, documentIDs, reportIDs, fileIDs); err != nil { - return fmt.Errorf("cannot send access email: %w", err) + profile := &coredata.MembershipProfile{} + if err := profile.LoadByIdentityIDAndOrganizationID(ctx, tx, scope, identity.ID, access.OrganizationID); err != nil { + return fmt.Errorf("cannot load profile: %w", err) } - } - return nil - }) + shouldSendEmail := false + now := time.Now() + + if len(documentIDs) > 0 { + shouldSendEmail = true + + if err := coredata.RejectOrRevokeByDocumentIDs(ctx, tx, scope, access.ID, documentIDs, now); err != nil { + return fmt.Errorf("cannot reject/revoke document accesses: %w", err) + } + } + + if len(reportIDs) > 0 { + shouldSendEmail = true + + if err := coredata.RejectOrRevokeByReportFileIDs(ctx, tx, scope, access.ID, reportIDs, now); err != nil { + return fmt.Errorf("cannot reject/revoke report accesses: %w", err) + } + } + + if len(fileIDs) > 0 { + shouldSendEmail = true + + if err := coredata.RejectOrRevokeByTrustCenterFileIDs(ctx, tx, scope, access.ID, fileIDs, now); err != nil { + return fmt.Errorf("cannot reject/revoke trust center file accesses: %w", err) + } + } + + if shouldSendEmail { + if err := s.sendPortalDocumentAccessRejectedEmail(ctx, tx, scope, access, profile, documentIDs, reportIDs, fileIDs); err != nil { + return fmt.Errorf("cannot send access email: %w", err) + } + } + + return nil + }, + ) } -func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail( +func (s *Service) sendPortalDocumentAccessRejectedEmail( ctx context.Context, tx pg.Tx, scope coredata.Scoper, @@ -659,7 +668,7 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail( } } - emailPresenterCfg, err := s.svc.TrustCenters.EmailPresenterConfig(ctx, scope, access.TrustCenterID) + emailPresenterCfg, err := s.GetPortalEmailPresenterConfig(ctx, scope, access.TrustCenterID) if err != nil { return fmt.Errorf("cannot get compliance page email presenter config: %w", err) } diff --git a/pkg/trust/trust_center_file_service.go b/pkg/complianceportal/visitor/portal_file_service.go similarity index 78% rename from pkg/trust/trust_center_file_service.go rename to pkg/complianceportal/visitor/portal_file_service.go index 4e1a1f609..fb3d2553f 100644 --- a/pkg/trust/trust_center_file_service.go +++ b/pkg/complianceportal/visitor/portal_file_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -34,11 +34,7 @@ import ( "go.probo.inc/probo/pkg/pdfutils" ) -type TrustCenterFileService struct { - svc *Service -} - -func (s *TrustCenterFileService) Get( +func (s *Service) GetPortalFile( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -46,7 +42,7 @@ func (s *TrustCenterFileService) Get( ) (*coredata.TrustCenterFile, error) { trustCenterFile := &coredata.TrustCenterFile{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := trustCenterFile.LoadByID(ctx, conn, scope, trustCenterFileID) @@ -72,7 +68,7 @@ func (s *TrustCenterFileService) Get( return trustCenterFile, nil } -func (s *TrustCenterFileService) ListForOrganizationId( +func (s *Service) ListPortalFilesForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -81,7 +77,7 @@ func (s *TrustCenterFileService) ListForOrganizationId( ) (*page.Page[*coredata.TrustCenterFile, coredata.TrustCenterFileOrderField], error) { var trustCenterFiles coredata.TrustCenterFiles - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := trustCenterFiles.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter) @@ -99,13 +95,13 @@ func (s *TrustCenterFileService) ListForOrganizationId( return page.NewPage(trustCenterFiles, cursor), nil } -func (s *TrustCenterFileService) ExportFile( +func (s *Service) ExportPortalFile( ctx context.Context, scope coredata.Scoper, trustCenterFileID gid.GID, email mail.Addr, ) ([]byte, string, error) { - fileData, mimeType, err := s.exportFileData(ctx, scope, trustCenterFileID) + fileData, mimeType, err := s.exportPortalFileData(ctx, scope, trustCenterFileID) if err != nil { return nil, "", fmt.Errorf("cannot export trust center file: %w", err) } @@ -122,15 +118,15 @@ func (s *TrustCenterFileService) ExportFile( return fileData, mimeType, nil } -func (s *TrustCenterFileService) ExportFileWithoutWatermark( +func (s *Service) ExportPortalFileWithoutWatermark( ctx context.Context, scope coredata.Scoper, trustCenterFileID gid.GID, ) ([]byte, string, error) { - return s.exportFileData(ctx, scope, trustCenterFileID) + return s.exportPortalFileData(ctx, scope, trustCenterFileID) } -func (s *TrustCenterFileService) exportFileData( +func (s *Service) exportPortalFileData( ctx context.Context, scope coredata.Scoper, trustCenterFileID gid.GID, @@ -140,27 +136,33 @@ func (s *TrustCenterFileService) exportFileData( file *coredata.File ) - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - trustCenterFile = &coredata.TrustCenterFile{} - if err := trustCenterFile.LoadByID(ctx, conn, scope, trustCenterFileID); err != nil { - return fmt.Errorf("cannot load trust center file: %w", err) - } + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + trustCenterFile = &coredata.TrustCenterFile{} + if err := trustCenterFile.LoadByID(ctx, conn, scope, trustCenterFileID); err != nil { + return fmt.Errorf("cannot load trust center file: %w", err) + } - file = &coredata.File{} - if err := file.LoadByID(ctx, conn, scope, trustCenterFile.FileID); err != nil { - return fmt.Errorf("cannot load file: %w", err) - } + file = &coredata.File{} + if err := file.LoadByID(ctx, conn, scope, trustCenterFile.FileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } - return nil - }) + return nil + }, + ) if err != nil { return nil, "", err } - result, err := s.svc.s3.GetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - }) + result, err := s.s3.GetObject( + ctx, + &s3.GetObjectInput{ + Bucket: new(s.bucket), + Key: new(file.FileKey), + }, + ) if err != nil { return nil, "", fmt.Errorf("cannot download file from S3: %w", err) } diff --git a/pkg/trust/trust_center_reference_service.go b/pkg/complianceportal/visitor/portal_reference_service.go similarity index 75% rename from pkg/trust/trust_center_reference_service.go rename to pkg/complianceportal/visitor/portal_reference_service.go index b18a38ccc..06de68ba6 100644 --- a/pkg/trust/trust_center_reference_service.go +++ b/pkg/complianceportal/visitor/portal_reference_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,11 +30,7 @@ import ( "go.probo.inc/probo/pkg/page" ) -type TrustCenterReferenceService struct { - svc *Service -} - -func (s TrustCenterReferenceService) ListForTrustCenterID( +func (s *Service) ListPortalReferencesForPortalID( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -42,14 +38,19 @@ func (s TrustCenterReferenceService) ListForTrustCenterID( ) (*page.Page[*coredata.TrustCenterReference, coredata.TrustCenterReferenceOrderField], error) { var references coredata.TrustCenterReferences - err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - err := references.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) - if err != nil { - return fmt.Errorf("cannot load trust center references: %w", err) - } + err := s.pg.WithConn( - return nil - }) + ctx, + + func(ctx context.Context, conn pg.Querier) error { + err := references.LoadByTrustCenterID(ctx, conn, scope, trustCenterID, cursor) + if err != nil { + return fmt.Errorf("cannot load trust center references: %w", err) + } + + return nil + }, + ) if err != nil { return nil, err } @@ -57,36 +58,39 @@ func (s TrustCenterReferenceService) ListForTrustCenterID( return page.NewPage(references, cursor), nil } -func (s TrustCenterReferenceService) GenerateLogoURL( +func (s *Service) GeneratePortalReferenceLogoURL( ctx context.Context, scope coredata.Scoper, referenceID gid.GID, ) (string, error) { reference := &coredata.TrustCenterReference{} - err := s.svc.pg.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error { - return reference.LoadByID(ctx, tx, scope, referenceID) - }) + err := s.pg.WithTx( + ctx, + func(ctx context.Context, tx pg.Tx) error { + return reference.LoadByID(ctx, tx, scope, referenceID) + }, + ) if err != nil { return "", fmt.Errorf("cannot load trust center reference: %w", err) } - file, err := s.svc.fileManager.GetPublicFile(ctx, reference.LogoFileID) + file, err := s.fileManager.GetPublicFile(ctx, reference.LogoFileID) if err != nil { return "", err } - return s.svc.fileManager.GenerateFileURL(file), nil + return s.fileManager.GenerateFileURL(file), nil } -func (s TrustCenterReferenceService) Get( +func (s *Service) GetPortalReference( ctx context.Context, scope coredata.Scoper, referenceID gid.GID, ) (*coredata.TrustCenterReference, error) { reference := &coredata.TrustCenterReference{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := reference.LoadByID(ctx, conn, scope, referenceID) diff --git a/pkg/trust/trust_center_service.go b/pkg/complianceportal/visitor/portal_service.go similarity index 79% rename from pkg/trust/trust_center_service.go rename to pkg/complianceportal/visitor/portal_service.go index 66f12b8ed..68bbadd07 100644 --- a/pkg/trust/trust_center_service.go +++ b/pkg/complianceportal/visitor/portal_service.go @@ -18,34 +18,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" - "errors" "fmt" - "net/url" "path/filepath" "time" "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" + "go.probo.inc/probo/pkg/complianceportal" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" ) -type TrustCenterService struct { - svc *Service -} - -func (s TrustCenterService) Get( +func (s *Service) GetPortal( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.TrustCenter, error) { var trustCenter *coredata.TrustCenter - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter = &coredata.TrustCenter{} @@ -63,14 +58,14 @@ func (s TrustCenterService) Get( return trustCenter, nil } -func (s TrustCenterService) GetByOrganizationID( +func (s *Service) GetPortalByOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, ) (*coredata.TrustCenter, error) { trustCenter := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := trustCenter.LoadByOrganizationID(ctx, conn, scope, organizationID) @@ -88,14 +83,14 @@ func (s TrustCenterService) GetByOrganizationID( return trustCenter, nil } -func (s TrustCenterService) GetNDAFile( +func (s *Service) GetPortalNDAFile( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.File, error) { var file *coredata.File - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter := &coredata.TrustCenter{} @@ -122,7 +117,7 @@ func (s TrustCenterService) GetNDAFile( return file, nil } -func (s TrustCenterService) GenerateNDAFileURL( +func (s *Service) GeneratePortalNDAFileURL( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, @@ -130,7 +125,7 @@ func (s TrustCenterService) GenerateNDAFileURL( ) (string, error) { var file *coredata.File - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter := &coredata.TrustCenter{} @@ -154,7 +149,7 @@ func (s TrustCenterService) GenerateNDAFileURL( return "", err } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return "", fmt.Errorf("cannot generate file URL: %w", err) } @@ -162,7 +157,7 @@ func (s TrustCenterService) GenerateNDAFileURL( return presignedURL, nil } -func (s TrustCenterService) GenerateLogoURL( +func (s *Service) GeneratePortalLogoURL( ctx context.Context, scope coredata.Scoper, compliancePageID gid.GID, @@ -171,7 +166,7 @@ func (s TrustCenterService) GenerateLogoURL( file := &coredata.File{} compliancePage := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -201,7 +196,7 @@ func (s TrustCenterService) GenerateLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -209,7 +204,7 @@ func (s TrustCenterService) GenerateLogoURL( return &presignedURL, nil } -func (s TrustCenterService) GenerateDarkLogoURL( +func (s *Service) GeneratePortalDarkLogoURL( ctx context.Context, scope coredata.Scoper, compliancePageID gid.GID, @@ -218,7 +213,7 @@ func (s TrustCenterService) GenerateDarkLogoURL( file := &coredata.File{} compliancePage := &coredata.TrustCenter{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -248,7 +243,7 @@ func (s TrustCenterService) GenerateDarkLogoURL( return nil, nil } - presignedURL, err := s.svc.fileManager.GeneratePresignedURL(ctx, file, expiresIn) + presignedURL, err := s.fileManager.GeneratePresignedURL(ctx, file, expiresIn) if err != nil { return nil, fmt.Errorf("cannot generate file URL: %w", err) } @@ -256,7 +251,7 @@ func (s TrustCenterService) GenerateDarkLogoURL( return &presignedURL, nil } -func (s *TrustCenterService) EmailPresenterConfig( +func (s *Service) GetPortalEmailPresenterConfig( ctx context.Context, scope coredata.Scoper, compliancePageID gid.GID, @@ -264,12 +259,12 @@ func (s *TrustCenterService) EmailPresenterConfig( var ( compliancePage = &coredata.TrustCenter{} organization = &coredata.Organization{} - customDomain *coredata.CustomDomain logoFile = &coredata.File{} - emailPresenterCfg = emails.DefaultPresenterConfig(s.svc.baseURL) + compliancePageURL string + emailPresenterCfg = emails.DefaultPresenterConfig(s.baseURL) ) - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil { @@ -286,13 +281,19 @@ func (s *TrustCenterService) EmailPresenterConfig( return fmt.Errorf("cannot load organization: %w", err) } - customDomain = &coredata.CustomDomain{} - if err := customDomain.LoadByOrganizationID(ctx, conn, scope, organization.ID); err != nil { - if !errors.Is(err, coredata.ErrResourceNotFound) { - return fmt.Errorf("cannot load custom domain: %w", err) - } + publicURL, err := complianceportal.PublicURLForTrustCenter( + ctx, + conn, + scope, + compliancePage, + s.baseDomain, + ) + if err != nil { + return fmt.Errorf("cannot resolve compliance page URL: %w", err) } + compliancePageURL = publicURL + return nil }, ) @@ -300,24 +301,7 @@ func (s *TrustCenterService) EmailPresenterConfig( return emailPresenterCfg, err } - parsedBaseURL, err := url.Parse(s.svc.baseURL) - if err != nil { - return emailPresenterCfg, fmt.Errorf("cannot parse base URL: %w", err) - } - - baseURL := url.URL{ - Scheme: parsedBaseURL.Scheme, - Host: parsedBaseURL.Host, - Path: "/trust/" + compliancePage.Slug, - } - - if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive { - baseURL.Host = customDomain.Domain - baseURL.Scheme = "https" - baseURL.Path = "" - } - - emailPresenterCfg.BaseURL = baseURL.String() + emailPresenterCfg.BaseURL = compliancePageURL if compliancePage.LogoFileID != nil { if logoFile.FileKey == "" { @@ -328,26 +312,26 @@ func (s *TrustCenterService) EmailPresenterConfig( emailPresenterCfg.SenderCompanyLogoPath = filepath.Join("/api/files/v1/public/", logoFile.ID.String()) emailPresenterCfg.SenderCompanyName = organization.Name - if organization.WebsiteURL != nil { - emailPresenterCfg.SenderCompanyWebsiteURL = *organization.WebsiteURL + if compliancePage.WebsiteURL != nil { + emailPresenterCfg.SenderCompanyWebsiteURL = *compliancePage.WebsiteURL } - if organization.HeadquarterAddress != nil { - emailPresenterCfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress + if compliancePage.HeadquarterAddress != nil { + emailPresenterCfg.SenderCompanyHeadquarterAddress = *compliancePage.HeadquarterAddress } } return emailPresenterCfg, nil } -func (s *TrustCenterService) GetMailingList( +func (s *Service) GetPortalMailingList( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, ) (*coredata.MailingList, error) { var mailingList *coredata.MailingList - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { trustCenter := &coredata.TrustCenter{} diff --git a/pkg/trust/report_service.go b/pkg/complianceportal/visitor/report_service.go similarity index 74% rename from pkg/trust/report_service.go rename to pkg/complianceportal/visitor/report_service.go index a06db6c48..f3d778860 100644 --- a/pkg/trust/report_service.go +++ b/pkg/complianceportal/visitor/report_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -35,17 +35,13 @@ import ( "go.probo.inc/probo/pkg/pdfutils" ) -type ReportService struct { - svc *Service -} - -func (s ReportService) Get( +func (s *Service) GetReport( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, fileID gid.GID, ) (*coredata.File, error) { - file, err := s.loadByID(ctx, scope, fileID) + file, err := s.loadReportByID(ctx, scope, fileID) if err != nil { return nil, err } @@ -56,7 +52,7 @@ func (s ReportService) Get( // check the given report file ID is linked to an audit in order to avoid // being able to get any file from the report request. - _, err = s.svc.Audits.GetByReportFileID(ctx, scope, fileID) + _, err = s.GetAuditByReportFileID(ctx, scope, fileID) if err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return nil, ErrReportNotFound @@ -68,14 +64,14 @@ func (s ReportService) Get( return file, nil } -func (s ReportService) loadByID( +func (s *Service) loadReportByID( ctx context.Context, scope coredata.Scoper, fileID gid.GID, ) (*coredata.File, error) { file := &coredata.File{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { if err := file.LoadActiveByID(ctx, conn, scope, fileID); err != nil { @@ -92,28 +88,31 @@ func (s ReportService) loadByID( return file, nil } -func (s ReportService) GenerateDownloadURL( +func (s *Service) GenerateReportDownloadURL( ctx context.Context, scope coredata.Scoper, fileID gid.GID, expiresIn time.Duration, ) (*string, error) { - file, err := s.loadByID(ctx, scope, fileID) + file, err := s.loadReportByID(ctx, scope, fileID) if err != nil { return nil, fmt.Errorf("cannot get file: %w", err) } - presignClient := s3.NewPresignClient(s.svc.s3) + presignClient := s3.NewPresignClient(s.s3) - presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - ResponseCacheControl: new("max-age=3600, public"), - ResponseContentType: new(file.MimeType), - ResponseContentDisposition: new(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)), - }, func(opts *s3.PresignOptions) { - opts.Expires = expiresIn - }) + presignedReq, err := presignClient.PresignGetObject( + ctx, + &s3.GetObjectInput{ + Bucket: new(s.bucket), + Key: new(file.FileKey), + ResponseCacheControl: new("max-age=3600, public"), + ResponseContentType: new(file.MimeType), + ResponseContentDisposition: new(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)), + }, func(opts *s3.PresignOptions) { + opts.Expires = expiresIn + }, + ) if err != nil { return nil, fmt.Errorf("cannot presign GetObject request: %w", err) } @@ -121,13 +120,13 @@ func (s ReportService) GenerateDownloadURL( return &presignedReq.URL, nil } -func (s ReportService) ExportPDF( +func (s *Service) ExportReportPDF( ctx context.Context, scope coredata.Scoper, reportID gid.GID, email mail.Addr, ) ([]byte, error) { - pdfData, err := s.exportPDFData(ctx, scope, reportID) + pdfData, err := s.exportReportPDFData(ctx, scope, reportID) if err != nil { return nil, fmt.Errorf("cannot export report PDF: %w", err) } @@ -140,28 +139,31 @@ func (s ReportService) ExportPDF( return watermarkedPDF, nil } -func (s ReportService) ExportPDFWithoutWatermark( +func (s *Service) ExportReportPDFWithoutWatermark( ctx context.Context, scope coredata.Scoper, reportID gid.GID, ) ([]byte, error) { - return s.exportPDFData(ctx, scope, reportID) + return s.exportReportPDFData(ctx, scope, reportID) } -func (s ReportService) exportPDFData( +func (s *Service) exportReportPDFData( ctx context.Context, scope coredata.Scoper, fileID gid.GID, ) ([]byte, error) { - file, err := s.loadByID(ctx, scope, fileID) + file, err := s.loadReportByID(ctx, scope, fileID) if err != nil { return nil, fmt.Errorf("cannot get file: %w", err) } - result, err := s.svc.s3.GetObject(ctx, &s3.GetObjectInput{ - Bucket: new(s.svc.bucket), - Key: new(file.FileKey), - }) + result, err := s.s3.GetObject( + ctx, + &s3.GetObjectInput{ + Bucket: new(s.bucket), + Key: new(file.FileKey), + }, + ) if err != nil { return nil, fmt.Errorf("cannot download PDF from S3: %w", err) } diff --git a/pkg/trust/rights_request_service.go b/pkg/complianceportal/visitor/rights_request_service.go similarity index 86% rename from pkg/trust/rights_request_service.go rename to pkg/complianceportal/visitor/rights_request_service.go index 9c2b5741f..34332ad77 100644 --- a/pkg/trust/rights_request_service.go +++ b/pkg/complianceportal/visitor/rights_request_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -41,22 +41,16 @@ import ( // the console can adjust it afterwards. const RightsRequestDeadlineDays = 30 -type ( - RightsRequestService struct { - svc *Service - } - - // CreateRightsRequest is a data subject request submitted from the trust - // portal. The organization comes from the current compliance page and the - // contact from the verified viewer's identity, so neither is client-supplied. - CreateRightsRequest struct { - OrganizationID gid.GID - RequestType coredata.RightsRequestType - DataSubject *string - Contact string - Details *string - } -) +// CreateRightsRequest is a data subject request submitted from the trust +// portal. The organization comes from the current compliance page and the +// contact from the verified viewer's identity, so neither is client-supplied. +type CreateRightsRequest struct { + OrganizationID gid.GID + RequestType coredata.RightsRequestType + DataSubject *string + Contact string + Details *string +} // Validate bounds the free-text fields with the same rules the console applies, // so this public portal mutation can't persist oversized or unsafe input. @@ -69,7 +63,7 @@ func (r *CreateRightsRequest) Validate() error { return v.Error() } -func (s *RightsRequestService) Create( +func (s *Service) CreateRightsRequest( ctx context.Context, scope coredata.Scoper, req *CreateRightsRequest, @@ -94,7 +88,7 @@ func (s *RightsRequestService) Create( UpdatedAt: now, } - err := s.svc.pg.WithTx( + err := s.pg.WithTx( ctx, func(ctx context.Context, tx pg.Tx) error { organization := &coredata.Organization{} @@ -127,7 +121,7 @@ func (s *RightsRequestService) Create( return request, nil } -func (s RightsRequestService) ListForOrganizationIDAndContact( +func (s *Service) ListRightsRequestsForOrganizationIDAndContact( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, @@ -136,7 +130,7 @@ func (s RightsRequestService) ListForOrganizationIDAndContact( ) (*page.Page[*coredata.RightsRequest, coredata.RightsRequestOrderField], error) { var requests coredata.RightsRequests - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := requests.LoadByOrganizationIDAndContact(ctx, conn, scope, organizationID, contact, cursor) diff --git a/pkg/trust/rights_request_service_test.go b/pkg/complianceportal/visitor/rights_request_service_test.go similarity index 97% rename from pkg/trust/rights_request_service_test.go rename to pkg/complianceportal/visitor/rights_request_service_test.go index 34e5d45c8..75e55072b 100644 --- a/pkg/trust/rights_request_service_test.go +++ b/pkg/complianceportal/visitor/rights_request_service_test.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -43,10 +43,10 @@ func TestRightsRequestService_CreateEnqueuesWebhook(t *testing.T) { scope := coredata.NewScope(organizationID.TenantID()) insertPortalRequestWebhookSubscription(t, client, scope, organizationID) - service := RightsRequestService{svc: &Service{pg: client}} + service := Service{pg: client} dataSubject := "Jane Doe" contact := "jane@example.com" - rightsRequest, err := service.Create( + rightsRequest, err := service.CreateRightsRequest( t.Context(), scope, &CreateRightsRequest{ diff --git a/pkg/trust/robots.txt.tmpl b/pkg/complianceportal/visitor/robots.txt.tmpl similarity index 100% rename from pkg/trust/robots.txt.tmpl rename to pkg/complianceportal/visitor/robots.txt.tmpl diff --git a/pkg/trust/service.go b/pkg/complianceportal/visitor/service.go similarity index 71% rename from pkg/trust/service.go rename to pkg/complianceportal/visitor/service.go index 225b9b62f..5df688b05 100644 --- a/pkg/trust/service.go +++ b/pkg/complianceportal/visitor/service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,13 +30,13 @@ import ( "go.gearno.de/kit/log" "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" + "go.probo.inc/probo/pkg/complianceportal" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/html2pdf" "go.probo.inc/probo/pkg/iam" - "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/resourcealias" "go.probo.inc/probo/pkg/slack" ) @@ -44,37 +44,23 @@ import ( const NDAConsentText = "By clicking \"Review and sign\", I consent to sign this document electronically and agree that my electronic signature has the same legal validity as a handwritten signature. If you have questions about the NDA, please contact security@probo.com." type ( + // Service is the visitor-facing compliance portal service. It exposes the + // public read operations for the trust center and its related resources as + // methods on a single type. Service struct { - pg *pg.Client - s3 *s3.Client - bucket string - proboSvc *probo.Service - slackSigningSecret string - baseURL string - iam *iam.Service - esign *esign.Service - html2pdfConverter *html2pdf.Converter - fileManager *filemanager.Service - logger *log.Logger - slack *slack.Service - TrustCenters *TrustCenterService - Documents *DocumentService - Audits *AuditService - ThirdParties *ThirdPartyService - Frameworks *FrameworkService - ComplianceFrameworks *ComplianceFrameworkService - TrustCenterAccesses *TrustCenterAccessService - TrustCenterReferences *TrustCenterReferenceService - - CompliancePortalCommitmentGroups *CompliancePortalCommitmentGroupService - CompliancePortalCommitments *CompliancePortalCommitmentService - - TrustCenterFiles *TrustCenterFileService - Reports *ReportService - Organizations *OrganizationService - ComplianceExternalURLs *ComplianceExternalURLService - RightsRequests *RightsRequestService - resourceAlias *resourcealias.Service + pg *pg.Client + s3 *s3.Client + bucket string + slackSigningSecret string + baseURL string + baseDomain string + iam *iam.Service + esign *esign.Service + html2pdfConverter *html2pdf.Converter + fileManager *filemanager.Service + logger *log.Logger + slack *slack.Service + resourceAlias *resourcealias.Service } ) @@ -83,6 +69,7 @@ func NewService( s3Client *s3.Client, bucket string, baseURL string, + baseDomain string, slackSigningSecret string, iam *iam.Service, esignSvc *esign.Service, @@ -98,6 +85,7 @@ func NewService( bucket: bucket, slackSigningSecret: slackSigningSecret, baseURL: baseURL, + baseDomain: baseDomain, iam: iam, esign: esignSvc, html2pdfConverter: html2pdfConverter, @@ -106,26 +94,11 @@ func NewService( slack: slack, resourceAlias: resourceAliasSvc, } - svc.TrustCenters = &TrustCenterService{svc: svc} - svc.Documents = &DocumentService{svc: svc, html2pdfConverter: html2pdfConverter} - svc.Audits = &AuditService{svc: svc} - svc.ThirdParties = &ThirdPartyService{svc: svc} - svc.Frameworks = &FrameworkService{svc: svc} - svc.ComplianceFrameworks = &ComplianceFrameworkService{svc: svc} - svc.TrustCenterAccesses = &TrustCenterAccessService{svc: svc, iamSvc: iam, logger: logger} - svc.TrustCenterReferences = &TrustCenterReferenceService{svc: svc} - svc.CompliancePortalCommitmentGroups = &CompliancePortalCommitmentGroupService{svc: svc} - svc.CompliancePortalCommitments = &CompliancePortalCommitmentService{svc: svc} - svc.TrustCenterFiles = &TrustCenterFileService{svc: svc} - svc.Reports = &ReportService{svc: svc} - svc.Organizations = &OrganizationService{svc: svc} - svc.ComplianceExternalURLs = &ComplianceExternalURLService{svc: svc} - svc.RightsRequests = &RightsRequestService{svc: svc} return svc } -func (s *Service) Get( +func (s *Service) GetPortalByID( ctx context.Context, id gid.GID, ) (*coredata.TrustCenter, error) { @@ -153,7 +126,7 @@ func (s *Service) Get( return trustCenter, nil } -func (s *Service) GetBySlug( +func (s *Service) GetPortalBySlug( ctx context.Context, slug string, ) (*coredata.TrustCenter, error) { @@ -181,7 +154,41 @@ func (s *Service) GetBySlug( return trustCenter, nil } -func (s *Service) GetByDomainName(ctx context.Context, domain string) (*coredata.TrustCenter, error) { +// GetEffectiveCanonicalHost returns the host a compliance page should be +// served under. It prefers the primary domain when its certificate is active, +// and otherwise falls back to the managed probopage subdomain. An empty string +// is returned when no serving host can be determined. +func (s *Service) GetPortalEffectiveCanonicalHost(ctx context.Context, trustCenterID gid.GID) (string, error) { + var host string + + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, conn, coredata.NewNoScope(), trustCenterID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + domain, err := complianceportal.EffectiveDomainForTrustCenter(ctx, conn, coredata.NewNoScope(), trustCenter) + if err != nil { + return err + } + + if domain != nil { + host = domain.Domain + } + + return nil + }, + ) + if err != nil { + return "", err + } + + return host, nil +} + +func (s *Service) GetPortalByDomainName(ctx context.Context, domain string) (*coredata.TrustCenter, error) { trustCenter := &coredata.TrustCenter{} err := s.pg.WithConn( @@ -196,17 +203,8 @@ func (s *Service) GetByDomainName(ctx context.Context, domain string) (*coredata return fmt.Errorf("cannot load custom domain: %w", err) } - var org coredata.Organization - if err := org.LoadByCustomDomainID(ctx, conn, coredata.NewNoScope(), customDomain.ID); err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return ErrPageNotFound - } - - return fmt.Errorf("cannot load organization: %w", err) - } - trustCenter = &coredata.TrustCenter{} - if err := trustCenter.LoadByOrganizationID(ctx, conn, coredata.NewNoScope(), org.ID); err != nil { + if err := trustCenter.LoadByDomainID(ctx, conn, customDomain.ID); err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return ErrPageNotFound } @@ -224,49 +222,32 @@ func (s *Service) GetByDomainName(ctx context.Context, domain string) (*coredata return trustCenter, err } -func (s *Service) GetCustomDomainByOrganizationID(ctx context.Context, organizationID gid.GID) (*coredata.CustomDomain, error) { - customDomain := &coredata.CustomDomain{} - - err := s.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - return customDomain.LoadByOrganizationID(ctx, conn, coredata.NewNoScope(), organizationID) - }, - ) - if err != nil { - if errors.Is(err, coredata.ErrResourceNotFound) { - return nil, ErrCustomDomainNotFound - } - - return nil, err - } - - return customDomain, err -} - -// EmailPresenterConfigByOrganizationID resolves the emails.PresenterConfig for +// GetPortalEmailPresenterConfigByOrganizationID resolves the emails.PresenterConfig for // the trust center that belongs to the given organization. This is used by the // esign certificate worker which needs per-org branding at render time. -func (s *Service) EmailPresenterConfigByOrganizationID(ctx context.Context, orgID gid.GID) (emails.PresenterConfig, error) { +func (s *Service) GetPortalEmailPresenterConfigByOrganizationID(ctx context.Context, orgID gid.GID) (emails.PresenterConfig, error) { var trustCenter coredata.TrustCenter scope := coredata.NewScopeFromObjectID(orgID) - err := s.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { - return trustCenter.LoadByOrganizationID(ctx, conn, scope, orgID) - }) + err := s.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + return trustCenter.LoadByOrganizationID(ctx, conn, scope, orgID) + }, + ) if err != nil { return emails.PresenterConfig{}, fmt.Errorf("cannot load trust center for org %s: %w", orgID, err) } - return s.TrustCenters.EmailPresenterConfig(ctx, scope, trustCenter.ID) + return s.GetPortalEmailPresenterConfig(ctx, scope, trustCenter.ID) } -func (s *Service) GetOrganizationByTrustCenterID( +func (s *Service) GetPortalOrganization( ctx context.Context, trustCenterID gid.GID, ) (*coredata.Organization, error) { - trustCenter, err := s.Get(ctx, trustCenterID) + trustCenter, err := s.GetPortalByID(ctx, trustCenterID) if err != nil { return nil, fmt.Errorf("cannot load trust center: %w", err) } @@ -286,7 +267,7 @@ func (s *Service) GetOrganizationByTrustCenterID( return org, nil } -func (s *Service) GetMembershipByCompliancePageIDAndIdentityID(ctx context.Context, compliancePageID gid.GID, identityID gid.GID) (*coredata.TrustCenterAccess, error) { +func (s *Service) GetPortalMembership(ctx context.Context, compliancePageID gid.GID, identityID gid.GID) (*coredata.TrustCenterAccess, error) { membership := &coredata.TrustCenterAccess{} err := s.pg.WithConn( @@ -312,7 +293,7 @@ func (s *Service) GetMembershipByCompliancePageIDAndIdentityID(ctx context.Conte return membership, nil } -func (s *Service) GetNDAFile( +func (s *Service) GetPortalNDAFileByID( ctx context.Context, compliancePageID gid.GID, ) (*coredata.File, error) { @@ -352,7 +333,7 @@ func (s *Service) GetNDAFile( return file, nil } -func (s *Service) ProvisionMember( +func (s *Service) ProvisionPortalMember( ctx context.Context, compliancePageID gid.GID, identityID gid.GID, diff --git a/pkg/trust/sitemap.xml.tmpl b/pkg/complianceportal/visitor/sitemap.xml.tmpl similarity index 100% rename from pkg/trust/sitemap.xml.tmpl rename to pkg/complianceportal/visitor/sitemap.xml.tmpl diff --git a/pkg/trust/third_party_service.go b/pkg/complianceportal/visitor/third_party_service.go similarity index 84% rename from pkg/trust/third_party_service.go rename to pkg/complianceportal/visitor/third_party_service.go index 91871df8e..327c1e297 100644 --- a/pkg/trust/third_party_service.go +++ b/pkg/complianceportal/visitor/third_party_service.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package trust +package visitor import ( "context" @@ -30,18 +30,14 @@ import ( "go.probo.inc/probo/pkg/page" ) -type ThirdPartyService struct { - svc *Service -} - -func (s ThirdPartyService) Get( +func (s *Service) GetThirdParty( ctx context.Context, scope coredata.Scoper, thirdPartyID gid.GID, ) (*coredata.ThirdParty, error) { thirdParty := &coredata.ThirdParty{} - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { err := thirdParty.LoadByID(ctx, conn, scope, thirdPartyID) @@ -59,22 +55,20 @@ func (s ThirdPartyService) Get( return thirdParty, nil } -func (s ThirdPartyService) ListForOrganizationId( +func (s *Service) ListThirdPartiesForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, cursor *page.Cursor[coredata.ThirdPartyOrderField], - filter *coredata.ThirdPartyFilter, ) (*page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField], error) { - if filter == nil { - filter = coredata.NewThirdPartyFilter(nil, nil, nil, nil, nil) - } - var thirdParties coredata.ThirdParties - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { + showOnTrustCenter := true + filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, nil, nil, nil) + err := thirdParties.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter) if err != nil { return fmt.Errorf("cannot load thirdParties: %w", err) @@ -90,14 +84,14 @@ func (s ThirdPartyService) ListForOrganizationId( return page.NewPage(thirdParties, cursor), nil } -func (s ThirdPartyService) ListDistinctTrustCenterCategoriesForOrganizationID( +func (s *Service) ListDistinctTrustCenterCategoriesForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, ) ([]coredata.ThirdPartyCategory, error) { var categories []coredata.ThirdPartyCategory - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { thirdParties := &coredata.ThirdParties{} @@ -119,14 +113,14 @@ func (s ThirdPartyService) ListDistinctTrustCenterCategoriesForOrganizationID( return categories, nil } -func (s ThirdPartyService) ListDistinctTrustCenterCountriesForOrganizationID( +func (s *Service) ListDistinctTrustCenterCountriesForOrganizationID( ctx context.Context, scope coredata.Scoper, organizationID gid.GID, ) ([]coredata.CountryCode, error) { var countries []coredata.CountryCode - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { thirdParties := &coredata.ThirdParties{} @@ -148,23 +142,24 @@ func (s ThirdPartyService) ListDistinctTrustCenterCountriesForOrganizationID( return countries, nil } -func (s ThirdPartyService) CountForTrustCenterId( +func (s *Service) CountThirdPartiesForPortalID( ctx context.Context, scope coredata.Scoper, trustCenterID gid.GID, - filter *coredata.ThirdPartyFilter, ) (int, error) { var count int - err := s.svc.pg.WithConn( + err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) (err error) { - trustCenter, err := s.svc.TrustCenters.Get(ctx, scope, trustCenterID) + trustCenter, err := s.GetPortal(ctx, scope, trustCenterID) if err != nil { return fmt.Errorf("cannot load trust center: %w", err) } thirdParties := &coredata.ThirdParties{} + showOnTrustCenter := true + filter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, nil, nil, nil) count, err = thirdParties.CountByOrganizationID(ctx, conn, scope, trustCenter.OrganizationID, filter) if err != nil { diff --git a/pkg/probo/actions.go b/pkg/probo/actions.go index 1d7738180..49e3faf4c 100644 --- a/pkg/probo/actions.go +++ b/pkg/probo/actions.go @@ -33,76 +33,6 @@ const ( ActionOrganizationContextGet = "core:organization-context:get" ActionOrganizationContextUpdate = "core:organization-context:update" - // TrustCenter actions - ActionTrustCenterGet = "core:trust-center:get" - ActionTrustCenterUpdate = "core:trust-center:update" - ActionTrustCenterGetNda = "core:trust-center:get-nda" - ActionTrustCenterNonDisclosureAgreementUpload = "core:trust-center:upload-nda" - ActionTrustCenterNonDisclosureAgreementDelete = "core:trust-center:delete-nda" - - // TrustCenterAccess actions - ActionTrustCenterAccessGet = "core:trust-center-access:get" - ActionTrustCenterAccessList = "core:trust-center-access:list" - ActionTrustCenterAccessCreate = "core:trust-center-access:create" - ActionTrustCenterAccessUpdate = "core:trust-center-access:update" - ActionTrustCenterAccessDelete = "core:trust-center-access:delete" - - // MailingListUpdate actions - ActionMailingListUpdateList = "core:mailing-list-update:list" - ActionMailingListUpdateCreate = "core:mailing-list-update:create" - ActionMailingListUpdateUpdate = "core:mailing-list-update:update" - ActionMailingListUpdateSend = "core:mailing-list-update:send" - ActionMailingListUpdateDelete = "core:mailing-list-update:delete" - - // MailingList actions - ActionMailingListUpdate = "core:mailing-list:update" - - // MailingListSubscriber actions - ActionMailingListSubscriberList = "core:mailing-list-subscriber:list" - ActionMailingListSubscriberCreate = "core:mailing-list-subscriber:create" - ActionMailingListSubscriberDelete = "core:mailing-list-subscriber:delete" - - // TrustCenterReference actions - ActionTrustCenterReferenceList = "core:trust-center-reference:list" - ActionTrustCenterReferenceGetLogoUrl = "core:trust-center-reference:get-logo-url" - ActionTrustCenterReferenceCreate = "core:trust-center-reference:create" - ActionTrustCenterReferenceUpdate = "core:trust-center-reference:update" - ActionTrustCenterReferenceDelete = "core:trust-center-reference:delete" - - // CompliancePortalCommitmentGroup actions - ActionCompliancePortalCommitmentGroupList = "core:compliance-portal-commitment-group:list" - ActionCompliancePortalCommitmentGroupCreate = "core:compliance-portal-commitment-group:create" - ActionCompliancePortalCommitmentGroupUpdate = "core:compliance-portal-commitment-group:update" - ActionCompliancePortalCommitmentGroupUpdateRank = "core:compliance-portal-commitment-group:update-rank" - ActionCompliancePortalCommitmentGroupDelete = "core:compliance-portal-commitment-group:delete" - - // CompliancePortalCommitment actions - ActionCompliancePortalCommitmentList = "core:compliance-portal-commitment:list" - ActionCompliancePortalCommitmentCreate = "core:compliance-portal-commitment:create" - ActionCompliancePortalCommitmentUpdate = "core:compliance-portal-commitment:update" - ActionCompliancePortalCommitmentUpdateRank = "core:compliance-portal-commitment:update-rank" - ActionCompliancePortalCommitmentDelete = "core:compliance-portal-commitment:delete" - - // ComplianceFramework actions - ActionComplianceFrameworkList = "core:compliance-framework:list" - ActionComplianceFrameworkCreate = "core:compliance-framework:create" - ActionComplianceFrameworkDelete = "core:compliance-framework:delete" - ActionComplianceFrameworkUpdateRank = "core:compliance-framework:update-rank" - - // ComplianceExternalURL actions - ActionComplianceExternalURLList = "core:compliance-external-url:list" - ActionComplianceExternalURLCreate = "core:compliance-external-url:create" - ActionComplianceExternalURLUpdate = "core:compliance-external-url:update" - ActionComplianceExternalURLDelete = "core:compliance-external-url:delete" - - // TrustCenterFile actions - ActionTrustCenterFileGet = "core:trust-center-file:get" - ActionTrustCenterFileList = "core:trust-center-file:list" - ActionTrustCenterFileGetFileUrl = "core:trust-center-file:get-file-url" - ActionTrustCenterFileUpdate = "core:trust-center-file:update" - ActionTrustCenterFileDelete = "core:trust-center-file:delete" - ActionTrustCenterFileCreate = "core:trust-center-file:create" - // ThirdParty actions ActionThirdPartyList = "core:thirdParty:list" ActionThirdPartyGet = "core:thirdParty:get" @@ -214,16 +144,17 @@ const ( ActionDocumentDeleteDraft = "core:document:delete-draft" // DocumentVersion actions - ActionDocumentVersionGet = "core:document-version:get" - ActionDocumentVersionList = "core:document-version:list" - ActionDocumentVersionExportPDF = "core:document-version:export-pdf" - ActionDocumentVersionSign = "core:document-version:sign" - ActionDocumentVersionVoidApproval = "core:document-version:void-approval" - ActionDocumentVersionApprove = "core:document-version:approve" - ActionDocumentVersionReject = "core:document-version:reject" - ActionDocumentVersionApprovalList = "core:document-version:approval-list" - ActionDocumentVersionPublish = "core:document-version:publish" - ActionDocumentVersionExport = "core:document-version:export" + ActionDocumentVersionGet = "core:document-version:get" + ActionDocumentVersionList = "core:document-version:list" + ActionDocumentVersionExportPDF = "core:document-version:export-pdf" + ActionDocumentVersionSign = "core:document-version:sign" + ActionDocumentVersionRequestApproval = "core:document-version:request-approval" + ActionDocumentVersionVoidApproval = "core:document-version:void-approval" + ActionDocumentVersionApprove = "core:document-version:approve" + ActionDocumentVersionReject = "core:document-version:reject" + ActionDocumentVersionApprovalList = "core:document-version:approval-list" + ActionDocumentVersionPublish = "core:document-version:publish" + ActionDocumentVersionExport = "core:document-version:export" // EmployeeDocument actions ActionEmployeeDocumentGet = "core:employee-document:get" @@ -306,11 +237,6 @@ const ( ActionProcessingActivityDelete = "core:processing-activity:delete" ActionProcessingActivityPublish = "core:processing-activity:publish" - // CustomDomain actions - ActionCustomDomainGet = "core:custom-domain:get" - ActionCustomDomainCreate = "core:custom-domain:create" - ActionCustomDomainDelete = "core:custom-domain:delete" - // File actions ActionFileGet = "core:file:get" @@ -342,9 +268,6 @@ const ( ActionTransferImpactAssessmentDelete = "core:transfer-impact-assessment:delete" ActionTransferImpactAssessmentPublish = "core:transfer-impact-assessment:publish" - // TrustCenterDocumentAccess actions - ActionTrustCenterDocumentAccessList = "core:trust-center-document-access:list" - // RightsRequest actions ActionRightsRequestList = "core:rights-request:list" ActionRightsRequestGet = "core:rights-request:get" diff --git a/pkg/probo/custom_domain_service.go b/pkg/probo/custom_domain_service.go deleted file mode 100644 index 60c883cb0..000000000 --- a/pkg/probo/custom_domain_service.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package probo - -import ( - "context" - "fmt" - - "go.gearno.de/kit/log" - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/certmanager" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/crypto/cipher" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/validator" -) - -type ( - CustomDomainService struct { - svc *Service - acmeService *certmanager.ACMEService - encryptionKey cipher.EncryptionKey - logger *log.Logger - } - - CreateCustomDomainRequest struct { - OrganizationID gid.GID - Domain string - } -) - -func (ccdr *CreateCustomDomainRequest) Validate() error { - v := validator.New() - - v.Check(ccdr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) - v.Check(ccdr.Domain, "domain", validator.Required(), validator.NotEmpty(), validator.Domain()) - - return v.Error() -} - -func (s *CustomDomainService) CreateCustomDomain( - ctx context.Context, scope coredata.Scoper, - req CreateCustomDomainRequest, -) (*coredata.CustomDomain, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("invalid request: %w", err) - } - - var domain *coredata.CustomDomain - - err := s.svc.pg.WithTx( - ctx, - func(ctx context.Context, tx pg.Tx) error { - domain = coredata.NewCustomDomain(scope.GetTenantID(), req.Domain) - domain.OrganizationID = req.OrganizationID - - if err := domain.Insert(ctx, tx, scope, s.encryptionKey); err != nil { - return fmt.Errorf("cannot insert custom domain: %w", err) - } - - var org coredata.Organization - if err := org.LoadByID(ctx, tx, scope, req.OrganizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - org.CustomDomainID = &domain.ID - if err := org.Update(ctx, scope, tx); err != nil { - return fmt.Errorf("cannot update organization: %w", err) - } - - return nil - }, - ) - if err != nil { - return nil, err - } - - return domain, nil -} - -func (s *CustomDomainService) DeleteCustomDomain( - ctx context.Context, scope coredata.Scoper, - organizationID gid.GID, -) error { - return s.svc.pg.WithTx( - ctx, - func(ctx context.Context, tx pg.Tx) error { - var org coredata.Organization - if err := org.LoadByID(ctx, tx, scope, organizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - if org.CustomDomainID == nil { - return fmt.Errorf("organization has no custom domain") - } - - domain := &coredata.CustomDomain{} - if err := domain.LoadByID(ctx, tx, scope, *org.CustomDomainID); err != nil { - return fmt.Errorf("cannot load domain: %w", err) - } - - if err := domain.Delete(ctx, tx, scope); err != nil { - return fmt.Errorf("cannot delete domain: %w", err) - } - - org.CustomDomainID = nil - if err := org.Update(ctx, scope, tx); err != nil { - return fmt.Errorf("cannot update organization: %w", err) - } - - return nil - }, - ) -} - -func (s *CustomDomainService) GetOrganizationCustomDomain( - ctx context.Context, scope coredata.Scoper, - organizationID gid.GID, -) (*coredata.CustomDomain, error) { - var domain *coredata.CustomDomain - - err := s.svc.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - var org coredata.Organization - if err := org.LoadByID(ctx, conn, scope, organizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - if org.CustomDomainID == nil { - return nil - } - - domain = &coredata.CustomDomain{} - if err := domain.LoadByID(ctx, conn, scope, *org.CustomDomainID); err != nil { - return fmt.Errorf("cannot load custom domain: %w", err) - } - - return nil - }, - ) - if err != nil { - return nil, err - } - - return domain, nil -} diff --git a/pkg/probo/oauth2_scopes.go b/pkg/probo/oauth2_scopes.go index d934d0cca..946941c86 100644 --- a/pkg/probo/oauth2_scopes.go +++ b/pkg/probo/oauth2_scopes.go @@ -34,9 +34,6 @@ const ( ScopeV1CommonThirdPartyRead coredata.OAuth2Scope = "v1:common-third-party:read" ScopeV1CommonThirdParty coredata.OAuth2Scope = "v1:common-third-party" - ScopeV1CompliancePageRead coredata.OAuth2Scope = "v1:compliance-page:read" - ScopeV1CompliancePage coredata.OAuth2Scope = "v1:compliance-page" - ScopeV1ConnectorRead coredata.OAuth2Scope = "v1:connector:read" ScopeV1Connector coredata.OAuth2Scope = "v1:connector" @@ -119,79 +116,6 @@ var OAuth2ScopeMappings = map[coredata.OAuth2Scope][]string{ ActionCommonThirdPartyGet, ActionCommonThirdPartyList, }, - ScopeV1CompliancePageRead: { - ActionTrustCenterGet, - ActionTrustCenterGetNda, - ActionTrustCenterAccessGet, - ActionTrustCenterAccessList, - ActionTrustCenterFileGet, - ActionTrustCenterFileList, - ActionTrustCenterFileGetFileUrl, - ActionTrustCenterReferenceList, - ActionTrustCenterReferenceGetLogoUrl, - ActionTrustCenterDocumentAccessList, - ActionMailingListUpdateList, - ActionMailingListSubscriberList, - ActionComplianceFrameworkList, - ActionComplianceExternalURLList, - ActionCompliancePortalCommitmentGroupList, - ActionCompliancePortalCommitmentList, - ActionCustomDomainGet, - }, - ScopeV1CompliancePage: { - ActionTrustCenterGet, - ActionTrustCenterGetNda, - ActionTrustCenterAccessGet, - ActionTrustCenterAccessList, - ActionTrustCenterFileGet, - ActionTrustCenterFileList, - ActionTrustCenterFileGetFileUrl, - ActionTrustCenterReferenceList, - ActionTrustCenterReferenceGetLogoUrl, - ActionTrustCenterDocumentAccessList, - ActionMailingListUpdateList, - ActionMailingListSubscriberList, - ActionComplianceFrameworkList, - ActionComplianceExternalURLList, - ActionCompliancePortalCommitmentGroupList, - ActionCompliancePortalCommitmentList, - ActionCustomDomainGet, - ActionTrustCenterUpdate, - ActionTrustCenterNonDisclosureAgreementUpload, - ActionTrustCenterNonDisclosureAgreementDelete, - ActionTrustCenterAccessCreate, - ActionTrustCenterAccessUpdate, - ActionTrustCenterAccessDelete, - ActionTrustCenterFileUpdate, - ActionTrustCenterFileDelete, - ActionTrustCenterFileCreate, - ActionTrustCenterReferenceCreate, - ActionTrustCenterReferenceUpdate, - ActionTrustCenterReferenceDelete, - ActionMailingListUpdateCreate, - ActionMailingListUpdateUpdate, - ActionMailingListUpdateSend, - ActionMailingListUpdateDelete, - ActionMailingListUpdate, - ActionMailingListSubscriberCreate, - ActionMailingListSubscriberDelete, - ActionComplianceFrameworkCreate, - ActionComplianceFrameworkDelete, - ActionComplianceFrameworkUpdateRank, - ActionComplianceExternalURLCreate, - ActionComplianceExternalURLUpdate, - ActionComplianceExternalURLDelete, - ActionCompliancePortalCommitmentGroupCreate, - ActionCompliancePortalCommitmentGroupUpdate, - ActionCompliancePortalCommitmentGroupUpdateRank, - ActionCompliancePortalCommitmentGroupDelete, - ActionCompliancePortalCommitmentCreate, - ActionCompliancePortalCommitmentUpdate, - ActionCompliancePortalCommitmentUpdateRank, - ActionCompliancePortalCommitmentDelete, - ActionCustomDomainCreate, - ActionCustomDomainDelete, - }, ScopeV1ConnectorRead: { ActionConnectorList, ActionConnectorGet, diff --git a/pkg/probo/policies.go b/pkg/probo/policies.go index 037d9d84e..bc61e7740 100644 --- a/pkg/probo/policies.go +++ b/pkg/probo/policies.go @@ -102,17 +102,6 @@ var ViewerPolicy = policy.NewPolicy( ActionRiskAssessmentScenarioGet, ActionRiskAssessmentScenarioList, ).WithSID("entity-read-access").When(organizationCondition), - policy.Allow( - ActionTrustCenterGet, - ActionTrustCenterAccessGet, ActionTrustCenterAccessList, - ActionTrustCenterDocumentAccessList, - ActionTrustCenterFileGet, ActionTrustCenterFileList, ActionTrustCenterFileGetFileUrl, - ActionTrustCenterReferenceList, ActionTrustCenterReferenceGetLogoUrl, - ActionCompliancePortalCommitmentGroupList, ActionCompliancePortalCommitmentList, - ActionComplianceFrameworkList, - ).WithSID("trust-center-read-access").When(organizationCondition), - - policy.Allow(ActionCustomDomainGet).WithSID("custom-domain-read").When(organizationCondition), policy.Allow(ActionOrganizationContextGet).WithSID("organization-context-read").When(organizationCondition), policy.Allow( ActionDocumentVersionExportPDF, ActionDocumentVersionSign, diff --git a/pkg/probo/service.go b/pkg/probo/service.go index e91c0463d..78d25c965 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -28,7 +28,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/certmanager" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/crypto/cipher" @@ -83,7 +82,6 @@ type ( llmClient *llm.Client llmConfig LLMConfig html2pdfConverter *html2pdf.Converter - acmeService *certmanager.ACMEService fileManager *filemanager.Service logger *log.Logger slack *slack.Service @@ -110,14 +108,6 @@ type ( Data *DatumService Audits *AuditService WebhookSubscriptions *WebhookSubscriptionService - TrustCenters *TrustCenterService - TrustCenterAccesses *TrustCenterAccessService - TrustCenterReferences *TrustCenterReferenceService - CompliancePortalCommitmentGroups *CompliancePortalCommitmentGroupService - CompliancePortalCommitments *CompliancePortalCommitmentService - TrustCenterFiles *TrustCenterFileService - ComplianceFrameworks *ComplianceFrameworkService - ComplianceExternalURLs *ComplianceExternalURLService Findings *FindingService Obligations *ObligationService RightsRequests *RightsRequestService @@ -127,7 +117,6 @@ type ( StatementsOfApplicability *StatementOfApplicabilityService GeneratedDocuments *GeneratedDocumentService Files *FileService - CustomDomains *CustomDomainService SlackMessages *slack.Service } ) @@ -143,7 +132,6 @@ func NewService( llmClient *llm.Client, llmConfig LLMConfig, html2pdfConverter *html2pdf.Converter, - acmeService *certmanager.ACMEService, fileManagerService *filemanager.Service, logger *log.Logger, slackService *slack.Service, @@ -168,7 +156,6 @@ func NewService( llmClient: llmClient, llmConfig: llmConfig, html2pdfConverter: html2pdfConverter, - acmeService: acmeService, fileManager: fileManagerService, logger: logger, slack: slackService, @@ -234,27 +221,6 @@ func NewService( svc.Data = &DatumService{svc: svc} svc.Audits = &AuditService{svc: svc} svc.WebhookSubscriptions = &WebhookSubscriptionService{svc: svc} - svc.TrustCenters = &TrustCenterService{svc: svc} - svc.TrustCenterAccesses = &TrustCenterAccessService{svc: svc} - svc.TrustCenterReferences = &TrustCenterReferenceService{svc: svc} - svc.CompliancePortalCommitmentGroups = &CompliancePortalCommitmentGroupService{svc: svc} - svc.CompliancePortalCommitments = &CompliancePortalCommitmentService{svc: svc} - svc.ComplianceFrameworks = &ComplianceFrameworkService{svc: svc} - svc.ComplianceExternalURLs = &ComplianceExternalURLService{svc: svc} - svc.TrustCenterFiles = &TrustCenterFileService{ - svc: svc, - fileValidator: filevalidation.NewValidator( - filevalidation.WithCategories( - filevalidation.CategoryData, - filevalidation.CategoryDocument, - filevalidation.CategoryImage, - filevalidation.CategoryPresentation, - filevalidation.CategorySpreadsheet, - filevalidation.CategoryText, - ), - filevalidation.WithMaxFileSize(10*1024*1024), // 10MB - ), - } svc.Findings = &FindingService{svc: svc} svc.Obligations = &ObligationService{svc: svc} svc.RightsRequests = &RightsRequestService{svc: svc} @@ -264,12 +230,6 @@ func NewService( svc.StatementsOfApplicability = &StatementOfApplicabilityService{svc: svc} svc.GeneratedDocuments = &GeneratedDocumentService{svc: svc} svc.Files = &FileService{svc: svc} - svc.CustomDomains = &CustomDomainService{ - svc: svc, - encryptionKey: encryptionKey, - acmeService: acmeService, - logger: logger.Named("custom_domains"), - } svc.SlackMessages = slackService return svc, nil @@ -406,28 +366,3 @@ func (s *Service) commitSuccessfulExport(ctx context.Context, exportJob *coredat }, ) } - -func (s *Service) LoadOrganizationByDomain(ctx context.Context, domain string) (gid.GID, error) { - var organizationID gid.GID - - err := s.pg.WithConn( - ctx, - func(ctx context.Context, conn pg.Querier) error { - var customDomain coredata.CustomDomain - if err := customDomain.LoadByDomain(ctx, conn, coredata.NewNoScope(), domain); err != nil { - return fmt.Errorf("cannot load custom domain: %w", err) - } - - var org coredata.Organization - if err := org.LoadByCustomDomainID(ctx, conn, coredata.NewNoScope(), customDomain.ID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - organizationID = org.ID - - return nil - }, - ) - - return organizationID, err -}