From a9a126899e4fdbc2bef57fa961894c3a2ef816d1 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 10 Jul 2026 15:13:46 +0200 Subject: [PATCH] Rewire IAM, mailman, and probod for the portal Wire the certificate manager and trust center base domain into IAM so organization creation provisions a managed default domain and certificate atomically. Email presenters in IAM and mailman resolve public URLs through the compliance portal resolver and read profile fields from the trust center. probod initializes the certmanager service and injects the new management and visitor services. Signed-off-by: Bryan Frimin --- pkg/iam/compliance_page_service.go | 48 ++---- pkg/iam/service.go | 9 + pkg/mailman/compliance_mailing_list.go | 62 +++---- pkg/mailman/service.go | 37 ++++- pkg/probod/probod.go | 218 +++++++++++++++---------- pkg/server/api/api.go | 11 +- pkg/server/server.go | 57 ++----- 7 files changed, 234 insertions(+), 208 deletions(-) diff --git a/pkg/iam/compliance_page_service.go b/pkg/iam/compliance_page_service.go index 306f4f55f..0b78a021f 100644 --- a/pkg/iam/compliance_page_service.go +++ b/pkg/iam/compliance_page_service.go @@ -22,14 +22,13 @@ package iam 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/resolver" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" ) @@ -96,7 +95,7 @@ func (s *CompliancePageService) EmailPresenterConfig(ctx context.Context, compli var ( compliancePage = &coredata.TrustCenter{} organization = &coredata.Organization{} - customDomain *coredata.CustomDomain + compliancePageURL string logoFile = &coredata.File{} emailPresenterCfg = emails.DefaultPresenterConfig(s.baseURL) ) @@ -120,13 +119,19 @@ func (s *CompliancePageService) EmailPresenterConfig(ctx context.Context, compli 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 := resolver.PublicURLForTrustCenter( + ctx, + conn, + scope, + compliancePage, + s.trustCenterBaseDomain, + ) + if err != nil { + return fmt.Errorf("cannot resolve compliance page URL: %w", err) } + compliancePageURL = publicURL + return nil }, ) @@ -134,24 +139,7 @@ func (s *CompliancePageService) EmailPresenterConfig(ctx context.Context, compli return emailPresenterCfg, err } - parsedBaseURL, err := url.Parse(s.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 == "" { @@ -162,12 +150,12 @@ func (s *CompliancePageService) EmailPresenterConfig(ctx context.Context, compli 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 } } diff --git a/pkg/iam/service.go b/pkg/iam/service.go index 54afd9b8d..f3428add3 100644 --- a/pkg/iam/service.go +++ b/pkg/iam/service.go @@ -33,6 +33,7 @@ import ( "go.gearno.de/kit/pg" "go.opentelemetry.io/otel/trace" "go.probo.inc/probo/pkg/baseurl" + "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" @@ -61,6 +62,9 @@ type ( magicLinkTokenValidity time.Duration sessionDuration time.Duration bucket string + encryptionKey cipher.EncryptionKey + trustCenterBaseDomain string + certManager *certmanager.Service certificate *x509.Certificate privateKey *rsa.PrivateKey logger *log.Logger @@ -90,6 +94,8 @@ type ( Bucket string TokenSecret string BaseURL *baseurl.BaseURL + TrustCenterBaseDomain string + CertManager *certmanager.Service EncryptionKey cipher.EncryptionKey Certificate *x509.Certificate PrivateKey *rsa.PrivateKey @@ -158,6 +164,9 @@ func NewService( magicLinkTokenValidity: cfg.MagicLinkTokenValidity, sessionDuration: cfg.SessionDuration, bucket: cfg.Bucket, + encryptionKey: cfg.EncryptionKey, + trustCenterBaseDomain: cfg.TrustCenterBaseDomain, + certManager: cfg.CertManager, certificate: cfg.Certificate, privateKey: cfg.PrivateKey, logger: cfg.Logger, diff --git a/pkg/mailman/compliance_mailing_list.go b/pkg/mailman/compliance_mailing_list.go index f4ef064ea..268e6bf22 100644 --- a/pkg/mailman/compliance_mailing_list.go +++ b/pkg/mailman/compliance_mailing_list.go @@ -29,6 +29,7 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/baseurl" + "go.probo.inc/probo/pkg/complianceportal/resolver" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/mail" @@ -62,12 +63,12 @@ func (s *Service) mailingListEmailConfig( mailingListID gid.GID, ) (emails.PresenterConfig, string, string, *mail.Addr, error) { var ( - mailingList = &coredata.MailingList{} - compliancePage = &coredata.TrustCenter{} - organization = &coredata.Organization{} - customDomain *coredata.CustomDomain - logoFile = &coredata.File{} - defaultCfg = emails.DefaultPresenterConfig(s.apiBaseURL.String()) + mailingList = &coredata.MailingList{} + compliancePage = &coredata.TrustCenter{} + organization = &coredata.Organization{} + compliancePageURL string + logoFile = &coredata.File{} + defaultCfg = emails.DefaultPresenterConfig(s.apiBaseURL.String()) ) scope := coredata.NewScopeFromObjectID(mailingListID) @@ -97,13 +98,19 @@ func (s *Service) mailingListEmailConfig( 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 := resolver.PublicURLForTrustCenter( + ctx, + conn, + scope, + compliancePage, + s.trustCenterBaseDomain, + ) + if err != nil { + return fmt.Errorf("cannot resolve compliance page URL: %w", err) } + compliancePageURL = publicURL + return nil }, ) @@ -111,7 +118,7 @@ func (s *Service) mailingListEmailConfig( return defaultCfg, "", "", nil, err } - cfg, compliancePageURL, err := s.presenterConfigFromTrustCenter(compliancePage, organization, customDomain, logoFile) + cfg, err := s.presenterConfigFromTrustCenter(compliancePage, organization, compliancePageURL, logoFile) if err != nil { return defaultCfg, "", "", nil, err } @@ -132,41 +139,24 @@ func (s *Service) mailingListEmailConfig( func (s *Service) presenterConfigFromTrustCenter( compliancePage *coredata.TrustCenter, organization *coredata.Organization, - customDomain *coredata.CustomDomain, + compliancePageURL string, logoFile *coredata.File, -) (emails.PresenterConfig, string, error) { +) (emails.PresenterConfig, error) { cfg := emails.DefaultPresenterConfig(s.apiBaseURL.String()) - - compliancePageBase := s.apiBaseURL.WithPath("/trust/" + compliancePage.ID.String()) - - if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive { - customBase, err := baseurl.Parse("https://" + customDomain.Domain) - if err != nil { - return cfg, "", fmt.Errorf("cannot parse custom domain URL: %w", err) - } - - compliancePageBase = customBase.WithPath("") - } - - compliancePageURL, err := compliancePageBase.String() - if err != nil { - return cfg, "", fmt.Errorf("cannot build compliance page URL: %w", err) - } - cfg.BaseURL = compliancePageURL if compliancePage.LogoFileID != nil && logoFile != nil && logoFile.FileKey != "" { cfg.SenderCompanyLogoPath = filepath.Join("/api/files/v1/public/", logoFile.ID.String()) cfg.SenderCompanyName = organization.Name - if organization.WebsiteURL != nil { - cfg.SenderCompanyWebsiteURL = *organization.WebsiteURL + if compliancePage.WebsiteURL != nil { + cfg.SenderCompanyWebsiteURL = *compliancePage.WebsiteURL } - if organization.HeadquarterAddress != nil { - cfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress + if compliancePage.HeadquarterAddress != nil { + cfg.SenderCompanyHeadquarterAddress = *compliancePage.HeadquarterAddress } } - return cfg, compliancePageURL, nil + return cfg, nil } diff --git a/pkg/mailman/service.go b/pkg/mailman/service.go index bbb30e3ce..eb2f2600a 100644 --- a/pkg/mailman/service.go +++ b/pkg/mailman/service.go @@ -51,17 +51,36 @@ const ( ) type Service struct { - pg *pg.Client - fm *filemanager.Service - tokenSecret string - apiBaseURL *baseurl.BaseURL - bucket string - encryptionKey cipher.EncryptionKey - logger *log.Logger + pg *pg.Client + fm *filemanager.Service + tokenSecret string + apiBaseURL *baseurl.BaseURL + trustCenterBaseDomain string + bucket string + encryptionKey cipher.EncryptionKey + logger *log.Logger } -func NewService(pgClient *pg.Client, fm *filemanager.Service, tokenSecret string, apiBaseURL *baseurl.BaseURL, bucket string, encryptionKey cipher.EncryptionKey, logger *log.Logger) *Service { - return &Service{pg: pgClient, fm: fm, tokenSecret: tokenSecret, apiBaseURL: apiBaseURL, bucket: bucket, encryptionKey: encryptionKey, logger: logger} +func NewService( + pgClient *pg.Client, + fm *filemanager.Service, + tokenSecret string, + apiBaseURL *baseurl.BaseURL, + trustCenterBaseDomain string, + bucket string, + encryptionKey cipher.EncryptionKey, + logger *log.Logger, +) *Service { + return &Service{ + pg: pgClient, + fm: fm, + tokenSecret: tokenSecret, + apiBaseURL: apiBaseURL, + trustCenterBaseDomain: trustCenterBaseDomain, + bucket: bucket, + encryptionKey: encryptionKey, + logger: logger, + } } type ( diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 6634d9469..d4ef82d35 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -52,6 +52,9 @@ import ( "go.probo.inc/probo/pkg/awsconfig" "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/certmanager" + "go.probo.inc/probo/pkg/complianceportal" + "go.probo.inc/probo/pkg/complianceportal/management" + trust "go.probo.inc/probo/pkg/complianceportal/visitor" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector/provider" "go.probo.inc/probo/pkg/cookiebanner" @@ -80,7 +83,6 @@ import ( "go.probo.inc/probo/pkg/server/trustedproxy" "go.probo.inc/probo/pkg/slack" "go.probo.inc/probo/pkg/thirdparty" - "go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/webhook" "golang.org/x/sync/errgroup" ) @@ -144,8 +146,9 @@ func New() *Implm { }, }, TrustCenter: TrustCenterConfig{ - HTTPAddr: ":80", - HTTPSAddr: ":443", + HTTPAddr: ":80", + HTTPSAddr: ":443", + BaseDomain: "probopage.com", }, AWS: AWSConfig{ Region: "us-east-1", @@ -491,54 +494,11 @@ func (impl *Implm) Run( oauth2ScopeRegistry := oauth2scope.NewRegistry(). Register(iam.IAMOAuth2ScopeMappings). Register(probo.OAuth2ScopeMappings). + Register(complianceportal.OAuth2ScopeMappings). Register(agentrun.OAuth2ScopeMappings). Register(accessreview.OAuth2ScopeMappings). Register(resourcealias.OAuth2ScopeMappings) - iamService, err := iam.NewService( - ctx, - pgClient, - fileManagerService, - hp, - iam.Config{ - DisableSignup: impl.cfg.Auth.DisableSignup, - InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second, - PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second, - MagicLinkTokenValidity: time.Duration(impl.cfg.Auth.MagicLinkTokenValidity) * time.Second, - SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, - Bucket: impl.cfg.AWS.Bucket, - TokenSecret: impl.cfg.Auth.Cookie.Secret, - BaseURL: baseURL, - EncryptionKey: encryptionKey, - Certificate: samlCert, - PrivateKey: samlKey, - Logger: l.Named("iam"), - TracerProvider: tp, - Registerer: r, - ConnectorRegistry: defaultConnectorRegistry, - DomainVerificationInterval: impl.cfg.Auth.SAML.DomainVerificationInterval(), - DomainVerificationResolverAddr: impl.cfg.Auth.SAML.DomainVerificationResolverAddr, - SCIMBridgeSyncInterval: time.Duration(impl.cfg.SCIMBridge.SyncInterval) * time.Second, - SCIMBridgePollInterval: time.Duration(impl.cfg.SCIMBridge.PollInterval) * time.Second, - GoogleOIDC: oidc.ProviderConfig{ - ClientID: impl.cfg.Auth.Google.ClientID, - ClientSecret: impl.cfg.Auth.Google.ClientSecret, - Enabled: impl.cfg.Auth.Google.Enabled, - }, - MicrosoftOIDC: oidc.ProviderConfig{ - ClientID: impl.cfg.Auth.Microsoft.ClientID, - ClientSecret: impl.cfg.Auth.Microsoft.ClientSecret, - Enabled: impl.cfg.Auth.Microsoft.Enabled, - }, - OAuth2ServerSigningKeys: oauth2SigningKeys, - OAuth2ServerOptions: oauth2ServerOptions(impl.cfg.Auth.OAuth2Server), - OAuth2ScopeRegistry: oauth2ScopeRegistry, - }, - ) - if err != nil { - return fmt.Errorf("cannot create iam service: %w", err) - } - var accountKey crypto.Signer if impl.cfg.CustomDomains.ACME.AccountKey != "" { accountKey, err = pemutil.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey)) @@ -569,6 +529,77 @@ func (impl *Implm) Run( return fmt.Errorf("cannot initialize ACME service: %w", err) } + customDomainRenewalInterval := time.Duration(impl.cfg.CustomDomains.RenewalInterval) * time.Second + if customDomainRenewalInterval == 0 { + customDomainRenewalInterval = time.Hour + } + + customDomainProvisionInterval := time.Duration(impl.cfg.CustomDomains.ProvisionInterval) * time.Second + if customDomainProvisionInterval == 0 { + customDomainProvisionInterval = 30 * time.Second + } + + certManagerService := certmanager.NewService( + pgClient, + acmeService, + encryptionKey, + certmanager.Config{ + CnameTarget: impl.cfg.CustomDomains.CnameTarget, + CAAIssuerDomain: impl.cfg.CustomDomains.CAAIssuerDomain, + ResolverAddr: impl.cfg.CustomDomains.ResolverAddr, + ManagedBaseDomain: impl.cfg.TrustCenter.BaseDomain, + RenewalInterval: customDomainRenewalInterval, + ProvisionInterval: customDomainProvisionInterval, + }, + l.Named("certmanager"), + ) + + iamService, err := iam.NewService( + ctx, + pgClient, + fileManagerService, + hp, + iam.Config{ + DisableSignup: impl.cfg.Auth.DisableSignup, + InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second, + PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second, + MagicLinkTokenValidity: time.Duration(impl.cfg.Auth.MagicLinkTokenValidity) * time.Second, + SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, + Bucket: impl.cfg.AWS.Bucket, + TokenSecret: impl.cfg.Auth.Cookie.Secret, + BaseURL: baseURL, + TrustCenterBaseDomain: impl.cfg.TrustCenter.BaseDomain, + EncryptionKey: encryptionKey, + Certificate: samlCert, + PrivateKey: samlKey, + Logger: l.Named("iam"), + TracerProvider: tp, + Registerer: r, + ConnectorRegistry: defaultConnectorRegistry, + DomainVerificationInterval: impl.cfg.Auth.SAML.DomainVerificationInterval(), + DomainVerificationResolverAddr: impl.cfg.Auth.SAML.DomainVerificationResolverAddr, + SCIMBridgeSyncInterval: time.Duration(impl.cfg.SCIMBridge.SyncInterval) * time.Second, + SCIMBridgePollInterval: time.Duration(impl.cfg.SCIMBridge.PollInterval) * time.Second, + GoogleOIDC: oidc.ProviderConfig{ + ClientID: impl.cfg.Auth.Google.ClientID, + ClientSecret: impl.cfg.Auth.Google.ClientSecret, + Enabled: impl.cfg.Auth.Google.Enabled, + }, + MicrosoftOIDC: oidc.ProviderConfig{ + ClientID: impl.cfg.Auth.Microsoft.ClientID, + ClientSecret: impl.cfg.Auth.Microsoft.ClientSecret, + Enabled: impl.cfg.Auth.Microsoft.Enabled, + }, + OAuth2ServerSigningKeys: oauth2SigningKeys, + OAuth2ServerOptions: oauth2ServerOptions(impl.cfg.Auth.OAuth2Server), + OAuth2ScopeRegistry: oauth2ScopeRegistry, + CertManager: certManagerService, + }, + ) + if err != nil { + return fmt.Errorf("cannot create iam service: %w", err) + } + slackService := slack.NewService( pgClient, impl.cfg.GetSlackSigningSecret(), @@ -586,7 +617,16 @@ func (impl *Implm) Run( l.Named("esign"), ) - mailmanService := mailman.NewService(pgClient, fileManagerService, impl.cfg.Auth.Cookie.Secret, baseURL, impl.cfg.AWS.Bucket, encryptionKey, l) + mailmanService := mailman.NewService( + pgClient, + fileManagerService, + impl.cfg.Auth.Cookie.Secret, + baseURL, + impl.cfg.TrustCenter.BaseDomain, + impl.cfg.AWS.Bucket, + encryptionKey, + l, + ) cookieBannerService := cookiebanner.NewService(pgClient, impl.cfg.Branding) @@ -605,7 +645,6 @@ func (impl *Implm) Run( MaxTokens: ref.UnrefOrZero(proboAgentCfg.MaxTokens), }, html2pdfConverter, - acmeService, fileManagerService, l.Named("probo"), slackService, @@ -620,11 +659,24 @@ func (impl *Implm) Run( resourceAliasService := resourcealias.NewService(pgClient) + managementService := management.NewService( + pgClient, + s3Client, + impl.cfg.AWS.Bucket, + baseURL.String(), + impl.cfg.TrustCenter.BaseDomain, + fileManagerService, + certManagerService, + slackService, + l.Named("compliance-portal-management"), + ) + trustService := trust.NewService( pgClient, s3Client, impl.cfg.AWS.Bucket, baseURL.String(), + impl.cfg.TrustCenter.BaseDomain, impl.cfg.GetSlackSigningSecret(), iamService, esignService, @@ -648,6 +700,7 @@ func (impl *Implm) Run( iamService.Authorizer.RegisterPolicySet(agentrun.PolicySet()) iamService.Authorizer.RegisterPolicySet(accessreview.PolicySet()) iamService.Authorizer.RegisterPolicySet(resourcealias.PolicySet()) + iamService.Authorizer.RegisterPolicySet(complianceportal.PolicySet()) thirdPartyService := thirdparty.NewService(pgClient, fileManagerService, thirdPartyVetter) riskManagementService := riskmanagement.NewService(pgClient) @@ -662,6 +715,7 @@ func (impl *Implm) Run( IAM: iamService, Trust: trustService, ESign: esignService, + CustomDomain: managementService, AccessReview: accessReviewService, AgentRun: agentRunService, Mailman: mailmanService, @@ -679,7 +733,6 @@ func (impl *Implm) Run( QueryCacheSize: impl.cfg.Api.GraphQL.QueryCacheSize, DisableSuggestion: impl.cfg.Api.GraphQL.DisableSuggestion, }, - CustomDomainCname: impl.cfg.CustomDomains.CnameTarget, TokenSecret: impl.cfg.Auth.Cookie.Secret, Logger: l.Named("http.server"), @@ -856,12 +909,22 @@ func (impl *Implm) Run( wg.Go( func() { - if err := esignService.Run(esignServiceCtx, trustService.EmailPresenterConfigByOrganizationID); err != nil { + if err := esignService.Run(esignServiceCtx, trustService.GetPortalEmailPresenterConfigByOrganizationID); err != nil { cancel(fmt.Errorf("esign service crashed: %w", err)) } }, ) + certManagerServiceCtx, stopCertManagerService := context.WithCancel(context.Background()) + + wg.Go( + func() { + if err := certManagerService.Run(certManagerServiceCtx); err != nil { + cancel(fmt.Errorf("certificate manager service crashed: %w", err)) + } + }, + ) + trackerPatternAnalysisWorker := cookiebanner.NewPatternAnalysisWorker(cookieBannerService, pgClient, l) trackerPatternAnalysisWorkerCtx, stopTrackerPatternAnalysisWorker := context.WithCancel(context.Background()) @@ -1033,8 +1096,7 @@ func (impl *Implm) Run( tp, pgClient, serverHandler.TrustCenterHandler(), - acmeService, - proboService, + trustService, encryptionKey, ); err != nil { cancel(fmt.Errorf("trust center server crashed: %w", err)) @@ -1048,6 +1110,7 @@ func (impl *Implm) Run( stopTrustCenterServer() stopWebhookWorker() stopESignService() + stopCertManagerService() stopTrackerPatternAnalysisWorker() stopTrackerPolicyWorker() stopTrackerMappingWorker() @@ -1186,7 +1249,7 @@ func (impl *Implm) runApiServer( return ctx.Err() } -func newTrustCenterHTTPRedirectHandler(proboService *probo.Service, l *log.Logger) http.Handler { +func newTrustCenterHTTPRedirectHandler(trustService *trust.Service, l *log.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -1202,11 +1265,15 @@ func newTrustCenterHTTPRedirectHandler(proboService *probo.Service, l *log.Logge return } - // Check if this domain is a trust center domain - _, err := proboService.LoadOrganizationByDomain(ctx, domain) - if err != nil { - // Not a trust center domain, return 404 - httpserver.RenderError(w, http.StatusNotFound, errors.New("not found")) + // Check if this domain is a trust center custom domain + if _, err := trustService.GetPortalByDomainName(ctx, domain); err != nil { + if errors.Is(err, trust.ErrPageNotFound) || errors.Is(err, coredata.ErrResourceNotFound) { + // Not a trust center domain, return 404 + httpserver.RenderError(w, http.StatusNotFound, errors.New("not found")) + return + } + + httpserver.RenderError(w, http.StatusInternalServerError, errors.New("internal server error")) return } @@ -1236,8 +1303,7 @@ func (impl *Implm) runTrustCenterServer( tp trace.TracerProvider, pgClient *pg.Client, trustRouter http.Handler, - acmeService *certmanager.ACMEService, - proboService *probo.Service, + trustService *trust.Service, encryptionKey cipher.EncryptionKey, ) error { tracer := tp.Tracer("go.probo.inc/probo/pkg/probod") @@ -1253,45 +1319,17 @@ func (impl *Implm) runTrustCenterServer( l.ErrorCtx(ctx, "cannot warm certificate cache", log.Error(err)) } - renewalInterval := time.Duration(impl.cfg.CustomDomains.RenewalInterval) * time.Second - if renewalInterval == 0 { - renewalInterval = time.Hour - } - - renewer := certmanager.NewRenewer(pgClient, acmeService, encryptionKey, renewalInterval, l) - - certProvisioningInterval := time.Duration(impl.cfg.CustomDomains.ProvisionInterval) * time.Second - if certProvisioningInterval == 0 { - certProvisioningInterval = 30 * time.Second - } - - certProvisioner := certmanager.NewProvisioner(pgClient, acmeService, encryptionKey, impl.cfg.CustomDomains.CnameTarget, impl.cfg.CustomDomains.CAAIssuerDomain, certProvisioningInterval, impl.cfg.CustomDomains.ResolverAddr, l) - g, ctx := errgroup.WithContext(ctx) l.Info("starting trust center services") span.AddEvent("Trust center services starting") - g.Go( - func() error { - l.Info("starting certificate renewer") - return renewer.Run(ctx) - }, - ) - - g.Go( - func() error { - l.Info("starting certificate provisioner") - return certProvisioner.Run(ctx) - }, - ) - httpACMEHandler := certmanager.NewACMEChallengeHandler( pgClient, l.Named("http_acme_handler"), ) - httpRedirectHandler := newTrustCenterHTTPRedirectHandler(proboService, l.Named("http_redirect")) + httpRedirectHandler := newTrustCenterHTTPRedirectHandler(trustService, l.Named("http_redirect")) httpServer := httpserver.NewServer( impl.cfg.TrustCenter.HTTPAddr, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index c66b3a910..9efff5709 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -34,6 +34,8 @@ import ( "go.probo.inc/probo/pkg/accessreview" "go.probo.inc/probo/pkg/agentrun" "go.probo.inc/probo/pkg/baseurl" + "go.probo.inc/probo/pkg/complianceportal/management" + trust "go.probo.inc/probo/pkg/complianceportal/visitor" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector/provider" "go.probo.inc/probo/pkg/cookiebanner" @@ -56,7 +58,6 @@ import ( "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/slack" "go.probo.inc/probo/pkg/thirdparty" - "go.probo.inc/probo/pkg/trust" ) type ( @@ -69,6 +70,7 @@ type ( IAM *iam.Service Trust *trust.Service ESign *esign.Service + CustomDomain *management.Service AccessReview *accessreview.Service AgentRun *agentrun.Service Slack *slack.Service @@ -204,6 +206,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.ResourceAlias, cfg.IAM, cfg.ESign, + cfg.CustomDomain, cfg.AccessReview, cfg.AgentRun, cfg.Mailman, @@ -236,6 +239,7 @@ func NewServer(cfg Config) (*Server, error) { mcpHandler: mcp_v1.NewMux( cfg.Logger.Named("mcp.v1"), cfg.Probo, + cfg.CustomDomain, cfg.ResourceAlias, cfg.ThirdParty, cfg.IAM, @@ -263,12 +267,12 @@ func NewServer(cfg Config) (*Server, error) { return true } - _, err := cfg.Trust.GetByDomainName(ctx, host) + _, err := cfg.Trust.GetPortalByDomainName(ctx, host) return err == nil }, func(ctx context.Context, host string) bool { - _, err := cfg.Trust.GetByDomainName(ctx, host) + _, err := cfg.Trust.GetPortalByDomainName(ctx, host) return err == nil }, cfg.GraphQLLimits, @@ -314,7 +318,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Mount("/console/v1", http.StripPrefix("/console/v1", s.consoleHandler)) r.Mount("/connect/v1", http.StripPrefix("/connect/v1", s.connectHandler)) r.Mount("/files/v1", http.StripPrefix("/files/v1", s.filesHandler)) - r.Mount("/trust/v1", http.StripPrefix("/trust/v1", s.compliancePageHandler)) r.Mount("/mcp/v1", http.StripPrefix("/mcp/v1", s.mcpHandler)) r.Mount("/slack/v1", http.StripPrefix("/slack/v1", s.slackHandler)) }) diff --git a/pkg/server/server.go b/pkg/server/server.go index d5657c56f..5306dc1dd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -23,8 +23,6 @@ package server import ( "errors" "net/http" - "path" - "strings" "github.com/go-chi/chi/v5" "go.gearno.de/kit/httpserver" @@ -33,6 +31,8 @@ import ( "go.probo.inc/probo/pkg/accessreview" "go.probo.inc/probo/pkg/agentrun" "go.probo.inc/probo/pkg/baseurl" + "go.probo.inc/probo/pkg/complianceportal/management" + trust "go.probo.inc/probo/pkg/complianceportal/visitor" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/connector/provider" "go.probo.inc/probo/pkg/cookiebanner" @@ -47,14 +47,13 @@ import ( "go.probo.inc/probo/pkg/riskmanagement" "go.probo.inc/probo/pkg/securecookie" "go.probo.inc/probo/pkg/server/api" - "go.probo.inc/probo/pkg/server/api/compliancepage" + "go.probo.inc/probo/pkg/server/api/complianceportal" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/mailactions" trust_web "go.probo.inc/probo/pkg/server/trust" console_web "go.probo.inc/probo/pkg/server/web" "go.probo.inc/probo/pkg/slack" "go.probo.inc/probo/pkg/thirdparty" - "go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/uri" ) @@ -68,6 +67,7 @@ type Config struct { IAM *iam.Service Trust *trust.Service ESign *esign.Service + CustomDomain *management.Service AccessReview *accessreview.Service AgentRun *agentrun.Service Slack *slack.Service @@ -109,6 +109,7 @@ func NewServer(cfg Config) (*Server, error) { IAM: cfg.IAM, Trust: cfg.Trust, ESign: cfg.ESign, + CustomDomain: cfg.CustomDomain, AccessReview: cfg.AccessReview, AgentRun: cfg.AgentRun, Slack: cfg.Slack, @@ -157,12 +158,12 @@ func NewServer(cfg Config) (*Server, error) { logger: cfg.Logger, } - server.setupRoutes(cfg.BaseURL.String()) + server.setupRoutes() return server, nil } -func (s *Server) setupRoutes(baseURL string) { +func (s *Server) setupRoutes() { // OIDC Discovery 1.0 §4 and RFC 8414 §3 both require the metadata // document at the issuer root under well-known paths. s.router.Get("/.well-known/openid-configuration", s.oidcDiscoveryHandler) @@ -172,12 +173,6 @@ func (s *Server) setupRoutes(baseURL string) { s.router.Mount("/api", http.StripPrefix("/api", s.apiServer)) s.router.Mount("/mail-actions", http.StripPrefix("/mail-actions", s.mailActionsHandler)) - s.router.Route("/trust/{slugOrId}", func(r chi.Router) { - r.Use(compliancepage.NewIDMiddleware(s.trustService, baseURL)) - r.Use(s.stripTrustPrefix) - r.Mount("/", s.trustCenterRouter()) - }) - s.router.Mount("/", s.consoleWebServer) } @@ -224,31 +219,10 @@ func (s *Server) handleCustomDomain404(w http.ResponseWriter, r *http.Request) { httpserver.RenderError(w, http.StatusNotFound, errors.New("not found")) } -func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - slugOrId := chi.URLParam(r, "slugOrId") - prefix := "/trust/" + slugOrId - - if r.URL.Path == prefix { - cleanPath := path.Clean(prefix) + "/" - http.Redirect(w, r, cleanPath, http.StatusMovedPermanently) - - return - } - - r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) - if r.URL.Path == "" { - r.URL.Path = "/" - } - - next.ServeHTTP(w, r) - }) -} - func (s *Server) trustCenterRouter() chi.Router { r := chi.NewRouter() - h := compliancepage.NewHandler(s.trustService) + h := complianceportal.NewHandler(s.trustService) r.Mount("/api/trust/v1", s.apiServer.CompliancePageHandler()) r.Get("/llms.txt", h.HandleLLMsTxt) @@ -262,7 +236,7 @@ func (s *Server) trustCenterRouter() chi.Router { func (s *Server) TrustCenterHandler() http.Handler { r := chi.NewRouter() - r.Use(compliancepage.NewSNIMiddleware(s.trustService)) + r.Use(complianceportal.NewSNIMiddleware(s.trustService)) r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") @@ -280,21 +254,26 @@ func (s *Server) TrustCenterHandler() http.Handler { func compliancePageHeadData(baseURL *baseurl.BaseURL, trustService *trust.Service) trust_web.HeadDataFunc { return func(r *http.Request) trust_web.HeadData { - tc := compliancepage.CompliancePageFromContext(r.Context()) + tc := complianceportal.CompliancePageFromContext(r.Context()) if tc == nil { return trust_web.HeadData{Title: "Compliance Page"} } - org, err := trustService.GetOrganizationByTrustCenterID(r.Context(), tc.ID) + org, err := trustService.GetPortalOrganization(r.Context(), tc.ID) if err != nil || org == nil { return trust_web.HeadData{Title: "Compliance Page"} } - compliancePageBaseURL := compliancepage.CompliancePageBaseURLFromContext(r.Context()) + compliancePageBaseURL := complianceportal.CompliancePageBaseURLFromContext(r.Context()) + + description := org.Name + " Compliance Page" + if tc.Description != nil && *tc.Description != "" { + description = *tc.Description + } headData := trust_web.HeadData{ Title: org.Name + " — Compliance", - Description: org.Name + " Compliance Page", + Description: description, OGURL: ref.UnrefOrZero(compliancePageBaseURL), }