BastionSSO/app/Http/Controllers/Api/ServerSystemUserController.php

308 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\ServerResource;
use App\Models\ServerUserBinding;
use App\Models\User;
use App\Services\ServerUserManagementClient;
use hg\apidoc\annotation as Apidoc;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
#[Apidoc\Title('服务器用户与用户组管理')]
class ServerSystemUserController extends Controller
{
public function __construct(private ServerUserManagementClient $client)
{
$this->middleware('auth:api');
$this->middleware('permission:platform.servers.manage,api')->except(['updateBoundPassword']);
}
#[Apidoc\Title('修改当前用户绑定服务器账号密码'), Apidoc\Method('PATCH'), Apidoc\Url('/servers/{id}/bound-system-user/password')]
public function updateBoundPassword(Request $request, int $id): JsonResponse
{
$validated = $request->validate([
'password' => ['required', 'string', 'min:6', 'max:255'],
]);
$server = $this->server($id);
$resource = ServerResource::query()->with('parent')->findOrFail($id);
/** @var User|null $user */
$user = auth('api')->user();
if (! $user || ! $this->canUpdateBoundPassword($user, $resource)) {
return response()->json([
'code' => 403,
'message' => '无权限修改该服务器账号密码',
'data' => null,
], 403);
}
$binding = ServerUserBinding::query()
->where('user_id', $user->id)
->where('server_resource_id', $server->id)
->first();
if (! $binding || trim((string) $binding->username) === '') {
throw ValidationException::withMessages([
'server' => ['当前用户未绑定该服务器账号。'],
]);
}
$result = $this->client->updatePassword($server, (string) $binding->username, $this->linuxPasswordHash((string) $validated['password']));
$binding->update([
'remote_exists' => true,
'last_synced_at' => now(),
]);
$this->auditLog($request, 'server_bound_user_password_update', [
'server_resource_id' => $server->id,
'metadata' => ['username' => $binding->username],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
}
#[Apidoc\Title('服务器用户管理元数据'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}/system-users/meta')]
public function meta(int $id): JsonResponse
{
$server = $this->server($id);
$users = $this->client->users($server);
$groups = $this->client->groups($server);
$userGroups = [];
foreach ($users as $user) {
$username = (string) ($user['username'] ?? '');
if ($username === '') {
continue;
}
try {
$userGroups[$username] = $this->client->userGroups($server, $username)['groups'] ?? [];
} catch (ValidationException) {
$userGroups[$username] = [];
}
}
$bindings = ServerUserBinding::query()
->with('user:id,nickname,email,phone')
->where('server_resource_id', $server->id)
->orderBy('username')
->get();
return response()->json([
'code' => 0,
'message' => 'ok',
'data' => [
'server' => $server,
'users' => $users,
'groups' => $groups,
'user_groups' => $userGroups,
'bindings' => $bindings,
],
]);
}
#[Apidoc\Title('创建服务器用户'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/system-users')]
public function storeUser(Request $request, int $id): JsonResponse
{
$validated = $request->validate([
'username' => ['required', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'],
'password_hash' => ['nullable', 'string', 'min:10', 'max:512'],
'password' => ['nullable', 'string', 'min:6', 'max:255'],
'primary_group' => ['nullable', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'],
'groups' => ['sometimes', 'array'],
'groups.*' => ['string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'],
'user_id' => ['nullable', 'integer', 'exists:users,id'],
]);
$server = $this->server($id);
$passwordHash = (string) ($validated['password_hash'] ?? '');
if ($passwordHash === '') {
$passwordHash = $this->linuxPasswordHash((string) ($validated['password'] ?? ''));
}
$payload = [
'username' => $validated['username'],
'password_hash' => $passwordHash,
'primary_group' => $validated['primary_group'] ?? null,
'groups' => array_values(array_unique($validated['groups'] ?? [])),
];
$result = $this->client->createUser($server, $payload);
$this->upsertBinding((int) ($validated['user_id'] ?? 0), $server, (string) $validated['username'], true);
$this->auditLog($request, 'server_system_user_create', [
'server_resource_id' => $server->id,
'metadata' => ['username' => $validated['username']],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result], 201);
}
#[Apidoc\Title('删除服务器用户'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}/system-users/{username}')]
public function destroyUser(Request $request, int $id, string $username): JsonResponse
{
$server = $this->server($id);
$result = $this->client->deleteUser($server, $username);
ServerUserBinding::query()
->where('server_resource_id', $server->id)
->where('username', $username)
->update(['remote_exists' => false, 'last_synced_at' => now()]);
$this->auditLog($request, 'server_system_user_delete', [
'server_resource_id' => $server->id,
'metadata' => ['username' => $username],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
}
#[Apidoc\Title('修改服务器用户密码'), Apidoc\Method('PATCH'), Apidoc\Url('/servers/{id}/system-users/{username}/password')]
public function updatePassword(Request $request, int $id, string $username): JsonResponse
{
$validated = $request->validate([
'password_hash' => ['nullable', 'string', 'min:10', 'max:512'],
'password' => ['nullable', 'string', 'min:6', 'max:255'],
]);
$server = $this->server($id);
$passwordHash = (string) ($validated['password_hash'] ?? '');
if ($passwordHash === '') {
$passwordHash = $this->linuxPasswordHash((string) ($validated['password'] ?? ''));
}
$result = $this->client->updatePassword($server, $username, $passwordHash);
$this->auditLog($request, 'server_system_user_password_update', [
'server_resource_id' => $server->id,
'metadata' => ['username' => $username],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
}
#[Apidoc\Title('创建服务器用户组'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/system-groups')]
public function storeGroup(Request $request, int $id): JsonResponse
{
$validated = $request->validate([
'groupname' => ['required', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'],
]);
$server = $this->server($id);
$result = $this->client->createGroup($server, (string) $validated['groupname']);
$this->auditLog($request, 'server_system_group_create', [
'server_resource_id' => $server->id,
'metadata' => ['groupname' => $validated['groupname']],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result], 201);
}
#[Apidoc\Title('删除服务器用户组'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}/system-groups/{groupname}')]
public function destroyGroup(Request $request, int $id, string $groupname): JsonResponse
{
$server = $this->server($id);
$result = $this->client->deleteGroup($server, $groupname);
$this->auditLog($request, 'server_system_group_delete', [
'server_resource_id' => $server->id,
'metadata' => ['groupname' => $groupname],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
}
#[Apidoc\Title('同步服务器用户组'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}/system-users/{username}/groups')]
public function syncUserGroups(Request $request, int $id, string $username): JsonResponse
{
$validated = $request->validate([
'groups' => ['present', 'array'],
'groups.*' => ['string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'],
'mode' => ['nullable', 'string', 'in:append,replace'],
]);
$server = $this->server($id);
$result = $this->client->syncUserGroups(
$server,
$username,
array_values(array_unique($validated['groups'])),
(string) ($validated['mode'] ?? 'replace'),
);
$this->auditLog($request, 'server_system_user_groups_update', [
'server_resource_id' => $server->id,
'metadata' => ['username' => $username, 'groups' => $validated['groups']],
]);
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
}
private function server(int $id): ServerResource
{
$server = ServerResource::query()->with('parent')->findOrFail($id);
return $server->parent_id ? $server->parent()->firstOrFail() : $server;
}
private function upsertBinding(int $userId, ServerResource $server, string $username, bool $remoteExists): void
{
if ($userId <= 0) {
return;
}
ServerUserBinding::query()->updateOrCreate(
['user_id' => $userId, 'server_resource_id' => $server->id],
['username' => $username, 'remote_exists' => $remoteExists, 'last_synced_at' => now()],
);
}
private function canUpdateBoundPassword(User $user, ServerResource $resource): bool
{
if ($user->can('platform.servers.view')) {
return true;
}
$serverId = (int) ($resource->parent_id ?: $resource->id);
$hasBinding = ServerUserBinding::query()
->where('user_id', $user->id)
->where('server_resource_id', $serverId)
->exists();
if (! $hasBinding) {
return false;
}
if (! $resource->parent_id) {
return true;
}
$pivot = $resource->users()
->where('users.id', $user->id)
->first()?->pivot;
if ($pivot && (bool) ($pivot->can_ssh || $pivot->can_sftp || $pivot->can_rdp)) {
return true;
}
return $user->can('resource.servers.use');
}
private function linuxPasswordHash(string $password): string
{
if ($password === '') {
throw ValidationException::withMessages([
'password' => ['请输入服务器账号密码。'],
]);
}
$salt = substr(strtr(base64_encode(random_bytes(12)), '+', '.'), 0, 16);
$hash = crypt($password, '$6$rounds=5000$'.$salt.'$');
if (! is_string($hash) || strlen($hash) < 10) {
throw ValidationException::withMessages([
'password' => ['服务器账号密码 Hash 生成失败。'],
]);
}
return $hash;
}
}