360 lines
14 KiB
PHP
360 lines
14 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'],
|
|
]);
|
|
$this->ensureStrongPassword((string) $validated['password']);
|
|
|
|
$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,
|
|
'force_password_change' => false,
|
|
'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(array_merge($server->default_user_groups ?? [], $validated['groups'] ?? []))),
|
|
'default_environment_variables' => (string) ($server->default_environment_variables ?? ''),
|
|
];
|
|
|
|
$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);
|
|
ServerUserBinding::query()
|
|
->where('server_resource_id', $server->id)
|
|
->where('username', $username)
|
|
->update([
|
|
'remote_exists' => true,
|
|
'force_password_change' => false,
|
|
'last_synced_at' => now(),
|
|
]);
|
|
$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('GET'), Apidoc\Url('/servers/{id}/system-users/{username}/environment')]
|
|
public function userEnvironment(int $id, string $username): JsonResponse
|
|
{
|
|
$server = $this->server($id);
|
|
$result = $this->client->userEnvironment($server, $username);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]);
|
|
}
|
|
|
|
#[Apidoc\Title('修改服务器用户环境变量'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}/system-users/{username}/environment')]
|
|
public function updateUserEnvironment(Request $request, int $id, string $username): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'content' => ['nullable', 'string', 'max:20000'],
|
|
]);
|
|
|
|
$server = $this->server($id);
|
|
$result = $this->client->updateUserEnvironment($server, $username, (string) ($validated['content'] ?? ''));
|
|
$this->auditLog($request, 'server_system_user_environment_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;
|
|
}
|
|
|
|
private function ensureStrongPassword(string $password): void
|
|
{
|
|
$types = 0;
|
|
$types += preg_match('/[a-z]/', $password) === 1 ? 1 : 0;
|
|
$types += preg_match('/[A-Z]/', $password) === 1 ? 1 : 0;
|
|
$types += preg_match('/\d/', $password) === 1 ? 1 : 0;
|
|
$types += preg_match('/[^a-zA-Z\d]/', $password) === 1 ? 1 : 0;
|
|
|
|
if ($types < 3) {
|
|
throw ValidationException::withMessages([
|
|
'password' => ['密码不能过于简单,需包含大小写字母、数字、特殊字符中的至少三类。'],
|
|
]);
|
|
}
|
|
}
|
|
}
|