mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-22 08:13:11 +01:00
Bind TwoFAccounts to Users & Add relevant authorizations with policies
This commit is contained in:
parent
ed3a17a4fb
commit
3c77503fb1
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
44
app/Policies/OwnershipTrait.php
Normal file
44
app/Policies/OwnershipTrait.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
136
app/Policies/TwoFAccountPolicy.php
Normal file
136
app/Policies/TwoFAccountPolicy.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user