256 lines
10 KiB
PHP
256 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\StoreBastionAccountRequest;
|
|
use App\Http\Requests\UpdateBastionAccountRequest;
|
|
use App\Models\BastionAccount;
|
|
use hg\apidoc\annotation as Apidoc;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
|
|
#[Apidoc\Title('堡垒机授权账号管理')]
|
|
class BastionAccountController extends Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
$this->middleware('auth:api');
|
|
$this->middleware('permission:platform.accounts.view,api')->only(['index', 'show']);
|
|
$this->middleware('permission:platform.accounts.manage,api')->only(['store', 'update', 'destroy', 'refreshToken', 'refreshTokenStatus']);
|
|
}
|
|
|
|
#[Apidoc\Title('账号列表'), Apidoc\Method('GET'), Apidoc\Url('/accounts')]
|
|
public function index(): JsonResponse
|
|
{
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->latest()->paginate(20)]);
|
|
}
|
|
|
|
#[Apidoc\Title('创建账号'), Apidoc\Method('POST'), Apidoc\Url('/accounts')]
|
|
public function store(StoreBastionAccountRequest $request): JsonResponse
|
|
{
|
|
$account = BastionAccount::query()->create($request->validated());
|
|
$this->auditLog($request, 'account_create', ['bastion_account_id' => $account->id]);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account], 201);
|
|
}
|
|
|
|
#[Apidoc\Title('账号详情'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}')]
|
|
public function show(int $id): JsonResponse
|
|
{
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->findOrFail($id)]);
|
|
}
|
|
|
|
#[Apidoc\Title('更新账号'), Apidoc\Method('PUT'), Apidoc\Url('/accounts/{id}')]
|
|
public function update(UpdateBastionAccountRequest $request, int $id): JsonResponse
|
|
{
|
|
$account = BastionAccount::query()->findOrFail($id);
|
|
$account->update($request->validated());
|
|
$this->auditLog($request, 'account_update', ['bastion_account_id' => $account->id]);
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account]);
|
|
}
|
|
|
|
#[Apidoc\Title('删除账号'), Apidoc\Method('DELETE'), Apidoc\Url('/accounts/{id}')]
|
|
public function destroy(Request $request, int $id): JsonResponse
|
|
{
|
|
$account = BastionAccount::query()->findOrFail($id);
|
|
$this->auditLog($request, 'account_delete', ['bastion_account_id' => $account->id]);
|
|
$account->delete();
|
|
|
|
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
|
}
|
|
|
|
#[Apidoc\Title('刷新账号token'), Apidoc\Method('POST'), Apidoc\Url('/accounts/{id}/refresh-token')]
|
|
public function refreshToken(Request $request, int $id): JsonResponse
|
|
{
|
|
$account = BastionAccount::query()->findOrFail($id);
|
|
$baseUrl = (string) config('services.bastion_token.base_url');
|
|
|
|
if ($baseUrl === '') {
|
|
return response()->json([
|
|
'code' => 500,
|
|
'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL',
|
|
'data' => null,
|
|
], 500);
|
|
}
|
|
|
|
$submitEndpoint = (string) config('services.bastion_token.submit_endpoint', '/bastion_token');
|
|
$timeout = (int) config('services.bastion_token.timeout', 30);
|
|
$taskTtlSeconds = (int) config('services.bastion_token.task_ttl_seconds', 1800);
|
|
$service = (string) config('services.bastion_token.service', 'https://myapp.cdu.edu.cn/index.html');
|
|
$verifySsl = (bool) config('services.bastion_token.verify_ssl', false);
|
|
|
|
try {
|
|
$submitResponse = Http::baseUrl($baseUrl)
|
|
->acceptJson()
|
|
->timeout($timeout)
|
|
->retry(2, 300, throw: false)
|
|
->post($submitEndpoint, [
|
|
'username' => $account->username,
|
|
'password' => $account->password,
|
|
'service' => $service,
|
|
'verify_ssl' => $verifySsl,
|
|
]);
|
|
|
|
if (! $submitResponse->successful()) {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => '提交 Token 刷新任务失败',
|
|
'data' => ['response' => $submitResponse->json()],
|
|
], 502);
|
|
}
|
|
|
|
$taskId = (string) data_get($submitResponse->json(), 'task_id', '');
|
|
|
|
if ($taskId === '') {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => 'Token 服务返回任务ID为空',
|
|
'data' => ['response' => $submitResponse->json()],
|
|
], 502);
|
|
}
|
|
} catch (ConnectionException|RequestException $exception) {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => '调用 Token 服务失败:'.$exception->getMessage(),
|
|
'data' => null,
|
|
], 502);
|
|
}
|
|
|
|
$cacheKey = $this->tokenTaskCacheKey($taskId);
|
|
Cache::put($cacheKey, [
|
|
'task_id' => $taskId,
|
|
'account_id' => $account->id,
|
|
'finished' => false,
|
|
], now()->addSeconds(max(60, $taskTtlSeconds)));
|
|
|
|
$this->auditLog($request, 'account_refresh_token_submit', ['bastion_account_id' => $account->id, 'task_id' => $taskId]);
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'Token 刷新任务已提交',
|
|
'data' => [
|
|
'task_id' => $taskId,
|
|
'status' => 'pending',
|
|
],
|
|
]);
|
|
}
|
|
|
|
#[Apidoc\Title('查询刷新账号token状态'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}/refresh-token/{taskId}')]
|
|
public function refreshTokenStatus(Request $request, int $id, string $taskId): JsonResponse
|
|
{
|
|
$account = BastionAccount::query()->findOrFail($id);
|
|
$baseUrl = (string) config('services.bastion_token.base_url');
|
|
|
|
if ($baseUrl === '') {
|
|
return response()->json([
|
|
'code' => 500,
|
|
'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL',
|
|
'data' => null,
|
|
], 500);
|
|
}
|
|
|
|
$cacheKey = $this->tokenTaskCacheKey($taskId);
|
|
$taskMeta = Cache::get($cacheKey);
|
|
|
|
if (! is_array($taskMeta) || (int) ($taskMeta['account_id'] ?? 0) !== $account->id) {
|
|
return response()->json([
|
|
'code' => 404,
|
|
'message' => '任务不存在或已过期',
|
|
'data' => ['task_id' => $taskId],
|
|
], 404);
|
|
}
|
|
|
|
$statusEndpoint = (string) config('services.bastion_token.status_endpoint', '/bastion_token/{task_id}');
|
|
$timeout = (int) config('services.bastion_token.timeout', 30);
|
|
$statusUrl = str_replace('{task_id}', $taskId, $statusEndpoint);
|
|
|
|
try {
|
|
$statusResponse = Http::baseUrl($baseUrl)
|
|
->acceptJson()
|
|
->timeout($timeout)
|
|
->retry(2, 300, throw: false)
|
|
->get($statusUrl);
|
|
} catch (ConnectionException|RequestException $exception) {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => '查询 Token 任务状态失败:'.$exception->getMessage(),
|
|
'data' => ['task_id' => $taskId],
|
|
], 502);
|
|
}
|
|
|
|
if (! $statusResponse->successful()) {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => '查询 Token 任务状态失败',
|
|
'data' => ['task_id' => $taskId, 'response' => $statusResponse->json()],
|
|
], 502);
|
|
}
|
|
|
|
$taskResult = $statusResponse->json();
|
|
$status = Str::lower((string) data_get($taskResult, 'status', 'pending'));
|
|
|
|
if ($status !== 'success') {
|
|
if ($status === 'error') {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => 'Token 刷新失败:'.((string) data_get($taskResult, 'message', '未知错误')),
|
|
'data' => ['task_id' => $taskId, 'status' => $status, 'result' => $taskResult],
|
|
], 502);
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'Token 刷新任务执行中',
|
|
'data' => ['task_id' => $taskId, 'status' => 'pending'],
|
|
]);
|
|
}
|
|
|
|
if (! (bool) ($taskMeta['finished'] ?? false)) {
|
|
$usmAuthentication = (string) data_get($taskResult, 'data.bastion.token_cookies.USM-AUTHENTICATION', '');
|
|
$usm = (string) data_get($taskResult, 'data.bastion.token_cookies.USM', '');
|
|
|
|
// 向后兼容旧版任务服务返回结构
|
|
if ($usmAuthentication === '' || $usm === '') {
|
|
$usmAuthentication = (string) data_get($taskResult, 'data.USM-AUTHENTICATION', $usmAuthentication);
|
|
$usm = (string) data_get($taskResult, 'data.USM', $usm);
|
|
}
|
|
|
|
if ($usmAuthentication === '' || $usm === '') {
|
|
return response()->json([
|
|
'code' => 502,
|
|
'message' => 'Token 服务返回数据缺失',
|
|
'data' => ['task_id' => $taskId, 'result' => $taskResult],
|
|
], 502);
|
|
}
|
|
|
|
$account->update([
|
|
'usm_authentication' => $usmAuthentication,
|
|
'usm' => $usm,
|
|
'last_token_refreshed_at' => now(),
|
|
]);
|
|
|
|
$taskMeta['finished'] = true;
|
|
Cache::put($cacheKey, $taskMeta, now()->addMinutes(10));
|
|
$this->auditLog($request, 'account_refresh_token', ['bastion_account_id' => $account->id, 'task_id' => $taskId]);
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'Token 刷新成功',
|
|
'data' => ['task_id' => $taskId, 'status' => 'success', 'account' => $account->fresh()],
|
|
]);
|
|
}
|
|
|
|
private function tokenTaskCacheKey(string $taskId): string
|
|
{
|
|
return 'bastion_token_task:'.$taskId;
|
|
}
|
|
}
|