diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..496875f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php + +matrix: + include: + - php: 5.6 + - php: 7 + +before_install: + - composer self-update + +install: + - composer install + +script: + - ./vendor/bin/phpunit --exclude-group=none + +branches: + only: + - master + - develop diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ebed73 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Broadway project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 7ae105d..d0546b9 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -# broadway-sensitive-data +broadway/sensitive-data +======================= + +Helpers for handling sensitive data with Broadway. + +[![Build Status](https://travis-ci.org/broadway/broadway-sensitive-data.svg?branch=master)](https://travis-ci.org/broadway/broadway-sensitive-data) + +## Installation + +``` +$ composer require broadway/broadway-sensitive-data +``` + +## About +In an Event Sourced environment you may have to deal with sensitive (e.g. personal) data +ending up in your event stream. You could encrypt your event stream or remove sensitive data +from your event stream after a certain amount or time (upcasting). Or you could choose not to +store sensitive data in you event stream altogether. That's where this project helps out. + +Imagine the use case where a customer wants to pay an order with a credit card and you're not +allowed to store the credit card number. + +A `PayWithCreditCardCommand` (with credit card number) should lead to a +`PaymentWithCreditCardRequestedEvent` (without the credit card number) but the `Processor` that +handles the event does need to know the credit card number. + +This project introduces a `SensitiveDataManager` which can be injected into a `CommandHandler` +to capture the sensitive data from the command and make it available to the `SensitiveDataProcessor` +hereby bypassing the event store. + +Pros: +* sensitive data is not stored in your event stream +* no need for encryption or upcasting of your events + +Cons: +* handling of sensitive data can only be done once per request + +## Example + +A detailed example with a test case can be found in the [`examples/`][examples] directory. + +[examples]: examples/ + +## License +This project is licensed under the MIT License - see the LICENSE file for details diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..90fe3b0 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "broadway/sensitive-data", + "description": "helpers for handling sensitive data with Broadway", + "type": "library", + "license": "MIT", + "require": { + "broadway/broadway": "^0.10" + }, + "require-dev": { + "phpunit/phpunit": "^5.2" + }, + "authors": [ + { + "name": "othillo", + "email": "othillo@othillo.nl" + } + ], + "autoload": { + "psr-4": { + "Broadway\\BroadwaySensitiveData\\EventHandling\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Broadway\\BroadwaySensitiveData\\EventHandling\\": "test/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.2.x-dev" + } + } +} diff --git a/examples/HandlingSensitiveData.php b/examples/HandlingSensitiveData.php new file mode 100644 index 0000000..f83d67c --- /dev/null +++ b/examples/HandlingSensitiveData.php @@ -0,0 +1,138 @@ +apply(new InvitedEvent($invitationId, $name)); + + return $invitation; + } + + /** + * Every aggregate root will expose its id. + * + * {@inheritDoc} + */ + public function getAggregateRootId() + { + return $this->invitationId; + } + + /** + * The "apply" method of the "InvitedEvent" + */ + protected function applyInvitedEvent(InvitedEvent $event) + { + $this->invitationId = $event->invitationId; + } +} + +/** + * A repository that will only store and retrieve Invitation aggregate roots. + * + * This repository uses the base class provided by the EventSourcing component. + */ +class InvitationRepository extends Broadway\EventSourcing\EventSourcingRepository +{ + public function __construct(Broadway\EventStore\EventStoreInterface $eventStore, Broadway\EventHandling\EventBusInterface $eventBus) + { + parent::__construct($eventStore, $eventBus, 'Invitation', new Broadway\EventSourcing\AggregateFactory\PublicConstructorAggregateFactory()); + } +} + +/* + * When using CQRS with commands, a lot of times you will find that you have a + * command object and a "dual" event. Mind though that this is not always the + * case. The following classes show the commands and events for our small + * domain model. + */ + +/* All commands and events below will cary the id of the aggregate root. For + * our convenience and readability later on we provide base classes that hold + * this data. + */ + +class InviteCommand +{ + public $invitationId; + public $name; + public $password; + + public function __construct($invitationId, $name, $password) + { + $this->invitationId = $invitationId; + $this->name = $name; + $this->password = $password; + } +} + +class InvitedEvent +{ + public $invitationId; + public $name; + + public function __construct($invitationId, $name) + { + $this->invitationId = $invitationId; + $this->name = $name; + } +} + +/* + * A command handler will be registered with the command bus and handle the + * commands that are dispatched. The command handler can be seen as a small + * layer between your application code and the actual domain code. + * + * In the end a command handler listens for commands and translates commands to + * method calls on the actual aggregate roots. + */ +class InvitationCommandHandler extends Broadway\CommandHandling\CommandHandler +{ + private $repository; + private $sensitiveDataManager; + + public function __construct( + Broadway\EventSourcing\EventSourcingRepository $repository, + \Broadway\BroadwaySensitiveData\EventHandling\SensitiveDataManager $sensitiveDataManager + ) { + $this->repository = $repository; + $this->sensitiveDataManager = $sensitiveDataManager; + } + + /** + * A new invite aggregate root is created and added to the repository. + */ + protected function handleInviteCommand(InviteCommand $command) + { + $invitation = Invitation::invite($command->invitationId, $command->name); + + $this->sensitiveDataManager->setSensitiveData( + new \Broadway\BroadwaySensitiveData\EventHandling\SensitiveData(['password' => $command->password]) + ); + + $this->repository->save($invitation); + } +} diff --git a/examples/HandlingSensitiveDataTest.php b/examples/HandlingSensitiveDataTest.php new file mode 100644 index 0000000..d478c22 --- /dev/null +++ b/examples/HandlingSensitiveDataTest.php @@ -0,0 +1,84 @@ +commandBus = new SimpleCommandBus(); + $this->eventBus = new TraceableEventBus(new SimpleEventBus()); + + $this->sensitiveDataProcessor = new MySensitiveDataProcessor(); + $sensitiveDataManager = new SensitiveDataManager([$this->sensitiveDataProcessor]); + + $commandHandler = new InvitationCommandHandler( + new InvitationRepository( + new InMemoryEventStore(), + $this->eventBus + ), + $sensitiveDataManager + ); + + $this->commandBus->subscribe($commandHandler); + $this->eventBus->subscribe($sensitiveDataManager); + } + + /** + * @test + */ + public function it_handles_sensitive_data() + { + $this->eventBus->trace(); + + $this->commandBus->dispatch(new InviteCommand('1583c029-de76-40ec-8674-de26767617d2', 'asm89', 'p4ssw0rd')); + + // the event should not contain sensitive data + $this->assertEquals([new InvitedEvent('1583c029-de76-40ec-8674-de26767617d2', 'asm89')], $this->eventBus->getEvents()); + + // the sensitive data should be available for the processor + $this->assertEquals([new SensitiveData(['password' => 'p4ssw0rd'])], $this->sensitiveDataProcessor->getRecordedSensitiveData()); + } +} + +class MySensitiveDataProcessor extends SensitiveDataProcessor +{ + private $recordedSensitiveData = []; + + protected function applyInvitedEvent(InvitedEvent $event, DomainMessage $domainMessage, SensitiveData $data = null) + { + $this->recordedSensitiveData[] = $data; + } + + public function getRecordedSensitiveData() + { + return $this->recordedSensitiveData; + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f77c62c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +Handling sensitive data +======================= + +A small example of an implementation of a small domain model. The example +consists of three files. The first file `HandlingSensitiveData` contains the implementation of +the domain model. The second file `HandlingSensitiveDataTest` contains a PHPUnit test suite +demonstrating the handling of sensitive data. + +The files contain comments about what is happening. + +The PHPUnit tests can be run by changing to this directory and running: + +```bash +$ phpunit . +PHPUnit 5.6.2 by Sebastian Bergmann. + +. + +Time: 22 ms, Memory: 4.00Mb + +OK (1 tests, 2 assertions) +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..3feda81 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./test/ + + + + + ./src/ + + + + + functional + + + diff --git a/src/SensitiveData.php b/src/SensitiveData.php new file mode 100644 index 0000000..3facfca --- /dev/null +++ b/src/SensitiveData.php @@ -0,0 +1,33 @@ +data = $data; + } + + /** + * @return mixed + */ + public function getData() + { + return $this->data; + } +} diff --git a/src/SensitiveDataEventListenerInterface.php b/src/SensitiveDataEventListenerInterface.php new file mode 100644 index 0000000..8d5e4ad --- /dev/null +++ b/src/SensitiveDataEventListenerInterface.php @@ -0,0 +1,19 @@ +subscribe($sensitiveDataProcessor); + } + } + + private function subscribe(SensitiveDataEventListenerInterface $sensitiveDataProcessor) + { + $this->sensitiveDataProcessors[] = $sensitiveDataProcessor; + } + + public function handle(DomainMessage $domainMessage) + { + foreach ($this->sensitiveDataProcessors as $processor) { + $processor->handle($domainMessage, $this->sensitiveData); + } + } + + public function setSensitiveData(SensitiveData $data) + { + $this->sensitiveData = $data; + } +} diff --git a/src/SensitiveDataProcessor.php b/src/SensitiveDataProcessor.php new file mode 100644 index 0000000..2b85269 --- /dev/null +++ b/src/SensitiveDataProcessor.php @@ -0,0 +1,36 @@ +getPayload(); + $method = $this->getApplyMethod($event); + + if (! method_exists($this, $method)) { + return; + } + + $this->$method($event, $domainMessage, $data); + } + + private function getApplyMethod($event) + { + $classParts = explode('\\', get_class($event)); + + return 'apply' . end($classParts); + } +} diff --git a/test/SensitiveDataManagerTest.php b/test/SensitiveDataManagerTest.php new file mode 100644 index 0000000..2a6b296 --- /dev/null +++ b/test/SensitiveDataManagerTest.php @@ -0,0 +1,42 @@ + 'bar']); + + $processor1 = $this->prophesize(SensitiveDataEventListenerInterface::class); + $processor1 + ->handle($domainMessage, $sensitiveData) + ->shouldBeCalled(); + + $processor2 = $this->prophesize(SensitiveDataEventListenerInterface::class); + $processor2 + ->handle($domainMessage, $sensitiveData) + ->shouldBeCalled(); + + $manager = new SensitiveDataManager([$processor1->reveal(), $processor2->reveal()]); + $manager->setSensitiveData($sensitiveData); + $manager->handle($domainMessage); + } +} diff --git a/test/SensitiveDataProcessorTest.php b/test/SensitiveDataProcessorTest.php new file mode 100644 index 0000000..91a93b3 --- /dev/null +++ b/test/SensitiveDataProcessorTest.php @@ -0,0 +1,58 @@ +assertFalse($testProcessor->isCalled()); + + $testProcessor->handle($this->createDomainMessage($testEvent), new SensitiveData(['foo' => 'bar'])); + + $this->assertTrue($testProcessor->isCalled()); + } + + private function createDomainMessage($event) + { + return DomainMessage::recordNow(1, 1, new Metadata([]), $event); + } +} + +class TestProcessor extends SensitiveDataProcessor +{ + private $isCalled = false; + + public function applyTestEvent($event, DomainMessage $domainMessage, SensitiveData $sensitiveData) + { + $this->isCalled = true; + } + + public function isCalled() + { + return $this->isCalled; + } +} + +class TestEvent +{ +} diff --git a/test/SensitiveDataTest.php b/test/SensitiveDataTest.php new file mode 100644 index 0000000..775facd --- /dev/null +++ b/test/SensitiveDataTest.php @@ -0,0 +1,26 @@ + 'bar']; + $sensitiveData = new SensitiveData($data); + + $this->assertEquals($data, $sensitiveData->getData()); + } +}