mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-06-04 02:05:35 +00:00
🚀 Enhance authorization and exception handling across Livewire components and SecurityMonitor
- **SecurityMonitor:** Added logic to record and prevent logging of locked-property exceptions, while ensuring non-security exceptions are properly forwarded. - **Livewire `Members/Admin`:** Centralized authorization logic in private methods, enforced access control on actions, and moved allowed pubkeys to class constant for maintainability. - **Livewire `News`:** Enforced authorization for editing and deleting news with guard methods and ensured unauthorized users can't access data. - **Bootstrap exceptions:** Implemented custom exception handling to record Livewire-related security issues while preventing redundant logs. - Updated tests with new behavior verification covering access control and exception responses.
This commit is contained in:
@@ -4,6 +4,8 @@ use App\Models\EinundzwanzigPleb;
|
||||
use App\Support\NostrAuth;
|
||||
use Livewire\Livewire;
|
||||
|
||||
const ALLOWED_ADMIN_PUBKEY = '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033';
|
||||
|
||||
it('denies access to unauthorized users', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->create();
|
||||
|
||||
@@ -73,3 +75,60 @@ it('displays einundzwanzig pleb table when authorized', function () {
|
||||
->assertSet('isAllowed', true)
|
||||
->assertSee('einundzwanzig-pleb-table');
|
||||
});
|
||||
|
||||
it('does not load the member list for unauthorized visitors', function () {
|
||||
EinundzwanzigPleb::factory()->count(3)->create();
|
||||
|
||||
Livewire::test('association.members.admin')
|
||||
->assertSet('isAllowed', false)
|
||||
->assertSet('plebs', []);
|
||||
});
|
||||
|
||||
it('forbids guests from exporting the member CSV', function () {
|
||||
Livewire::test('association.members.admin')
|
||||
->call('exportCsv')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('forbids unauthorized members from exporting the member CSV', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->create();
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.members.admin')
|
||||
->call('exportCsv')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('forbids unauthorized members from accepting an application', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->create();
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.members.admin')
|
||||
->call('acceptPleb')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('forbids unauthorized members from rejecting an application', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->create();
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.members.admin')
|
||||
->call('deletePleb')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('lets an authorized member pass the authorization guard', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->create([
|
||||
'pubkey' => ALLOWED_ADMIN_PUBKEY,
|
||||
]);
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.members.admin')
|
||||
->call('acceptPleb')
|
||||
->assertStatus(200)
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
@@ -112,6 +112,58 @@ it('can delete news entry', function () {
|
||||
expect(Notification::find($news->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('forbids guests from deleting news', function () {
|
||||
$author = EinundzwanzigPleb::factory()->create();
|
||||
$news = Notification::factory()->create([
|
||||
'einundzwanzig_pleb_id' => $author->id,
|
||||
]);
|
||||
|
||||
Livewire::test('association.news')
|
||||
->call('delete')
|
||||
->assertForbidden();
|
||||
|
||||
expect(Notification::find($news->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('forbids non-board members from deleting news', function () {
|
||||
$author = EinundzwanzigPleb::factory()->create();
|
||||
$news = Notification::factory()->create([
|
||||
'einundzwanzig_pleb_id' => $author->id,
|
||||
]);
|
||||
$pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create();
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.news')
|
||||
->call('delete')
|
||||
->assertForbidden();
|
||||
|
||||
expect(Notification::find($news->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('forbids non-board members from creating news', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create();
|
||||
|
||||
NostrAuth::login($pleb->pubkey);
|
||||
|
||||
Livewire::test('association.news')
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
|
||||
expect(Notification::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('does not load news for unauthorized visitors', function () {
|
||||
$author = EinundzwanzigPleb::factory()->create();
|
||||
Notification::factory()->count(2)->create([
|
||||
'einundzwanzig_pleb_id' => $author->id,
|
||||
]);
|
||||
|
||||
Livewire::test('association.news')
|
||||
->assertSet('isAllowed', false)
|
||||
->assertSet('news', []);
|
||||
});
|
||||
|
||||
it('displays news list', function () {
|
||||
$pleb = EinundzwanzigPleb::factory()->active()->withPaidCurrentYear()->create();
|
||||
$news1 = Notification::factory()->create();
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
use App\Models\SecurityAttempt;
|
||||
use App\Services\SecurityMonitor;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -145,6 +147,38 @@ it('truncates long values', function () {
|
||||
expect(strlen($attempt->user_agent))->toBeLessThanOrEqual(500);
|
||||
});
|
||||
|
||||
it('records a security attempt when a locked-property exception is reported through the handler', function () {
|
||||
$exception = new CannotUpdateLockedPropertyException('isLoggedIn');
|
||||
|
||||
app(ExceptionHandler::class)->report($exception);
|
||||
|
||||
expect(SecurityAttempt::count())->toBe(1)
|
||||
->and(SecurityAttempt::first()->target_property)->toBe('isLoggedIn');
|
||||
});
|
||||
|
||||
it('does not forward locked-property exceptions to the default log stack', function () {
|
||||
Log::spy();
|
||||
|
||||
app(ExceptionHandler::class)->report(new CannotUpdateLockedPropertyException('isLoggedIn'));
|
||||
|
||||
expect(SecurityAttempt::count())->toBe(1);
|
||||
|
||||
Log::shouldNotHaveReceived('log');
|
||||
Log::shouldNotHaveReceived('error');
|
||||
Log::shouldNotHaveReceived('critical');
|
||||
Log::shouldNotHaveReceived('warning');
|
||||
});
|
||||
|
||||
it('still forwards non-security exceptions to the default log stack', function () {
|
||||
Log::spy();
|
||||
|
||||
app(ExceptionHandler::class)->report(new RuntimeException('boom'));
|
||||
|
||||
expect(SecurityAttempt::count())->toBe(0);
|
||||
|
||||
Log::shouldHaveReceived('error');
|
||||
});
|
||||
|
||||
it('handles X-Forwarded-For header', function () {
|
||||
$exception = new CannotUpdateLockedPropertyException('test');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user