Skip to content

Commit c748fa9

Browse files
authored
fix: exclude any permissions not defined internally when updating or creating subusers (pterodactyl#4416)
1 parent 597821b commit c748fa9

File tree

3 files changed

+154
-31
lines changed

3 files changed

+154
-31
lines changed

app/Http/Controllers/Api/Client/Servers/SubuserController.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,23 @@ public function delete(DeleteSubuserRequest $request, Server $server)
190190
}
191191

192192
/**
193-
* Returns the default permissions for all subusers to ensure none are ever removed wrongly.
193+
* Returns the default permissions for subusers and parses out any permissions
194+
* that were passed that do not also exist in the internally tracked list of
195+
* permissions.
194196
*/
195197
protected function getDefaultPermissions(Request $request): array
196198
{
197-
return array_unique(array_merge($request->input('permissions') ?? [], [Permission::ACTION_WEBSOCKET_CONNECT]));
199+
$allowed = Permission::permissions()
200+
->map(function ($value, $prefix) {
201+
return array_map(function ($value) use ($prefix) {
202+
return "$prefix.$value";
203+
}, array_keys($value['keys']));
204+
})
205+
->flatten()
206+
->all();
207+
208+
$cleaned = array_intersect($request->input('permissions') ?? [], $allowed);
209+
210+
return array_unique(array_merge($cleaned, [Permission::ACTION_WEBSOCKET_CONNECT]));
198211
}
199212
}

app/Policies/ServerPolicy.php

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,22 @@
22

33
namespace Pterodactyl\Policies;
44

5-
use Carbon\Carbon;
65
use Pterodactyl\Models\User;
76
use Pterodactyl\Models\Server;
8-
use Illuminate\Contracts\Cache\Repository as CacheRepository;
97

108
class ServerPolicy
119
{
12-
/**
13-
* @var \Illuminate\Contracts\Cache\Repository
14-
*/
15-
private $cache;
16-
17-
/**
18-
* ServerPolicy constructor.
19-
*/
20-
public function __construct(CacheRepository $cache)
21-
{
22-
$this->cache = $cache;
23-
}
24-
2510
/**
2611
* Checks if the user has the given permission on/for the server.
27-
*
28-
* @param string $permission
29-
*
30-
* @return bool
3112
*/
32-
protected function checkPermission(User $user, Server $server, $permission)
13+
protected function checkPermission(User $user, Server $server, string $permission): bool
3314
{
34-
$key = sprintf('ServerPolicy.%s.%s', $user->uuid, $server->uuid);
35-
36-
$permissions = $this->cache->remember($key, Carbon::now()->addSeconds(5), function () use ($user, $server) {
37-
/** @var \Pterodactyl\Models\Subuser|null $subuser */
38-
$subuser = $server->subusers()->where('user_id', $user->id)->first();
39-
40-
return $subuser ? $subuser->permissions : [];
41-
});
15+
$subuser = $server->subusers->where('user_id', $user->id)->first();
16+
if (!$subuser || empty($permission)) {
17+
return false;
18+
}
4219

43-
return in_array($permission, $permissions);
20+
return in_array($permission, $subuser->permissions);
4421
}
4522

4623
/**
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server\Subuser;
4+
5+
use Pterodactyl\Models\User;
6+
use Pterodactyl\Models\Subuser;
7+
use Pterodactyl\Models\Permission;
8+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
9+
10+
class UpdateSubuserTest extends ClientApiIntegrationTestCase
11+
{
12+
/**
13+
* Test that the correct permissions are applied to the account when making updates
14+
* to a subusers permissions.
15+
*/
16+
public function testCorrectPermissionsAreRequiredForUpdating()
17+
{
18+
[$user, $server] = $this->generateTestAccount(['user.read']);
19+
20+
$subuser = Subuser::factory()
21+
->for(User::factory()->create())
22+
->for($server)
23+
->create([
24+
'permissions' => ['control.start'],
25+
]);
26+
27+
$this->postJson(
28+
$endpoint = "/api/client/servers/$server->uuid/users/{$subuser->user->uuid}",
29+
$data = [
30+
'permissions' => [
31+
'control.start',
32+
'control.stop',
33+
],
34+
]
35+
)
36+
->assertUnauthorized();
37+
38+
$this->actingAs($subuser->user)->postJson($endpoint, $data)->assertForbidden();
39+
$this->actingAs($user)->postJson($endpoint, $data)->assertForbidden();
40+
41+
$server->subusers()->where('user_id', $user->id)->update([
42+
'permissions' => [
43+
Permission::ACTION_USER_UPDATE,
44+
Permission::ACTION_CONTROL_START,
45+
Permission::ACTION_CONTROL_STOP,
46+
],
47+
]);
48+
49+
$this->postJson($endpoint, $data)->assertOk();
50+
}
51+
52+
/**
53+
* Tests that permissions for the account are updated and any extraneous values
54+
* we don't know about are removed.
55+
*/
56+
public function testPermissionsAreSavedToAccount()
57+
{
58+
[$user, $server] = $this->generateTestAccount();
59+
60+
/** @var \Pterodactyl\Models\Subuser $subuser */
61+
$subuser = Subuser::factory()
62+
->for(User::factory()->create())
63+
->for($server)
64+
->create([
65+
'permissions' => ['control.restart', 'websocket.connect', 'foo.bar'],
66+
]);
67+
68+
$this->actingAs($user)
69+
->postJson("/api/client/servers/$server->uuid/users/{$subuser->user->uuid}", [
70+
'permissions' => [
71+
'control.start',
72+
'control.stop',
73+
'control.stop',
74+
'foo.bar',
75+
'power.fake',
76+
],
77+
])
78+
->assertOk();
79+
80+
$subuser->refresh();
81+
$this->assertEqualsCanonicalizing(
82+
['control.start', 'control.stop', 'websocket.connect'],
83+
$subuser->permissions
84+
);
85+
}
86+
87+
/**
88+
* Ensure a subuser cannot assign permissions to an account that they do not have
89+
* themselves.
90+
*/
91+
public function testUserCannotAssignPermissionsTheyDoNotHave()
92+
{
93+
[$user, $server] = $this->generateTestAccount([Permission::ACTION_USER_READ, Permission::ACTION_USER_UPDATE]);
94+
95+
$subuser = Subuser::factory()
96+
->for(User::factory()->create())
97+
->for($server)
98+
->create(['permissions' => ['foo.bar']]);
99+
100+
$this->actingAs($user)
101+
->postJson("/api/client/servers/$server->uuid/users/{$subuser->user->uuid}", [
102+
'permissions' => [Permission::ACTION_USER_READ, Permission::ACTION_CONTROL_CONSOLE],
103+
])
104+
->assertForbidden();
105+
106+
$this->assertEqualsCanonicalizing(['foo.bar'], $subuser->refresh()->permissions);
107+
}
108+
109+
/**
110+
* Test that a user cannot update thyself.
111+
*/
112+
public function testUserCannotUpdateSelf()
113+
{
114+
[$user, $server] = $this->generateTestAccount([Permission::ACTION_USER_READ, Permission::ACTION_USER_UPDATE]);
115+
116+
$this->actingAs($user)
117+
->postJson("/api/client/servers/$server->uuid/users/$user->uuid", [])
118+
->assertForbidden();
119+
}
120+
121+
/**
122+
* Test that an error is returned if you attempt to update a subuser on a different account.
123+
*/
124+
public function testCannotUpdateSubuserForDifferentServer()
125+
{
126+
[$user, $server] = $this->generateTestAccount();
127+
[$user2] = $this->generateTestAccount(['foo.bar']);
128+
129+
$this->actingAs($user)
130+
->postJson("/api/client/servers/$server->uuid/users/$user2->uuid", [])
131+
->assertNotFound();
132+
}
133+
}

0 commit comments

Comments
 (0)