admin 접근 Slack에서 Discord 인터랙션으로 이관 (Ed25519 + allowlist·채널 게이트) - #668
Conversation
- bcprov-jdk18on 1.80 (Spring Boot 4 BOM 미관리, Maven Central 최신 안정) - raw 32B 공개키를 Ed25519PublicKeyParameters 로 다루려 BC 사용 (JDK EdEC raw-key 디코딩 까다로움)
- 진입 표면만 Slack→Discord 교체, grant 링크·IP 캡처·세션·게이트 필터는 재사용 - DiscordInteractionVerifier: Ed25519 검증 + 3분 replay 윈도우(캡처 재전송 시 링크 유출 방지) - DiscordAccessController: PING→PONG·서명검증·채널 게이트·allowlist 게이트·ephemeral embed(grant/list/revoke/allow) - Slack 완전 제거: 컨트롤러·verifier·테스트 삭제, SlackIdentity→GrantIdentity - AdminProperties/application.yml: slackSigningSecret → discordPublicKey·adminUserIds·adminChannelId, grantTokenTtl 5m→3m - deploy.yml: 브릭 가드·env·docker run 의 SLACK_ADMIN_SIGNING_SECRET → DISCORD 3종 - discord-admin-setup.md: 슬래시커맨드·endpoint URL 운영 셋업 문서
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
WalkthroughAdmin 백오피스 접근용 슬래시커맨드 인증을 Slack HMAC-SHA256 방식에서 Discord Ed25519 서명검증으로 전환했다. AdminProperties/application.yml의 시크릿 설정, DiscordInteractionVerifier, DiscordAccessController를 새로 추가하고 기존 Slack 관련 컴포넌트(SlackAccessController, SlackSignatureVerifier 및 테스트)를 제거했으며, 배포 워크플로우 가드와 운영 문서도 함께 갱신했다. ChangesSlack → Discord Admin 접근 제어 전환
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Discord
participant DiscordAccessController
participant DiscordInteractionVerifier
participant AdminAllowlistService
participant AdminSession
Discord->>DiscordAccessController: POST /admin-access/discord
DiscordAccessController->>DiscordInteractionVerifier: verify(signature, timestamp, rawBody)
DiscordInteractionVerifier-->>DiscordAccessController: true/false
alt 서명 실패
DiscordAccessController-->>Discord: 401
else PING
DiscordAccessController-->>Discord: PONG
else 허용된 채널/유저
DiscordAccessController->>AdminAllowlistService: issueGrantToken(userId, name)
AdminAllowlistService-->>DiscordAccessController: token
DiscordAccessController-->>Discord: grant 링크 embed
Discord->>DiscordAccessController: GET /admin-access/grant?token=...
DiscordAccessController->>AdminAllowlistService: consumeGrantToken(token)
AdminAllowlistService-->>DiscordAccessController: GrantIdentity
DiscordAccessController->>AdminAllowlistService: allow(ip)
DiscordAccessController->>AdminSession: establish()
DiscordAccessController-->>Discord: /admin 리다이렉트
else 미허용
DiscordAccessController-->>Discord: 접근 거부 embed
end
Assessment against linked issues
Out-of-scope changes
Suggested labels: Suggested reviewers: 백엔드 팀 시니어 리뷰어 💬 한마디: Slack 시그니처에서 Ed25519로 갈아탄 솜씨, 서명검증계의 이사(移徙)치고는 꽤 깔끔하네요! 다만 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
discord-admin-setup.md (1)
44-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win코드펜스에 언어 지정이 빠졌습니다(MD040).
markdownlint가 잡아준 부분인데, 고치는 데 몇 초 안 걸리니 같이 정리하시죠.
🔧 제안
-``` +```text https://<prod API 도메인>/admin-access/discord</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@discord-admin-setup.mdaround lines 44 - 46, MD040 is triggered because the
fenced block in the Discord admin setup doc has no language tag; update the code
fence around the admin URL example to use an explicit language identifier,
matching the markdownlint suggestion, so the snippet is recognized correctly.</details> <!-- cr-comment:v1:d4bb9d2aa6470c92b92126a5 --> _Source: Linters/SAST tools_ </blockquote></details> <details> <summary>src/main/kotlin/com/depromeet/piki/admin/config/AdminProperties.kt (1)</summary><blockquote> `36-41`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _⚡ Quick win_ **`toString()`에서 `discordAdminChannelId`가 빠졌습니다.** KDoc(라인 16)에서 `discordAdminChannelId`가 비면 fail-closed로 전부 거부된다고 명시할 만큼 중요한 설정인데, 정작 `toString()` 출력에는 포함되지 않았습니다. 배포 후 "채널 게이트가 왜 항상 막히지?"를 디버깅할 때 로그만 봐서는 이 값이 설정됐는지 확인할 방법이 없습니다. `discordPublicKey`, `discordAdminUserIds`와 같은 패턴(민감하지 않은 수준으로 마스킹)으로 같이 노출해주시죠. <details> <summary>🔧 제안</summary> ```diff override fun toString(): String = "AdminProperties(enabled=$enabled, environmentGate=$environmentGate, " + "discordPublicKey=${if (discordPublicKey.isBlank()) "<none>" else "<set>"}, " + "discordAdminUserIds=${discordAdminUserIds.size} ids, " + + "discordAdminChannelId=${if (discordAdminChannelId.isBlank()) "<none>" else "<set>"}, " + "allowlistTtl=$allowlistTtl, grantTokenTtl=$grantTokenTtl, localBypass=$localBypass)"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/com/depromeet/piki/admin/config/AdminProperties.kt` around lines 36 - 41, `AdminProperties.toString()`에서 `discordAdminChannelId`가 누락되어 있어 채널 게이트 설정 상태를 로그로 확인할 수 없습니다. `AdminProperties`의 `toString()` 구현에 `discordPublicKey`와 `discordAdminUserIds`처럼 민감도에 맞게 마스킹한 `discordAdminChannelId` 출력 항목을 추가하고, 비어 있으면 `<none>`, 설정됐으면 `<set>` 같은 형태로 표시되도록 수정하세요.src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt (1)
48-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win컨트롤러 라우팅 로직 자체는 테스트가 전혀 없습니다.
discord-admin-setup.md(61번 줄)에 "컨트롤러 통합테스트는@ConditionalOnAdminEnabled가 테스트 컨텍스트에서 꺼져 있어 안 둔다"고 적혀 있는데, 이건@SpringBootTest류 통합 테스트 얘기입니다. 이 컨트롤러는 생성자 주입 구조라 Spring 컨텍스트 없이DiscordAccessController(mockVerifier, mockAllowlistService, mockAuditService, adminProperties, ObjectMapper())로 직접 인스턴스화하고MockHttpServletRequest/MockHttpServletResponse(spring-test에 이미 의존성 있음)를 넘겨.discord()를 호출하는 순수 단위테스트는 컨텍스트 캐시 문제와 무관하게 가능합니다.PING/채널게이트/allowlist게이트/서브커맨드 라우팅(list·revoke·allow·grant)처럼 분기가 많은 곳이라, verifier만 단위테스트로 커버하고 컨트롤러 라우팅은 완전 무테스트로 두는 건 회귀 위험이 좀 있어 보입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt` around lines 48 - 94, `DiscordAccessController.discord`의 라우팅 분기에는 단위테스트가 없어서 PING, 채널 게이트, allowlist 게이트, 그리고 `firstOption`/`optionValue` 기반 서브커맨드 분기(list, revoke, allow, 기본 grant link)를 직접 검증할 수 없습니다. Spring 컨텍스트 없이 `DiscordAccessController`를 생성자 주입으로 직접 인스턴스화하고, `MockHttpServletRequest`와 `MockHttpServletResponse`로 `.discord()`를 호출하는 순수 단위테스트를 추가해 각 분기에서 기대하는 응답과 status, 그리고 `auditService.record` 호출 여부를 검증하세요.build.gradle.kts (1)
57-60: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
bcprov-jdk18on:1.80에 알려진 CVE 있음 — 최신 1.84로 올리는 걸 권장합니다.1.80 버전은 Direct vulnerabilities: CVE-2026-5598CVE-2026-0636 대상입니다. CVE-2026-0636은 Affected versions of this package are vulnerable to LDAP Injection via the parseDN handling and the LDAP store helpers in X509LDAPCertStoreSpi and LDAPStoreHelper. CVE-2026-5598은 Non-constant time comparisons risk private key leakage in FrodoKEM.
지금 코드는
Ed25519Signer/Ed25519PublicKeyParameters만 써서 LDAP·FrodoKEM 경로와는 무관하지만, 어차피 Newer Version Available · 1.80→1.84 · (0 changes) 이므로 굳이 취약 버전에 머물 이유가 없습니다. SCA/의존성 스캐너(예: Dependabot, Snyk)에서 계속 알림이 뜰 테니 미리 올려두는 게 마음 편합니다.🔧 제안
- implementation("org.bouncycastle:bcprov-jdk18on:1.80") + implementation("org.bouncycastle:bcprov-jdk18on:1.84")버전업 시 Ed25519 관련 API(
Ed25519PublicKeyParameters,Ed25519Signer) 시그니처 변경 여부는 확인 부탁드립니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@build.gradle.kts` around lines 57 - 60, The BouncyCastle dependency in build.gradle.kts is pinned to a vulnerable version, so update the bcprov-jdk18on artifact from 1.80 to 1.84. Keep the existing Ed25519 usage intact, but verify that the symbols used here and in the related signature verification code (`Ed25519Signer`, `Ed25519PublicKeyParameters`) still compile and behave the same after the version bump.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy.yml:
- Line 702: The `secret_guard` failure label is inconsistent between the alert
text and the deployment summary, so update the summary block that still says `❌
중단 (Slack signing secret 누락 가드)` to match the renamed Discord admin secret
wording. Use the existing `CAUSE` handling and the `secret_guard` summary logic
in the deploy workflow to replace the stale Slack reference with the same
Discord admin secret message used elsewhere so both reports describe the same
failure consistently.
---
Nitpick comments:
In `@build.gradle.kts`:
- Around line 57-60: The BouncyCastle dependency in build.gradle.kts is pinned
to a vulnerable version, so update the bcprov-jdk18on artifact from 1.80 to
1.84. Keep the existing Ed25519 usage intact, but verify that the symbols used
here and in the related signature verification code (`Ed25519Signer`,
`Ed25519PublicKeyParameters`) still compile and behave the same after the
version bump.
In `@discord-admin-setup.md`:
- Around line 44-46: MD040 is triggered because the fenced block in the Discord
admin setup doc has no language tag; update the code fence around the admin URL
example to use an explicit language identifier, matching the markdownlint
suggestion, so the snippet is recognized correctly.
In `@src/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.kt`:
- Around line 48-94: `DiscordAccessController.discord`의 라우팅 분기에는 단위테스트가 없어서
PING, 채널 게이트, allowlist 게이트, 그리고 `firstOption`/`optionValue` 기반 서브커맨드 분기(list,
revoke, allow, 기본 grant link)를 직접 검증할 수 없습니다. Spring 컨텍스트 없이
`DiscordAccessController`를 생성자 주입으로 직접 인스턴스화하고, `MockHttpServletRequest`와
`MockHttpServletResponse`로 `.discord()`를 호출하는 순수 단위테스트를 추가해 각 분기에서 기대하는 응답과
status, 그리고 `auditService.record` 호출 여부를 검증하세요.
In `@src/main/kotlin/com/depromeet/piki/admin/config/AdminProperties.kt`:
- Around line 36-41: `AdminProperties.toString()`에서 `discordAdminChannelId`가
누락되어 있어 채널 게이트 설정 상태를 로그로 확인할 수 없습니다. `AdminProperties`의 `toString()` 구현에
`discordPublicKey`와 `discordAdminUserIds`처럼 민감도에 맞게 마스킹한 `discordAdminChannelId`
출력 항목을 추가하고, 비어 있으면 `<none>`, 설정됐으면 `<set>` 같은 형태로 표시되도록 수정하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 185fda0c-c4f5-40a5-a5cb-5c8dcfca239d
📒 Files selected for processing (12)
.github/workflows/deploy.ymlbuild.gradle.ktsdiscord-admin-setup.mdsrc/main/kotlin/com/depromeet/piki/admin/access/AdminAllowlistService.ktsrc/main/kotlin/com/depromeet/piki/admin/access/DiscordAccessController.ktsrc/main/kotlin/com/depromeet/piki/admin/access/DiscordInteractionVerifier.ktsrc/main/kotlin/com/depromeet/piki/admin/access/SlackAccessController.ktsrc/main/kotlin/com/depromeet/piki/admin/access/SlackSignatureVerifier.ktsrc/main/kotlin/com/depromeet/piki/admin/config/AdminProperties.ktsrc/main/resources/application.ymlsrc/test/kotlin/com/depromeet/piki/admin/access/DiscordInteractionVerifierTest.ktsrc/test/kotlin/com/depromeet/piki/admin/access/SlackSignatureVerifierTest.kt
💤 Files with no reviewable changes (3)
- src/main/kotlin/com/depromeet/piki/admin/access/SlackSignatureVerifier.kt
- src/test/kotlin/com/depromeet/piki/admin/access/SlackSignatureVerifierTest.kt
- src/main/kotlin/com/depromeet/piki/admin/access/SlackAccessController.kt
- Discord 앱은 인터랙션 엔드포인트가 앱당 1개라 Slack 처럼 커맨드별 URL 로 환경 분리 불가. /piki-admin env:<dev|staging|prod> 로 한 커맨드에서 환경 골라 그 env 링크 발급 - grant 토큰 Redis랜덤(env별) → HMAC 서명 stateless(GrantTokenCodec). 발급 env 와 소비 env 가 달라도 공유키로 검증, env 바인딩 + nonce setIfAbsent 로 one-time - AdminProperties/application.yml: environment·grantSigningKey·grantHosts - deploy.yml: ADMIN_ENVIRONMENT + ADMIN_GRANT_SIGNING_KEY(브릭 가드 4번째) 배선 - list/revoke 는 cross-env allowlist 관리라 후속. GrantTokenCodecTest 7케이스
- AdminSession: slackName/slackUserId → actorName/userId. 로그 actor 값은 이미 Discord username 이고 네이밍만 slack 잔재였음 - AdminAccessFilter·AdminAnnouncementController·AdminTemplateController 호출부·주석
- deploy.yml: secret_guard 실패 요약 STATUS 가 여전히 "Slack signing secret 누락 가드" 이던 것 → "Discord admin secret" (CAUSE 와 일치). 놓친 주석 하나도 Discord 로 - AdminProperties.toString: discordAdminChannelId·environment 노출 추가, grantSigningKey 는 크리덴셜이라 set 여부만 마스킹 - bcprov-jdk18on 1.80 → 1.84 (알려진 CVE·최신 안정, canonical maven-metadata 로 확인 — solrsearch 는 stale 이었음) - discord-admin-setup.md: 코드펜스 언어 지정(text) 추가 (MD040)
|
CodeRabbit review body nitpick 4건 처리 (대부분
|
- username(핸들, 예 sevineleven) 대신 Discord 서버 별명 → 표시이름 → 핸들 순 fallback 으로 사람이 알아보기 쉬운 이름을 로그·감사에 남긴다
- admin Discord 슬래시커맨드 등록·엔드포인트 URL 설정은 일회성 운영 절차라 리포에 두지 않고 추적 해제. 절차는 이슈 #650 코멘트로 이관해 보존
Situation
Task
Action
검증·게이트 (신규)
DiscordInteractionVerifier): Discord 가{timestamp}{body}를 앱 개인키로 서명해 보낸다. BouncyCastle 로 raw 32B 공개키를 검증(JDK EdEC raw-key 디코딩이 까다로워 BC). 3분 replay 윈도우 - 캡처한 유효 요청을 재전송해 응답으로 grant 링크를 빼가는 걸 막는다.DISCORD_ADMIN_USER_IDS)만 링크·관리 명령 사용. 아니면 거부 embed.DISCORD_ADMIN_CHANNEL_ID)에서만. 봇이 여러 채널에 있어도 admin 인터랙션을 한 채널로 국한. 미설정이면 fail-closed.컨트롤러 (교체)
DiscordAccessController: PING(type1)→PONG(엔드포인트 등록 검증), 커맨드(type2) → 서명·채널·allowlist 게이트 → ephemeral embed 로 grant/list/revoke/allow. 응답이 ephemeral 이라 링크가 채널에 안 샌다./admin-access/grant)·IP 캡처·세션은 진입 표면과 무관해 그대로 둔다.Slack 제거 + 설정 전환
SlackAccessController·SlackSignatureVerifier·테스트 삭제,SlackIdentity→GrantIdentity.AdminProperties/application.yml:slackSigningSecret→discordPublicKey·discordAdminUserIds·discordAdminChannelId. grant 링크 수명 5분→3분.SLACK_ADMIN_SIGNING_SECRET하나 체크에서 Discord admin 3종(공개키·allowlist·채널)이 모두 있어야 배포되게. env·docker run 배선도 전환.채택 안 한 안
Result
DiscordInteractionVerifierTest(BC 키페어로 실제 서명·검증 - 유효/본문변조/다른키/replay 3분 경계/미설정 등 11케이스) 통과.@ConditionalOnAdminEnabled(테스트 컨텍스트 off)라 로드 안 되고, 켜면 컨텍스트 캐시가 깨진다. verifier 단위테스트로 커버(Slack 시절과 동일 결). 근거는 discord-admin-setup.md 에.연관 이슈
Summary by CodeRabbit
New Features
Bug Fixes
Tests