Skip to content

Commit

Permalink
Add anonymizer feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mazsudo committed Mar 4, 2020
1 parent ad86f0b commit a1cdb88
Show file tree
Hide file tree
Showing 30 changed files with 1,877 additions and 0 deletions.
82 changes: 82 additions & 0 deletions Annotations/AnonymizedEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

/*
* This file is part of the ekino/data-protection-bundle project.
*
* (c) Ekino
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ekino\DataProtectionBundle\Annotations;

/**
* Class AnonymizedEntity.
*
* @Annotation
* @Target({"CLASS"})
*
* @author Benoit Mazière <[email protected]>
*/
final class AnonymizedEntity
{
public const ACTION_ANONYMIZE = 'anonymize';
public const ACTION_TRUNCATE = 'truncate';
private const ACTION_CHOICES = [self::ACTION_ANONYMIZE, self::ACTION_TRUNCATE];

/**
* @var string
*/
private $action = self::ACTION_ANONYMIZE;

/**
* Add where sql condition on which not apply anonymization.
*
* @var string|null
*/
private $exceptWhereClause;

public function __construct(iterable $options)
{
foreach ($options as $key => $value) {
if (!property_exists($this, $key)) {
throw new \InvalidArgumentException(sprintf('Option "%s" does not exist', $key));
}

$this->$key = $value;
}

$this->validateAction();
}

public function getAction(): string
{
return $this->action;
}

public function getExceptWhereClause(): ?string
{
return $this->exceptWhereClause;
}

public function isTruncateAction(): bool
{
return static::ACTION_TRUNCATE === $this->action;
}

public function isAnonymizeAction(): bool
{
return static::ACTION_ANONYMIZE === $this->action;
}

private function validateAction(): void
{
if (!\in_array($this->action, static::ACTION_CHOICES, true)) {
throw new \InvalidArgumentException(sprintf('Action "%s" is not allowed. Allowed actions are: %s',
$this->action, implode(', ', static::ACTION_CHOICES)));
}
}
}
136 changes: 136 additions & 0 deletions Annotations/AnonymizedProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

