Harden token permission handling
This commit is contained in:
parent
095a1f0db0
commit
7f135e0f6d
2 changed files with 105 additions and 5 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Laravel\Sanctum\PersonalAccessToken;
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class ApiTokens extends Component
|
class ApiTokens extends Component
|
||||||
|
|
@ -29,8 +30,10 @@ class ApiTokens extends Component
|
||||||
|
|
||||||
public $isApiEnabled;
|
public $isApiEnabled;
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
public bool $canUseRootPermissions = false;
|
public bool $canUseRootPermissions = false;
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
public bool $canUseWritePermissions = false;
|
public bool $canUseWritePermissions = false;
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
@ -54,7 +57,7 @@ private function getTokens()
|
||||||
public function updatedPermissions($permissionToUpdate)
|
public function updatedPermissions($permissionToUpdate)
|
||||||
{
|
{
|
||||||
// Check if user is trying to use restricted permissions
|
// Check if user is trying to use restricted permissions
|
||||||
if ($permissionToUpdate == 'root' && ! $this->canUseRootPermissions) {
|
if ($permissionToUpdate == 'root' && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
|
||||||
$this->dispatch('error', 'You do not have permission to use root permissions.');
|
$this->dispatch('error', 'You do not have permission to use root permissions.');
|
||||||
// Remove root from permissions if it was somehow added
|
// Remove root from permissions if it was somehow added
|
||||||
$this->permissions = array_diff($this->permissions, ['root']);
|
$this->permissions = array_diff($this->permissions, ['root']);
|
||||||
|
|
@ -62,7 +65,7 @@ public function updatedPermissions($permissionToUpdate)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($permissionToUpdate, ['write', 'write:sensitive']) && ! $this->canUseWritePermissions) {
|
if (in_array($permissionToUpdate, ['write', 'write:sensitive'], true) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
|
||||||
$this->dispatch('error', 'You do not have permission to use write permissions.');
|
$this->dispatch('error', 'You do not have permission to use write permissions.');
|
||||||
// Remove write permissions if they were somehow added
|
// Remove write permissions if they were somehow added
|
||||||
$this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']);
|
$this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']);
|
||||||
|
|
@ -72,7 +75,7 @@ public function updatedPermissions($permissionToUpdate)
|
||||||
|
|
||||||
if ($permissionToUpdate == 'root') {
|
if ($permissionToUpdate == 'root') {
|
||||||
$this->permissions = ['root'];
|
$this->permissions = ['root'];
|
||||||
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
|
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions, true)) {
|
||||||
$this->permissions[] = 'read';
|
$this->permissions[] = 'read';
|
||||||
} elseif ($permissionToUpdate == 'deploy') {
|
} elseif ($permissionToUpdate == 'deploy') {
|
||||||
$this->permissions = ['deploy'];
|
$this->permissions = ['deploy'];
|
||||||
|
|
@ -90,11 +93,11 @@ public function addNewToken()
|
||||||
$this->authorize('create', PersonalAccessToken::class);
|
$this->authorize('create', PersonalAccessToken::class);
|
||||||
|
|
||||||
// Validate permissions based on user role
|
// Validate permissions based on user role
|
||||||
if (in_array('root', $this->permissions) && ! $this->canUseRootPermissions) {
|
if (in_array('root', $this->permissions, true) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
|
||||||
throw new \Exception('You do not have permission to create tokens with root permissions.');
|
throw new \Exception('You do not have permission to create tokens with root permissions.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! $this->canUseWritePermissions) {
|
if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
|
||||||
throw new \Exception('You do not have permission to create tokens with write permissions.');
|
throw new \Exception('You do not have permission to create tokens with write permissions.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
97
tests/Feature/ApiTokenLivewireAuthorizationTest.php
Normal file
97
tests/Feature/ApiTokenLivewireAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Security\ApiTokens;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create([
|
||||||
|
'id' => 0,
|
||||||
|
'is_api_enabled' => true,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api token permission flags are locked', function (string $property) {
|
||||||
|
$property = new ReflectionProperty(ApiTokens::class, $property);
|
||||||
|
|
||||||
|
expect($property->getAttributes(Locked::class))->not->toBeEmpty();
|
||||||
|
})->with([
|
||||||
|
'root permission flag' => 'canUseRootPermissions',
|
||||||
|
'write permission flag' => 'canUseWritePermissions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('member cannot tamper with root permission flag', function () {
|
||||||
|
$member = User::factory()->create();
|
||||||
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||||
|
|
||||||
|
$this->actingAs($member);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(ApiTokens::class)
|
||||||
|
->set('canUseRootPermissions', true);
|
||||||
|
})->throws(CannotUpdateLockedPropertyException::class);
|
||||||
|
|
||||||
|
test('member cannot create root token through tampered permissions payload', function () {
|
||||||
|
$member = User::factory()->create();
|
||||||
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||||
|
|
||||||
|
$this->actingAs($member);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(ApiTokens::class)
|
||||||
|
->set('description', 'pwned-root-token')
|
||||||
|
->set('expiresInDays', 30)
|
||||||
|
->set('permissions', ['root'])
|
||||||
|
->call('addNewToken');
|
||||||
|
|
||||||
|
expect($member->tokens()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('member can still create read token', function () {
|
||||||
|
$member = User::factory()->create();
|
||||||
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||||
|
|
||||||
|
$this->actingAs($member);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(ApiTokens::class)
|
||||||
|
->set('description', 'read-token')
|
||||||
|
->set('expiresInDays', 30)
|
||||||
|
->set('permissions', ['read'])
|
||||||
|
->call('addNewToken')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$token = $member->tokens()->latest()->first();
|
||||||
|
|
||||||
|
expect($token)->not->toBeNull()
|
||||||
|
->and($token->abilities)->toBe(['read']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner can create root token', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$this->team->members()->attach($owner->id, ['role' => 'owner']);
|
||||||
|
|
||||||
|
$this->actingAs($owner);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(ApiTokens::class)
|
||||||
|
->set('description', 'root-token')
|
||||||
|
->set('expiresInDays', 30)
|
||||||
|
->set('permissions', ['root'])
|
||||||
|
->call('addNewToken')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
$token = $owner->tokens()->latest()->first();
|
||||||
|
|
||||||
|
expect($token)->not->toBeNull()
|
||||||
|
->and($token->abilities)->toBe(['root']);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue