418 lines
15 KiB
PHP
418 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\ServerResource;
|
|
use App\Models\ServerUserBinding;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Client\Request;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class ServerSystemUserManagementTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_admin_can_read_server_system_user_meta(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users' => Http::response([
|
|
['username' => 'alice', 'uid' => 1000, 'gid' => 1000, 'home_dir' => '/home/alice', 'shell' => '/bin/bash'],
|
|
]),
|
|
'http://node.test/groups' => Http::response([
|
|
['groupname' => 'dev', 'gid' => 1000, 'members' => ['alice']],
|
|
]),
|
|
'http://node.test/users/alice/groups' => Http::response(['username' => 'alice', 'groups' => ['dev']]),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($admin, 'api')->getJson('/servers/'.$server->id.'/system-users/meta');
|
|
|
|
$response
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.users.0.username', 'alice')
|
|
->assertJsonPath('data.groups.0.groupname', 'dev')
|
|
->assertJsonPath('data.user_groups.alice.0', 'dev');
|
|
}
|
|
|
|
public function test_creating_user_can_create_remote_server_account_and_binding(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/alice' => Http::response(['detail' => 'not found'], 404),
|
|
'http://node.test/users' => Http::response(['status' => 'ok', 'message' => 'User created.'], 201),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$response = $this->actingAs($admin, 'api')->postJson('/users', [
|
|
'nickname' => 'Alice',
|
|
'email' => 'alice@example.com',
|
|
'phone' => '13800138000',
|
|
'password' => 'secret123',
|
|
'server_bindings' => [[
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'create_remote' => true,
|
|
'groups' => [],
|
|
]],
|
|
]);
|
|
|
|
$response->assertCreated()->assertJsonPath('code', 0);
|
|
$user = User::query()->where('email', 'alice@example.com')->firstOrFail();
|
|
|
|
$this->assertDatabaseHas('server_user_bindings', [
|
|
'user_id' => $user->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
'force_password_change' => true,
|
|
]);
|
|
|
|
Http::assertSent(function (Request $request): bool {
|
|
return $request->method() === 'POST'
|
|
&& $request->url() === 'http://node.test/users'
|
|
&& $request['username'] === 'alice'
|
|
&& $request['groups'] === ['dev']
|
|
&& $request['default_environment_variables'] === 'export A=1'
|
|
&& is_string($request['password_hash'])
|
|
&& str_starts_with($request['password_hash'], '$6$');
|
|
});
|
|
}
|
|
|
|
public function test_binding_existing_remote_user_marks_password_change_required(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/alice' => Http::response([
|
|
'username' => 'alice',
|
|
'uid' => 1000,
|
|
'gid' => 1000,
|
|
'home_dir' => '/home/alice',
|
|
'shell' => '/bin/bash',
|
|
]),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$response = $this->actingAs($admin, 'api')->putJson('/users/'.$admin->id, [
|
|
'server_bindings' => [[
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
]],
|
|
]);
|
|
|
|
$response->assertOk()->assertJsonPath('code', 0);
|
|
$this->assertDatabaseHas('server_user_bindings', [
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
'force_password_change' => true,
|
|
]);
|
|
}
|
|
|
|
public function test_unbinding_can_delete_remote_user(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/alice' => Http::response(['status' => 'ok']),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
'force_password_change' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($admin, 'api')->putJson('/users/'.$admin->id, [
|
|
'server_unbindings' => [[
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'delete_remote' => true,
|
|
]],
|
|
]);
|
|
|
|
$response->assertOk()->assertJsonPath('code', 0);
|
|
$this->assertDatabaseMissing('server_user_bindings', ['user_id' => $admin->id, 'server_resource_id' => $server->id]);
|
|
Http::assertSent(fn (Request $request): bool => $request->method() === 'DELETE' && $request->url() === 'http://node.test/users/alice');
|
|
}
|
|
|
|
public function test_deleting_sso_user_does_not_delete_remote_server_user(): void
|
|
{
|
|
Http::fake();
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
$target = User::factory()->create();
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $target->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'target',
|
|
'remote_exists' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($admin, 'api')->deleteJson('/users/'.$target->id);
|
|
|
|
$response->assertOk()->assertJsonPath('code', 0);
|
|
$this->assertDatabaseMissing('server_user_bindings', ['user_id' => $target->id]);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_server_list_includes_bound_username_without_exposing_token(): void
|
|
{
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
$resource = ServerResource::query()->create([
|
|
'parent_id' => $server->id,
|
|
'name' => 'ssh',
|
|
'display_name' => 'SSH',
|
|
'internal_ip' => '10.0.0.10',
|
|
'asset_id' => 1,
|
|
'account_id' => 2,
|
|
'protocols' => ['SSH'],
|
|
'is_active' => true,
|
|
]);
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'admin',
|
|
'remote_exists' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($admin, 'api')->getJson('/servers');
|
|
|
|
$response
|
|
->assertOk()
|
|
->assertJsonPath('data.data.0.user_api_configured', true)
|
|
->assertJsonMissingPath('data.data.0.user_api_token');
|
|
|
|
$resourcePayload = collect($response->json('data.data'))->firstWhere('id', $resource->id);
|
|
$this->assertSame('admin', $resourcePayload['server_username'] ?? null);
|
|
}
|
|
|
|
public function test_resource_use_requires_password_change_for_bound_account(): void
|
|
{
|
|
Http::fake();
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
$resource = ServerResource::query()->create([
|
|
'parent_id' => $server->id,
|
|
'name' => 'ssh',
|
|
'display_name' => 'SSH',
|
|
'internal_ip' => '10.0.0.10',
|
|
'asset_id' => 1,
|
|
'account_id' => 2,
|
|
'protocols' => ['SSH'],
|
|
'is_active' => true,
|
|
]);
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
'force_password_change' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($admin, 'api')->postJson('/servers/'.$resource->id.'/use', [
|
|
'account_name' => 'alice',
|
|
'password' => 'secret',
|
|
'protocol' => 'SSH',
|
|
]);
|
|
|
|
$response
|
|
->assertStatus(423)
|
|
->assertJsonPath('data.reason', 'server_password_change_required');
|
|
}
|
|
|
|
public function test_bound_password_update_requires_strong_password_and_clears_flag(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/alice/password' => Http::response(['status' => 'ok']),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
ServerUserBinding::query()->create([
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'username' => 'alice',
|
|
'remote_exists' => true,
|
|
'force_password_change' => true,
|
|
]);
|
|
|
|
$weak = $this->actingAs($admin, 'api')->patchJson('/servers/'.$server->id.'/bound-system-user/password', [
|
|
'password' => 'abcdef',
|
|
]);
|
|
$weak->assertStatus(422);
|
|
|
|
$strong = $this->actingAs($admin, 'api')->patchJson('/servers/'.$server->id.'/bound-system-user/password', [
|
|
'password' => 'Abc123!',
|
|
]);
|
|
$strong->assertOk()->assertJsonPath('code', 0);
|
|
|
|
$this->assertDatabaseHas('server_user_bindings', [
|
|
'user_id' => $admin->id,
|
|
'server_resource_id' => $server->id,
|
|
'force_password_change' => false,
|
|
]);
|
|
}
|
|
|
|
public function test_server_environment_endpoints_proxy_to_user_management_api(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/environment' => Http::response(['status' => 'ok', 'updated_count' => 2]),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$default = $this->actingAs($admin, 'api')->putJson('/servers/'.$server->id.'/default-environment', [
|
|
'content' => 'export DEFAULT=1',
|
|
]);
|
|
$default->assertOk()->assertJsonPath('data.default_environment_variables', 'export DEFAULT=1');
|
|
|
|
$all = $this->actingAs($admin, 'api')->putJson('/servers/'.$server->id.'/system-users/environment', [
|
|
'content' => 'export ALL=1',
|
|
]);
|
|
$all
|
|
->assertOk()
|
|
->assertJsonPath('data.updated_count', 2)
|
|
->assertJsonPath('data.all_user_environment_variables', 'export ALL=1');
|
|
|
|
$this->assertDatabaseHas('server_resources', [
|
|
'id' => $server->id,
|
|
'all_user_environment_variables' => 'export ALL=1',
|
|
]);
|
|
}
|
|
|
|
public function test_user_management_api_error_message_is_extracted_from_detail_array(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/alice/environment' => Http::response([
|
|
'detail' => [
|
|
['message' => 'permission denied'],
|
|
],
|
|
], 422),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$response = $this->actingAs($admin, 'api')->getJson('/servers/'.$server->id.'/system-users/alice/environment');
|
|
|
|
$response
|
|
->assertStatus(422)
|
|
->assertJsonPath('errors.server.0', 'permission denied');
|
|
}
|
|
|
|
public function test_server_environment_endpoint_returns_partial_failures(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users/environment' => Http::response([
|
|
'status' => 'ok',
|
|
'message' => 'User environments updated.',
|
|
'updated_users' => ['alice'],
|
|
'failed_users' => [
|
|
['username' => 'bob', 'code' => 'system_permission_denied', 'message' => 'permission denied'],
|
|
],
|
|
'updated_count' => 1,
|
|
'failed_count' => 1,
|
|
]),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$response = $this->actingAs($admin, 'api')->putJson('/servers/'.$server->id.'/system-users/environment', [
|
|
'content' => 'export ALL=1',
|
|
]);
|
|
|
|
$response
|
|
->assertOk()
|
|
->assertJsonPath('data.updated_count', 1)
|
|
->assertJsonPath('data.failed_count', 1)
|
|
->assertJsonPath('data.failed_users.0.username', 'bob');
|
|
}
|
|
|
|
public function test_default_user_groups_are_saved_and_used_when_creating_system_user(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'http://node.test/users' => Http::response(['status' => 'ok', 'message' => 'User created.'], 201),
|
|
]);
|
|
|
|
$admin = $this->admin();
|
|
$server = $this->server();
|
|
|
|
$save = $this->actingAs($admin, 'api')->putJson('/servers/'.$server->id.'/default-user-groups', [
|
|
'groups' => ['dev', 'ops', 'dev'],
|
|
]);
|
|
$save
|
|
->assertOk()
|
|
->assertJsonPath('data.default_user_groups.0', 'dev')
|
|
->assertJsonPath('data.default_user_groups.1', 'ops');
|
|
|
|
$create = $this->actingAs($admin, 'api')->postJson('/servers/'.$server->id.'/system-users', [
|
|
'username' => 'alice',
|
|
'password' => 'secret123',
|
|
'groups' => ['extra'],
|
|
]);
|
|
$create->assertCreated();
|
|
|
|
Http::assertSent(function (Request $request): bool {
|
|
return $request->method() === 'POST'
|
|
&& $request->url() === 'http://node.test/users'
|
|
&& $request['username'] === 'alice'
|
|
&& $request['groups'] === ['dev', 'ops', 'extra'];
|
|
});
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
$user = User::factory()->create();
|
|
Role::query()->firstOrCreate(['name' => 'admin', 'guard_name' => 'api']);
|
|
$user->assignRole('admin');
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function server(): ServerResource
|
|
{
|
|
return ServerResource::query()->create([
|
|
'name' => 'server01',
|
|
'display_name' => 'Server 01',
|
|
'internal_ip' => '10.0.0.10',
|
|
'user_api_base_url' => 'http://node.test',
|
|
'user_api_token' => 'secret-token',
|
|
'default_environment_variables' => 'export A=1',
|
|
'default_user_groups' => ['dev'],
|
|
'asset_id' => 1,
|
|
'account_id' => null,
|
|
'protocols' => [],
|
|
'is_active' => true,
|
|
]);
|
|
}
|
|
}
|