Delete the user in a GDPR compatible way (#1151)

* Delete the user in a GDPR compatible way

* Block user from calls

* Style fix
This commit is contained in:
Nabeel S 2021-04-23 10:33:13 -04:00 committed by GitHub
parent f8c7bc31f5
commit 14d0e99a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 145 additions and 18 deletions

View File

@ -0,0 +1,38 @@
<?php
namespace App\Exceptions;
class UserNotFound extends AbstractHttpException
{
public function __construct()
{
parent::__construct(
404,
'User not found'
);
}
/**
* Return the RFC 7807 error type (without the URL root)
*/
public function getErrorType(): string
{
return 'user-not-found';
}
/**
* Get the detailed error string
*/
public function getErrorDetails(): string
{
return $this->getMessage();
}
/**
* Return an array with the error details, merged with the RFC7807 response
*/
public function getErrorMetadata(): array
{
return [];
}
}

View File

@ -252,15 +252,12 @@ class UserController extends Controller
public function destroy($id) public function destroy($id)
{ {
$user = $this->userRepo->findWithoutFail($id); $user = $this->userRepo->findWithoutFail($id);
if (empty($user)) { if (empty($user)) {
Flash::error('User not found'); Flash::error('User not found');
return redirect(route('admin.users.index')); return redirect(route('admin.users.index'));
} }
$this->userRepo->delete($id); $this->userSvc->removeUser($user);
Flash::success('User deleted successfully.'); Flash::success('User deleted successfully.');
return redirect(route('admin.users.index')); return redirect(route('admin.users.index'));

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Contracts\Controller; use App\Contracts\Controller;
use App\Exceptions\UserNotFound;
use App\Http\Resources\Bid as BidResource; use App\Http\Resources\Bid as BidResource;
use App\Http\Resources\Pirep as PirepResource; use App\Http\Resources\Pirep as PirepResource;
use App\Http\Resources\Subfleet as SubfleetResource; use App\Http\Resources\Subfleet as SubfleetResource;
@ -91,6 +92,10 @@ class UserController extends Controller
public function get($id) public function get($id)
{ {
$user = $this->userSvc->getUser($id); $user = $this->userSvc->getUser($id);
if ($user === null) {
throw new UserNotFound();
}
return new UserResource($user); return new UserResource($user);
} }
@ -108,6 +113,9 @@ class UserController extends Controller
{ {
$user_id = $this->getUserId($request); $user_id = $this->getUserId($request);
$user = $this->userSvc->getUser($user_id); $user = $this->userSvc->getUser($user_id);
if ($user === null) {
throw new UserNotFound();
}
// Add a bid // Add a bid
if ($request->isMethod('PUT') || $request->isMethod('POST')) { if ($request->isMethod('PUT') || $request->isMethod('POST')) {
@ -146,6 +154,10 @@ class UserController extends Controller
public function fleet(Request $request) public function fleet(Request $request)
{ {
$user = $this->userRepo->find($this->getUserId($request)); $user = $this->userRepo->find($this->getUserId($request));
if ($user === null) {
throw new UserNotFound();
}
$subfleets = $this->userSvc->getAllowableSubfleets($user); $subfleets = $this->userSvc->getAllowableSubfleets($user);
return SubfleetResource::collection($subfleets); return SubfleetResource::collection($subfleets);

View File

@ -24,6 +24,7 @@ use Carbon\Carbon;
* @property int status * @property int status
* @property int state * @property int state
* @property Carbon landing_time * @property Carbon landing_time
* @property float fuel_onboard
*/ */
class Aircraft extends Model class Aircraft extends Model
{ {

View File

@ -11,6 +11,7 @@ class UserState extends Enum
public const REJECTED = 2; public const REJECTED = 2;
public const ON_LEAVE = 3; public const ON_LEAVE = 3;
public const SUSPENDED = 4; public const SUSPENDED = 4;
public const DELETED = 5;
protected static $labels = [ protected static $labels = [
self::PENDING => 'user.state.pending', self::PENDING => 'user.state.pending',
@ -18,5 +19,6 @@ class UserState extends Enum
self::REJECTED => 'user.state.rejected', self::REJECTED => 'user.state.rejected',
self::ON_LEAVE => 'user.state.on_leave', self::ON_LEAVE => 'user.state.on_leave',
self::SUSPENDED => 'user.state.suspended', self::SUSPENDED => 'user.state.suspended',
self::DELETED => 'user.state.deleted',
]; ];
} }

View File

@ -38,6 +38,8 @@ use Laratrust\Traits\LaratrustUserTrait;
* @property string last_pirep_id * @property string last_pirep_id
* @property Pirep last_pirep * @property Pirep last_pirep
* @property UserFieldValue[] fields * @property UserFieldValue[] fields
* @property Role[] roles
* @property Subfleet[] subfleets
* *
* @mixin \Illuminate\Database\Eloquent\Builder * @mixin \Illuminate\Database\Eloquent\Builder
* @mixin \Illuminate\Notifications\Notifiable * @mixin \Illuminate\Notifications\Notifiable

View File

@ -46,7 +46,7 @@ class EventHandler extends Listener
* *
* @param \App\Contracts\Notification $notification * @param \App\Contracts\Notification $notification
*/ */
protected function notifyAdmins($notification) protected function notifyAdmins(\App\Contracts\Notification $notification)
{ {
$admin_users = User::whereRoleIs('admin')->get(); $admin_users = User::whereRoleIs('admin')->get();
@ -67,8 +67,12 @@ class EventHandler extends Listener
* @param User $user * @param User $user
* @param \App\Contracts\Notification $notification * @param \App\Contracts\Notification $notification
*/ */
protected function notifyUser($user, $notification) protected function notifyUser(User $user, \App\Contracts\Notification $notification)
{ {
if ($user->state === UserState::DELETED) {
return;
}
try { try {
$user->notify($notification); $user->notify($notification);
} catch (Exception $e) { } catch (Exception $e) {
@ -90,7 +94,7 @@ class EventHandler extends Listener
} }
/** @var Collection $users */ /** @var Collection $users */
$users = User::where($where)->get(); $users = User::where($where)->where('state', '<>', UserState::DELETED)->get();
if (empty($users) || $users->count() === 0) { if (empty($users) || $users->count() === 0) {
return; return;
} }

View File

@ -147,6 +147,7 @@ class PirepService extends Service
$dupe_pirep = $this->findDuplicate($pirep); $dupe_pirep = $this->findDuplicate($pirep);
if ($dupe_pirep !== false) { if ($dupe_pirep !== false) {
$pirep = $dupe_pirep; $pirep = $dupe_pirep;
Log::info('Found duplicate PIREP, id='.$dupe_pirep->id);
if ($pirep->cancelled) { if ($pirep->cancelled) {
throw new \App\Exceptions\PirepCancelled($pirep); throw new \App\Exceptions\PirepCancelled($pirep);
} }
@ -293,9 +294,11 @@ class PirepService extends Service
$time_limit = Carbon::now('UTC')->subMinutes($minutes)->toDateTimeString(); $time_limit = Carbon::now('UTC')->subMinutes($minutes)->toDateTimeString();
$where = [ $where = [
'user_id' => $pirep->user_id, 'user_id' => $pirep->user_id,
'airline_id' => $pirep->airline_id, 'airline_id' => $pirep->airline_id,
'flight_number' => $pirep->flight_number, 'flight_number' => $pirep->flight_number,
'dpt_airport_id' => $pirep->dpt_airport_id,
'arr_airport_id' => $pirep->arr_airport_id,
]; ];
if (filled($pirep->route_code)) { if (filled($pirep->route_code)) {

View File

@ -9,12 +9,14 @@ use App\Events\UserStatsChanged;
use App\Exceptions\PilotIdNotFound; use App\Exceptions\PilotIdNotFound;
use App\Exceptions\UserPilotIdExists; use App\Exceptions\UserPilotIdExists;
use App\Models\Airline; use App\Models\Airline;
use App\Models\Bid;
use App\Models\Enums\PirepState; use App\Models\Enums\PirepState;
use App\Models\Enums\UserState; use App\Models\Enums\UserState;
use App\Models\Pirep; use App\Models\Pirep;
use App\Models\Rank; use App\Models\Rank;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Models\UserFieldValue;
use App\Repositories\AircraftRepository; use App\Repositories\AircraftRepository;
use App\Repositories\AirlineRepository; use App\Repositories\AirlineRepository;
use App\Repositories\SubfleetRepository; use App\Repositories\SubfleetRepository;
@ -23,6 +25,7 @@ use App\Support\Units\Time;
use App\Support\Utils; use App\Support\Utils;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use function is_array; use function is_array;
@ -60,14 +63,19 @@ class UserService extends Service
* *
* @param $user_id * @param $user_id
* *
* @return User * @return User|null
*/ */
public function getUser($user_id): User public function getUser($user_id): ?User
{ {
/** @var User $user */
$user = $this->userRepo $user = $this->userRepo
->with(['airline', 'bids', 'rank']) ->with(['airline', 'bids', 'rank'])
->find($user_id); ->find($user_id);
if ($user->state === UserState::DELETED) {
return null;
}
// Load the proper subfleets to the rank // Load the proper subfleets to the rank
$user->rank->subfleets = $this->getAllowableSubfleets($user); $user->rank->subfleets = $this->getAllowableSubfleets($user);
$user->subfleets = $user->rank->subfleets; $user->subfleets = $user->rank->subfleets;
@ -117,6 +125,33 @@ class UserService extends Service
return $user; return $user;
} }
/**
* Remove the user. But don't actually delete them - set the name to deleted, email to
* something random
*
* @param User $user
*
* @throws \Exception
*/
public function removeUser(User $user)
{
$user->name = 'Deleted User';
$user->email = Utils::generateApiKey().'@deleted-user.com';
$user->api_key = Utils::generateApiKey();
$user->password = Hash::make(Utils::generateApiKey());
$user->state = UserState::DELETED;
$user->save();
// Detach all roles from this user
$user->detachRoles($user->roles);
// Delete any fields which might have personal information
UserFieldValue::where('user_id', $user->id)->delete();
// Remove any bids
Bid::where('user_id', $user->id)->delete();
}
/** /**
* Add a user to a given role * Add a user to a given role
* *
@ -125,7 +160,7 @@ class UserService extends Service
* *
* @return User * @return User
*/ */
public function addUserToRole(User $user, $roleName): User public function addUserToRole(User $user, string $roleName): User
{ {
$role = Role::where(['name' => $roleName])->first(); $role = Role::where(['name' => $roleName])->first();
$user->attachRole($role); $user->attachRole($role);

View File

@ -8,5 +8,6 @@ return [
'rejected' => 'Rejected', 'rejected' => 'Rejected',
'on_leave' => 'On Leave', 'on_leave' => 'On Leave',
'suspended' => 'Suspended', 'suspended' => 'Suspended',
'deleted' => 'Deleted',
], ],
]; ];

View File

@ -8,5 +8,6 @@ return [
'rejected' => 'Rechazado', 'rejected' => 'Rechazado',
'on_leave' => 'De vacaciones', 'on_leave' => 'De vacaciones',
'suspended' => 'Suspendido', 'suspended' => 'Suspendido',
'deleted' => 'Borrar',
], ],
]; ];

View File

@ -8,5 +8,6 @@ return [
'rejected' => 'Rifiutato', 'rejected' => 'Rifiutato',
'on_leave' => 'In Ferie', 'on_leave' => 'In Ferie',
'suspended' => 'Sospeso', 'suspended' => 'Sospeso',
'deleted' => 'Cancellato',
], ],
]; ];

View File

@ -8,5 +8,6 @@ return [
'rejected' => 'Rejeitado', 'rejected' => 'Rejeitado',
'on_leave' => 'Em licença', 'on_leave' => 'Em licença',
'suspended' => 'Suspensa', 'suspended' => 'Suspensa',
'deleted' => 'Excluído',
], ],
]; ];

View File

@ -418,20 +418,26 @@ class AcarsTest extends TestCase
/** /**
* Post a PIREP into a PREFILE state and post ACARS * Post a PIREP into a PREFILE state and post ACARS
*
* @throws \Exception
*/ */
public function testAcarsUpdates() public function testAcarsUpdates()
{ {
$subfleet = $this->createSubfleetWithAircraft(2); $subfleet = $this->createSubfleetWithAircraft(2);
$rank = $this->createRank(10, [$subfleet['subfleet']->id]); $rank = $this->createRank(10, [$subfleet['subfleet']->id]);
$this->user = factory(User::class)->create( /** @var User user */
[ $this->user = factory(User::class)->create([
'rank_id' => $rank->id, 'rank_id' => $rank->id,
] ]);
);
/** @var Airport $airport */
$airport = factory(Airport::class)->create(); $airport = factory(Airport::class)->create();
/** @var Airline $airline */
$airline = factory(Airline::class)->create(); $airline = factory(Airline::class)->create();
/** @var Aircraft $aircraft */
$aircraft = $subfleet['aircraft']->random(); $aircraft = $subfleet['aircraft']->random();
$uri = '/api/pireps/prefile'; $uri = '/api/pireps/prefile';

View File

@ -311,6 +311,29 @@ class UserTest extends TestCase
$this->assertEquals(4, $user3->pilot_id); $this->assertEquals(4, $user3->pilot_id);
} }
public function testUserPilotDeleted()
{
$new_user = factory(User::class)->make()->toArray();
$new_user['password'] = Hash::make('secret');
$admin_user = $this->userSvc->createUser($new_user);
$new_user = factory(User::class)->make()->toArray();
$new_user['password'] = Hash::make('secret');
$user = $this->userSvc->createUser($new_user);
$this->assertEquals($user->id, $user->pilot_id);
// Delete the user
$this->userSvc->removeUser($user);
$response = $this->get('/api/user/'.$user->id, [], $admin_user);
$response->assertStatus(404);
// Get from the DB
$user = User::find($user->id);
$this->assertEquals('Deleted User', $user->name);
$this->assertNotEquals($new_user['password'], $user->password);
}
/** /**
* Test that a user's name is private * Test that a user's name is private
*/ */