Skip to content

fix(planning): bind signed workspace authority to request - #188

Merged
seonghobae merged 13 commits into
mainfrom
fix/planning-request-bound-authority-d39c946
Aug 11, 2026
Merged

fix(planning): bind signed workspace authority to request#188
seonghobae merged 13 commits into
mainfrom
fix/planning-request-bound-authority-d39c946

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Security outcome

Planning's short-lived signed workspace context currently authenticates only workspace + timestamp, so a captured fresh context can be replayed across Planning HTTP methods and resource paths that share the same secret. This branch closes that authority-confusion boundary by binding signatures to the exact Planning method and resource path.

Test-first evidence

The first commit adds focused RED coverage proving that a Planning search signature must not authorize POST /v1/goals or a different resource path. The implementation will then migrate Planning verification plus every in-repository signer to a versioned request-bound context without accepting the legacy context on migrated public routes.

Scope

  • Planning HTTP authority verification and controller bindings
  • Web Planning search / durable Today signers
  • Gateway Planning Today signer
  • realistic replay regression coverage

No cross-service persistence, browser-selected workspace authority, gate weakening, or unrelated repository changes.

Summary by CodeRabbit

  • 보안 개선
    • Planning 요청 서명이 HTTP 메서드와 경로에绑定되어, 다른 요청에 재사용되지 않도록 강화되었습니다.
    • 허용되지 않은 메서드나 경로의 요청은 인증 오류로 거부됩니다.
  • 버그 수정
    • 검색, Today, 목표, 프로젝트 및 태스크 요청이 실제 요청 정보와 일치할 때만 정상 처리됩니다.
    • 요청별 인증 검증이 일관되게 적용됩니다.
  • 테스트
    • 위조되거나 다른 요청에 재사용된 서명 차단 검증이 보강되었습니다.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0af586a0-f198-4bf6-a8e0-adcd032aedeb

📥 Commits

Reviewing files that changed from the base of the PR and between 38f41a0 and efe2a42.

📒 Files selected for processing (1)
  • apps/gateway/src/today-composition.ts
📝 Walkthrough

Walkthrough

Planning 컨텍스트 서명이 HTTP 메서드와 경로에 바인딩되도록 변경되었습니다. 웹 클라이언트, Gateway, Planning 라우트가 동일한 요청 바인딩을 사용합니다. 잘못된 바인딩과 서명 재사용을 거부하는 테스트가 추가되었습니다.

Changes

Planning 요청 바인딩

Layer / File(s) Summary
서버 바인딩 검증 및 HMAC 계약
apps/planning-service/src/http-boundary.ts
허용된 HTTP 메서드와 Planning 경로를 검증합니다. HMAC 입력에 workspace, 발급 시각, 메서드, 경로를 포함합니다.
클라이언트 서명 생성
apps/web/app/planning-search-client.ts, apps/web/app/today-sync-client.ts
요청 바인딩을 검증하고 life-os.planning-context.v2 서명을 생성합니다. 검색과 Today 요청에 메서드와 경로를 전달합니다.
Gateway Today 서명 연결
apps/gateway/src/today-composition.ts, apps/gateway/src/today-composition.test.ts
Planning Today 경로를 서명 입력에 포함합니다. 바인딩이 없으면 기존 workspace 서명 형식을 사용합니다.
Planning 라우트 바인딩 연결
apps/planning-service/src/main.ts
검색, Today, 목표, 프로젝트, 태스크 라우트가 실제 메서드와 경로를 requireTrustedWorkspaceContext에 전달합니다.
요청 바인딩 검증 테스트
apps/planning-service/src/http-boundary.test.ts, apps/planning-service/src/planning-controller-authority.test.ts, apps/planning-service/src/request-bound-workspace-context.test.ts
정상 서명과 만료·변조·누락 컨텍스트를 검증합니다. 메서드 또는 리소스 경로가 변경된 서명 재사용을 401로 거부하는지 확인합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebClient
  participant Gateway
  participant PlanningRoute
  participant ContextVerifier
  WebClient->>Gateway: Planning 요청 전달
  Gateway->>Gateway: 메서드·경로를 포함한 v2 HMAC 생성
  Gateway->>PlanningRoute: 바인딩된 HMAC 헤더와 요청 전달
  PlanningRoute->>ContextVerifier: 요청 바인딩과 workspace context 검증
  ContextVerifier-->>PlanningRoute: 검증된 workspace context 또는 401
  PlanningRoute-->>WebClient: Planning 응답 반환
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 Planning 요청에 대한 서명된 workspace 권한을 요청에 결합하는 주요 변경 사항을 정확하고 간결하게 설명합니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/planning-request-bound-authority-d39c946

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/planning-service/src/request-bound-workspace-context.test.ts Fixed
Comment thread apps/planning-service/src/request-bound-workspace-context.test.ts Fixed
Comment thread apps/planning-service/src/request-bound-workspace-context.test.ts Fixed
Comment thread apps/planning-service/src/request-bound-workspace-context.test.ts Fixed
@seonghobae
seonghobae marked this pull request as ready for review August 11, 2026 02:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/planning-service/src/request-bound-workspace-context.test.ts (1)

