Resolve custom domain SSL via certificate
Replace flattened SSL fields with a certificate relation loaded through certmanager, and resolve domain slots from IDs already on the trust center instead of reloading the compliance page. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -249,31 +249,21 @@ enum TrustCenterFileOrderField
|
||||
}
|
||||
|
||||
enum SSLStatus
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus") {
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CertificateStatus") {
|
||||
PENDING
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusPending"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusPending")
|
||||
PROVISIONING
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusProvisioning"
|
||||
value: "go.probo.inc/probo/pkg/coredata.CertificateStatusProvisioning"
|
||||
)
|
||||
ACTIVE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusActive"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusActive")
|
||||
RENEWING
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusRenewing"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusRenewing")
|
||||
EXPIRED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusExpired"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusExpired")
|
||||
FAILED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusFailed"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusFailed")
|
||||
}
|
||||
|
||||
input TrustCenterAccessOrder
|
||||
@@ -648,9 +638,7 @@ type CustomDomain implements Node {
|
||||
organization: Organization!
|
||||
domain: String!
|
||||
managed: Boolean!
|
||||
sslStatus: SSLStatus!
|
||||
sslExpiresAt: Datetime
|
||||
provisioningError: String
|
||||
certificate: Certificate @goField(forceResolver: true)
|
||||
dnsRecords: [DNSRecordInstruction!]!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
@@ -658,6 +646,13 @@ type CustomDomain implements Node {
|
||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type Certificate {
|
||||
id: ID!
|
||||
status: SSLStatus!
|
||||
expiresAt: Datetime
|
||||
provisioningError: String
|
||||
}
|
||||
|
||||
type DNSRecordInstruction {
|
||||
type: String!
|
||||
name: String!
|
||||
|
||||
@@ -85,26 +85,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// newCustomDomainType loads the domain's certificate (when present) and builds
|
||||
// the GraphQL CustomDomain type with its certificate-backed SSL fields.
|
||||
func (r *Resolver) newCustomDomainType(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
domain *coredata.CustomDomain,
|
||||
) (*types.CustomDomain, error) {
|
||||
var cert *coredata.Certificate
|
||||
if domain != nil && domain.CertificateID != nil {
|
||||
var err error
|
||||
cert, err = r.certManager.Get(ctx, scope, *domain.CertificateID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load certificate", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return types.NewCustomDomain(domain, cert, r.customDomainCname), nil
|
||||
}
|
||||
|
||||
func NewMux(
|
||||
logger *log.Logger,
|
||||
proboSvc *probo.Service,
|
||||
|
||||
@@ -124,6 +124,30 @@ func (r *compliancePortalCommitmentGroupConnectionResolver) TotalCount(ctx conte
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Certificate is the resolver for the certificate field.
|
||||
func (r *customDomainResolver) Certificate(ctx context.Context, obj *types.CustomDomain) (*types.Certificate, error) {
|
||||
if obj.Certificate == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := r.certManager.Get(ctx, scope, obj.Certificate.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot load certificate", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewCertificate(cert), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *customDomainResolver) Permission(ctx context.Context, obj *types.CustomDomain, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
@@ -893,13 +917,8 @@ func (r *mutationResolver) CreateCustomDomain(ctx context.Context, input types.C
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
customDomain, err := r.newCustomDomainType(ctx, scope, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.CreateCustomDomainPayload{
|
||||
CustomDomain: customDomain,
|
||||
CustomDomain: types.NewCustomDomain(domain, r.customDomainCname),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1169,42 +1188,50 @@ func (r *trustCenterResolver) MailingList(ctx context.Context, obj *types.TrustC
|
||||
|
||||
// DefaultDomain is the resolver for the defaultDomain field.
|
||||
func (r *trustCenterResolver) DefaultDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
||||
if obj.DefaultDomain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domain, err := r.management.GetDefaultDomain(ctx, scope, obj.ID)
|
||||
domain, err := r.management.GetDomain(ctx, scope, obj.DefaultDomain.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot load default domain", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
if domain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return r.newCustomDomainType(ctx, scope, domain)
|
||||
return types.NewCustomDomain(domain, r.customDomainCname), nil
|
||||
}
|
||||
|
||||
// CustomDomain is the resolver for the customDomain field.
|
||||
func (r *trustCenterResolver) CustomDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
||||
if obj.CustomDomain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domain, err := r.management.GetCustomDomain(ctx, scope, obj.ID)
|
||||
domain, err := r.management.GetDomain(ctx, scope, obj.CustomDomain.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot load custom domain", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
if domain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return r.newCustomDomainType(ctx, scope, domain)
|
||||
return types.NewCustomDomain(domain, r.customDomainCname), nil
|
||||
}
|
||||
|
||||
// PublicURL is the resolver for the publicUrl field.
|
||||
|
||||
34
pkg/server/api/console/v1/types/certificate.go
Normal file
34
pkg/server/api/console/v1/types/certificate.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func NewCertificate(c *coredata.Certificate) *Certificate {
|
||||
return &Certificate{
|
||||
ID: c.ID,
|
||||
Status: c.Status,
|
||||
ExpiresAt: c.SSLExpiresAt,
|
||||
ProvisioningError: c.ProvisioningError,
|
||||
}
|
||||
}
|
||||
@@ -24,46 +24,34 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// NewCustomDomain builds the GraphQL CustomDomain type. The TLS lifecycle now
|
||||
// lives on the linked certificate; when cert is nil (certificate not yet
|
||||
// created) the domain reports a pending SSL status.
|
||||
func NewCustomDomain(d *coredata.CustomDomain, cert *coredata.Certificate, cnameTarget string) *CustomDomain {
|
||||
result := &CustomDomain{
|
||||
func NewCustomDomain(d *coredata.CustomDomain, cnameTarget string) *CustomDomain {
|
||||
domain := &CustomDomain{
|
||||
ID: d.ID,
|
||||
Organization: &Organization{
|
||||
ID: d.OrganizationID,
|
||||
},
|
||||
Domain: d.Domain,
|
||||
Managed: d.Managed,
|
||||
SslStatus: coredata.CustomDomainSSLStatusPending,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
Domain: d.Domain,
|
||||
Managed: d.Managed,
|
||||
DNSRecords: convertDNSRecords(d, cnameTarget),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
}
|
||||
|
||||
if cert != nil {
|
||||
result.SslStatus = coredata.CustomDomainSSLStatus(cert.Status)
|
||||
result.SslExpiresAt = cert.SSLExpiresAt
|
||||
result.ProvisioningError = cert.ProvisioningError
|
||||
if d.CertificateID != nil {
|
||||
domain.Certificate = &Certificate{ID: *d.CertificateID}
|
||||
}
|
||||
|
||||
// Convert DNS records
|
||||
result.DNSRecords = convertDNSRecords(d, cnameTarget)
|
||||
|
||||
return result
|
||||
return domain
|
||||
}
|
||||
|
||||
func convertDNSRecords(d *coredata.CustomDomain, cnameTarget string) []*DNSRecordInstruction {
|
||||
var records []*DNSRecordInstruction
|
||||
|
||||
// For HTTP-01 challenges, we just need the domain to point to our servers via CNAME
|
||||
record := &DNSRecordInstruction{
|
||||
Type: "CNAME",
|
||||
Name: d.Domain,
|
||||
Value: cnameTarget,
|
||||
TTL: 300,
|
||||
Purpose: "Point domain to Probo servers",
|
||||
return []*DNSRecordInstruction{
|
||||
{
|
||||
Type: "CNAME",
|
||||
Name: d.Domain,
|
||||
Value: cnameTarget,
|
||||
TTL: 300,
|
||||
Purpose: "Point domain to Probo servers",
|
||||
},
|
||||
}
|
||||
records = append(records, record)
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ type TrustCenter struct {
|
||||
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
|
||||
CustomLinks *ComplianceCustomLinkConnection `json:"customLinks"`
|
||||
MailingList *MailingList `json:"mailingList,omitempty"`
|
||||
DefaultDomain *CustomDomain `json:"defaultDomain,omitempty"`
|
||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
||||
Permission bool `json:"permission"`
|
||||
}
|
||||
|
||||
@@ -82,5 +84,13 @@ func NewTrustCenter(tc *coredata.TrustCenter) *TrustCenter {
|
||||
trustCenter.Nda = &File{ID: *tc.NonDisclosureAgreementFileID}
|
||||
}
|
||||
|
||||
if tc.DefaultDomainID != nil {
|
||||
trustCenter.DefaultDomain = &CustomDomain{ID: *tc.DefaultDomainID}
|
||||
}
|
||||
|
||||
if tc.CustomDomainID != nil {
|
||||
trustCenter.CustomDomain = &CustomDomain{ID: *tc.CustomDomainID}
|
||||
}
|
||||
|
||||
return trustCenter
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user