Skip to content

Commit

Permalink
Merge pull request #405 from quetzyg/feat/audit-redactors
Browse files Browse the repository at this point in the history
[FEAT] Audit redactors
  • Loading branch information
quetzyg committed Apr 9, 2018
2 parents 8e6273d + eeffc10 commit 6164f4f
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 2 deletions.
11 changes: 11 additions & 0 deletions config/audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@

'threshold' => 0,

/*
|--------------------------------------------------------------------------
| Redact Audits
|--------------------------------------------------------------------------
|
| Redact attribute data when auditing?
|
*/

'redact' => false,

/*
|--------------------------------------------------------------------------
| Audit Driver
Expand Down
59 changes: 57 additions & 2 deletions src/Auditable.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
namespace OwenIt\Auditing;

use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use OwenIt\Auditing\Contracts\AuditRedactor;
use OwenIt\Auditing\Contracts\IpAddressResolver;
use OwenIt\Auditing\Contracts\UrlResolver;
use OwenIt\Auditing\Contracts\UserAgentResolver;
Expand Down Expand Up @@ -96,8 +98,8 @@ protected function resolveAuditExclusions()
if (!$this->getAuditTimestamps()) {
array_push($this->excludedAttributes, static::CREATED_AT, static::UPDATED_AT);

if (defined('static::DELETED_AT')) {
$this->excludedAttributes[] = static::DELETED_AT;
if (in_array(SoftDeletes::class, class_uses_recursive($this))) {
$this->excludedAttributes[] = $this->getDeletedAtColumn();
}
}

Expand Down Expand Up @@ -216,6 +218,33 @@ public function readyForAuditing(): bool
return $this->isEventAuditable($this->auditEvent);
}

/**
* Redact attribute value.
*
* @param string $attribute
* @param mixed $value
*
* @throws AuditingException
*
* @return mixed
*/
protected function redactAttributeValue(string $attribute, $value)
{
$auditRedactors = $this->getAuditRedactors();

if (!array_key_exists($attribute, $auditRedactors)) {
return $value;
}

$auditRedactor = $auditRedactors[$attribute];

if (is_subclass_of($auditRedactor, AuditRedactor::class)) {
return call_user_func([$auditRedactor, 'redact'], $value);
}

throw new AuditingException('Invalid AuditRedactor implementation');
}

/**
* {@inheritdoc}
*/
Expand All @@ -239,6 +268,16 @@ public function toAudit(): array

list($old, $new) = $this->$attributeGetter();

if ($this->getAuditRedactors() && Config::get('audit.redact', false)) {
foreach ($old as $attribute => $value) {
$old[$attribute] = $this->redactAttributeValue($attribute, $value);
}

foreach ($new as $attribute => $value) {
$new[$attribute] = $this->redactAttributeValue($attribute, $value);
}
}

$userForeignKey = Config::get('audit.user.foreign_key', 'user_id');

$tags = implode(',', $this->generateTags());
Expand Down Expand Up @@ -503,6 +542,14 @@ public function getAuditThreshold(): int
return $this->auditThreshold ?? Config::get('audit.threshold', 0);
}

/**
* {@inheritdoc}
*/
public function getAuditRedactors(): array
{
return $this->auditRedactors ?? [];
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -534,6 +581,14 @@ public function transitionTo(Contracts\Audit $audit, bool $old = false): Contrac
));
}

// Redacted data should not be used when transitioning states
if ($auditRedactors = $this->getAuditRedactors()) {
throw new AuditableTransitionException(
'Cannot transition states when Audit redactors are set',
$auditRedactors
);
}

// The attribute compatibility between the Audit and the Auditable model must be met
$modified = $audit->getModified();

Expand Down
27 changes: 27 additions & 0 deletions src/Contracts/AuditRedactor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the Laravel Auditing package.
*
* @author Antério Vieira <anteriovieira@gmail.com>
* @author Quetzy Garcia <quetzyg@altek.org>
* @author Raphael França <raphaelfrancabsb@gmail.com>
* @copyright 2015-2018
*
* For the full copyright and license information,
* please view the LICENSE.md file that was distributed
* with this source code.
*/

namespace OwenIt\Auditing\Contracts;

interface AuditRedactor
{
/**
* Redact a value.
*
* @param mixed $value
*
* @return string
*/
public static function redact($value): string;
}
7 changes: 7 additions & 0 deletions src/Contracts/Auditable.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ public function getAuditDriver();
*/
public function getAuditThreshold(): int;

/**
* Get the Audit redactors.
*
* @return array
*/
public function getAuditRedactors(): array;

/**
* Transform the data before performing an audit.
*
Expand Down
32 changes: 32 additions & 0 deletions src/Redactors/LeftRedactor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* This file is part of the Laravel Auditing package.
*
* @author Antério Vieira <anteriovieira@gmail.com>
* @author Quetzy Garcia <quetzyg@altek.org>
* @author Raphael França <raphaelfrancabsb@gmail.com>
* @copyright 2015-2018
*
* For the full copyright and license information,
* please view the LICENSE.md file that was distributed
* with this source code.
*/

namespace OwenIt\Auditing\Redactors;

