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 <bryan@probo.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user