Skip to content

Commit 63f945b

Browse files
committed
Add test coverage to cehck the authorization state of client resources
1 parent e8dcd30 commit 63f945b

File tree

8 files changed

+369
-3
lines changed

8 files changed

+369
-3
lines changed

database/factories/ModelFactory.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Pterodactyl\Models\Node;
88
use Faker\Generator as Faker;
99
use Pterodactyl\Models\ApiKey;
10+
use Pterodactyl\Models\Backup;
11+
use Pterodactyl\Models\Permission;
1012

1113
/** @var \Illuminate\Database\Eloquent\Factory $factory */
1214
/*
@@ -134,7 +136,9 @@
134136
});
135137

136138
$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) {
137-
return [];
139+
return [
140+
'permissions' => [Permission::ACTION_WEBSOCKET_CONNECT],
141+
];
138142
});
139143

140144
$factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
@@ -161,7 +165,7 @@
161165
'database' => str_random(10),
162166
'username' => str_random(10),
163167
'remote' => '%',
164-
'password' => $password ?: bcrypt('test123'),
168+
'password' => $password ?: encrypt('test123'),
165169
'created_at' => Carbon::now()->toDateTimeString(),
166170
'updated_at' => Carbon::now()->toDateTimeString(),
167171
];
@@ -196,3 +200,12 @@
196200
'updated_at' => Carbon::now()->toDateTimeString(),
197201
];
198202
});
203+
204+
$factory->define(Pterodactyl\Models\Backup::class, function (Faker $faker) {
205+
return [
206+
'uuid' => Uuid::uuid4()->toString(),
207+
'is_successful' => true,
208+
'name' => $faker->sentence,
209+
'disk' => Backup::ADAPTER_WINGS,
210+
];
211+
});

tests/Integration/Api/Client/ClientApiIntegrationTestCase.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
use Pterodactyl\Models\User;
1111
use Webmozart\Assert\Assert;
1212
use Pterodactyl\Models\Server;
13+
use Pterodactyl\Models\Backup;
1314
use Pterodactyl\Models\Subuser;
1415
use Pterodactyl\Models\Location;
1516
use Pterodactyl\Models\Schedule;
17+
use Pterodactyl\Models\Database;
1618
use Illuminate\Support\Collection;
1719
use Pterodactyl\Models\Allocation;
20+
use Pterodactyl\Models\DatabaseHost;
1821
use Pterodactyl\Tests\Integration\TestResponse;
1922
use Pterodactyl\Tests\Integration\IntegrationTestCase;
2023
use Pterodactyl\Transformers\Api\Client\BaseClientTransformer;
@@ -26,6 +29,9 @@ abstract class ClientApiIntegrationTestCase extends IntegrationTestCase
2629
*/
2730
protected function tearDown(): void
2831
{
32+
Database::query()->forceDelete();
33+
DatabaseHost::query()->forceDelete();
34+
Backup::query()->forceDelete();
2935
Server::query()->forceDelete();
3036
Node::query()->forceDelete();
3137
Location::query()->forceDelete();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Allocation;
4+
5+
use Pterodactyl\Models\Subuser;
6+
use Pterodactyl\Models\Schedule;
7+
use Pterodactyl\Models\Allocation;
8+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
9+
10+
class AllocationAuthorizationTest extends ClientApiIntegrationTestCase
11+
{
12+
/**
13+
* @param string $method
14+
* @param string $endpoint
15+
* @dataProvider methodDataProvider
16+
*/
17+
public function testAccessToAServersAllocationsIsRestrictedProperly(string $method, string $endpoint)
18+
{
19+
// The API $user is the owner of $server1.
20+
[$user, $server1] = $this->generateTestAccount();
21+
// Will be a subuser of $server2.
22+
$server2 = $this->createServerModel();
23+
// And as no access to $server3.
24+
$server3 = $this->createServerModel();
25+
26+
// Set the API $user as a subuser of server 2, but with no permissions
27+
// to do anything with the allocations for that server.
28+
factory(Subuser::class)->create(['server_id' => $server2->id, 'user_id' => $user->id]);
29+
30+
$allocation1 = factory(Allocation::class)->create(['server_id' => $server1->id, 'node_id' => $server1->node_id]);
31+
$allocation2 = factory(Allocation::class)->create(['server_id' => $server2->id, 'node_id' => $server2->node_id]);
32+
$allocation3 = factory(Allocation::class)->create(['server_id' => $server3->id, 'node_id' => $server3->node_id]);
33+
34+
// This is the only valid call for this test, accessing the allocation for the same
35+
// server that the API user is the owner of.
36+
$response = $this->actingAs($user)->json($method, $this->link($server1, "/network/allocations/" . $allocation1->id . $endpoint));
37+
$this->assertTrue($response->status() <= 204 || $response->status() === 400 || $response->status() === 422);
38+
39+
// This request fails because the allocation is valid for that server but the user
40+
// making the request is not authorized to perform that action.
41+
$this->actingAs($user)->json($method, $this->link($server2, "/network/allocations/" . $allocation2->id . $endpoint))->assertForbidden();
42+
43+
// Both of these should report a 404 error due to the allocations being linked to
44+
// servers that are not the same as the server in the request, or are assigned
45+
// to a server for which the user making the request has no access to.
46+
$this->actingAs($user)->json($method, $this->link($server1, "/network/allocations/" . $allocation2->id . $endpoint))->assertNotFound();
47+
$this->actingAs($user)->json($method, $this->link($server1, "/network/allocations/" . $allocation3->id . $endpoint))->assertNotFound();
48+
$this->actingAs($user)->json($method, $this->link($server2, "/network/allocations/" . $allocation3->id . $endpoint))->assertNotFound();
49+
$this->actingAs($user)->json($method, $this->link($server3, "/network/allocations/" . $allocation3->id . $endpoint))->assertNotFound();
50+
}
51+
52+
/**
53+
* @return \string[][]
54+
*/
55+
public function methodDataProvider(): array
56+
{
57+
return [
58+
["POST", ""],
59+
["DELETE", ""],
60+
["POST", "/primary"],
61+
];
62+
}
63+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Backup;
4+
5+
use Mockery;
6+
use Carbon\CarbonImmutable;
7+
use Pterodactyl\Models\Backup;
8+
use Pterodactyl\Models\Subuser;
9+
use Pterodactyl\Services\Backups\DeleteBackupService;
10+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
11+
12+
class BackupAuthorizationTest extends ClientApiIntegrationTestCase
13+
{
14+
/**
15+
* @param string $method
16+
* @param string $endpoint
17+
* @dataProvider methodDataProvider
18+
*/
19+
public function testAccessToAServersBackupIsRestrictedProperly(string $method, string $endpoint)
20+
{
21+
// The API $user is the owner of $server1.
22+
[$user, $server1] = $this->generateTestAccount();
23+
// Will be a subuser of $server2.
24+
$server2 = $this->createServerModel();
25+
// And as no access to $server3.
26+
$server3 = $this->createServerModel();
27+
28+
// Set the API $user as a subuser of server 2, but with no permissions
29+
// to do anything with the backups for that server.
30+
factory(Subuser::class)->create(['server_id' => $server2->id, 'user_id' => $user->id]);
31+
32+
$backup1 = factory(Backup::class)->create(['server_id' => $server1->id, 'completed_at' => CarbonImmutable::now()]);
33+
$backup2 = factory(Backup::class)->create(['server_id' => $server2->id, 'completed_at' => CarbonImmutable::now()]);
34+
$backup3 = factory(Backup::class)->create(['server_id' => $server3->id, 'completed_at' => CarbonImmutable::now()]);
35+
36+
$this->instance(DeleteBackupService::class, $mock = Mockery::mock(DeleteBackupService::class));
37+
38+
if ($method === 'DELETE') {
39+
$mock->expects('handle')->andReturnUndefined();
40+
}
41+
42+
// This is the only valid call for this test, accessing the backup for the same
43+
// server that the API user is the owner of.
44+
$this->actingAs($user)->json($method, $this->link($server1, "/backups/" . $backup1->uuid . $endpoint))
45+
->assertStatus($method === 'DELETE' ? 204 : 200);
46+
47+
// This request fails because the backup is valid for that server but the user
48+
// making the request is not authorized to perform that action.
49+
$this->actingAs($user)->json($method, $this->link($server2, "/backups/" . $backup2->uuid . $endpoint))->assertForbidden();
50+
51+
// Both of these should report a 404 error due to the backup being linked to
52+
// servers that are not the same as the server in the request, or are assigned
53+
// to a server for which the user making the request has no access to.
54+
$this->actingAs($user)->json($method, $this->link($server1, "/backups/" . $backup2->uuid . $endpoint))->assertNotFound();
55+
$this->actingAs($user)->json($method, $this->link($server1, "/backups/" . $backup3->uuid . $endpoint))->assertNotFound();
56+
$this->actingAs($user)->json($method, $this->link($server2, "/backups/" . $backup3->uuid . $endpoint))->assertNotFound();
57+
$this->actingAs($user)->json($method, $this->link($server3, "/backups/" . $backup3->uuid . $endpoint))->assertNotFound();
58+
}
59+
60+
/**
61+
* @return \string[][]
62+
*/
63+
public function methodDataProvider(): array
64+
{
65+
return [
66+
["GET", ""],
67+
["GET", "/download"],
68+
["DELETE", ""],
69+
];
70+
}
71+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Database;
4+
5+
use Mockery;
6+
use Pterodactyl\Models\Subuser;
7+
use Pterodactyl\Models\Database;
8+
use Pterodactyl\Models\DatabaseHost;
9+
use Pterodactyl\Contracts\Extensions\HashidsInterface;
10+
use Pterodactyl\Services\Databases\DatabasePasswordService;
11+
use Pterodactyl\Services\Databases\DatabaseManagementService;
12+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
13+
14+
class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase
15+
{
16+
/**
17+
* @param string $method
18+
* @param string $endpoint
19+
* @dataProvider methodDataProvider
20+
*/
21+
public function testAccessToAServersDatabasesIsRestrictedProperly(string $method, string $endpoint)
22+
{
23+
// The API $user is the owner of $server1.
24+
[$user, $server1] = $this->generateTestAccount();
25+
// Will be a subuser of $server2.
26+
$server2 = $this->createServerModel();
27+
// And as no access to $server3.
28+
$server3 = $this->createServerModel();
29+
30+
$host = factory(DatabaseHost::class)->create([]);
31+
32+
// Set the API $user as a subuser of server 2, but with no permissions
33+
// to do anything with the databases for that server.
34+
factory(Subuser::class)->create(['server_id' => $server2->id, 'user_id' => $user->id]);
35+
36+
$database1 = factory(Database::class)->create(['server_id' => $server1->id, 'database_host_id' => $host->id]);
37+
$database2 = factory(Database::class)->create(['server_id' => $server2->id, 'database_host_id' => $host->id]);
38+
$database3 = factory(Database::class)->create(['server_id' => $server3->id, 'database_host_id' => $host->id]);
39+
40+
$this->instance(DatabasePasswordService::class, $mock = Mockery::mock(DatabasePasswordService::class));
41+
$this->instance(DatabaseManagementService::class, $mock2 = Mockery::mock(DatabaseManagementService::class));
42+
43+
if ($method === 'POST') {
44+
$mock->expects('handle')->andReturnUndefined();
45+
} else {
46+
$mock2->expects('delete')->andReturnUndefined();
47+
}
48+
49+
$hashids = $this->app->make(HashidsInterface::class);
50+
// This is the only valid call for this test, accessing the database for the same
51+
// server that the API user is the owner of.
52+
$this->actingAs($user)->json($method, $this->link($server1, "/databases/" . $hashids->encode($database1->id) . $endpoint))
53+
->assertStatus($method === 'DELETE' ? 204 : 200);
54+
55+
// This request fails because the database is valid for that server but the user
56+
// making the request is not authorized to perform that action.
57+
$this->actingAs($user)->json($method, $this->link($server2, "/databases/" . $hashids->encode($database2->id) . $endpoint))->assertForbidden();
58+
59+
// Both of these should report a 404 error due to the database being linked to
60+
// servers that are not the same as the server in the request, or are assigned
61+
// to a server for which the user making the request has no access to.
62+
$this->actingAs($user)->json($method, $this->link($server1, "/databases/" . $hashids->encode($database2->id) . $endpoint))->assertNotFound();
63+
$this->actingAs($user)->json($method, $this->link($server1, "/databases/" . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
64+
$this->actingAs($user)->json($method, $this->link($server2, "/databases/" . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
65+
$this->actingAs($user)->json($method, $this->link($server3, "/databases/" . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
66+
}
67+
68+
/**
69+
* @return \string[][]
70+
*/
71+
public function methodDataProvider(): array
72+
{
73+
return [
74+
["POST", "/rotate-password"],
75+
["DELETE", ""],
76+
];
77+
}
78+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Schedule;
4+
5+
use Pterodactyl\Models\Subuser;
6+
use Pterodactyl\Models\Schedule;
7+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
8+
9+
class ScheduleAuthorizationTest extends ClientApiIntegrationTestCase
10+
{
11+
/**
12+
* Tests that a subuser with access to two servers cannot improperly access a resource
13+
* on Server A when providing a URL that points to Server B. This prevents a regression
14+
* in the code where controllers didn't properly validate that a resource was assigned
15+
* to the server that was also present in the URL.
16+
*
17+
* The comments within the test code itself are better at explaining exactly what is
18+
* being tested and protected against.
19+
*
20+
* @param string $method
21+
* @param string $endpoint
22+
* @dataProvider methodDataProvider
23+
*/
24+
public function testAccessToAServersSchedulesIsRestrictedProperly(string $method, string $endpoint)
25+
{
26+
// The API $user is the owner of $server1.
27+
[$user, $server1] = $this->generateTestAccount();
28+
// Will be a subuser of $server2.
29+
$server2 = $this->createServerModel();
30+
// And as no access to $server3.
31+
$server3 = $this->createServerModel();
32+
33+
// Set the API $user as a subuser of server 2, but with no permissions
34+
// to do anything with the schedules for that server.
35+
factory(Subuser::class)->create(['server_id' => $server2->id, 'user_id' => $user->id]);
36+
37+
$schedule1 = factory(Schedule::class)->create(['server_id' => $server1->id]);
38+
$schedule2 = factory(Schedule::class)->create(['server_id' => $server2->id]);
39+
$schedule3 = factory(Schedule::class)->create(['server_id' => $server3->id]);
40+
41+
// This is the only valid call for this test, accessing the schedule for the same
42+
// server that the API user is the owner of.
43+
$response = $this->actingAs($user)->json($method, $this->link($server1, "/schedules/" . $schedule1->id . $endpoint));
44+
$this->assertTrue($response->status() <= 204 || $response->status() === 400 || $response->status() === 422);
45+
46+
// This request fails because the schedule is valid for that server but the user
47+
// making the request is not authorized to perform that action.
48+
$this->actingAs($user)->json($method, $this->link($server2, "/schedules/" . $schedule2->id . $endpoint))->assertForbidden();
49+
50+
// Both of these should report a 404 error due to the schedules being linked to
51+
// servers that are not the same as the server in the request, or are assigned
52+
// to a server for which the user making the request has no access to.
53+
$this->actingAs($user)->json($method, $this->link($server1, "/schedules/" . $schedule2->id . $endpoint))->assertNotFound();
54+
$this->actingAs($user)->json($method, $this->link($server1, "/schedules/" . $schedule3->id . $endpoint))->assertNotFound();
55+
$this->actingAs($user)->json($method, $this->link($server2, "/schedules/" . $schedule3->id . $endpoint))->assertNotFound();
56+
$this->actingAs($user)->json($method, $this->link($server3, "/schedules/" . $schedule3->id . $endpoint))->assertNotFound();
57+
}
58+
59+
/**
60+
* @return \string[][]
61+
*/
62+
public function methodDataProvider(): array
63+
{
64+
return [
65+
["GET", ""],
66+
["POST", ""],
67+
["DELETE", ""],
68+
["POST", "/execute"],
69+
["POST", "/tasks"],
70+
];
71+
}
72+
}

0 commit comments

Comments
 (0)