Skip to content

1.9.0

Choose a tag to compare

@goldnead goldnead released this 05 Aug 06:03
· 66 commits to main since this release

Fixed — the flat driver leaked webhook credentials between brands

It had no brand concept at all. FileStore was a singleton bound to one path and nothing under Repositories/FlatFile read or wrote a brand, so content/webhooks/ held one undifferentiated set and every brand read every brand's hooks. The eloquent driver scoped correctly the whole time, so the same install isolated or did not depending on a value that reads like a storage preference.

This is worse here than in a CRM. A webhook config carries a destination URL and the credentials it authenticates with. A cross-brand read is not "one tenant sees another's data" — it hands over a bearer token. A test in FlatDriverBrandIsolationTest asserts precisely that, and it failed before this release:

Failed asserting that '…"auth_config":{"token":"super-secret-token"}…'
does not contain "super-secret-token".

Firing a hook from the wrong brand also posts one tenant's payload to another tenant's endpoint.

Brands now live in the path:

content/webhooks/{brand}/outbound/{handle}.yaml
content/webhooks/{brand}/inbound/{handle}.yaml
content/webhooks/{brand}/rules/{handle}.yaml
content/webhooks/{brand}/templates/{handle}.yaml

A brand: key inside each YAML was rejected: the handle is the filename, so a key would give every definition two identities that can disagree, and a missing or misspelt one would fall through to the default brand — a leak that reads like a typo. With a directory the isolation is structural and visible in ls.

The semantics live in one class, Storage\BrandSegments, matching statamic-marketing and statamic-leadhub so all three flat drivers behave the same way.

  • [fix] Single-brand installs change nothing. No directory, no move, nothing to run.
  • [fix] The pre-brand layout is read as the default brand's, and only that brand's, so an install that enables multi-brand does not lose its hooks.
  • [fix] Fail closed. Multi-brand with no current brand reads nothing rather than everything, matching the eloquent global scope.
  • [fix] A write lands where the definition already lives, so an un-migrated install gains no second copy that shadows the original.

Added — webhook-manager:migrate-flat-brands

Moves the pre-brand layout into a brand directory.

php artisan webhook-manager:migrate-flat-brands --dry-run
php artisan webhook-manager:migrate-flat-brands
php artisan webhook-manager:migrate-flat-brands --brand=acme

Only ever moves. Never overwrites — a target that already exists means a finished migration or a genuine conflict, and neither is resolved by clobbering — never deletes, and a second run is a no-op.

Notes

Segment resolution is memoised per brand identity; it is consulted on every file operation and the key carries the brand, so a BrandContext::runFor() switch inside one process invalidates it.

tests/Feature/FlatDriverBrandIsolationTest.php and MigrateFlatBrandsCommandTest.php cover this. Five of the six isolation cases failed before the change; the sixth is the single-brand case, which must not change and does not.

Found by auditing all three flat drivers after the same defect was fixed in statamic-leadhub 1.11.0. statamic-marketing has been correct since 1.6 and needed nothing.