Bind TwoFAccounts to Users & Add relevant authorizations with policies

This commit is contained in:
Bubka 2023-02-22 20:21:36 +01:00
parent ed3a17a4fb
commit 3c77503fb1
10 changed files with 325 additions and 30 deletions

View File

@ -17,6 +17,7 @@
use App\Facades\TwoFAccounts;
use App\Http\Controllers\Controller;
use App\Models\TwoFAccount;
use App\Helpers\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
@ -29,7 +30,7 @@ class TwoFAccountController extends Controller
*/
public function index(Request $request)
{
return new TwoFAccountCollection(TwoFAccount::ordered()->get());
return new TwoFAccountCollection($request->user()->twofaccounts->sortBy('order_column'));
}
/**
@ -40,6 +41,8 @@ public function index(Request $request)
*/
public function show(TwoFAccount $twofaccount)
{
$this->authorize('view', $twofaccount);
return new TwoFAccountReadResource($twofaccount);
}
@ -51,6 +54,8 @@ public function show(TwoFAccount $twofaccount)
*/
public function store(TwoFAccountDynamicRequest $request)
{
$this->authorize('create', TwoFAccount::class);
// Two possible cases :
// - The most common case, an URI is provided by the QuickForm, thanks to a QR code live scan or file upload
// -> We use that URI to define the account
@ -65,7 +70,7 @@ public function store(TwoFAccountDynamicRequest $request)
} else {
$twofaccount->fillWithOtpParameters($validated);
}
$twofaccount->save();
$request->user()->twofaccounts()->save($twofaccount);
// Possible group association
Groups::assign($twofaccount->id);
@ -84,10 +89,12 @@ public function store(TwoFAccountDynamicRequest $request)
*/
public function update(TwoFAccountUpdateRequest $request, TwoFAccount $twofaccount)
{
$this->authorize('update', $twofaccount);
$validated = $request->validated();
$twofaccount->fillWithOtpParameters($validated);
$twofaccount->save();
$request->user()->twofaccounts()->save($twofaccount);
return (new TwoFAccountReadResource($twofaccount))
->response()
@ -125,6 +132,9 @@ public function reorder(TwoFAccountReorderRequest $request)
{
$validated = $request->validated();
$twofaccounts = TwoFAccount::whereIn('id', $validated['orderedIds'])->get();
$this->authorize('updateEach', [$twofaccounts[0], $twofaccounts]);
TwoFAccount::setNewOrder($validated['orderedIds']);
return response()->json(['message' => 'order saved'], 200);
@ -161,7 +171,10 @@ public function export(TwoFAccountBatchRequest $request)
], 400);
}
return new TwoFAccountExportCollection(TwoFAccounts::export($validated['ids']));
$twofaccounts = TwoFAccounts::export($validated['ids']);
$this->authorize('viewEach', [$twofaccounts[0], $twofaccounts]);
return new TwoFAccountExportCollection($twofaccounts);
}
/**
@ -178,6 +191,7 @@ public function otp(Request $request, $id = null)
// The request input is the ID of an existing account
if ($id) {
$twofaccount = TwoFAccount::findOrFail((int) $id);
$this->authorize('view', $twofaccount);
}
// The request input is an uri
@ -213,7 +227,7 @@ public function otp(Request $request, $id = null)
*/
public function count(Request $request)
{
return response()->json(['count' => TwoFAccount::count()], 200);
return response()->json(['count' => $request->user()->twofaccounts->count()], 200);
}
/**
@ -233,7 +247,12 @@ public function withdraw(TwoFAccountBatchRequest $request)
], 400);
}
TwoFAccounts::withdraw($validated['ids']);
$ids = Helpers::commaSeparatedToArray($validated['ids']);
$twofaccounts = TwoFAccount::whereIn('id', $ids)->get();
$this->authorize('updateEach', [$twofaccounts[0], $twofaccounts]);
TwoFAccounts::withdraw($ids);
return response()->json(['message' => 'accounts withdrawn'], 200);
}
@ -246,6 +265,8 @@ public function withdraw(TwoFAccountBatchRequest $request)
*/
public function destroy(TwoFAccount $twofaccount)
{
$this->authorize('delete', $twofaccount);
TwoFAccounts::delete($twofaccount->id);
return response()->json(null, 204);
@ -268,6 +289,11 @@ public function batchDestroy(TwoFAccountBatchRequest $request)
], 400);
}
$ids = Helpers::commaSeparatedToArray($validated['ids']);
$twofaccounts = TwoFAccount::whereIn('id', $ids)->get();
$this->authorize('deleteEach', [$twofaccounts[0], $twofaccounts]);
TwoFAccounts::delete($validated['ids']);
return response()->json(null, 204);

