Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anonymizer feature #27

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
mazsudo marked this conversation as resolved.
Show resolved Hide resolved
->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