/*
* This file is part of the ekino/data-protection-bundle project.
*
* (c) Ekino
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ekino\DataProtectionBundle\Annotations;

/**
* Class AnonymizedProperty.
*
* @Annotation
* @Target({"PROPERTY"})
*
* @author Benoit Mazière <[email protected]>
*/
final class AnonymizedProperty
{
public const TYPE_STATIC = 'static';
public const TYPE_COMPOSED = 'composed';
public const TYPE_EXPRESSION = 'expression';
private const TYPE_CHOICES = [self::TYPE_STATIC, self::TYPE_COMPOSED, self::TYPE_EXPRESSION];

/**
* @var mixed|null
*/
private $value;

/**
* Can be of type static (fixed value) or composed (mix of static & existing field value).
*
* @var string
*/
private $type = self::TYPE_STATIC;

/**
* @var string
*/
private $fieldName;

/**
* @var string
*/
private $columnName;

public function __construct(iterable $options)
{
foreach ($options as $key => $value) {
if (!property_exists($this, $key)) {
throw new \InvalidArgumentException(sprintf('Option "%s" does not exist', $key));
}

$this->$key = $value;
}

$this->validateType();
}

public function getValue()
{
return $this->value;
}

public function getType(): string
{
return $this->type;
}

public function getFieldName(): string
{
return $this->fieldName;
}

public function setFieldName(string $fieldName): self
{
$this->fieldName = $fieldName;

return $this;
}

public function getColumnName(): string
{
return $this->columnName;
}

public function setColumnName(string $columnName): self
{
$this->columnName = $columnName;

return $this;
}

public function isStatic(): bool
{
return static::TYPE_STATIC === $this->type;
}

public function isComposed(): bool
{
return static::TYPE_COMPOSED === $this->type;
}

public function isExpression(): bool
{
return static::TYPE_EXPRESSION === $this->type;
}

public function extractComposedFieldFromValue(): string
{
preg_match('/<(\w*)>/', $this->value, $matches);

return $matches[1] ?? '';
}

public function explodeComposedFieldValue(): array
{
preg_match('/(.*)<(\w*)>(.*)/', $this->value, $matches);

return $matches ?? [];
}

private function validateType(): void
{
if (!\in_array($this->type, static::TYPE_CHOICES, true)) {
throw new \InvalidArgumentException(sprintf('Type "%s" is not allowed. Allowed types are: %s',
$this->type, implode(', ', static::TYPE_CHOICES)));
}
}
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ master
* Drop support for PHP 7.1
* Add PHP 7.4 in CI
* Upgrade PhpUnit to 8
* Add command to anonymize database through annotation configuration

v1.1.0
------
Expand Down
127 changes: 127 additions & 0 deletions Command/AnonymizeDataCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

/*
* This file is part of the ekino/data-protection-bundle project.
*
* (c) Ekino
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Ekino\DataProtectionBundle\Command;

use Ekino\DataProtectionBundle\Meta\AnonymizedMetadataBuilder;
use Ekino\DataProtectionBundle\Meta\AnonymizedMetadataValidator;
use Ekino\DataProtectionBundle\QueryBuilder\AnonymizedQueryBuilder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

/**
* Class AnonymizeDataCommand
*
* @author Benoit Mazière <[email protected]>
*/
final class AnonymizeDataCommand extends Command
{
/**
* {@inheritdoc}
*/
protected static $defaultName = 'ekino-data-protection:anonymize';

protected $anonymizedMetadataBuilder;

protected $anonymizedMetadataValidator;

protected $anonymizedQueryBuilder;

public function __construct(
AnonymizedMetadataBuilder $anonymizedMetadataBuilder,
AnonymizedMetadataValidator $anonymizedMetadataValidator,
AnonymizedQueryBuilder $anonymizedQueryBuilder
)
{
parent::__construct();

$this->anonymizedMetadataBuilder = $anonymizedMetadataBuilder;
$this->anonymizedMetadataValidator = $anonymizedMetadataValidator;
$this->anonymizedQueryBuilder = $anonymizedQueryBuilder;
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this->setDescription('Anonymize database based on entities annotations')
->addOption('force', null, InputOption::VALUE_NONE, 'Set this parameter to execute this action')
->setHelp('Usage: `bin/console ekino-data-protection:anonymize`')
;
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln(sprintf('<info>Anonymization starts</info>'));

$anonymizedMetadatas = $this->anonymizedMetadataBuilder->build();
$queries = [];

foreach ($anonymizedMetadatas as $anonymizedMetadata) {
$this->anonymizedMetadataValidator->validate($anonymizedMetadata);
$queries[] = $this->anonymizedQueryBuilder->buildQuery($anonymizedMetadata);
}

if (!$input->getOption('force')) {
$output->writeln('<error>ATTENTION:</error> This operation should not be executed in a production environment.');
$output->writeln('');
$output->writeln('<info>Would annoymize your database according to your configuration.</info>');
$output->writeln('Please run the operation with --force to execute');
$output->writeln('<error>Some data will be lost/anonymized!</error>');

$this->displayQueries($queries, $output);

return 0;
}

$question = 'Are you sure you wish to continue & anonymize your database? (y/n)';

if (! $this->canExecute($question, $input, $output)) {
$output->writeln('<error>Anonymization cancelled!</error>');

return 1;
}

$this->displayQueries($queries, $output);
// @todo execute queries
$output->writeln(sprintf('<info>Anonymization ends</info>'));

return 0;
}

private function displayQueries(array $queries, OutputInterface $output): void
{
$output->writeln('<error>Following queries have been built and will be executed:</error>');

foreach ($queries as $query) {
$output->writeln(sprintf('<info>%s</info>', $query));
}
}

private function askConfirmation(string $question, InputInterface $input, OutputInterface $output): bool
{
return $this->getHelper('question')->ask($input, $output, new ConfirmationQuestion($question));
}

private function canExecute(string $question, InputInterface $input, OutputInterface $output ): bool
{
return ! $input->isInteractive() || $this->askConfirmation($question, $input, $output);
}
}
Loading

0 comments on commit a1cdb88

Please sign in to comment.