View File

@ -5,6 +5,14 @@
use App\Services\TwoFAccountService;
use Illuminate\Support\Facades\Facade;
/**
* @method static void withdraw(int|array|string $ids)
* @method \Illuminate\Support\Collection<int|string, \App\Models\TwoFAccount> migrate(string $migrationPayload)
* @method static \Illuminate\Support\Collection<int, \App\Models\TwoFAccount> export(int|array|string $ids)
* @method static int delete(int|array|string $ids)
*
* @see \App\Services\TwoFAccountService
*/
class TwoFAccounts extends Facade
{
protected static function getFacadeAccessor()

View File

@ -26,4 +26,21 @@ public static function PadToBase32Format(?string $str) : string
{
return blank($str) ? '' : strtoupper(str_pad($str, (int) ceil(strlen($str) / 8) * 8, '='));
}
/**
* Identify comma separated list of values and explode it to an array of values
*
* @param mixed $ids
*/
public static function commaSeparatedToArray($ids) : mixed
{
if (is_string($ids)) {
$regex = "/^\d+(,{1}\d+)*$/";
if (preg_match($regex, $ids)) {
$ids = explode(',', $ids);
}
}
return $ids;
}
}

View File

@ -695,4 +695,9 @@ private function encryptOrReturn(mixed $value) : mixed
// should be replaced by laravel 8 attribute encryption casting
return Settings::get('useEncryption') ? Crypt::encryptString($value) : $value;
}
public function buildSortQuery()
{
return static::query()->where('user_id', $this->user_id);
}
}

View File

