fix(message_bizppurio): webhook 미들웨어 self-gate 전환 + 발송 시나리오 scale 면제

라우트 파일에서 직접 부착하던 webhook IP 화이트리스트를 plugin.php
getMiddleware self-gate 선언으로 전환한다(코어 확장 미들웨어 규정).
테스트 게이트 활성 헬퍼를 이식해 webhook 보안 테스트를 복구하고,
단건 발송 Job 시나리오에 scale 축 면제를 명시한다(볼륨 무관 상수 메모리).
webhook API 문서에 self-gate 부착 방식을 함께 보강한다.
This commit is contained in:
chym1217
2026-07-28 12:40:18 +09:00
parent 9dee9f5e35
commit f2c11d2edb
5 changed files with 62 additions and 3 deletions
@@ -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 멱등이 중복 처리를 막는다.
@@ -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<int, array<string, mixed>>
*/
public function getMiddleware(): array
{
return [
[
'class' => BizppurioWebhookIpWhitelist::class,
'groups' => ['api'],
'targets' => [
'api.plugins.sirsoft-message_bizppurio.webhook',
],
],
];
}
}
@@ -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 () {
@@ -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();
}
/**
@@ -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·