Address remaining compliance portal review nits

Fill in certificate renewal processing, preserve OAuth and JWKS
edge cases, embed the compliance-portal app in production builds,
and close the smaller portal routing and n8n update gaps.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-17 12:29:42 +02:00
parent bc78f08334
commit 6da00604ed
10 changed files with 127 additions and 23 deletions

View File

@@ -329,9 +329,9 @@ pkg/server/api/complianceportal/v1/schema.graphql: pkg/server/api/complianceport
$(NPM) --workspace $@ run check
$(NPM) --workspace $@ run build
.PHONY: @probo/trust
@probo/trust: NODE_ENV=production
@probo/trust: relay
.PHONY: @probo/compliance-portal
@probo/compliance-portal: NODE_ENV=production
@probo/compliance-portal: relay
$(NPM) --workspace $@ run check
$(NPM) --workspace $@ run build

View File

@@ -176,14 +176,16 @@ export async function execute(
}
`;
const input: Record<string, unknown> = { trustCenterId };
const input: Record<string, unknown> = {
trustCenterId,
title,
description,
websiteUrl,
email,
headquarterAddress,
};
if (active !== undefined) input.active = active;
if (searchEngineIndexing) input.searchEngineIndexing = searchEngineIndexing;
if (description) input.description = description;
if (websiteUrl) input.websiteUrl = websiteUrl;
if (email) input.email = email;
if (headquarterAddress) input.headquarterAddress = headquarterAddress;
if (title) input.title = title;
const responseData = await proboApiRequest.call(this, query, { input });

View File

@@ -91,8 +91,38 @@ func (h *renewHandler) Claim(ctx context.Context) (coredata.Certificate, error)
return certificate, nil
}
func (h *renewHandler) Process(_ context.Context, _ coredata.Certificate) error {
return nil
func (h *renewHandler) Process(ctx context.Context, certificate coredata.Certificate) error {
return h.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
fullCertificate := &coredata.Certificate{}
if err := fullCertificate.LoadByIDForUpdateSkipLocked(ctx, tx, coredata.NewNoScope(), certificate.ID); err != nil {
return fmt.Errorf("cannot load certificate for renewal: %w", err)
}
if fullCertificate.Status != coredata.CertificateStatusRenewing {
return nil
}
fullCertificate.HTTPChallengeToken = nil
fullCertificate.HTTPChallengeKeyAuth = nil
fullCertificate.HTTPChallengeURL = nil
fullCertificate.HTTPOrderURL = nil
fullCertificate.ProvisioningError = nil
if err := fullCertificate.Update(ctx, tx, coredata.NewNoScope()); err != nil {
return fmt.Errorf("cannot prepare certificate for renewal: %w", err)
}
h.logger.InfoCtx(
ctx,
"prepared certificate for renewal",
log.String("hostname", fullCertificate.Hostname),
)
return nil
},
)
}
func (h *renewHandler) RecoverStale(ctx context.Context) error {

View File

@@ -78,8 +78,6 @@ func PublicURLForTrustCenter(
host = byID[*trustCenter.CustomDomainID].Domain
case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil:
host = byID[*trustCenter.DefaultDomainID].Domain
case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil:
host = byID[*trustCenter.CustomDomainID].Domain
}
if host == "" {

View File

@@ -389,7 +389,10 @@ WITH combined AS (
COALESCE(cf.organization_id, tc.organization_id) AS organization_id,
COALESCE(cf.trust_center_id, tc.id) AS trust_center_id,
f.id AS framework_id,
ROW_NUMBER() OVER (ORDER BY f.created_at, f.id) AS rank,
CASE
WHEN cf.id IS NOT NULL THEN cf.rank
ELSE ROW_NUMBER() OVER (ORDER BY f.created_at, f.id)
END AS rank,
CASE WHEN cf.id IS NULL THEN 'NONE' ELSE 'PUBLIC' END AS visibility,
COALESCE(cf.created_at, f.created_at) AS created_at,
COALESCE(cf.updated_at, f.updated_at) AS updated_at

View File

@@ -121,14 +121,23 @@ func RSAPublicKeyFromJWK(jwk JWK) (*rsa.PublicKey, error) {
return nil, fmt.Errorf("cannot decode rsa exponent: %w", err)
}
e := new(big.Int).SetBytes(eBytes)
if !e.IsInt64() || e.Sign() <= 0 {
return nil, fmt.Errorf("cannot convert jwk to rsa public key: invalid rsa exponent")
}
return &rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: int(new(big.Int).SetBytes(eBytes).Int64()),
E: int(e.Int64()),
}, nil
}
// PublicKeyFromJWKS returns the RSA public key matching the given key ID.
func PublicKeyFromJWKS(jwks *JWKS, kid string) (*rsa.PublicKey, error) {
if jwks == nil {
return nil, fmt.Errorf("cannot find signing key %q in jwks: jwks is nil", kid)
}
for _, key := range jwks.Keys {
if key.KeyID == kid {
return RSAPublicKeyFromJWK(key)
@@ -181,6 +190,10 @@ func VerifyJWT(raw string, pubKey *rsa.PublicKey) ([]byte, error) {
// VerifyJWTWithJWKS verifies an RS256 JWT using the matching key from a JWKS.
func VerifyJWTWithJWKS(raw string, jwks *JWKS) ([]byte, error) {
if jwks == nil {
return nil, fmt.Errorf("cannot verify jwt: jwks is nil")
}
parts := strings.Split(raw, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("cannot verify jwt: invalid format")

View File

@@ -325,6 +325,21 @@ func TestRSAPublicKeyFromJWK(t *testing.T) {
require.Error(t, err)
},
)
t.Run(
"rejects invalid rsa exponent",
func(t *testing.T) {
t.Parallel()
invalidExponent := jwk
invalidExponent.E = base64.RawURLEncoding.EncodeToString(
new(big.Int).Lsh(big.NewInt(1), 128).Bytes(),
)
_, err := jose.RSAPublicKeyFromJWK(invalidExponent)
require.Error(t, err)
},
)
}
func TestPublicKeyFromJWKS(t *testing.T) {
@@ -358,6 +373,16 @@ func TestPublicKeyFromJWKS(t *testing.T) {
require.Error(t, err)
},
)
t.Run(
"errors when jwks is nil",
func(t *testing.T) {
t.Parallel()
_, err := jose.PublicKeyFromJWKS(nil, "kid-1")
require.Error(t, err)
},
)
}
func TestVerifyJWT(t *testing.T) {
@@ -559,4 +584,17 @@ func TestVerifyJWTWithJWKS(t *testing.T) {
require.Error(t, err)
},
)
t.Run(
"rejects nil jwks",
func(t *testing.T) {
t.Parallel()
token, err := jose.SignJWT(key, "kid-1", map[string]string{"sub": "test"})
require.NoError(t, err)
_, err = jose.VerifyJWTWithJWKS(token, nil)
require.Error(t, err)
},
)
}

View File

@@ -83,7 +83,14 @@ func AuthorizationURLWithQuery(
return "", fmt.Errorf("cannot parse authorization endpoint: %w", err)
}
u.RawQuery = query.Encode()
merged := u.Query()
for key, values := range query {
for _, value := range values {
merged.Add(key, value)
}
}
u.RawQuery = merged.Encode()
return u.String(), nil
}

View File

@@ -90,6 +90,26 @@ func TestAuthorizationURLWithQuery(t *testing.T) {
)
}
func TestAuthorizationURLWithQueryPreservesEndpointQuery(t *testing.T) {
t.Parallel()
authorizationEndpoint := uri.URI(
"https://auth.example.com/api/connect/v1/oauth2/authorize?tenant=acme",
)
query := url.Values{}
query.Set("client_id", "https://trust.example.com/.well-known/oauth-client-metadata")
query.Set("response_type", "code")
got, err := oauth2.AuthorizationURLWithQuery(authorizationEndpoint, query)
require.NoError(t, err)
parsed, err := url.Parse(got)
require.NoError(t, err)
assert.Equal(t, "acme", parsed.Query().Get("tenant"))
assert.Equal(t, "https://trust.example.com/.well-known/oauth-client-metadata", parsed.Query().Get("client_id"))
assert.Equal(t, "code", parsed.Query().Get("response_type"))
}
func TestNewMetadata(t *testing.T) {
t.Parallel()

View File

@@ -16,11 +16,9 @@ package complianceportal_v1
import (
"context"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.gearno.de/x/ref"
"go.probo.inc/probo/pkg/baseurl"
@@ -123,17 +121,12 @@ func NewMux(cfg MuxConfig) (http.Handler, error) {
)
r.Handle("/*", webServer)
r.NotFound(handleCustomDomain404)
},
)
return r, nil
}
func handleCustomDomain404(w http.ResponseWriter, r *http.Request) {
httpserver.RenderError(w, http.StatusNotFound, errors.New("not found"))
}
func compliancePageHeadData() HeadDataFunc {
return func(r *http.Request) HeadData {
tc := complianceportal.CompliancePageFromContext(r.Context())