mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-11 02:50:29 +00:00
✨ 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:
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
Reference in New Issue
Block a user