23-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

expectInvalid가 자신의 sentinel 오류를 다시 잡습니다.

operation()이 성공하면 26행이 Error('Expected request-bound context to be rejected')를 던집니다. 이 오류는 바로 아래 27행의 catch가 잡습니다. 테스트는 여전히 실패합니다. 그러나 실패 메시지는 "HttpException이 아님"으로 표시됩니다. 의도한 메시지는 사라집니다. 회귀가 발생하면 원인 파악이 느려집니다.

expect(...).toThrow() 대신 sentinel을 try 블록 밖으로 옮겨 주세요.

💚 제안 수정
 function expectInvalid(operation: () => unknown): void {
+  let succeeded = false;
   try {
     operation();
-    throw new Error('Expected request-bound context to be rejected');
+    succeeded = true;
   } catch (error) {
+    if (succeeded) throw error;
     expect(error).toBeInstanceOf(HttpException);
     expect((error as HttpException).getStatus()).toBe(401);
     expect((error as HttpException).getResponse()).toEqual({
       type: 'about:blank',
       title: 'Trusted gateway context is invalid',
       status: 401,
       code: 'invalid_gateway_context',
     });
+    return;
   }
+  throw new Error('Expected request-bound context to be rejected');
 }
🤖 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 `@apps/planning-service/src/request-bound-workspace-context.test.ts` around
lines 23 - 37, Update expectInvalid so the sentinel Error is thrown outside the
try/catch, preventing it from being caught and reported as an HttpException
assertion failure. Keep operation() inside the try block and preserve the
existing status and response assertions for errors it actually throws.
apps/web/app/planning-search-client.ts (1)

165-177: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

requirePlanningBindingmethod를 검증하지 않고, 경로 규칙이 서버 허용목록보다 느슨합니다.

requirePlanningBindingpath만 검사합니다. method는 타입 수준에서만 제한됩니다. apps/web/app/today-sync-client.ts 344-347행은 request.method as 'GET' | 'PUT'로 단언해서 값을 전달합니다. 현재는 상위 코드가 메서드를 먼저 검사하므로 안전합니다. 그러나 런타임 검증이 없으므로 이후 호출자가 추가되면 임의 문자열이 서명 입력에 들어갈 수 있습니다.

또한 이 검증기는 /v1/ 접두사만 요구합니다. apps/planning-service/src/http-boundary.ts 72-127행은 정확한 라우트 조합만 허용합니다. 클라이언트가 서버 허용목록 밖의 바인딩을 서명하면 요청은 401로 실패합니다. 보안 위험은 없습니다. 다만 신규 라우트 추가 시 두 목록이 조용히 어긋날 수 있습니다.

