Skip to content

KMS-693: Implement token strategy for KMS CMR reconciliation - #125

Open
cgokey wants to merge 6 commits into
mainfrom
KMS-693
Open

KMS-693: Implement token strategy for KMS CMR reconciliation #125
cgokey wants to merge 6 commits into
mainfrom
KMS-693

Conversation

@cgokey

@cgokey cgokey commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Overview

What is the feature?

This change adds support for using the CMR system token as the primary runtime token for metadata-correction writeback and reconciliation-related authorization, with CMR_WRITER_TOKEN retained as the fallback when the system token is unavailable.

What is the Solution?

  • Read the CMR system token from an SSM parameter whose name is provided by environment/config.
  • Fall back to CMR_WRITER_TOKEN when the system token is not available.
  • Update the KMS authorizer to allow requests authenticated with the resolved system token.
  • Pass the system-token parameter name through Bamboo/CDK into the relevant Lambdas and grant the metadata correction service permission to read that SSM parameter.
  • Add an optional AUTHORIZATION_HEADER to the local create_unique_keyword.sh helper so protected environments can be exercised without rewriting the script.

What areas of the application does this impact?

  • Metadata correction writeback token resolution
  • KMS API authorization
  • CDK/Bamboo runtime configuration for token lookup
  • Local keyword-creation helper script

Testing

Verify you can hit a KMS authenticated endpoint with the system token.
Try publishing a new version, verify KMS can properly use the system token for write access.

Checklist

  • I have added automated tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

Summary by CodeRabbit

  • New Features

    • Added SSM-backed CMR system/writer token resolution via an optional parameter name, with support for Bearer-prefixed values and whitespace trimming.
    • Updated the EDL authorizer to allow requests early when the provided token matches the configured system token.
    • Added optional AUTHORIZATION_HEADER support to local keyword generation.
  • Bug Fixes

    • Improved token fallback behavior when secure parameter retrieval fails or returns invalid data.
    • Metadata writeback now fetches authorization once up front and skips authentication lookups when not enabled.
  • Tests / Chores

    • Expanded test coverage and added the SSM SDK dependency.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds SSM-backed CMR system-token resolution, supports system-token authorization, propagates parameter configuration through CDK and deployment environments, and updates metadata writeback and local request authentication behavior.

Changes

CMR system token authentication

