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), }