Bind Groups to Users & Add relevant authorizations with policies

This commit is contained in:
Bubka 2023-02-23 16:40:53 +01:00
parent 3c77503fb1
commit e0f0afc505
9 changed files with 200 additions and 48 deletions

View File

@ -9,6 +9,7 @@
use App\Facades\Groups; use App\Facades\Groups;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Group; use App\Models\Group;
use Illuminate\Http\Request;
class GroupController extends Controller class GroupController extends Controller
{ {
@ -17,9 +18,9 @@ class GroupController extends Controller
* *
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/ */
public function index() public function index(Request $request)
{ {
$groups = Groups::getAll(); $groups = Groups::prependTheAllGroup($request->user()->groups, $request->user()->id);
return GroupResource::collection($groups); return GroupResource::collection($groups);
} }
@ -32,9 +33,11 @@ public function index()
*/ */
public function store(GroupStoreRequest $request) public function store(GroupStoreRequest $request)
{ {
$this->authorize('create', Group::class);
$validated = $request->validated(); $validated = $request->validated();
$group = Groups::create($validated); $group = $request->user()->groups()->create($validated);
return (new GroupResource($group)) return (new GroupResource($group))
->response() ->response()
@ -49,6 +52,8 @@ public function store(GroupStoreRequest $request)
*/ */
public function show(Group $group) public function show(Group $group)
{ {
$this->authorize('view', $group);
return new GroupResource($group); return new GroupResource($group);
} }
@ -61,6 +66,8 @@ public function show(Group $group)
*/ */
public function update(GroupStoreRequest $request, Group $group) public function update(GroupStoreRequest $request, Group $group)
{ {
$this->authorize('update', $group);
$validated = $request->validated(); $validated = $request->validated();
Groups::update($group, $validated); Groups::update($group, $validated);
@ -77,6 +84,8 @@ public function update(GroupStoreRequest $request, Group $group)
*/ */
public function assignAccounts(GroupAssignRequest $request, Group $group) public function assignAccounts(GroupAssignRequest $request, Group $group)
{ {
$this->authorize('update', $group);
$validated = $request->validated(); $validated = $request->validated();
Groups::assign($validated['ids'], $group); Groups::assign($validated['ids'], $group);
@ -85,16 +94,16 @@ public function assignAccounts(GroupAssignRequest $request, Group $group)
} }
/** /**
* Get accounts assign to the group * Get accounts assigned to the group
* *
* @param \App\Models\Group $group * @param \App\Models\Group $group
* @return \App\Api\v1\Resources\TwoFAccountCollection * @return \App\Api\v1\Resources\TwoFAccountCollection
*/ */
public function accounts(Group $group) public function accounts(Group $group)
{ {
$twofaccounts = Groups::getAccounts($group); $this->authorize('view', $group);
return new TwoFAccountCollection($twofaccounts); return new TwoFAccountCollection($group->twofaccounts());
} }
/** /**
@ -105,6 +114,8 @@ public function accounts(Group $group)
*/ */
public function destroy(Group $group) public function destroy(Group $group)
{ {
$this->authorize('delete', $group);
Groups::delete($group->id); Groups::delete($group->id);
return response()->json(null, 204); return response()->json(null, 204);

View File

@ -3,6 +3,7 @@
namespace App\Api\v1\Controllers; namespace App\Api\v1\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\TwoFAccount;
use App\Services\LogoService; use App\Services\LogoService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -52,11 +53,17 @@ public function fetch(Request $request, LogoService $logoService)
/** /**
* delete an icon * delete an icon
* *
* @param \Illuminate\Http\Request $request
* @param string $icon * @param string $icon
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function delete(string $icon) public function delete(string $icon, Request $request)
{ {
// An icon affected to someone else's twofaccount cannot be deleted
if ($icon && TwoFAccount::where('icon', $icon)->where('user_id', '<>', $request->user()->id)->count() > 0) {
abort(403, 'unauthorized');
}
Storage::disk('icons')->delete($icon); Storage::disk('icons')->delete($icon);
return response()->json(null, 204); return response()->json(null, 204);

View File

@ -17,6 +17,8 @@ class QrCodeController extends Controller
*/ */
public function show(TwoFAccount $twofaccount) public function show(TwoFAccount $twofaccount)
{ {
$this->authorize('view', $twofaccount);
$uri = $twofaccount->getURI(); $uri = $twofaccount->getURI();
return response()->json(['qrcode' => QrCode::encode($uri)], 200); return response()->json(['qrcode' => QrCode::encode($uri)], 200);

View File

@ -4,6 +4,7 @@
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class GroupStoreRequest extends FormRequest class GroupStoreRequest extends FormRequest
{ {
@ -25,7 +26,12 @@ public function authorize()
public function rules() public function rules()
{ {
return [ return [
'name' => 'required|string|max:32|unique:groups', 'name' => [
'required',
'string',
'max:32',
Rule::unique('groups')->where(fn ($query) => $query->where('user_id', $this->user()->id))
]
]; ];
} }
} }

View File

@ -44,6 +44,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'is_admin' => 'boolean', 'is_admin' => 'boolean',
'twofaccounts_count' => 'integer', 'twofaccounts_count' => 'integer',
'groups_count' => 'integer',
]; ];
/** /**
@ -107,4 +108,14 @@ public function twofaccounts()
{ {
return $this->hasMany(\App\Models\TwoFAccount::class); return $this->hasMany(\App\Models\TwoFAccount::class);
} }
/**
* Get the Groups of the user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Group>
*/
public function groups()
{
return $this->hasMany(\App\Models\Group::class);
}
} }

View File

@ -0,0 +1,134 @@
<?php
namespace App\Policies;
use App\Models\Group;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Auth\Access\HandlesAuthorization;
class GroupPolicy
{
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\Group $group
* @return \Illuminate\Auth\Access\Response|bool
*/
public function view(User $user, Group $group)
{
return $this->isOwnerOf($user, $group);
}
/**
* Determine whether the user can view all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool
*/
public function viewEach(User $user, Group $group, $groups)
{
return $this->isOwnerOfEach($user, $groups);
}
/**
* 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\Group $group
* @return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user, Group $group)
{
return $this->isOwnerOf($user, $group);
}
/**
* Determine whether the user can update all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool
*/
public function updateEach(User $user, Group $group, $groups)
{
return $this->isOwnerOfEach($user, $groups);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user, Group $group)
{
return $this->isOwnerOf($user, $group);
}
/**
* Determine whether the user can delete all provided models.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @param \Illuminate\Support\Collection<int, \App\Models\Group> $groups
* @return \Illuminate\Auth\Access\Response|bool
*/
public function deleteEach(User $user, Group $group, $groups)
{
return $this->isOwnerOfEach($user, $groups);
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @return \Illuminate\Auth\Access\Response|bool
*/
public function restore(User $user, Group $group)
{
return $this->isOwnerOf($user, $group);
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Group $group
* @return \Illuminate\Auth\Access\Response|bool
*/
public function forceDelete(User $user, Group $group)
{
return $this->isOwnerOf($user, $group);
}
}

View File

@ -6,7 +6,9 @@
use App\Extensions\WebauthnCredentialBroker; use App\Extensions\WebauthnCredentialBroker;
use App\Facades\Settings; use App\Facades\Settings;
use App\Models\TwoFAccount; use App\Models\TwoFAccount;
use App\Models\Group;
use App\Policies\TwoFAccountPolicy; use App\Policies\TwoFAccountPolicy;
use App\Policies\GroupPolicy;
use App\Services\Auth\ReverseProxyGuard; use App\Services\Auth\ReverseProxyGuard;
use Illuminate\Auth\Passwords\DatabaseTokenRepository; use Illuminate\Auth\Passwords\DatabaseTokenRepository;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -23,6 +25,7 @@ class AuthServiceProvider extends ServiceProvider
*/ */
protected $policies = [ protected $policies = [
TwoFAccount::class => TwoFAccountPolicy::class, TwoFAccount::class => TwoFAccountPolicy::class,
Group::class => GroupPolicy::class,
]; ];
/** /**

View File

@ -11,30 +11,21 @@
class GroupService class GroupService
{ {
/** /**
* Returns all existing groups * Prepends the pseudo group named 'All' to a group collection
* *
* @param Collection<int, Group> $groups
* @return Collection<int, Group> * @return Collection<int, Group>
*/ */
public static function getAll() : Collection public static function prependTheAllGroup(Collection $groups, int $userId) : Collection
{ {
// We return the complete collection of groups $theAllGroup = new Group([
// stored in db plus a pseudo group corresponding to 'All'
//
// This pseudo group contains all twofaccounts regardless
// of the user created group they belong to.
// Get the user created groups
$groups = Group::withCount('twofaccounts')->get();
// Create the pseudo group
$allGroup = new Group([
'name' => __('commons.all'), 'name' => __('commons.all'),
]); ]);
$allGroup->id = 0; $theAllGroup->id = 0;
$allGroup->twofaccounts_count = TwoFAccount::count(); $theAllGroup->twofaccounts_count = TwoFAccount::where('user_id', $userId)->count();
return $groups->prepend($allGroup); return $groups->prepend($theAllGroup);
} }
/** /**
@ -138,19 +129,6 @@ public static function assign($ids, Group $group = null) : void
} }
} }
/**
* Finds twofaccounts assigned to the group
*
* @param \App\Models\Group $group The group
* @return Collection<int, TwoFAccount> The assigned accounts
*/
public static function getAccounts(Group $group) : Collection
{
$twofaccounts = $group->twofaccounts()->where('group_id', $group->id)->get();
return $twofaccounts;
}
/** /**
* Determines the destination group * Determines the destination group
* *

View File

@ -97,19 +97,19 @@
/** /**
* Delete a group (after confirmation) * Delete a group (after confirmation)
*/ */
deleteGroup(id) { async deleteGroup(id) {
if(confirm(this.$t('groups.confirm.delete'))) { if(confirm(this.$t('groups.confirm.delete'))) {
this.axios.delete('/api/v1/groups/' + id) await this.axios.delete('/api/v1/groups/' + id).then(response => {
// Remove the deleted group from the collection
this.groups = this.groups.filter(a => a.id !== id)
this.$notify({ type: 'is-success', text: this.$t('groups.group_successfully_deleted') })
// Remove the deleted group from the collection // Reset persisted group filter to 'All' (groupId=0)
this.groups = this.groups.filter(a => a.id !== id) // (backend will save to change automatically)
this.$notify({ type: 'is-success', text: this.$t('groups.group_successfully_deleted') }) if( parseInt(this.$root.userPreferences.activeGroup) === id ) {
this.$root.userPreferences.activeGroup = 0
// Reset persisted group filter to 'All' (groupId=0) }
// (backend will save to change automatically) })
if( parseInt(this.$root.userPreferences.activeGroup) === id ) {
this.$root.userPreferences.activeGroup = 0
}
} }
} }