class LeftRedactor implements \OwenIt\Auditing\Contracts\AuditRedactor
{
/**
* {@inheritdoc}
*/
public static function redact($value): string
{
$total = strlen($value);
$tenth = ceil($total / 10);

// Make sure single character strings get redacted
$length = ($total > $tenth) ? ($total - $tenth) : 1;

return str_pad(substr($value, $length), $total, '#', STR_PAD_LEFT);
}
}
32 changes: 32 additions & 0 deletions src/Redactors/RightRedactor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* This file is part of the Laravel Auditing package.
*
* @author Antério Vieira <anteriovieira@gmail.com>
* @author Quetzy Garcia <quetzyg@altek.org>
* @author Raphael França <raphaelfrancabsb@gmail.com>
* @copyright 2015-2018
*
* For the full copyright and license information,
* please view the LICENSE.md file that was distributed
* with this source code.
*/

namespace OwenIt\Auditing\Redactors;

class RightRedactor implements \OwenIt\Auditing\Contracts\AuditRedactor
{
/**
* {@inheritdoc}
*/
public static function redact($value): string
{
$total = strlen($value);
$tenth = ceil($total / 10);

// Make sure single character strings get redacted
$length = ($total > $tenth) ? ($total - $tenth) : 1;

return str_pad(substr($value, 0, -$length), $total, '#', STR_PAD_RIGHT);
}
}
134 changes: 134 additions & 0 deletions tests/Unit/AuditableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
use OwenIt\Auditing\Exceptions\AuditingException;
use OwenIt\Auditing\Models\Audit;
use OwenIt\Auditing\Redactors\LeftRedactor;
use OwenIt\Auditing\Redactors\RightRedactor;
use OwenIt\Auditing\Tests\Models\Article;
use OwenIt\Auditing\Tests\Models\User;

Expand Down Expand Up @@ -450,6 +452,115 @@ public function itReturnsTheAuditDataIncludingUserAttributes()
], $auditData, true);
}

/**
* @group Auditable::setAuditEvent
* @group Auditable::toAudit
* @test
*/
public function itExcludesAttributesFromTheAuditDataWhenInStrictMode()
{
$this->app['config']->set('audit.strict', true);

$model = factory(Article::class)->make([
'title' => 'How To Audit Eloquent Models',
'content' => 'First step: install the laravel-auditing package.',
'reviewed' => 1,
'published_at' => Carbon::now(),
]);

$model->setHidden([
'reviewed',
]);

$model->setVisible([
'title',
'content',
]);

$model->setAuditEvent('created');

$this->assertCount(10, $auditData = $model->toAudit());

$this->assertArraySubset([
'old_values' => [],
'new_values' => [
'title' => 'How To Audit Eloquent Models',
'content' => 'First step: install the laravel-auditing package.',
],
'event' => 'created',
'auditable_id' => null,
'auditable_type' => Article::class,
'user_id' => null,
'url' => 'console',
'ip_address' => '127.0.0.1',
'user_agent' => 'Symfony/3.X',
'tags' => null,
], $auditData, true);
}

/**
* @group Auditable::setAuditEvent
* @group Auditable::toAudit
* @test
*/
public function itFailsWhenTheAuditRedactorImplementationIsInvalid()
{
$this->expectException(AuditingException::class);
$this->expectExceptionMessage('Invalid AuditRedactor implementation');

$this->app['config']->set('audit.redact', true);

$model = factory(Article::class)->make();

$model->auditRedactors = [
'title' => 'invalidAuditRedactor',
];

$model->setAuditEvent('created');

$model->toAudit();
}

/**
* @group Auditable::setAuditEvent
* @group Auditable::toAudit
* @test
*/
public function itRedactsTheAuditData()
{
$this->app['config']->set('audit.redact', true);

$model = factory(Article::class)->make([
'title' => 'How To Audit Models',
'content' => 'N/A',
'reviewed' => 0,
]);

$model->syncOriginal();

$model->title = 'How To Audit Eloquent Models';
$model->content = 'First step: install the laravel-auditing package.';
$model->reviewed = 1;

$model->setAuditEvent('updated');

$model->auditRedactors = [
'title' => RightRedactor::class,
'content' => LeftRedactor::class,
];

$this->assertArraySubset([
'old_values' => [
'title' => 'Ho#################',
'content' => '##A',
],
'new_values' => [
'title' => 'How#########################',
'content' => '############################################kage.',
],
], $model->toAudit(), true);
}

/**
* @group Auditable::setAuditEvent
* @group Auditable::transformAudit
Expand Down Expand Up @@ -783,6 +894,29 @@ public function itFailsToTransitionWhenTheAuditAuditableIdDoesNotMatchTheModelId
$secondModel->transitionTo($firstAudit);
}

/**
* @group Auditable::transitionTo
* @test
*/
public function itFailsToTransitionWhenAuditRedactorsAreSet()
{
$this->expectException(AuditableTransitionException::class);
$this->expectExceptionMessage('Cannot transition states when Audit redactors are set');

$model = factory(Article::class)->create();

$model->auditRedactors = [
'title' => RightRedactor::class,
];

$audit = factory(Audit::class)->create([
'auditable_id' => $model->getKey(),
'auditable_type' => Article::class,
]);

$model->transitionTo($audit);
}

/**
* @group Auditable::transitionTo
* @test
Expand Down

0 comments on commit 6164f4f

Please sign in to comment.