@ -10,7 +10,6 @@
use Illuminate\Support\Facades\Log;
use Laragear\WebAuthn\WebAuthnAuthentication;
use Laravel\Passport\HasApiTokens;
use Illuminate\Support\Arr;
class User extends Authenticatable implements WebAuthnAuthenticatable
{
@ -42,8 +41,9 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
* @var array<string,string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'email_verified_at' => 'datetime',
'is_admin' => 'boolean',
'twofaccounts_count' => 'integer',
];
/**
@ -97,4 +97,14 @@ static function ($query) use ($id) {
}
)->first();
}
/**
* Get the TwoFAccounts of the user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TwoFAccount>
*/
public function twofaccounts()
{
return $this->hasMany(\App\Models\TwoFAccount::class);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Contracts\Support\Arrayable;
trait OwnershipTrait
{
/**
* Ownership of single item condition
*
* @param \App\Models\User $user
* @param mixed $item
* @return bool
*/
protected function isOwnerOf(User $user, mixed $item)
{
return $user->id === $item->user_id;
}
/**
* Ownership of collection condition
*
* @template TKey of array-key
* @template TValue
*
* @param \App\Models\User $user
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return bool
*/
protected function isOwnerOfEach(User $user, $items)
{
foreach ($items as $item) {
if (! $this->isOwnerOf($user, $item)) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Policies;
use App\Models\TwoFAccount;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Auth\Access\HandlesAuthorization;
class TwoFAccountPolicy
{
use HandlesAuthorization, OwnershipTrait;
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function viewAny(User $user)
{
return false;
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool
*/
public function view(User $user, TwoFAccount $twofaccount)
{
return $this->isOwnerOf($user, $twofaccount);
}
/**
* Determine whether the user can view all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @param \Illuminate\Support\Collection<int, \App\Models\TwoFAccount> $twofaccounts
* @return \Illuminate\Auth\Access\Response|bool
*/
public function viewEach(User $user, TwoFAccount $twofaccount, $twofaccounts)
{
return $this->isOwnerOfEach($user, $twofaccounts);
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
return true;
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user, TwoFAccount $twofaccount)
{
return $this->isOwnerOf($user, $twofaccount);
// ? Response::allow()
// : Response::deny('You do not own this post.');
}
/**
* Determine whether the user can update all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @param \Illuminate\Support\Collection<int, \App\Models\TwoFAccount> $twofaccounts
* @return \Illuminate\Auth\Access\Response|bool
*/
public function updateEach(User $user, TwoFAccount $twofaccount, $twofaccounts)
{
return $this->isOwnerOfEach($user, $twofaccounts);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user, TwoFAccount $twofaccount)
{
return $this->isOwnerOf($user, $twofaccount);
}
/**
* Determine whether the user can delete all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @param \Illuminate\Support\Collection<int, \App\Models\TwoFAccount> $twofaccounts
* @return \Illuminate\Auth\Access\Response|bool
*/
public function deleteEach(User $user, TwoFAccount $twofaccount, $twofaccounts)
{
return $this->isOwnerOfEach($user, $twofaccounts);
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool
*/
public function restore(User $user, TwoFAccount $twofaccount)
{
return $this->isOwnerOf($user, $twofaccount);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\TwoFAccount $twofaccount
* @return \Illuminate\Auth\Access\Response|bool
*/
public function forceDelete(User $user, TwoFAccount $twofaccount)
{
return $this->isOwnerOf($user, $twofaccount);
}
}

View File

@ -5,6 +5,8 @@
use App\Extensions\RemoteUserProvider;
use App\Extensions\WebauthnCredentialBroker;
use App\Facades\Settings;
use App\Models\TwoFAccount;
use App\Policies\TwoFAccountPolicy;
use App\Services\Auth\ReverseProxyGuard;
use Illuminate\Auth\Passwords\DatabaseTokenRepository;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -20,7 +22,7 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
TwoFAccount::class => TwoFAccountPolicy::class,
];
/**

View File

@ -4,6 +4,7 @@
use App\Factories\MigratorFactoryInterface;
use App\Models\TwoFAccount;
use App\Helpers\Helpers;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@ -31,7 +32,7 @@ public static function withdraw($ids) : void
{
// $ids as string could be a comma-separated list of ids
// so in this case we explode the string to an array
$ids = self::commaSeparatedToArray($ids);
$ids = Helpers::commaSeparatedToArray($ids);
// whereIn() expects an array
$ids = is_array($ids) ? $ids : func_get_args();
@ -66,7 +67,7 @@ public function migrate(string $migrationPayload) : Collection
*/
public static function export($ids) : Collection
{
$ids = self::commaSeparatedToArray($ids);
$ids = Helpers::commaSeparatedToArray($ids);
$twofaccounts = TwoFAccount::whereIn('id', $ids)->get();
return $twofaccounts;
@ -82,7 +83,7 @@ public static function delete($ids) : int
{
// $ids as string could be a comma-separated list of ids
// so in this case we explode the string to an array
$ids = self::commaSeparatedToArray($ids);
$ids = Helpers::commaSeparatedToArray($ids);
Log::info(sprintf('Deletion of TwoFAccounts #%s requested', is_array($ids) ? implode(',#', $ids) : $ids));
$deleted = TwoFAccount::destroy($ids);
@ -117,20 +118,4 @@ private static function markAsDuplicate(Collection $twofaccounts) : Collection
return $twofaccounts;
}
/**
* Explode a comma separated list of IDs to an array of IDs
*
* @param int|array|string $ids
*/
private static function commaSeparatedToArray($ids) : mixed
{
if (is_string($ids)) {
$regex = "/^\d+(,{1}\d+)*$/";
if (preg_match($regex, $ids)) {
$ids = explode(',', $ids);
}
}
return $ids;
}
}

View File

@ -0,0 +1,62 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('twofaccounts', function (Blueprint $table) {
$table->unsignedInteger('user_id')
->after('id')
->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
Schema::table('groups', function (Blueprint $table) {
$table->unsignedInteger('user_id')
->after('id')
->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
if ($legacySingleUser = DB::table('users')->first()) {
DB::table('twofaccounts')->update(['user_id' => $legacySingleUser->id]);
DB::table('groups')->update(['user_id' => $legacySingleUser->id]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::whenTableHasColumn('twofaccounts', 'user_id', function (Blueprint $table) {
// cannot drop foreign keys in SQLite:
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign(['user_id']);
}
$table->dropColumn('user_id');
});
Schema::whenTableHasColumn('groups', 'user_id', function (Blueprint $table) {
// cannot drop foreign keys in SQLite:
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign(['user_id']);
}
$table->dropColumn('user_id');
});
}
};