diff --git a/GNUmakefile b/GNUmakefile index d9a499b45..abad75e5f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -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 diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts index a26262f5c..5c42991fa 100644 --- a/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts @@ -176,14 +176,16 @@ export async function execute( } `; - const input: Record = { trustCenterId }; + const input: Record = { + 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 }); diff --git a/pkg/certmanager/renew_worker.go b/pkg/certmanager/renew_worker.go index fe625af27..77efb5eaa 100644 --- a/pkg/certmanager/renew_worker.go +++ b/pkg/certmanager/renew_worker.go @@ -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 { diff --git a/pkg/complianceportal/resolver/resolver.go b/pkg/complianceportal/resolver/resolver.go index 382c66242..67b84c4bd 100644 --- a/pkg/complianceportal/resolver/resolver.go +++ b/pkg/complianceportal/resolver/resolver.go @@ -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 == "" { diff --git a/pkg/coredata/compliance_framework.go b/pkg/coredata/compliance_framework.go index 122a6eb8d..415897278 100644 --- a/pkg/coredata/compliance_framework.go +++ b/pkg/coredata/compliance_framework.go @@ -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 diff --git a/pkg/crypto/jose/jose.go b/pkg/crypto/jose/jose.go index 9b3e15c2b..23df634b8 100644 --- a/pkg/crypto/jose/jose.go +++ b/pkg/crypto/jose/jose.go @@ -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") diff --git a/pkg/crypto/jose/jose_test.go b/pkg/crypto/jose/jose_test.go index 60f097612..474036121 100644 --- a/pkg/crypto/jose/jose_test.go +++ b/pkg/crypto/jose/jose_test.go @@ -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) + }, + ) } diff --git a/pkg/iam/oauth2/discovery.go b/pkg/iam/oauth2/discovery.go index 05b94c37d..a71e2a1bd 100644 --- a/pkg/iam/oauth2/discovery.go +++ b/pkg/iam/oauth2/discovery.go @@ -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 } diff --git a/pkg/iam/oauth2/metadata_test.go b/pkg/iam/oauth2/metadata_test.go index a638202f0..a887b04e3 100644 --- a/pkg/iam/oauth2/metadata_test.go +++ b/pkg/iam/oauth2/metadata_test.go @@ -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() diff --git a/pkg/server/api/complianceportal/v1/mux.go b/pkg/server/api/complianceportal/v1/mux.go index cd0c44ac7..05a5a9650 100644 --- a/pkg/server/api/complianceportal/v1/mux.go +++ b/pkg/server/api/complianceportal/v1/mux.go @@ -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())