Layer / File(s) Summary
SSM-backed token resolution
serverless/src/shared/getCmrWriterToken.js, serverless/src/shared/__tests__/*, package.json
Token resolution checks SSM first, normalizes bearer values, falls back to CMR_WRITER_TOKEN, and supports missing-token results.
System-token authorizer path
serverless/src/edlAuthorizer/*, serverless/src/getConcepts/__tests__/handler.test.js
The authorizer allows matching system tokens as cmr-system and otherwise continues EDL profile validation.
Metadata writeback token usage
serverless/src/shared/writeCorrectedMetadataToCmr.js, serverless/src/shared/__tests__/writeCorrectedMetadataToCmr.test.js
Writeback resolves the token once when enabled and reuses the resolved authorization value for CMR requests.
Parameter configuration and permissions
cdk/app/lib/*, cdk/bin/main.ts, bin/deploy-bamboo.sh, scripts/local/create_unique_keyword.sh
The SSM parameter name is propagated into Lambda environments, deployment containers, and local requests, with conditional ssm:GetParameter permissions.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant edlAuthorizer
  participant getCmrSystemToken
  participant SSM
  participant fetchEdlProfile
  Client->>edlAuthorizer: Send authorization token
  edlAuthorizer->>getCmrSystemToken: Resolve system token
  getCmrSystemToken->>SSM: Get configured parameter
  SSM-->>getCmrSystemToken: Return token
  getCmrSystemToken-->>edlAuthorizer: Return normalized token
  alt Presented token matches system token
    edlAuthorizer-->>Client: Allow as cmr-system
  else Token does not match
    edlAuthorizer->>fetchEdlProfile: Validate presented token
    fetchEdlProfile-->>edlAuthorizer: Return profile or failure
    edlAuthorizer-->>Client: Allow or deny
  end
Loading

Possibly related PRs

  • nasa/kms#114: Both changes modify MetadataCorrectionSetup Lambda configuration and IAM permissions.
  • nasa/kms#120: Both changes modify metadata writeback request handling in writeCorrectedMetadataToCmr.js.

Suggested reviewers: eudoroolivares2016, william-valencia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes the main change: implementing a token strategy for KMS/CMR reconciliation.
Description check ✅ Passed The description matches the template’s core sections and explains the feature, solution, impact, testing, and checklist; only some testing details are sparse.
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 KMS-693
🔧 Fix failing CI
  • Fix failing CI in branch KMS-693

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.

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.71%. Comparing base (e55397c) to head (6171a75).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #125      +/-   ##
==========================================
- Coverage   99.73%   99.71%   -0.03%     
==========================================
  Files         229      234       +5     
  Lines        6029     6305     +276     
  Branches     1773     1854      +81     
==========================================
+ Hits         6013     6287     +274     
- Misses         14       16       +2     
  Partials        2        2              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

🧹 Nitpick comments (4)
serverless/src/shared/getCmrWriterToken.js (2)

68-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider logger.error for SSM read failures.

When CMR_SYSTEM_TOKEN_PARAMETER_NAME is configured, a failed read means system-token authorization silently stops working (authorizer denies, writeback skips). A warn-level log with only error.message makes that outage easy to miss; error level plus the parameter name would be more actionable.

🤖 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 `@serverless/src/shared/getCmrWriterToken.js` around lines 68 - 74, Update the
SSM read failure handling in getCmrWriterToken to use logger.error instead of
logger.warn, and include CMR_SYSTEM_TOKEN_PARAMETER_NAME in the structured log
context alongside the existing error message.

26-34: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the SSM client and resolved token; this runs on every authorizer invocation.

createSsmClient() builds a fresh SSMClient per call and readSystemTokenFromSsm issues a GetParameter on every getCmrSystemToken()/getCmrWriterToken() call. Since serverless/src/edlAuthorizer/handler.js (line 54) calls this on every request, each authorized API call pays an SSM round trip and risks SSM GetParameter throttling under load. A module-scoped client plus a short-TTL cached value keeps warm invocations off the SSM hot path.

♻️ Sketch: reuse client, cache token with TTL
+let cachedSsmClient
+let cachedToken
+let cachedTokenExpiresAt = 0
+const TOKEN_CACHE_TTL_MS = 5 * 60 * 1000
+
 const createSsmClient = () => {
+  if (cachedSsmClient) {
+    return cachedSsmClient
+  }
+
   const endpoint = String(process.env.AWS_ENDPOINT_URL || '').trim()
   const region = String(process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || '').trim()
 
-  return new SSMClient({
+  cachedSsmClient = new SSMClient({
     ...(endpoint ? { endpoint } : {}),
     ...(region ? { region } : {})
   })
+
+  return cachedSsmClient
 }
 const readSystemTokenFromSsm = async (parameterName) => {
   if (!parameterName) {
     return ''
   }
 
+  if (cachedToken !== undefined && Date.now() < cachedTokenExpiresAt) {
+    return cachedToken
+  }
+
   try {
     const response = await createSsmClient().send(new GetParameterCommand({
       Name: parameterName,
       WithDecryption: true
     }))
 
-    return String(response.Parameter?.Value || '').trim()
+    cachedToken = String(response.Parameter?.Value || '').trim()
+    cachedTokenExpiresAt = Date.now() + TOKEN_CACHE_TTL_MS
+
+    return cachedToken
   } catch (error) {

Also applies to: 56-75

🤖 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 `@serverless/src/shared/getCmrWriterToken.js` around lines 26 - 34, Update
createSsmClient, readSystemTokenFromSsm, and the
getCmrSystemToken/getCmrWriterToken flow to reuse a module-scoped SSMClient and
cache the resolved token with a short TTL. Only issue GetParameter when the
cached value is absent or expired, while preserving existing token resolution
and error behavior.
serverless/src/edlAuthorizer/handler.js (2)

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

Duplicate bearer-strip logic; export the shared helper instead.

This is the same normalization as stripBearerPrefix in serverless/src/shared/getCmrWriterToken.js (lines 43-48). If one copy changes, presented tokens and configured tokens stop comparing equal and system-token auth silently breaks. Export the shared helper and reuse it here.

🤖 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 `@serverless/src/edlAuthorizer/handler.js` around lines 16 - 21, Remove the
duplicate normalizePresentedToken implementation and export the existing
stripBearerPrefix helper from getCmrWriterToken.js. Import and reuse
stripBearerPrefix in the authorizer token normalization flow so presented and
configured tokens share the same behavior.

57-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use a timing-safe comparison for the system token.

presentedToken === resolvedSystemToken short-circuits on the first differing byte. Since this compares a long-lived shared secret on a public authorizer endpoint, prefer a constant-time compare (e.g. crypto.timingSafeEqual over equal-length digests of both values).

🔒 Sketch
+import { createHash, timingSafeEqual } from 'crypto'
+
+const tokensMatch = (a, b) => timingSafeEqual(
+  createHash('sha256').update(a).digest(),
+  createHash('sha256').update(b).digest()
+)
-    if (resolvedSystemToken && presentedToken === resolvedSystemToken) {
+    if (resolvedSystemToken && presentedToken && tokensMatch(presentedToken, resolvedSystemToken)) {
🤖 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 `@serverless/src/edlAuthorizer/handler.js` around lines 57 - 61, Update the
system-token validation in the authorizer handler to use a timing-safe
comparison instead of direct equality. Compare equal-length cryptographic
digests of presentedToken and resolvedSystemToken with crypto.timingSafeEqual,
while preserving the existing success policy and ensuring mismatched token
lengths cannot cause an exception.
🤖 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.

Nitpick comments:
In `@serverless/src/edlAuthorizer/handler.js`:
- Around line 16-21: Remove the duplicate normalizePresentedToken implementation
and export the existing stripBearerPrefix helper from getCmrWriterToken.js.
Import and reuse stripBearerPrefix in the authorizer token normalization flow so
presented and configured tokens share the same behavior.
- Around line 57-61: Update the system-token validation in the authorizer
handler to use a timing-safe comparison instead of direct equality. Compare
equal-length cryptographic digests of presentedToken and resolvedSystemToken
with crypto.timingSafeEqual, while preserving the existing success policy and
ensuring mismatched token lengths cannot cause an exception.

In `@serverless/src/shared/getCmrWriterToken.js`:
- Around line 68-74: Update the SSM read failure handling in getCmrWriterToken
to use logger.error instead of logger.warn, and include
CMR_SYSTEM_TOKEN_PARAMETER_NAME in the structured log context alongside the
existing error message.
- Around line 26-34: Update createSsmClient, readSystemTokenFromSsm, and the
getCmrSystemToken/getCmrWriterToken flow to reuse a module-scoped SSMClient and
cache the resolved token with a short TTL. Only issue GetParameter when the
cached value is absent or expired, while preserving existing token resolution
and error behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4757239c-204e-4303-be4e-98e36fa0ee46

📥 Commits

Reviewing files that changed from the base of the PR and between 75261d7 and 46915fa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • bin/deploy-bamboo.sh
  • cdk/app/lib/CmrEventProcessingStack.ts
  • cdk/app/lib/KmsStack.ts
  • cdk/app/lib/helper/KmsLambdaFunctions.ts
  • cdk/app/lib/helper/MetadataCorrectionSetup.ts
  • cdk/bin/main.ts
  • package.json
  • scripts/local/create_unique_keyword.sh
  • serverless/src/edlAuthorizer/__tests__/handler.test.js
  • serverless/src/edlAuthorizer/handler.js
  • serverless/src/getConcepts/__tests__/handler.test.js
  • serverless/src/shared/__tests__/getCmrWriterToken.test.js
  • serverless/src/shared/__tests__/writeCorrectedMetadataToCmr.test.js
  • serverless/src/shared/getCmrWriterToken.js
  • serverless/src/shared/writeCorrectedMetadataToCmr.js

@cgokey cgokey changed the title Kms 693 KMS-693: Determine token strategy for KMS CMR reconciliation Jul 28, 2026
@cgokey cgokey changed the title KMS-693: Determine token strategy for KMS CMR reconciliation KMS-693: Implement token strategy for KMS CMR reconciliation Jul 28, 2026

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

🧹 Nitpick comments (2)
serverless/src/edlAuthorizer/handler.js (1)

40-47: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider a timing-safe comparison for the system token.

presentedToken === resolvedSystemToken is a non-constant-time comparison of a secret bearer value. For defense-in-depth, consider crypto.timingSafeEqual (with equal-length buffers, e.g., hash both sides first) instead of plain ===.

import { timingSafeEqual, createHash } from 'node:crypto'

const safeEquals = (a, b) => {
  const bufA = createHash('sha256').update(String(a)).digest()
  const bufB = createHash('sha256').update(String(b)).digest()

  return timingSafeEqual(bufA, bufB)
}
🤖 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 `@serverless/src/edlAuthorizer/handler.js` around lines 40 - 47, Replace the
plain equality check in the system-token branch of the authorizer handler with a
timing-safe comparison using the existing token values, hashing or otherwise
normalizing both operands to equal-length buffers before calling
crypto.timingSafeEqual. Preserve the current authorization success path and
policy generation behavior.
serverless/src/shared/getCmrWriterToken.js (1)

95-103: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No caching of the SSM-resolved token/client — every call pays a network round trip.

getCmrSystemToken (and transitively getCmrWriterToken) performs a live GetParameterCommand call on every invocation with no in-memory memoization of either the resolved token or the SSMClient instance. Since this is called for every metadata writeback attempt (and separately, on every edlAuthorizer invocation), this adds latency/cost per call and increases exposure to SSM throttling under load. Consider caching the resolved token (with a short TTL to allow rotation) and/or the SSMClient instance at module scope so warm Lambda containers reuse them across invocations.

Also applies to: 115-129

🤖 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 `@serverless/src/shared/getCmrWriterToken.js` around lines 95 - 103, Add
module-scope memoization for the SSM client and the resolved token used by
getCmrSystemToken, with a short expiration so token rotation remains effective.
Reuse the cached value while valid, otherwise call readSystemTokenFromSsm and
refresh the cache; preserve the existing authorization-header resolution and
undefined fallback. Ensure getCmrWriterToken transitively benefits without
changing its public 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.

Nitpick comments:
In `@serverless/src/edlAuthorizer/handler.js`:
- Around line 40-47: Replace the plain equality check in the system-token branch
of the authorizer handler with a timing-safe comparison using the existing token
values, hashing or otherwise normalizing both operands to equal-length buffers
before calling crypto.timingSafeEqual. Preserve the current authorization
success path and policy generation behavior.

In `@serverless/src/shared/getCmrWriterToken.js`:
- Around line 95-103: Add module-scope memoization for the SSM client and the
resolved token used by getCmrSystemToken, with a short expiration so token
rotation remains effective. Reuse the cached value while valid, otherwise call
readSystemTokenFromSsm and refresh the cache; preserve the existing
authorization-header resolution and undefined fallback. Ensure getCmrWriterToken
transitively benefits without changing its public behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 808a1452-4f63-4a37-b092-0b9c680ff290

📥 Commits

Reviewing files that changed from the base of the PR and between 46915fa and 6171a75.

📒 Files selected for processing (6)
  • serverless/src/edlAuthorizer/__tests__/handler.test.js
  • serverless/src/edlAuthorizer/handler.js
  • serverless/src/shared/__tests__/getCmrWriterToken.test.js
  • serverless/src/shared/__tests__/writeCorrectedMetadataToCmr.test.js
  • serverless/src/shared/getCmrWriterToken.js
  • serverless/src/shared/writeCorrectedMetadataToCmr.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • serverless/src/shared/writeCorrectedMetadataToCmr.js

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