♻️ 메서드 런타임 검증 추가 제안
 function requirePlanningBinding(
   binding: PlanningContextRequestBinding,
 ): PlanningContextRequestBinding {
   if (
+    (binding.method !== 'GET' &&
+      binding.method !== 'POST' &&
+      binding.method !== 'PUT') ||
     !binding.path.startsWith('/v1/') ||
     binding.path.length > 256 ||
     /[\u0000-\u001f\u007f?#]/u.test(binding.path)
   ) {
     throw new Error('Planning request binding is invalid');
   }
   return binding;
 }

As per coding guidelines: "Treat every external response, stored JSON value, environment value, model output, and connector result as untrusted until bounded and validated."

🤖 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 `@apps/web/app/planning-search-client.ts` around lines 165 - 177, Update
requirePlanningBinding to validate binding.method at runtime, accepting only the
methods supported by the Planning client rather than relying on the TypeScript
type. Replace the broad /v1/ prefix check with validation against the exact
server-supported method/path combinations, keeping the existing length and
control-character restrictions. Reject any unrecognized method or route with the
existing invalid-binding error.

Source: Coding guidelines

apps/planning-service/src/main.ts (1)

113-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

라우트 데코레이터와 서명 경로가 각각 따로 선언되어 어긋날 수 있습니다.

각 핸들러는 @Get('today/:date') 같은 데코레이터와 '/v1/today/${date}' 같은 문자열 리터럴을 별도로 유지합니다. 두 값은 컴파일러가 연결하지 않습니다. 라우트 경로를 나중에 변경하면 서명 바인딩이 조용히 어긋납니다. 그 결과는 컴파일 오류가 아니라 런타임 401입니다.

라우트별 경로 상수를 한 곳에 정의하고, 데코레이터와 바인딩이 같은 상수를 사용하도록 정리해 주세요. 반복되는 process.env.PLANNING_GATEWAY_CONTEXT_SECRET 인자도 같은 헬퍼로 모을 수 있습니다.

Also applies to: 138-138, 172-172

🤖 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 `@apps/planning-service/src/main.ts` at line 113, Centralize each route path in
shared route constants and update the corresponding controller decorators and
signature-binding entries in main.ts to reuse those constants, including the
routes near the GET /v1/search entries and the additional affected bindings.
Extract the repeated process.env.PLANNING_GATEWAY_CONTEXT_SECRET argument into a
shared helper used by all route bindings, preserving existing authorization
behavior.
🤖 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 `@apps/planning-service/src/http-boundary.test.ts`:
- Around line 11-14: Extend the tests around requirePlanningRequestBinding to
cover rejection of unsupported methods, non-string paths, paths longer than 256
characters, paths containing control characters, ?, or #, and nested-path
identifiers that are not UUIDv4 values. Follow the existing patterns in
planning-controller-authority.test.ts and assert each invalid binding is
rejected.

In `@apps/planning-service/src/main.ts`:
- Line 205: Gateway Today의 today-composition 요청 서명 로직에서 life-os.workspace.v1
바인딩을 life-os.planning-context.v2로 변경하세요. /v1/today/${safeDate} 요청의 HTTP 메서드와 경로가
Planning service 검증값 및 Web Today와 동일하게 서명되도록 업데이트하고, 관련 서명 설정만 수정하세요.

---

Nitpick comments:
In `@apps/planning-service/src/main.ts`:
- Line 113: Centralize each route path in shared route constants and update the
corresponding controller decorators and signature-binding entries in main.ts to
reuse those constants, including the routes near the GET /v1/search entries and
the additional affected bindings. Extract the repeated
process.env.PLANNING_GATEWAY_CONTEXT_SECRET argument into a shared helper used
by all route bindings, preserving existing authorization behavior.

In `@apps/planning-service/src/request-bound-workspace-context.test.ts`:
- Around line 23-37: Update expectInvalid so the sentinel Error is thrown
outside the try/catch, preventing it from being caught and reported as an
HttpException assertion failure. Keep operation() inside the try block and
preserve the existing status and response assertions for errors it actually
throws.

In `@apps/web/app/planning-search-client.ts`:
- Around line 165-177: Update requirePlanningBinding to validate binding.method
at runtime, accepting only the methods supported by the Planning client rather
than relying on the TypeScript type. Replace the broad /v1/ prefix check with
validation against the exact server-supported method/path combinations, keeping
the existing length and control-character restrictions. Reject any unrecognized
method or route with the existing invalid-binding error.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f90f1872-bc80-414e-aede-afe154f71c3a

📥 Commits

Reviewing files that changed from the base of the PR and between d39c946 and c32338f.

📒 Files selected for processing (7)
  • apps/planning-service/src/http-boundary.test.ts
  • apps/planning-service/src/http-boundary.ts
  • apps/planning-service/src/main.ts
  • apps/planning-service/src/planning-controller-authority.test.ts
  • apps/planning-service/src/request-bound-workspace-context.test.ts
  • apps/web/app/planning-search-client.ts
  • apps/web/app/today-sync-client.ts

Comment thread apps/planning-service/src/http-boundary.test.ts
Comment thread apps/planning-service/src/main.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/today-composition.ts (1)

370-375: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

workspaceContextHeaders의 서명 계약을 문서화하십시오.

이 함수는 planningBinding 유무에 따라 legacy workspace v1 서명 또는 request-bound Planning v2 서명을 생성합니다. 새 기여자가 이 fallback 계약과 반환 헤더를 구현 코드 없이 이해할 수 있도록 TSDoc을 추가하십시오.

제안 수정
+/**
+ * Creates trusted workspace headers for an upstream service request.
+ *
+ * Uses the Planning v2 request-bound signature when `planningBinding` exists.
+ * Uses the workspace v1 signature for services that do not use Planning bindings.
+ */
 function workspaceContextHeaders(

As per coding guidelines, "Production declarations must include explanatory docstrings sufficient for a new contributor to understand the contract without reconstructing the implementation."

🤖 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 `@apps/gateway/src/today-composition.ts` around lines 370 - 375, 함수
workspaceContextHeaders의 선언부에 TSDoc을 추가하여 planningBinding이 없으면 legacy workspace
v1 서명을, 있으면 요청의 method와 path에 바인딩된 Planning v2 서명을 생성한다는 fallback 계약을 설명하십시오. 또한
반환값이 서명 검증에 필요한 workspace 컨텍스트 헤더를 담은 읽기 전용 레코드임을 문서화하고, 각 매개변수와 선택적
planningBinding의 의미를 명시하십시오.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@apps/gateway/src/today-composition.ts`:
- Around line 370-375: 함수 workspaceContextHeaders의 선언부에 TSDoc을 추가하여
planningBinding이 없으면 legacy workspace v1 서명을, 있으면 요청의 method와 path에 바인딩된
Planning v2 서명을 생성한다는 fallback 계약을 설명하십시오. 또한 반환값이 서명 검증에 필요한 workspace 컨텍스트 헤더를
담은 읽기 전용 레코드임을 문서화하고, 각 매개변수와 선택적 planningBinding의 의미를 명시하십시오.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: beec6947-a344-4cb9-9364-c8908a9b010c

📥 Commits

Reviewing files that changed from the base of the PR and between c32338f and 38f41a0.

📒 Files selected for processing (4)
  • apps/gateway/src/today-composition.test.ts
  • apps/gateway/src/today-composition.ts
  • apps/planning-service/src/http-boundary.test.ts
  • apps/planning-service/src/request-bound-workspace-context.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/planning-service/src/request-bound-workspace-context.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants