BastionSSO/tests/Feature/ServerSystemUserManagementTest.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,
]);
}
}