Add Super-Admin tools for managing any model

- 🛠️ Introduced generic Super-Admin MCP tools, including `list-models`, `describe-model`, `list-records`, `show-record`, `create-record`, and `update-record`.
- 🛡️ Restricted modification of critical fields (e.g., passwords, roles, tokens) to enhance security.
-  Added extensive feature tests for Super-Admin functionality and access control.
- 📜 Increased pagination length to accommodate new tools on a single page.
- 🔗 Registered Super-Admin tools in `EinundzwanzigServer`.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-08 13:39:04 +02:00
parent 3a507cced2
commit 8c68b19138
14 changed files with 810 additions and 6 deletions
+21
View File
@@ -76,6 +76,27 @@ it('completes a Lightning login and redirects to the dashboard when a recent Log
$this->assertAuthenticatedAs($user);
});
it('resumes the intended OAuth url after a Lightning login instead of going to the dashboard', function () {
$user = User::factory()->create();
$k1 = bin2hex(random_bytes(32));
LoginKey::factory()->create([
'user_id' => $user->id,
'k1' => $k1,
'created_at' => now(),
]);
$intended = url('/oauth/authorize?client_id=1&response_type=code&scope=mcp:use');
$response = $this->withSession([
'lang_country' => 'de-DE',
'locale' => 'de',
'url.intended' => $intended,
])->get(route('auth.ln.complete', ['k1' => $k1]));
$response->assertRedirect($intended);
$this->assertAuthenticatedAs($user);
});
it('redirects to login when the LoginKey is older than 5 minutes', function () {
$user = User::factory()->create();
$k1 = bin2hex(random_bytes(32));
@@ -17,7 +17,7 @@ it('registers every domain tool on the server', function () {
$property = (new ReflectionClass(EinundzwanzigServer::class))->getProperty('tools');
$tools = $property->getDefaultValue();
expect($tools)->toHaveCount(32)
expect($tools)->toHaveCount(38)
->and($tools)->toContain(CreateMeetupTool::class)
->and($tools)->toContain(UpdateCourseEventTool::class)
->and($tools)->toContain(SearchCitiesTool::class);
+23
View File
@@ -1,6 +1,7 @@
<?php
use App\Models\User;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\Passport;
it('configures the passport-backed api guard', function () {
@@ -97,3 +98,25 @@ it('rejects an authorize request that uses plain PKCE instead of S256', function
$this->get('/oauth/authorize?response_type=code&client_id=1&redirect_uri=https%3A%2F%2Fclaude.ai%2Fcb&code_challenge=abc123&code_challenge_method=plain')
->assertStatus(400);
});
it('redirects a guest from the authorize endpoint to login and stores the intended url', function () {
$clients = app(ClientRepository::class);
$client = $clients->createAuthorizationCodeGrantClient(
name: 'Claude',
redirectUris: ['https://claude.ai/api/mcp/auth_callback'],
confidential: false,
);
$response = $this->get('/oauth/authorize?'.http_build_query([
'client_id' => $client->getKey(),
'redirect_uri' => 'https://claude.ai/api/mcp/auth_callback',
'response_type' => 'code',
'scope' => 'mcp:use',
'state' => 'xyz',
'code_challenge' => str_repeat('a', 43),
'code_challenge_method' => 'S256',
]));
$response->assertRedirect(route('login'));
expect(session('url.intended'))->toContain('/oauth/authorize');
});
+133
View File
@@ -0,0 +1,133 @@
<?php
use App\Mcp\Servers\EinundzwanzigServer;
use App\Mcp\Tools\SuperAdmin\SuperAdminCreateRecordTool;
use App\Mcp\Tools\SuperAdmin\SuperAdminDescribeModelTool;
use App\Mcp\Tools\SuperAdmin\SuperAdminListModelsTool;
use App\Mcp\Tools\SuperAdmin\SuperAdminListRecordsTool;
use App\Mcp\Tools\SuperAdmin\SuperAdminShowRecordTool;
use App\Mcp\Tools\SuperAdmin\SuperAdminUpdateRecordTool;
use App\Models\Country;
use App\Models\Meetup;
use App\Models\User;
use Spatie\Permission\Models\Role;
function superAdmin(): User
{
Role::findOrCreate('super-admin');
return User::factory()->create()->assignRole('super-admin');
}
it('lets a super-admin update a meetup created by someone else', function () {
$owner = User::factory()->create();
$meetup = Meetup::factory()->create(['name' => 'Altname', 'created_by' => $owner->id]);
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminUpdateRecordTool::class, [
'model' => 'meetup',
'id' => $meetup->id,
'attributes' => ['name' => 'Vom Admin geändert'],
])
->assertOk();
$this->assertDatabaseHas('meetups', [
'id' => $meetup->id,
'name' => 'Vom Admin geändert',
]);
});
it('lets a super-admin create any model', function () {
$country = Country::factory()->create();
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminCreateRecordTool::class, [
'model' => 'city',
'attributes' => [
'name' => 'Adminstadt',
'country_id' => $country->id,
'longitude' => 1.0,
'latitude' => 2.0,
],
])
->assertOk();
$this->assertDatabaseHas('cities', ['name' => 'Adminstadt']);
});
it('lists editable models and describes their columns', function () {
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminListModelsTool::class)
->assertOk()
->assertSee('meetup');
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminDescribeModelTool::class, ['model' => 'meetup'])
->assertOk()
->assertSee('telegram_link');
});
it('lists and shows records of any model', function () {
$meetup = Meetup::factory()->create(['name' => 'Sichtbar']);
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminListRecordsTool::class, ['model' => 'meetup'])
->assertOk()
->assertSee('Sichtbar');
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminShowRecordTool::class, ['model' => 'meetup', 'id' => $meetup->id])
->assertOk()
->assertSee('Sichtbar');
});
it('reports an unknown model with the list of available ones', function () {
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminShowRecordTool::class, ['model' => 'gibtsnicht', 'id' => 1])
->assertHasErrors();
});
it('refuses to write password or role fields via super-admin tools', function () {
$target = User::factory()->create(['name' => 'Unverändert']);
$originalPassword = $target->password;
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminUpdateRecordTool::class, [
'model' => 'user',
'id' => $target->id,
'attributes' => ['name' => 'Neu', 'password' => 'gehackt'],
])
->assertHasErrors();
$fresh = $target->fresh();
expect($fresh->name)->toBe('Unverändert')
->and($fresh->password)->toBe($originalPassword);
});
it('refuses to write further protected fields like nostr or email verification', function () {
$target = User::factory()->create(['nostr' => 'npub-original']);
EinundzwanzigServer::actingAs(superAdmin())
->tool(SuperAdminUpdateRecordTool::class, [
'model' => 'user',
'id' => $target->id,
'attributes' => ['nostr' => 'npub-gefälscht'],
])
->assertHasErrors();
expect($target->fresh()->nostr)->toBe('npub-original');
});
it('denies a non super-admin from using the super-admin tools', function () {
$meetup = Meetup::factory()->create(['name' => 'Geschützt']);
EinundzwanzigServer::actingAs(User::factory()->create())
->tool(SuperAdminUpdateRecordTool::class, [
'model' => 'meetup',
'id' => $meetup->id,
'attributes' => ['name' => 'Hijack'],
])
->assertHasErrors();
$this->assertDatabaseHas('meetups', ['id' => $meetup->id, 'name' => 'Geschützt']);
});