diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 3807e57637..595dc5e567 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -255,11 +255,7 @@ public class ApplicationProperties { */ private boolean allowPrivateS3Endpoints = false; - /** - * Largest inbound document (bytes) the webhook source receiver will accept in one delivery. - * A larger body is rejected with 413 before anything is written to the spool, so an - * unauthenticated caller cannot fill the disk. Default 100 MB. - */ + /** Max inbound webhook body (bytes); over it is rejected 413. Default 100 MB. */ private long webhookMaxBytes = 104857600L; } diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index 9a116a4b25..08d941872b 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -198,8 +198,7 @@ public class RequestUriUtils { || trimmedUri.startsWith("/readiness") || trimmedUri.startsWith( "/api/v1/mobile-scanner/") // Mobile scanner endpoints (no auth) - // Policy webhook source receiver - authenticated per-request by its HMAC signature, - // not a login session, so it must bypass the session auth wall. + // Webhook receiver: authenticated per-request by HMAC signature, not a session. || trimmedUri.startsWith("/api/v1/webhooks/") || trimmedUri.startsWith("/v1/api-docs") // Workflow participant endpoints - access controlled by share tokens, not login diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/InputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/InputSource.java index aa3863fc1d..c9a6270d77 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/InputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/InputSource.java @@ -23,13 +23,7 @@ public interface InputSource { /** Throws {@link IllegalArgumentException} on bad config. Called on save to fail fast. */ default void validate(InputSpec spec) {} - /** - * Normalise a source's options just before it is persisted, so a source type can populate - * server-owned config the client neither supplies nor controls. {@code isCreate} is true only - * for a brand-new source (blank id). Runs before {@link #validate}. The default returns the - * options unchanged; the webhook source overrides it to mint its routing id and signing secret - * on create. Must not mutate the argument. - */ + /** Normalise a source's options before persistence (default no-op); must not mutate the arg. */ default Map prepareOptionsForSave( Map options, boolean isCreate) { return options; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java index 27a215d3af..8932e7420b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java @@ -26,25 +26,7 @@ import stirling.software.proprietary.policy.webhook.WebhookConfig; import stirling.software.proprietary.policy.webhook.WebhookIds; import stirling.software.proprietary.policy.webhook.WebhookSpool; -/** - * A push source: documents arrive by signed HTTP POST to {@code /api/v1/webhooks/{webhookId}} and - * are staged for the referencing policies. Where they are staged depends on the source's config - * (see {@link WebhookConfig}): - * - * - * - *

Either way the {@link ResolveContext} ledger does the claiming, "mode" is "consume" (default: - * a staged document is removed once every policy that claimed it has settled successfully) or - * "snapshot" (stateless), and "webhookId"/"signingSecret" are minted server-side on create. Empty - * staging reads as "verifiably no files". - */ +/** Push source: deliveries staged to an S3 connection or the local spool, read via the ledger. */ @Slf4j @Service @RequiredArgsConstructor @@ -55,9 +37,7 @@ public class WebhookInputSource implements InputSource { private final WebhookSpool spool; private final FileReadinessChecker readinessChecker; - // Deliveries staged to an S3 connection are read/claimed/consumed exactly like an S3 source, so - // that whole path is reused rather than reimplemented; a connection-less webhook uses the - // spool. + // Connection-backed staging delegates to the S3 source; else the local spool. private final S3InputSource s3InputSource; @Override @@ -74,16 +54,12 @@ public class WebhookInputSource implements InputSource { public void validate(InputSpec spec) { WebhookConfig config = WebhookConfig.from(spec.options()); if (config.usesConnection()) { - // Resolves the connection with the caller present, so save fails if their team can't - // use it (the ownership check the delivery path then trusts). + // Resolve with the caller present so save fails if they can't use the connection. s3InputSource.validate(stagingSpec(config)); } } - /** - * The S3 view of a webhook's durable staging: its connection plus the reserved per-webhook - * prefix, as an {@code s3} input spec {@link S3InputSource} can resolve directly. - */ + /** The webhook's durable staging as an {@code s3} input spec for {@link S3InputSource}. */ private static InputSpec stagingSpec(WebhookConfig config) { return new InputSpec( "s3", @@ -96,11 +72,7 @@ public class WebhookInputSource implements InputSource { config.mode())); } - /** - * Mint the routing id and signing secret on create, so the delivery URL and its HMAC key are - * always server-generated. An edit (or any options that already carry a webhookId) is left - * untouched, so the URL a sender is already configured against never changes underneath them. - */ + /** Mint the routing id + signing secret on create; an existing webhook is left untouched. */ @Override public Map prepareOptionsForSave( Map options, boolean isCreate) { @@ -125,16 +97,12 @@ public class WebhookInputSource implements InputSource { public List resolve(InputSpec spec, ResolveContext ctx) throws IOException { WebhookConfig config = WebhookConfig.from(spec.options()); if (config.usesConnection()) { - // Durable staging: the delivery objects live in the connection's bucket and are read, - // claimed, and consumed by the S3 source (no principal here - the save-time check the - // resolver trusts already ran). + // Durable staging read by the S3 source (no principal; the save-time check is trusted). return s3InputSource.resolve(stagingSpec(config), ctx); } Path dir = spool.dirFor(config.webhookId()); if (!Files.isDirectory(dir)) { - // No deliveries yet (or none since the last consume): a verifiably empty source, so the - // sweep may prune ledger rows for files that are gone. Unlike the folder source a - // missing directory is normal here, not an unmounted-drive error. + // No deliveries yet: verifiably empty (a missing dir is normal here, not an error). ctx.reportPresent(List.of()); return List.of(); } @@ -182,13 +150,7 @@ public class WebhookInputSource implements InputSource { return work; } - /** - * Settle at the claimed version, then remove the spooled file only when it is still that - * version and every policy that claimed it has settled DONE - the same consensus delete the - * folder and S3 sources use, so a shared webhook feeding several policies keeps a delivery - * until all are done and one failure parks it for everyone. A failed run settles ERROR and - * never deletes. - */ + /** Settle, then delete when unchanged and every claimant settled DONE (consensus). */ private static void completeConsumed( ResolveContext ctx, String identity, Path file, String claimGate, boolean success) { ctx.settle(identity, claimGate, null, success); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java index 1011cb4e71..a219f86c14 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java @@ -231,11 +231,7 @@ public class SourceController { .validate(spec); } - /** - * Let the matching source type populate server-owned config just before persistence (a webhook - * mints its routing id and signing secret on create). An unknown type is left untouched; {@link - * #validateConfig} then rejects it with a clear message. - */ + /** Let the source type populate server-owned config before persistence. */ private Source withPreparedOptions(Source source, boolean isCreate) { InputSpec spec = source.toInputSpec(); InputSource input = inputSourceFor(spec).orElse(null); @@ -243,16 +239,11 @@ public class SourceController { return source; } Map prepared = input.prepareOptionsForSave(source.options(), isCreate); - // A source type must return the (possibly augmented) options; if it returns null, keep the - // originals rather than let the Source record normalise null to an empty map and wipe - // config. + // Treat a null return as "unchanged" (else the record wipes config to empty). return prepared == null ? source : withOptions(source, prepared); } - /** - * Reveal server-minted secrets (a webhook's signing secret) once, on the create response only, - * so the operator can copy them; every other read - including an edit - is masked. - */ + /** Reveal a webhook's minted secret once on the create response; other reads mask. */ private static Source revealOnCreate(Source saved, boolean isCreate) { if (isCreate && WEBHOOK_TYPE.equals(saved.type())) { return saved; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceView.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceView.java index a043518f2c..e76f3a534f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceView.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceView.java @@ -2,13 +2,7 @@ package stirling.software.proprietary.policy.source; import java.util.List; -/** - * One row in the Sources overview: a persisted input connection shown exactly once, with how many - * policies reference it (and which) and how many documents it has fed into runs ({@code docsTotal} - * lifetime plus the trailing 24-hour and 30-day windows). {@code webhookPath} is set only for a - * webhook source - the server-relative delivery path senders POST to - and is null for every other - * type; the client turns it into an absolute URL. - */ +/** One Sources-overview row; {@code webhookPath} is a webhook's delivery path, else null. */ public record SourceView( String id, String name, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java index 304ecaaede..75b477dcc3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java @@ -20,16 +20,7 @@ import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; import stirling.software.proprietary.policy.webhook.WebhookConfig; -/** - * Fires policies when a document is delivered to one of their webhook sources. Delivery is push, so - * the receiver calls {@link #fireForWebhook} the moment it spools a file and the referencing - * policies run immediately. As with folder-watch, the instant fire is a latency optimisation, not - * the source of truth: a periodic reconcile ({@code watchReconcileSeconds}) re-runs every webhook - * policy to cover deliveries that landed while the process was down and any fire that was missed. - * Redundant runs are harmless because the input source does the claiming. - * - *

The spool is on local disk, so - like folder-watch - this assumes a single node. - */ +/** Fires webhook policies on delivery plus a periodic reconcile safety net. */ @Slf4j @Service @RequiredArgsConstructor @@ -37,8 +28,7 @@ import stirling.software.proprietary.policy.webhook.WebhookConfig; public class WebhookTrigger implements PolicyTrigger { static final String TYPE = "webhook"; - // The webhook source type; equal to the trigger type but a distinct concept (source vs - // trigger). + // The webhook source type (equals the trigger type but a distinct concept). private static final String WEBHOOK_SOURCE_TYPE = "webhook"; private final PolicyStore policyStore; @@ -98,12 +88,7 @@ public class WebhookTrigger implements PolicyTrigger { } } - /** - * Run every webhook policy that references the delivered-to source. Called by the receiver - * right after it spools a document, so processing starts without waiting for the next - * reconcile. A LIGHT sweep: the periodic reconcile does the full-listing pass. Best-effort - a - * run failure is logged and the others still fire. - */ + /** Run every webhook policy referencing the delivered-to source (LIGHT, best-effort). */ public void fireForWebhook(String webhookId) { for (Policy policy : policyStore.findByTriggerType(TYPE)) { if (!referencesWebhook(policy, webhookId)) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookConfig.java index 5ff2f7b176..31c89174d1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookConfig.java @@ -2,20 +2,7 @@ package stirling.software.proprietary.policy.webhook; import java.util.Map; -/** - * Config for a webhook input source, parsed from a spec's options map. A webhook is a push source: - * external systems POST documents to a signed URL keyed by {@code webhookId}, and each delivery is - * verified against {@code signingSecret} before it is staged for the referencing policies. Both - * {@code webhookId} (the public URL token) and {@code signingSecret} (the HMAC key) are generated - * server-side on create - a client never supplies them. - * - *

Where deliveries are staged depends on {@code connectionId}: absent, they go to a node-local - * spool directory (self-hosted); present, they go to the S3 {@code IntegrationConfig} connection it - * references, under a reserved per-webhook prefix, so the staging is durable and reachable from any - * node (the model hosted/SaaS needs). Either way {@code mode} is "consume" (default: a staged - * document is removed once every policy that claimed it has settled successfully) or "snapshot" - * (stateless, every run re-reads whatever is currently staged). - */ +/** Webhook source config: server-minted ids, staging via an S3 connection or the local spool. */ public record WebhookConfig( String webhookId, String signingSecret, Long connectionId, boolean snapshot) { @@ -55,11 +42,7 @@ public record WebhookConfig( return connectionId != null; } - /** - * The reserved key prefix this webhook's deliveries are staged under inside its connection's - * bucket. Server-derived from the (unguessable) id, so two webhooks sharing a connection never - * collide and an S3 source pointed at the same bucket won't see another webhook's inbox. - */ + /** Reserved per-webhook staging prefix inside the connection's bucket. */ public String stagingPrefix() { return STAGING_ROOT + "/" + webhookId; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookIds.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookIds.java index ed36fbba18..0fd0aff1f2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookIds.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookIds.java @@ -4,17 +4,10 @@ import java.security.SecureRandom; import java.util.Base64; import java.util.regex.Pattern; -/** - * Generates and validates the two server-side tokens a webhook source carries: the public {@code - * webhookId} that routes a delivery URL to a source, and the {@code signingSecret} that - * authenticates each delivery. Both are URL-safe base64 of {@link SecureRandom} bytes, so both are - * unguessable; the id is the routing capability and the secret is the HMAC key. The id's character - * set is constrained so it can be used verbatim as a single path segment and as a spool directory - * name without traversal risk. - */ +/** Generates and validates a webhook's routing id and signing secret. */ public final class WebhookIds { - /** URL-safe base64 without padding: exactly the characters a single path segment allows. */ + /** URL-safe base64 without padding: the characters a single path segment allows. */ private static final Pattern VALID_ID = Pattern.compile("^[A-Za-z0-9_-]{16,128}$"); private static final SecureRandom RANDOM = new SecureRandom(); @@ -22,9 +15,7 @@ public final class WebhookIds { private WebhookIds() {} - /** - * A fresh routing token (~24 chars). Not secret, but unguessable so it cannot be enumerated. - */ + /** A fresh routing token (~24 chars), unguessable so it cannot be enumerated. */ public static String newWebhookId() { return randomToken(18); } @@ -34,9 +25,7 @@ public final class WebhookIds { return randomToken(32); } - /** - * Whether {@code id} is a well-formed webhook id, safe as a path segment and directory name. - */ + /** Whether {@code id} is well-formed and safe as a path segment / directory name. */ public static boolean isValidId(String id) { return id != null && VALID_ID.matcher(id).matches(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookReceiverController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookReceiverController.java index 647c5e2c95..a974be90b0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookReceiverController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookReceiverController.java @@ -38,22 +38,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -/** - * Public receiver for webhook input sources. External systems POST a document to {@code - * /api/v1/webhooks/{webhookId}}; the request is authenticated not by a login session but by an - * HMAC-SHA256 signature of the exact body under the source's signing secret, so this endpoint sits - * outside the session auth wall (see {@code RequestUriUtils.isPublicAuthEndpoint}). A verified - * delivery is spooled and the referencing policies are fired immediately. - * - *

The request body is the raw document bytes, sent with a binary content type - {@code - * application/pdf} or {@code application/octet-stream} (e.g. {@code curl --data-binary @file.pdf -H - * 'Content-Type: application/pdf'}). A form content type ({@code - * application/x-www-form-urlencoded}) is rejected upstream because the container tries to parse the - * body as form fields. The signature header is {@code sha256=}. Delivery is rejected before - * anything is written if the id is unknown (404), the signature is missing or wrong (401), the - * source is paused (403), or the body is empty (400) or larger than {@code - * policies.webhookMaxBytes} (413). - */ +/** Public receiver: HMAC-verifies a signed delivery, stages it, and fires the policies. */ @Slf4j @RestController @RequestMapping("/api/v1/webhooks") @@ -151,11 +136,7 @@ public class WebhookReceiverController { } } - /** - * Stage a delivery to the webhook's S3 connection under its reserved prefix. Resolved with no - * principal - the referencing source's right to use the connection was checked when it was - * saved - so a public delivery never needs the caller to hold connection rights. - */ + /** Stage a delivery to the S3 connection (save-time access check trusted). */ private String stageToConnection(WebhookConfig config, String filename, byte[] body) { S3Config s3 = connectionResolver.resolve( @@ -191,11 +172,7 @@ public class WebhookReceiverController { return prefix + "/"; } - /** - * Read the body into memory, capped at {@code policies.webhookMaxBytes}. Reading one byte past - * the limit is enough to reject an over-sized (or unbounded, chunked) delivery with 413 before - * it can fill memory or the disk. - */ + /** Read the body into memory, capped at {@code policies.webhookMaxBytes}. */ private byte[] readBoundedBody(HttpServletRequest request) { long maxBytes = applicationProperties.getPolicies().getWebhookMaxBytes(); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSignatures.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSignatures.java index 9f4f9ae221..d34c46e112 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSignatures.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSignatures.java @@ -9,13 +9,7 @@ import java.util.HexFormat; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -/** - * HMAC-SHA256 signing scheme for webhook deliveries. The sender signs the exact request body bytes - * with the source's {@code signingSecret} and presents the result as {@code sha256=} in the - * signature header; the receiver recomputes it and compares in constant time. Signing the raw body - * (rather than form fields) keeps the contract unambiguous - what is signed is exactly what is - * delivered - and matches how established webhook providers (Stripe, GitHub) sign payloads. - */ +/** HMAC-SHA256 signing of a webhook delivery's raw body (header sha256=). */ public final class WebhookSignatures { private static final String ALGORITHM = "HmacSHA256"; @@ -23,18 +17,12 @@ public final class WebhookSignatures { private WebhookSignatures() {} - /** - * The header value a sender should present for {@code body}: {@code sha256=}. - */ + /** The header a sender presents for {@code body}: {@code sha256=}. */ public static String sign(String signingSecret, byte[] body) { return PREFIX + HexFormat.of().formatHex(hmac(signingSecret, body)); } - /** - * Whether {@code presented} (a {@code sha256=} header value, or a bare hex string) is a - * valid signature of {@code body} under {@code signingSecret}. Constant-time in the compared - * bytes; a malformed or missing header is simply false, never an exception. - */ + /** Whether the presented signature is valid for {@code body}; false on bad input. */ public static boolean verify(String signingSecret, byte[] body, String presented) { if (signingSecret == null || presented == null || body == null) { return false; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSpool.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSpool.java index 323f496af5..2888fe22f7 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSpool.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/webhook/WebhookSpool.java @@ -11,16 +11,7 @@ import org.springframework.stereotype.Component; import stirling.software.common.configuration.InstallationPathConfig; -/** - * Server-owned staging area for webhook deliveries. Each webhook source gets its own directory, - * named by its (unguessable, path-segment-safe) {@code webhookId}, under a fixed root inside - * Stirling's install path - never a user-supplied location, so this is not subject to the folder - * source's allow-listing. The receiver writes each accepted document here atomically; {@link - * stirling.software.proprietary.policy.input.WebhookInputSource} then reads it exactly as the - * folder source reads a watched directory, reusing the whole claim/consume ledger. A delivered file - * keeps its original name for downstream output naming, prefixed with a unique token so two - * deliveries of the same name never collide. - */ +/** Server-owned per-webhook staging directory under the install path. */ @Component @ConditionalOnBooleanProperty(name = "policies.enabled") public class WebhookSpool { @@ -54,12 +45,7 @@ public class WebhookSpool { return dir; } - /** - * Write one delivered document into the webhook's spool, atomically so a partial write is never - * picked up: staged under a hidden {@code .part} name and moved into place. Returns the final - * path. The original filename (sanitised to a bare, safe basename) is preserved after a unique - * prefix. - */ + /** Write a delivery into the spool atomically (staged .part, then moved into place). */ public Path store(String webhookId, String filename, byte[] content) throws IOException { Path dir = dirFor(webhookId); Files.createDirectories(dir); @@ -90,19 +76,12 @@ public class WebhookSpool { return spoolFileName; } - /** - * The object-key suffix for an S3-staged delivery: a unique segment then the sanitised name - * ({@code /}). A subfolder rather than a name prefix so the object's basename - - * and thus the resolved input's filename - stays the clean original name, matching the local - * spool. - */ + /** S3 object-key suffix {@code /}; the subfolder keeps the basename clean. */ public static String objectKeySuffix(String filename) { return UUID.randomUUID().toString().replace("-", "") + "/" + sanitize(filename); } - /** - * The sanitised display name of a staged delivery (the basename of {@link #objectKeySuffix}). - */ + /** The staged delivery's display name (basename of {@link #objectKeySuffix}). */ public static String objectDisplayName(String filename) { return sanitize(filename); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java b/app/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java index 3402b89bb3..6a0b4f4e49 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java @@ -20,9 +20,7 @@ public final class SecretMasker { private static final Pattern SENSITIVE = RegexPatternUtils.getInstance() .getPattern( - // secret[_-]?access[_-]?key and signing[_-]?secret precede plain secret - // so camelCase keys like secretAccessKey and signingSecret (whose inner - // "secret" has no word boundary before/after it) still match. + // specific keys precede plain "secret" so camelCase still matches. "(?i)\\b(password|token|secret[_-]?access[_-]?key|signing[_-]?secret|secret|api[_-]?key|authorization|auth|jwt|cred|cert)\\b"); private SecretMasker() {} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/input/WebhookInputSourceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/input/WebhookInputSourceTest.java index bc01f50676..58b51c5b83 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/input/WebhookInputSourceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/input/WebhookInputSourceTest.java @@ -34,11 +34,7 @@ import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.webhook.WebhookConfig; import stirling.software.proprietary.policy.webhook.WebhookSpool; -/** - * Tests for {@link WebhookInputSource}: spooled deliveries are read through the ledger like folder - * files (consume removes them once processed, snapshot stays stateless), a not-yet-created spool is - * an empty source rather than an error, and options gain a routing id + signing secret on create. - */ +/** Tests for {@link WebhookInputSource}: ledger-backed read/consume, and id/secret minting. */ @ExtendWith(MockitoExtension.class) class WebhookInputSourceTest { @@ -193,8 +189,7 @@ class WebhookInputSourceTest { source.validate(spec); - // Save-time validation defers to the S3 source, which resolves + ownership-checks the - // connection (the check the delivery path later trusts). + // Save-time validation defers to the S3 source, which ownership-checks the connection. verify(s3InputSource).validate(any()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java index 325b8e8776..027093a6bd 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java @@ -70,9 +70,7 @@ class SourceControllerTest { policyGuard, new InProcessSourceDocCounter()); triggerManager = mock(PolicyTriggerManager.class); - // A permissive input source so config validation passes and save can be exercised. Model a - // real source's prepareOptionsForSave as a pass-through (a bare mock would return an empty - // map for the Map-typed method and wipe the config). + // A permissive input source; stub prepareOptionsForSave as a pass-through. InputSource folderInput = mock(InputSource.class); when(folderInput.supports(any())).thenReturn(true); when(folderInput.prepareOptionsForSave(any(), anyBoolean())) diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java index a21968b638..4fda098590 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java @@ -27,10 +27,7 @@ import stirling.software.proprietary.policy.source.Source; import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; -/** - * Tests for {@link WebhookTrigger}'s dispatch: a delivery fires only the policies that reference - * the delivered-to webhook, and validation requires at least one webhook source. - */ +/** Tests for {@link WebhookTrigger}: fires only policies referencing the delivered-to webhook. */ @ExtendWith(MockitoExtension.class) class WebhookTriggerTest { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookReceiverControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookReceiverControllerTest.java index d0cf31cb2e..0e1ce4b50b 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookReceiverControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookReceiverControllerTest.java @@ -31,11 +31,7 @@ import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.trigger.WebhookTrigger; import stirling.software.proprietary.policy.webhook.WebhookReceiverController.WebhookDeliveryResponse; -/** - * Tests for the public webhook receiver: a signature-valid delivery is spooled and fires the - * trigger; an unknown id, wrong signature, paused source, empty body, or over-sized body is - * rejected before anything is stored. - */ +/** Tests for the public webhook receiver: valid delivery spools + fires; bad requests rejected. */ class WebhookReceiverControllerTest { private static final String WEBHOOK_ID = "receivertestid12"; diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookS3ConnectionMinioTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookS3ConnectionMinioTest.java index 8a50c4cfcf..b88ad837b5 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookS3ConnectionMinioTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/webhook/WebhookS3ConnectionMinioTest.java @@ -54,13 +54,7 @@ import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.S3Object; import tools.jackson.databind.ObjectMapper; -/** - * End-to-end test of a connection-backed webhook against a real S3 API (MinIO): a signed delivery - * is staged by the receiver into the S3 connection's bucket under the reserved per-webhook prefix, - * then read and consumed by the source (which delegates to {@link S3InputSource}). Proves the - * durable, multi-node staging model hosted deployments use, through the production S3 client - * factory. - */ +/** End-to-end connection-backed webhook against MinIO: stage, read via S3 delegation, consume. */ @Testcontainers(disabledWithoutDocker = true) class WebhookS3ConnectionMinioTest { @@ -112,8 +106,7 @@ class WebhookS3ConnectionMinioTest { ApplicationProperties properties = new ApplicationProperties(); properties.getPolicies().setAllowPrivateS3Endpoints(true); - // A resolver over a stored S3 connection pointing at MinIO. No SecurityContext in the test, - // so the resolver's ownership check is skipped (the delivery path's trust-the-save model). + // A resolver over a MinIO S3 connection; no principal here, so ownership is skipped. S3ConnectionResolver resolver = resolverFor(minioConnection()); S3ConnectionPool pool = new S3ConnectionPool(properties); S3InputSource s3 = new S3InputSource(pool, resolver); diff --git a/frontend/editor/src/portal-saas/components/sources/creatableSourceTypes.ts b/frontend/editor/src/portal-saas/components/sources/creatableSourceTypes.ts index 276ef09e38..3c13bacb86 100644 --- a/frontend/editor/src/portal-saas/components/sources/creatableSourceTypes.ts +++ b/frontend/editor/src/portal-saas/components/sources/creatableSourceTypes.ts @@ -4,13 +4,7 @@ import { type CreatableSourceType, } from "@portal/components/sources/sourceTypes"; -/** - * Hosted deployments don't rely on the server's local filesystem. Folder sources - * are dropped outright (denied by the backend's FolderAccessGuard). Webhook stays - * - it works in hosted by staging deliveries to a durable S3 connection - but its - * connection becomes required, since the node-local fallback can't be relied on - * across a multi-node fleet. Cloud sources such as S3 are unchanged. - */ +/** Hosted drops folder (no server FS) and requires webhook's S3 connection (node-local isn't durable). */ export function creatableSourceTypes(): CreatableSourceType[] { return CREATABLE_SOURCE_TYPES.filter((type) => type.type !== "folder").map( (type) => diff --git a/frontend/editor/src/portal/components/sources/sourceTypes.ts b/frontend/editor/src/portal/components/sources/sourceTypes.ts index c73ac1078b..3878c6b04b 100644 --- a/frontend/editor/src/portal/components/sources/sourceTypes.ts +++ b/frontend/editor/src/portal/components/sources/sourceTypes.ts @@ -189,9 +189,7 @@ export const CREATABLE_SOURCE_TYPES: CreatableSourceType[] = [ ], }, { - // The delivery URL + signing secret are minted server-side on create (revealed once). An S3 - // connection makes staging durable + multi-node (required in hosted); left blank, deliveries - // stage to the server's local disk (self-hosted only). + // URL + secret minted server-side (revealed once); an S3 connection makes staging durable, else local disk. type: WEBHOOK_SOURCE_TYPE, labelKey: "portal.sources.types.webhook.label", descriptionKey: "portal.sources.types.webhook.description", diff --git a/frontend/editor/src/portal/mocks/handlers/sources.ts b/frontend/editor/src/portal/mocks/handlers/sources.ts index f5679be653..3f1b8f7ef1 100644 --- a/frontend/editor/src/portal/mocks/handlers/sources.ts +++ b/frontend/editor/src/portal/mocks/handlers/sources.ts @@ -234,8 +234,7 @@ export const sourcesHandlers = [ ? store.find((s) => s.id === incoming.id) : undefined; const id = existing?.id ?? nextId(); - // A new webhook has its routing id + signing secret minted server-side, then revealed once on - // this create response (the store keeps them; later reads mask the secret). + // A new webhook's id + secret are minted server-side and revealed once on this create response. let options = incoming.options ?? {}; if (incoming.type === "webhook" && !existing && !options.webhookId) { options = { diff --git a/frontend/editor/src/portal/views/SourceBuilder.tsx b/frontend/editor/src/portal/views/SourceBuilder.tsx index dff0a31184..38eafbe94b 100644 --- a/frontend/editor/src/portal/views/SourceBuilder.tsx +++ b/frontend/editor/src/portal/views/SourceBuilder.tsx @@ -123,8 +123,7 @@ export function SourceBuilder() { ); const canSave = name.trim() !== "" && requiredComplete && !submitting; - // An existing webhook's delivery URL, shown read-only in the edit view (the secret is not - it - // is revealed only once, at creation). + // An existing webhook's delivery URL, read-only in edit (the secret is revealed only at creation). const editingWebhookId = isEdit && sourceState.data?.type === WEBHOOK_SOURCE_TYPE ? String(sourceState.data.options?.webhookId ?? "") @@ -149,8 +148,7 @@ export function SourceBuilder() { options, enabled, }); - // A new webhook returns its server-minted routing id + signing secret once; reveal them - // (with the delivery URL) before leaving so the operator can copy the secret. + // A new webhook returns its minted id + secret once; reveal them (with the URL) before leaving. if (!isEdit && type.type === WEBHOOK_SOURCE_TYPE) { const webhookId = String(saved.options?.webhookId ?? ""); const secret = String(saved.options?.signingSecret ?? "");