From f2c11d2edb5cda963594ac1895fb25870ab63b4c Mon Sep 17 00:00:00 2001 From: chym1217 Date: Mon, 27 Jul 2026 09:39:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(message=5Fbizppurio):=20webhook=20=EB=AF=B8?= =?UTF-8?q?=EB=93=A4=EC=9B=A8=EC=96=B4=20self-gate=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?+=20=EB=B0=9C=EC=86=A1=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4?= =?UTF-8?q?=20scale=20=EB=A9=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 라우트 파일에서 직접 부착하던 webhook IP 화이트리스트를 plugin.php getMiddleware self-gate 선언으로 전환한다(코어 확장 미들웨어 규정). 테스트 게이트 활성 헬퍼를 이식해 webhook 보안 테스트를 복구하고, 단건 발송 Job 시나리오에 scale 축 면제를 명시한다(볼륨 무관 상수 메모리). webhook API 문서에 self-gate 부착 방식을 함께 보강한다. --- .../docs/api/webhook.md | 2 +- .../sirsoft-message_bizppurio/plugin.php | 23 ++++++++++++ .../src/routes/api.php | 3 +- .../tests/PluginTestCase.php | 36 +++++++++++++++++++ .../scenarios/dispatch-history-webhook.yaml | 1 + 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/plugins/_bundled/sirsoft-message_bizppurio/docs/api/webhook.md b/plugins/_bundled/sirsoft-message_bizppurio/docs/api/webhook.md index ad7d5e9b..f8cea569 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/docs/api/webhook.md +++ b/plugins/_bundled/sirsoft-message_bizppurio/docs/api/webhook.md @@ -78,7 +78,7 @@ Content-Type: application/json 비즈뿌리오가 문자·알림톡 발송 결과를 URL PUSH 로 통보하는 리포트 수신 엔드포인트다. 운영자가 이 주소를 비즈뿌리오에 등록하면(환경설정 화면의 리포트 수신 주소), 발송 후 결과가 이 엔드포인트로 전송된다. -- **인증**: 코어 토큰/IDV 미들웨어를 라우트 레벨에서 제외하고, 인증을 IP 화이트리스트로 대체한다. 화이트리스트 밖 IP 는 403. +- **인증**: 코어 토큰/IDV 미들웨어를 라우트 레벨에서 제외하고, 인증을 IP 화이트리스트로 대체한다. 화이트리스트 밖 IP 는 403. IP 화이트리스트는 `plugin.php::getMiddleware()` 에서 이 라우트명(`api.plugins.sirsoft-message_bizppurio.webhook`)으로 self-gate 선언하며, 코어 게이트가 요청 시점에 부착한다(라우트 파일 직접 부착 아님). - **처리**: `REFKEY` 로 발송 이력을 조회한다. 없으면(위조/미매칭) 200 으로 흡수한다. 이미 리포트가 반영된 이력(`reported_at` 존재)이면 replay 로 판정해 멱등 처리한다. 그 외에는 `RESULT` 코드를 분류(성공/실패/잔액부족)해 상태를 전이하고 `media`·`fallback_status`·`raw_payload`·`reported_at` 을 기록한다. - **잔액부족**: `RESULT` 가 9070(문자)/7436(알림톡)이면 이력을 실패로 뒤집고 관리자에게 자체 알림을 1회 발송한다. - 응답은 항상 200 이다(비즈뿌리오가 실패 응답을 재전송하지 않도록). replay 멱등이 중복 처리를 막는다. diff --git a/plugins/_bundled/sirsoft-message_bizppurio/plugin.php b/plugins/_bundled/sirsoft-message_bizppurio/plugin.php index ddd6f56b..4e4923f2 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/plugin.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/plugin.php @@ -12,6 +12,7 @@ use App\Models\NotificationDefinition; use Database\Seeders\NotificationDefinitionSeeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Plugins\Sirsoft\MessageBizppurio\Http\Middleware\BizppurioWebhookIpWhitelist; use Plugins\Sirsoft\MessageBizppurio\Listeners\GuestPhoneExtractListener; use Plugins\Sirsoft\MessageBizppurio\Listeners\LinkNotificationLogListener; use Plugins\Sirsoft\MessageBizppurio\Listeners\RegisterNotificationChannelsListener; @@ -450,4 +451,26 @@ class Plugin extends AbstractPlugin ValidateBizppurioSettingsListener::class, ]; } + + /** + * 확장 미들웨어 선언 (self-gate) + * + * webhook(URL PUSH) 리포트 수신 엔드포인트에만 IP 화이트리스트를 부착한다. + * 라우트 파일에서 직접 부착하지 않고 코어 게이트(ExtensionMiddlewareGate)가 + * 요청 시점에 라우트 이름을 대조해 매칭될 때만 실행한다. + * + * @return array> + */ + public function getMiddleware(): array + { + return [ + [ + 'class' => BizppurioWebhookIpWhitelist::class, + 'groups' => ['api'], + 'targets' => [ + 'api.plugins.sirsoft-message_bizppurio.webhook', + ], + ], + ]; + } } diff --git a/plugins/_bundled/sirsoft-message_bizppurio/src/routes/api.php b/plugins/_bundled/sirsoft-message_bizppurio/src/routes/api.php index 78df0822..63dfb6a5 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/src/routes/api.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/src/routes/api.php @@ -8,7 +8,6 @@ use Plugins\Sirsoft\MessageBizppurio\Controllers\Admin\AlimtalkTemplateControlle use Plugins\Sirsoft\MessageBizppurio\Controllers\Admin\DispatchResultController; use Plugins\Sirsoft\MessageBizppurio\Controllers\Admin\NotificationBindingController; use Plugins\Sirsoft\MessageBizppurio\Controllers\BizppurioWebhookController; -use Plugins\Sirsoft\MessageBizppurio\Http\Middleware\BizppurioWebhookIpWhitelist; /* |-------------------------------------------------------------------------- @@ -24,9 +23,9 @@ use Plugins\Sirsoft\MessageBizppurio\Http\Middleware\BizppurioWebhookIpWhitelist // // api 그룹에 appendToGroup 된 토큰/IDV 미들웨어를 라우트 레벨에서 제외(코어 무수정)하고, // 인증은 IP 화이트리스트로 대체한다(계획서 D13). 항상 200 응답(멱등). +// IP 화이트리스트는 plugin.php::getMiddleware() 에서 이 라우트명으로 self-gate 선언한다. Route::post('/webhook', [BizppurioWebhookController::class, 'handle']) ->withoutMiddleware([EnforceIdentityPolicy::class, RefreshTokenExpiration::class]) - ->middleware(BizppurioWebhookIpWhitelist::class) ->name('webhook'); Route::prefix('admin')->name('admin.')->middleware(['auth:sanctum', 'admin'])->group(function () { diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/PluginTestCase.php index da7b1562..e957d824 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/PluginTestCase.php @@ -2,8 +2,12 @@ namespace Plugins\Sirsoft\MessageBizppurio\Tests; +use App\Enums\ExtensionStatus; +use App\Extension\ExtensionMiddlewareRegistry; use App\Extension\HookListenerRegistrar; use App\Extension\HookManager; +use App\Extension\PluginManager; +use App\Models\Plugin; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; @@ -54,6 +58,38 @@ abstract class PluginTestCase extends TestCase $this->isolatePluginStorage(); $this->snapshotHookManager(); $this->snapshotHookListenerRegistrar(); + $this->activateSelfForMiddlewareGate(); + } + + /** + * 자기 플러그인을 미들웨어 게이트 인덱스에 활성 등록한다. + * + * getMiddleware() self-gate 선언(webhook IP 화이트리스트)은 코어 게이트가 + * 활성 플러그인 registry 를 대조해 실행하므로, 테스트 환경에서도 plugins 테이블 + * 활성 행 + PluginManager 인스턴스 등록 + 인덱스 무효화가 있어야 매칭된다. + * 누락 시 게이트가 미들웨어를 부착하지 못해 차단 IP 도 통과한다. + */ + protected function activateSelfForMiddlewareGate(): void + { + Plugin::query()->updateOrCreate( + ['identifier' => 'sirsoft-message_bizppurio'], + [ + 'vendor' => 'sirsoft', + 'name' => json_encode(['ko' => 'sirsoft-message_bizppurio', 'en' => 'sirsoft-message_bizppurio']), + 'version' => '1.0.0', + 'status' => ExtensionStatus::Active->value, + ] + ); + PluginManager::invalidatePluginStatusCache(); + + $pluginManager = $this->app->make(PluginManager::class); + $property = new \ReflectionProperty($pluginManager, 'plugins'); + $property->setAccessible(true); + $plugins = $property->getValue($pluginManager); + $plugins['sirsoft-message_bizppurio'] = new \Plugins\Sirsoft\MessageBizppurio\Plugin; + $property->setValue($pluginManager, $plugins); + + ExtensionMiddlewareRegistry::flush(); } /** diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/dispatch-history-webhook.yaml b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/dispatch-history-webhook.yaml index 6f22db53..9d04e6b5 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/dispatch-history-webhook.yaml +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/dispatch-history-webhook.yaml @@ -1,3 +1,4 @@ +# audit:allow scenario-scale-axis-required reason: SendMessageJob 은 알림 1건당 1 dispatch(payload 1건·refkey 1건)로 볼륨 무관 상수 메모리다. 루프/chunk/대량 적재가 없어 #79 형 붕괴 구조가 아니므로 scale 축이 무의미하다. # audit:allow test-scenario-coverage reason: | # 발송 결과 기록·webhook 수신·결과코드 해석의 axes cross product 는 PHPUnit # (BizppurioWebhookTest·ResultCodeResolverTest·SmsChannelDriverTest·SendMessageJobTest·