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:
@@ -329,9 +329,9 @@ pkg/server/api/complianceportal/v1/schema.graphql: pkg/server/api/complianceport
|
|||||||
$(NPM) --workspace $@ run check
|
$(NPM) --workspace $@ run check
|
||||||
$(NPM) --workspace $@ run build
|
$(NPM) --workspace $@ run build
|
||||||
|
|
||||||
.PHONY: @probo/trust
|
.PHONY: @probo/compliance-portal
|
||||||
@probo/trust: NODE_ENV=production
|
@probo/compliance-portal: NODE_ENV=production
|
||||||
@probo/trust: relay
|
@probo/compliance-portal: relay
|
||||||
$(NPM) --workspace $@ run check
|
$(NPM) --workspace $@ run check
|
||||||
$(NPM) --workspace $@ run build
|
$(NPM) --workspace $@ run build
|
||||||
|
|
||||||
|
|||||||
@@ -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 (active !== undefined) input.active = active;
|
||||||
if (searchEngineIndexing) input.searchEngineIndexing = searchEngineIndexing;
|
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 });
|
const responseData = await proboApiRequest.call(this, query, { input });
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,38 @@ func (h *renewHandler) Claim(ctx context.Context) (coredata.Certificate, error)
|
|||||||
return certificate, nil
|
return certificate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *renewHandler) Process(_ context.Context, _ coredata.Certificate) error {
|
func (h *renewHandler) Process(ctx context.Context, certificate coredata.Certificate) error {
|
||||||
return nil
|
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 {
|
func (h *renewHandler) RecoverStale(ctx context.Context) error {
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ func PublicURLForTrustCenter(
|
|||||||
host = byID[*trustCenter.CustomDomainID].Domain
|
host = byID[*trustCenter.CustomDomainID].Domain
|
||||||
case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil:
|
case trustCenter.DefaultDomainID != nil && byID[*trustCenter.DefaultDomainID] != nil:
|
||||||
host = byID[*trustCenter.DefaultDomainID].Domain
|
host = byID[*trustCenter.DefaultDomainID].Domain
|
||||||
case trustCenter.CustomDomainID != nil && byID[*trustCenter.CustomDomainID] != nil:
|
|
||||||
host = byID[*trustCenter.CustomDomainID].Domain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == "" {
|
if host == "" {
|
||||||
|
|||||||
@@ -389,7 +389,10 @@ WITH combined AS (
|
|||||||
COALESCE(cf.organization_id, tc.organization_id) AS organization_id,
|
COALESCE(cf.organization_id, tc.organization_id) AS organization_id,
|
||||||
COALESCE(cf.trust_center_id, tc.id) AS trust_center_id,
|
COALESCE(cf.trust_center_id, tc.id) AS trust_center_id,
|
||||||
f.id AS framework_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,
|
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.created_at, f.created_at) AS created_at,
|
||||||
COALESCE(cf.updated_at, f.updated_at) AS updated_at
|
COALESCE(cf.updated_at, f.updated_at) AS updated_at
|
||||||
|
|||||||
@@ -121,14 +121,23 @@ func RSAPublicKeyFromJWK(jwk JWK) (*rsa.PublicKey, error) {
|
|||||||
return nil, fmt.Errorf("cannot decode rsa exponent: %w", err)
|
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{
|
return &rsa.PublicKey{
|
||||||
N: new(big.Int).SetBytes(nBytes),
|
N: new(big.Int).SetBytes(nBytes),
|
||||||
E: int(new(big.Int).SetBytes(eBytes).Int64()),
|
E: int(e.Int64()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicKeyFromJWKS returns the RSA public key matching the given key ID.
|
// PublicKeyFromJWKS returns the RSA public key matching the given key ID.
|
||||||
func PublicKeyFromJWKS(jwks *JWKS, kid string) (*rsa.PublicKey, error) {
|
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 {
|
for _, key := range jwks.Keys {
|
||||||
if key.KeyID == kid {
|
if key.KeyID == kid {
|
||||||
return RSAPublicKeyFromJWK(key)
|
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.
|
// VerifyJWTWithJWKS verifies an RS256 JWT using the matching key from a JWKS.
|
||||||
func VerifyJWTWithJWKS(raw string, jwks *JWKS) ([]byte, error) {
|
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, ".")
|
parts := strings.Split(raw, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return nil, fmt.Errorf("cannot verify jwt: invalid format")
|
return nil, fmt.Errorf("cannot verify jwt: invalid format")
|
||||||
|
|||||||
@@ -325,6 +325,21 @@ func TestRSAPublicKeyFromJWK(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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) {
|
func TestPublicKeyFromJWKS(t *testing.T) {
|
||||||
@@ -358,6 +373,16 @@ func TestPublicKeyFromJWKS(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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) {
|
func TestVerifyJWT(t *testing.T) {
|
||||||
@@ -559,4 +584,17 @@ func TestVerifyJWTWithJWKS(t *testing.T) {
|
|||||||
require.Error(t, err)
|
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)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,14 @@ func AuthorizationURLWithQuery(
|
|||||||
return "", fmt.Errorf("cannot parse authorization endpoint: %w", err)
|
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
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestNewMetadata(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ package complianceportal_v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"go.gearno.de/kit/httpserver"
|
|
||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.gearno.de/x/ref"
|
"go.gearno.de/x/ref"
|
||||||
"go.probo.inc/probo/pkg/baseurl"
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
@@ -123,17 +121,12 @@ func NewMux(cfg MuxConfig) (http.Handler, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
r.Handle("/*", webServer)
|
r.Handle("/*", webServer)
|
||||||
r.NotFound(handleCustomDomain404)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCustomDomain404(w http.ResponseWriter, r *http.Request) {
|
|
||||||
httpserver.RenderError(w, http.StatusNotFound, errors.New("not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func compliancePageHeadData() HeadDataFunc {
|
func compliancePageHeadData() HeadDataFunc {
|
||||||
return func(r *http.Request) HeadData {
|
return func(r *http.Request) HeadData {
|
||||||
tc := complianceportal.CompliancePageFromContext(r.Context())
|
tc := complianceportal.CompliancePageFromContext(r.Context())
|
||||||
|
|||||||
Reference in New Issue
Block a user