-
Notifications
You must be signed in to change notification settings - Fork 0
feat(planning): expose trusted data-rights contributor transport #194
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 all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
610c5d4
test(planning): define trusted data-rights transport contract
seonghobae c17d728
feat(planning): authenticate data-rights contributor transport
seonghobae ae86a1c
feat(planning): expose authenticated data-rights contributor
seonghobae 87e4145
fix(planning): keep domain response types on planning boundary
seonghobae b7720a0
test(planning): prove data-rights controller authority flow
seonghobae 3cf2f06
docs(planning): expose data-rights verifier configuration
seonghobae 47ae5f8
style(planning): restore canonical formatter output
seonghobae b4c7a4d
Merge branch 'main' into feat/planning-data-rights-http-v1
opencode-agent[bot] 558cf3e
test(planning): bind contributor controller to actual request
seonghobae 48d7579
fix(planning): bind data-rights signature to actual request
seonghobae 31f61c5
test(planning): assert exact data-rights rejection status
seonghobae ec30110
chore(env): preserve current secret examples in sorted order
seonghobae 93360db
merge main into planning data-rights transport
seonghobae fa71180
Merge branch 'main' into feat/planning-data-rights-http-v1
github-actions[bot] 17c9e6b
docs(planning): explain data-rights request binding
seonghobae 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
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
135 changes: 135 additions & 0 deletions
135
apps/planning-service/src/planning-data-rights-controller-authority.test.ts
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,135 @@ | ||
| import { createHmac, randomBytes } from 'node:crypto'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| import { PlanningDataRightsController } from './main'; | ||
| import { | ||
| DATA_RIGHTS_CONTRIBUTOR_CONTRACT_VERSION, | ||
| type DataRightsContributorResponse, | ||
| } from './planning-data-rights'; | ||
| import type { PlanningRuntime } from './planning-runtime'; | ||
|
|
||
| const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111'; | ||
| const USER_ID = '22222222-2222-4222-8222-222222222222'; | ||
| const REQUEST_ID = '33333333-3333-4333-8333-333333333333'; | ||
| const SECRET = randomBytes(32).toString('base64url'); | ||
| const CONTRIBUTOR_PATH = '/v1/internal/data-rights/contributor'; | ||
| const HTTP_REQUEST = Object.freeze({ | ||
| method: 'POST', | ||
| originalUrl: CONTRIBUTOR_PATH, | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const request = Object.freeze({ | ||
| contractVersion: DATA_RIGHTS_CONTRIBUTOR_CONTRACT_VERSION, | ||
| operation: 'export' as const, | ||
| workspaceId: WORKSPACE_ID, | ||
| requestedByUserId: USER_ID, | ||
| requestId: REQUEST_ID, | ||
| }); | ||
|
|
||
| /** Signs one exact Planning contributor request at the supplied Unix second. */ | ||
| function signature(issuedAt: string): string { | ||
| return createHmac('sha256', SECRET) | ||
| .update( | ||
| [ | ||
| 'life-os.planning-data-rights-context.v1', | ||
| request.contractVersion, | ||
| request.workspaceId, | ||
| request.requestedByUserId, | ||
| request.requestId, | ||
| request.operation, | ||
| '-', | ||
| issuedAt, | ||
| 'POST', | ||
| CONTRIBUTOR_PATH, | ||
| ].join('\n'), | ||
| 'utf8', | ||
| ) | ||
| .digest('base64url'); | ||
| } | ||
|
|
||
| /** Creates the smallest runtime-shaped collaborator observable by the controller. */ | ||
| function controllerWith(handle: ReturnType<typeof vi.fn>): PlanningDataRightsController { | ||
| const runtime = { | ||
| dataRightsContributor: { handle }, | ||
| } as unknown as PlanningRuntime; | ||
| return new PlanningDataRightsController(runtime); | ||
| } | ||
|
|
||
| afterEach(() => { | ||
| delete process.env.PLANNING_DATA_RIGHTS_CONTEXT_SECRET; | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| describe('Planning data-rights controller authority', () => { | ||
| it('passes only a verified normalized request to the owning contributor', async () => { | ||
| process.env.PLANNING_DATA_RIGHTS_CONTEXT_SECRET = SECRET; | ||
| const response: DataRightsContributorResponse = { | ||
| contractVersion: DATA_RIGHTS_CONTRIBUTOR_CONTRACT_VERSION, | ||
| contributor: 'planning.service', | ||
| requestId: REQUEST_ID, | ||
| operation: 'export', | ||
| schemaVersion: 'planning.data-rights.v1', | ||
| recordCount: 0, | ||
| sha256: '0'.repeat(64), | ||
| data: {}, | ||
| }; | ||
| const handle = vi.fn().mockResolvedValue(response); | ||
| const controller = controllerWith(handle); | ||
| const issuedAt = String(Math.floor(Date.now() / 1000)); | ||
|
|
||
| await expect( | ||
| controller.contribute( | ||
| HTTP_REQUEST, | ||
| issuedAt, | ||
| signature(issuedAt), | ||
| request, | ||
| ), | ||
| ).resolves.toEqual(response); | ||
| expect(handle).toHaveBeenCalledTimes(1); | ||
| expect(handle).toHaveBeenCalledWith(request); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| it('rejects forged authority before the contributor can observe a request', async () => { | ||
| process.env.PLANNING_DATA_RIGHTS_CONTEXT_SECRET = SECRET; | ||
| const handle = vi.fn(); | ||
| const controller = controllerWith(handle); | ||
| const issuedAt = String(Math.floor(Date.now() / 1000)); | ||
|
|
||
| await expect( | ||
| controller.contribute(HTTP_REQUEST, issuedAt, 'A'.repeat(43), request), | ||
| ).rejects.toMatchObject({ status: 401 }); | ||
| expect(handle).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('rejects a signature replayed onto a different actual HTTP binding', async () => { | ||
| process.env.PLANNING_DATA_RIGHTS_CONTEXT_SECRET = SECRET; | ||
| const handle = vi.fn(); | ||
| const controller = controllerWith(handle); | ||
| const issuedAt = String(Math.floor(Date.now() / 1000)); | ||
|
|
||
| await expect( | ||
| controller.contribute( | ||
| { method: 'GET', originalUrl: CONTRIBUTOR_PATH }, | ||
| issuedAt, | ||
| signature(issuedAt), | ||
| request, | ||
| ), | ||
| ).rejects.toMatchObject({ status: 401 }); | ||
| expect(handle).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('fails closed when the service verifier is not configured', async () => { | ||
| const handle = vi.fn(); | ||
| const controller = controllerWith(handle); | ||
| const issuedAt = String(Math.floor(Date.now() / 1000)); | ||
|
|
||
| await expect( | ||
| controller.contribute( | ||
| HTTP_REQUEST, | ||
| issuedAt, | ||
| signature(issuedAt), | ||
| request, | ||
| ), | ||
| ).rejects.toMatchObject({ status: 503 }); | ||
| expect(handle).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
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.