Repository navigation
Add event sourcing to the book and tutorial, and enforce wallet ownership in the sample - #19
Merged
Merged
Conversation
added 25 commits
October 13, 2026 12:01
… its three #[EventType] events
…m history and snapshots, refuse a stale writer
…and moving money in one transaction
…he projection commands run, list and rebuild
…nd the loan book agree on the renamed field
…ppend's transaction, and roll them back with it
…tore and rebuild its read model from the streams
…on every integration lane
…nConfiguration adjacent, as Chapter 12 quotes them
…r and outbox publication in the sample README
…s projection as the reader's own files
…s, a checkpointed projection, an upcaster and outbox publication, in both editions
… what W3 shipped, in both editions
… into a read model; What's Next becomes Step 15, in both languages
… Tutorial Step 14
…and Lumen sample work under [Unreleased]
…WalletPermissionEvaluator behind hasPermission(#command, 'debit')
WithdrawHandler and RepayLoanHandler were guarded by hasRole('ADMIN') or
hasRole('WALLET_OWNER'), so any customer holding the role could withdraw from,
pay out of, or repay a loan against any wallet. The rule now also asks
hasPermission(#command, 'debit'), answered by a sample PermissionEvaluator bean
that displaces the deny-all default and grants the debit only when the wallet's
owner_id is the principal's name (for RepayLoan, the wallet the loan was paid
into), failing closed on unknown targets, permissions and ids.
… WalletPermissionEvaluator, and Chapters 4, 6, 9A, 9B and the README follow, in both editions
…only the source wallet's owner or an admin can move money out of it POST /api/v1/wallets/transfers had no #[PreAuthorize]: anyone, even an anonymous caller, could debit any wallet by naming it as a transfer's source. TransferHandler now carries the same rule as WithdrawHandler and RepayLoanHandler, and WalletPermissionEvaluator answers 'debit' on a Transfer for the owner of its source wallet only (owning the destination grants nothing; every other target still fails closed). The tests that transferred without a principal now act as the source wallet's owner and keep their assertions; the two rollback tests also assert that the cause is the credit leg's ConflictException, so a security refusal can no longer make them pass for the wrong reason. New coverage: an anonymous transfer is a 401 and a non-owner's a 403 problem+json with no balance moved on either wallet, and the owner and an ADMIN succeed, over REST and the bus. Chapter 4's WalletController listing follows the class comment.
…, 9 and 10 and the README, and Chapter 10's exercise 2 now guards a read instead, in both editions The TransferHandler listings (Chapter 6, README) show the #[PreAuthorize] line and its comment; Chapter 9's TransferSecurityTest excerpt signs in as the source wallet's owner and says why the rollback test asserts its cause; Chapter 10's WalletPermissionEvaluator listing names the Transfer target, the proof section shows the non-owner transfer refused with no money moved, and exercise 2, which asked the reader to add this very guard, now asks them to guard GetBalance with a 'view' permission and count what that costs.
…rry the cause's RFC 9457 extension members and title, so a refused command publishes requiredAuthorities like a refused controller method The fix is in the framework, not the book. MethodSecurityRefusal puts the rule's authorities on the AuthorizationException as `requiredAuthorities` on purpose: its class comment says the wire gets them "as an RFC 9457 extension member so a client can render 'you need ROLE_MANAGER'", and docs/modules/security.md documents the member on every method-security denial. Nothing omits it for security reasons; only the class, method and expression are kept off the wire, and they still are. The member was lost by accident: DefaultCommandBus wraps every failure in CommandProcessingException, which copied the cause's code, status, category and severity but not its extensions or title, and ProblemDetailsRenderer builds the document from the wrapper. So the same rule refusing the same principal published the member from a controller and dropped it from a command (Chapter 4's 403 for a refused withdraw showed it; the real response did not carry it). Both wrappers now pass the cause's extensions() and title() to the parent, which also keeps a `field`, `allowed` or own title of any FireflyException a handler throws. `detail` is unchanged: it still names the command, which the book and docs already show. Regression tests: the cqrs taxonomy test pins both wrappers (and the empty case), packages/security renders a controller refusal and a real bus refusal through the real renderer and requires the same document apart from detail, and Lumen's REST suite asserts the member on a refused withdraw.
…ers 4, 7 and 10 and the cqrs and security module guides say a refused command keeps requiredAuthorities, in both editions
…s' extension members under [Unreleased]
…skipped lane no longer reaches for an absent application
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds event sourcing to larafly by example (EN + ES) and the tutorial, and fixes the authorization defects that writing it uncovered.
Book, tutorial and sample
New Chapter 9B, Event Sourcing. The Lumen sample's loan is event-sourced and stored in the database event store:
firefly.orchestration.persistence.provider=event-sourced.Every listing is real code with tests on every integration lane.
Supporting pages. Updated the glossary, Appendix A, preface, Chapter 9A,
book/README.mdand both manifests, and swept the book for statements W3 made false.Tutorial Step 14 (EN + ES). Greetings are recorded as events and projected into a read model. The code is proven as the reader's own
App\files. "What's Next" moves to Step 15.Site. Getting started, the index and the README now name event sourcing.
Fixes
WALLET_OWNERcould withdraw from, pay out of, repay a loan against, or transfer from any wallet.TransferHandlerhad no guard at all, so even anonymous callers could transfer.WalletPermissionEvaluator(aPermissionEvaluatorbean) grantsdebitonly to the wallet's owner; anything it does not recognize is refused.hasRole('ADMIN') or (hasRole('WALLET_OWNER') and hasPermission(#command, 'debit')).CommandProcessingExceptionandQueryProcessingExceptiondropped the cause's extension members and title. A method-security refusal raised through the bus therefore lost its documentedrequiredAuthorities. Both wrappers now carry them through, and a regression test requires the bus and controller documents to be identical apart fromdetail.afterEach.Verification
composer check: exit 0, 6782 passed.bash scripts/build-docs.sh: exit 0.