-
Notifications
You must be signed in to change notification settings - Fork 19
feat: MCP サーバ用 OAuth2 scope と firewall を追加 #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
4fd463e
feat: MCP サーバ用 OAuth2 scope と firewall を追加
dotani1111 5132fad
fix: OAuth クライアント一覧で identifier にハイフンを含むと 500 になるのを修正
dotani1111 99889fe
feat: 管理画面から MCP トークンを発行・失効できるようにする
dotani1111 2a41051
test: MCP トークン発行・失効の契約テストを追加
dotani1111 735a585
fix: OAuth authorize の同意フローテストを redirectUris 込みの fixture に修正
dotani1111 ab51b5a
feat: MCP の OAuth 自動ディスカバリ (.well-known / DCR) を追加
dotani1111 4940127
test: OAuth ディスカバリ/DCR の契約テストを追加
dotani1111 8d7932c
fix: MCP PAT クライアント生成を plugin enable から初回発行時に移す
dotani1111 9649345
fix: MCP サーバ前提の統合テストを本体 mcp ジョブへ委譲し firewall 期待値を更新
dotani1111 58009c5
feat: DCR 動的登録クライアントの死蔵掃除コマンドを追加
dotani1111 46d00fa
feat: 認可応答に RFC 9207 iss を付与する
dotani1111 4e9d66a
fix: iss を認可応答の query/fragment 両方に付与する
dotani1111 367b54b
docs(mcp): レビュー指摘でコメントの用語と説明を精緻化
dotani1111 d79221f
fix(mcp): /admin/mcp に mcp read scope を要求する access_control を追加する
dotani1111 20bd5f3
fix(mcp): MCP 未搭載の本体では access_control 追加を skip する
dotani1111 090b6e1
fix(mcp): access_control の role を直書きし本体非依存で phpstan を通す
dotani1111 3f8e5fd
fix(mcp): PR #190 レビュー対応 (scope 永続化 / base path / docblock / 有効日数)
dotani1111 2764f14
fix(mcp): PR #190 CodeRabbit 対応 (認可応答の iss 上書き / トークン画面の no-store)
dotani1111 40463b5
docs(mcp): 動作確認環境の PHP 要件を 8.2 以上に更新する
dotani1111 591ccac
fix(mcp): DCR レート制限を IP 単位→グローバルの順で消費する
dotani1111 3e68530
fix(mcp): 認可応答の iss 付与で redirect_uri のクエリを原文保持する
dotani1111 a6553b3
fix(mcp): MCP トークン発行を ADMIN 権限に限定する
dotani1111 150f7ec
feat(mcp): TRUSTED_HOSTS 未設定を警告し discovery の Host 偽装リスクを顕在化する
dotani1111 c2ffb10
fix(mcp): MCP トークンの有効日数上限を 180 日に短縮する
dotani1111 27c1ca4
fix(mcp): PKCE(S256) 必須を明示し public クライアントの認可コード横取りを防ぐ
dotani1111 50d5a71
test(mcp): 有効日数上限 180 日を回帰ガードするテストを追加
dotani1111 6ef142f
test(mcp): PKCE 必須が挙動として効くことを縛るテストを追加
dotani1111 76ac5e8
Merge branch '4.4' into feat/mcp-server-scorp
dotani1111 171d8ef
fix(mcp): コメント・表記をコードに合わせる
dotani1111 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of EC-CUBE | ||
| * | ||
| * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. | ||
| * | ||
| * http://www.ec-cube.co.jp/ | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Plugin\Api44\Command; | ||
|
|
||
| use Plugin\Api44\Service\DcrClientCleaner; | ||
| use Psr\Log\LoggerInterface; | ||
| use Symfony\Component\Console\Attribute\AsCommand; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Input\InputOption; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use Symfony\Component\Console\Style\SymfonyStyle; | ||
|
|
||
| /** | ||
| * DCR (動的クライアント登録) で量産される死蔵クライアントを掃除する。 | ||
| * | ||
| * 登録から --days を過ぎ、 有効な access/refresh トークンを持たない DCR クライアントを削除する。 | ||
| * 定期実行 (cron) を想定。 --dry-run で対象だけ確認できる。 | ||
| * 掃除対象は追跡レコードのある client のみ (導入前/記録漏れの client は対象外)。 | ||
| */ | ||
| #[AsCommand( | ||
| name: 'eccube:api:mcp:cleanup-dcr-clients', | ||
| description: 'Remove abandoned DCR-registered OAuth clients that have no valid tokens.' | ||
| )] | ||
| class CleanupDcrClientsCommand extends Command | ||
| { | ||
| private const DEFAULT_GRACE_DAYS = 30; | ||
|
|
||
| public function __construct( | ||
| private readonly DcrClientCleaner $cleaner, | ||
| private readonly LoggerInterface $logger, | ||
| ) { | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| protected function configure(): void | ||
| { | ||
| $this | ||
| ->addOption('days', null, InputOption::VALUE_REQUIRED, 'Grace period in days; clients registered within this window are never removed', (string) self::DEFAULT_GRACE_DAYS) | ||
| ->addOption('dry-run', null, InputOption::VALUE_NONE, 'List clients that would be removed without deleting them'); | ||
| } | ||
|
|
||
| protected function execute(InputInterface $input, OutputInterface $output): int | ||
| { | ||
| $io = new SymfonyStyle($input, $output); | ||
|
|
||
| $days = filter_var($input->getOption('days'), FILTER_VALIDATE_INT); | ||
| if (false === $days || $days < 0) { | ||
| $io->error('--days must be a non-negative integer.'); | ||
|
|
||
| return Command::INVALID; | ||
| } | ||
| $dryRun = (bool) $input->getOption('dry-run'); | ||
|
|
||
| try { | ||
| $result = $this->cleaner->cleanup(new \DateTime(), $days, $dryRun); | ||
| } catch (\Throwable $e) { | ||
| // 破壊的操作の失敗を握り潰さず記録する (トランザクションは rollback 済み = 未変更) | ||
| $this->logger->error('DCR cleanup failed', ['exception' => $e, 'days' => $days, 'dry_run' => $dryRun]); | ||
| $io->error('DCR cleanup failed; no clients were removed. See logs for details.'); | ||
|
|
||
| return Command::FAILURE; | ||
| } | ||
|
|
||
| // 破壊的・自動実行の監査証跡として、 いつ何件消したかを残す | ||
| $this->logger->info('DCR cleanup completed', [ | ||
| 'dry_run' => $dryRun, | ||
| 'examined' => $result->examinedCount, | ||
| 'kept_active' => $result->keptActiveCount, | ||
| 'deleted' => $result->deletedCount(), | ||
| 'deleted_client_ids' => $result->deletedClientIdentifiers, | ||
| ]); | ||
|
|
||
| $io->title($dryRun ? 'DCR cleanup (dry-run)' : 'DCR cleanup'); | ||
| $io->listing($result->deletedClientIdentifiers); | ||
| $io->table(['examined', 'kept (active)', $dryRun ? 'to delete' : 'deleted'], [ | ||
| [$result->examinedCount, $result->keptActiveCount, $result->deletedCount()], | ||
| ]); | ||
| $io->success(sprintf( | ||
| '%s %d abandoned DCR client(s); kept %d active.', | ||
| $dryRun ? 'Would remove' : 'Removed', | ||
| $result->deletedCount(), | ||
| $result->keptActiveCount, | ||
| )); | ||
|
|
||
| return Command::SUCCESS; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of EC-CUBE | ||
| * | ||
| * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. | ||
| * | ||
| * http://www.ec-cube.co.jp/ | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Plugin\Api44\Controller\Admin; | ||
|
|
||
| use Eccube\Controller\AbstractController; | ||
| use Eccube\Entity\Member; | ||
| use Plugin\Api44\Form\Type\Admin\McpTokenType; | ||
| use Plugin\Api44\Repository\McpTokenRepository; | ||
| use Plugin\Api44\Service\McpTokenService; | ||
| use Symfony\Component\HttpFoundation\RedirectResponse; | ||
| use Symfony\Component\HttpFoundation\Request; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
| use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||
| use Symfony\Component\Routing\Attribute\Route; | ||
|
|
||
| class McpTokenController extends AbstractController | ||
| { | ||
| public function __construct( | ||
| private readonly McpTokenService $mcpTokenService, | ||
| private readonly McpTokenRepository $mcpTokenRepository, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * MCP トークン発行画面。 送信時にトークンを発行し、 JWT を 1 度だけ表示する。 | ||
| */ | ||
| #[Route(path: '/%eccube_admin_route%/api/oauth/mcp/new', name: 'admin_api_mcp_token_new', methods: ['GET', 'POST'])] | ||
| public function create(Request $request): Response | ||
| { | ||
| $form = $this->createForm(McpTokenType::class); | ||
| $form->handleRequest($request); | ||
|
|
||
| if ($form->isSubmitted() && $form->isValid()) { | ||
| $member = $this->getUser(); | ||
| if (!$member instanceof Member) { | ||
| throw new AccessDeniedHttpException(); | ||
| } | ||
|
|
||
| try { | ||
| // sub はフォーム入力でなく操作中の Member 固定 (なりすまし防止) | ||
| $token = $this->mcpTokenService->issue( | ||
| $member, | ||
| (string) $form->get('label')->getData(), | ||
| $form->get('scopes')->getData(), | ||
| (int) $form->get('expire')->getData(), | ||
| ); | ||
|
|
||
| // 発行直後のみ JWT を表示する (再表示不可)。 redirect すると失われるため render する | ||
| return $this->render('@Api44/admin/OAuth/mcp_token_issued.twig', [ | ||
| 'token' => $token, | ||
| 'label' => (string) $form->get('label')->getData(), | ||
| ]); | ||
| } catch (\Exception $e) { | ||
| $this->addError(trans('admin.common.save_error'), 'admin'); | ||
| // 例外クラスと発行者を残し、 万一の発行失敗を追跡可能にする | ||
| log_error('MCP トークン発行エラー', ['exception' => $e, 'member_id' => $member->getId()]); | ||
| } | ||
| } | ||
|
|
||
| return $this->render('@Api44/admin/OAuth/mcp_token.twig', [ | ||
| 'form' => $form->createView(), | ||
| ]); | ||
| } | ||
|
|
||
| /** | ||
| * MCP トークンを失効する (league token を revoke → 即 401)。 | ||
| */ | ||
| #[Route(path: '/%eccube_admin_route%/api/oauth/mcp/revoke/{id}', requirements: ['id' => '\d+'], name: 'admin_api_mcp_token_revoke', methods: ['DELETE'])] | ||
| public function revoke(Request $request, int $id): RedirectResponse | ||
| { | ||
| $this->isTokenValid(); | ||
|
|
||
| $mcpToken = $this->mcpTokenRepository->find($id); | ||
| if (null === $mcpToken) { | ||
| $this->addError('admin.common.delete_error_already_deleted', 'admin'); | ||
|
|
||
| return $this->redirectToRoute('admin_api_oauth'); | ||
| } | ||
|
|
||
| try { | ||
| $found = $this->mcpTokenService->revoke($mcpToken); | ||
| if ($found) { | ||
| $this->addSuccess('admin.common.delete_complete', 'admin'); | ||
| } else { | ||
| // 失効対象の league token が見つからなかった (期限切れ削除済み等)。 メタは削除したが操作者に明示する | ||
| $this->addWarning('api.admin.oauth.mcp_token.revoke__not_found', 'admin'); | ||
| } | ||
| } catch (\Exception $e) { | ||
| $this->addError('admin.common.delete_error', 'admin'); | ||
| log_error('MCP トークン失効エラー', ['exception' => $e, 'id' => $id]); | ||
| } | ||
|
|
||
| return $this->redirectToRoute('admin_api_oauth'); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.