Skip to content

Commit

Permalink
BCDA-8230: Remove hashed MBI (#982)
Browse files Browse the repository at this point in the history
## 🎫 Ticket

https://jira.cms.gov/browse/BCDA-8230

## 🛠 Changes

- Switch to POST endpoints with unhashed MBI

## ℹ️ Context

<!-- Why were these changes made? Add background context suitable for a
non-technical audience. -->

<!-- If any of the following security implications apply, this PR must
not be merged without Stephen Walter's approval. Explain in this section
and add @SJWalter11 as a reviewer.
  - Adds a new software dependency or dependencies.
  - Modifies or invalidates one or more of our security controls.
  - Stores or transmits data that was not stored or transmitted before.
- Requires additional review of security implications for other reasons.
-->

## 🧪 Validation

<!-- How were the changes verified? Did you fully test the acceptance
criteria in the ticket? Provide reproducible testing instructions and
screenshots if applicable. -->

---------

Co-authored-by: carlpartridge <[email protected]>
  • Loading branch information
kyeah and carlpartridge authored Oct 9, 2024
1 parent d066d6a commit bd692a5
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 293 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
**/bcdaworker/archive/*
**/bcdaworker/data/*
**/bcdaworker/tmp/*
**/bcdaworker/tmpdata
**/bcdaworker/tmpdata/*
**/bcdaworker/TEMP
**/bcdaworker/TEMP/*
**/.git
**/data
**/test_results
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ bcda/swaggerui
bcdaworker/data/*
bcdaworker/archive/*
bcdaworker/tmpdata/*
bcdaworker/TEMP/*
.env.sh
.envrc
bcda/pending_delete_dir/*
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ repos:
rev: v1.0.0-rc.1
hooks:
- id: golangci-lint-pkg
args: ['--new']
args: ['--new', '-v']
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.23.1
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ package:
-e GPG_SEC_KEY_FILE='${GPG_SEC_KEY_FILE}' \
-v ${PWD}:/go/src/github.com/CMSgov/bcda-app packaging $(version)

setup-tests:
# Clean up any existing data to ensure we spin up container in a known state.
docker compose -f docker-compose.test.yml rm -fsv tests
docker compose -f docker-compose.test.yml build tests

LINT_TIMEOUT ?= 3m
lint:
docker compose -f docker-compose.test.yml build tests
lint: setup-tests
docker compose -f docker-compose.test.yml run \
--rm tests golangci-lint run --exclude="(conf\.(Un)?[S,s]etEnv)" --exclude="github\.com\/stretchr\/testify\/suite\.Suite contains sync\.RWMutex" --timeout=$(LINT_TIMEOUT) --verbose
# TODO: Remove the exclusion of G301 as part of BCDA-8414
docker compose -f docker-compose.test.yml run --rm tests gosec -exclude=G301 ./... ./optout

smoke-test:
docker compose -f docker-compose.test.yml build tests
smoke-test: setup-tests
test/smoke_test/smoke_test.sh $(env) $(maintenanceMode)

postman:
Expand All @@ -45,8 +47,7 @@ postman:
--global-var v2Disabled=false \
--global-var maintenanceMode=$(maintenanceMode)

unit-test: unit-test-ssas unit-test-db unit-test-localstack load-fixtures-ssas
docker compose -f docker-compose.test.yml build tests
unit-test: unit-test-ssas unit-test-db unit-test-localstack load-fixtures-ssas setup-tests
@docker compose -f docker-compose.test.yml run --rm tests bash scripts/unit_test.sh

unit-test-ssas:
Expand Down Expand Up @@ -82,8 +83,7 @@ unit-test-db-snapshot:
# Target takes a snapshot of the currently running postgres instance used for unit testing and updates the db/testing/docker-entrypoint-initdb.d/dump.pgdata file
docker compose -f docker-compose.test.yml exec db-unit-test sh -c 'PGPASSWORD=$$POSTGRES_PASSWORD pg_dump -U postgres --format custom --file=/docker-entrypoint-initdb.d/dump.pgdata --create $$POSTGRES_DB'

performance-test:
docker compose -f docker-compose.test.yml build tests
performance-test: setup-tests
docker compose -f docker-compose.test.yml run --rm -w /go/src/github.com/CMSgov/bcda-app/test/performance_test tests sh performance_test.sh

test:
Expand Down
75 changes: 28 additions & 47 deletions bcda/client/bluebutton.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package client

import (
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"os"
Expand All @@ -16,7 +15,7 @@ import (
"time"

"github.com/ccoveille/go-safecast"
"github.com/cenkalti/backoff/v4"
backoff "github.com/cenkalti/backoff/v4"
"github.com/newrelic/go-agent/v3/newrelic"

"github.com/CMSgov/bcda-app/bcda/client/fhir"
Expand All @@ -32,7 +31,6 @@ import (
"github.com/sirupsen/logrus"

"github.com/pborman/uuid"
"golang.org/x/crypto/pbkdf2"
)

var logger logrus.FieldLogger
Expand Down Expand Up @@ -67,7 +65,7 @@ type APIClient interface {
GetExplanationOfBenefit(jobData models.JobEnqueueArgs, patientID string, claimsWindow ClaimsWindow) (*fhirModels.Bundle, error)
GetPatient(jobData models.JobEnqueueArgs, patientID string) (*fhirModels.Bundle, error)
GetCoverage(jobData models.JobEnqueueArgs, beneficiaryID string) (*fhirModels.Bundle, error)
GetPatientByIdentifierHash(jobData models.JobEnqueueArgs, hashedIdentifier string) (string, error)
GetPatientByMbi(jobData models.JobEnqueueArgs, mbi string) (string, error)
GetClaim(jobData models.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhirModels.Bundle, error)
GetClaimResponse(jobData models.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhirModels.Bundle, error)
}
Expand Down Expand Up @@ -163,18 +161,16 @@ func (bbc *BlueButtonClient) GetPatient(jobData models.JobEnqueueArgs, patientID
return bbc.getBundleData(u, jobData, header)
}

func (bbc *BlueButtonClient) GetPatientByIdentifierHash(jobData models.JobEnqueueArgs, hashedIdentifier string) (string, error) {
params := GetDefaultParams()

// FHIR spec requires a FULLY qualified namespace so this is in fact the argument, not a URL
params.Set("identifier", fmt.Sprintf("https://bluebutton.cms.gov/resources/identifier/%s|%v", "mbi-hash", hashedIdentifier))
func (bbc *BlueButtonClient) GetPatientByMbi(jobData models.JobEnqueueArgs, mbi string) (string, error) {
params := url.Values{}

u, err := bbc.getURL("Patient", params)
u, err := bbc.getURL("Patient/_search", params)
if err != nil {
return "", err
}

return bbc.getRawData(jobData, u)
body := fmt.Sprintf(`{"identifier":"http://hl7.org/fhir/sid/us-mbi|%s"}`, mbi)
return bbc.getRawData(jobData, "POST", u, strings.NewReader(body))
}

func (bbc *BlueButtonClient) GetCoverage(jobData models.JobEnqueueArgs, beneficiaryID string) (*fhirModels.Bundle, error) {
Expand All @@ -194,48 +190,38 @@ func (bbc *BlueButtonClient) GetClaim(jobData models.JobEnqueueArgs, mbi string,
header := make(http.Header)
header.Add("IncludeTaxNumbers", "true")

mbiHash, err := HashIdentifier(mbi)
if err != nil {
return nil, err
}

params := GetDefaultParams()
params.Set("mbi", mbiHash)
params.Set("excludeSAMHSA", "true")

updateParamWithServiceDate(&params, claimsWindow)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)

u, err := bbc.getURL("Claim", params)
u, err := bbc.getURL("Claim/_search", params)
if err != nil {
return nil, err
}

return bbc.getBundleData(u, jobData, header)
body := fmt.Sprintf(`{"identifier":"http://hl7.org/fhir/sid/us-mbi|%s"}`, mbi)
return bbc.postBundleData(u, jobData, header, strings.NewReader(body))
}

func (bbc *BlueButtonClient) GetClaimResponse(jobData models.JobEnqueueArgs, mbi string, claimsWindow ClaimsWindow) (*fhirModels.Bundle, error) {
header := make(http.Header)
header.Add("IncludeTaxNumbers", "true")

mbiHash, err := HashIdentifier(mbi)
if err != nil {
return nil, err
}

params := GetDefaultParams()
params.Set("mbi", mbiHash)
params.Set("excludeSAMHSA", "true")

updateParamWithServiceDate(&params, claimsWindow)
updateParamWithLastUpdated(&params, jobData.Since, jobData.TransactionTime)

u, err := bbc.getURL("ClaimResponse", params)
u, err := bbc.getURL("ClaimResponse/_search", params)
if err != nil {
return nil, err
}

return bbc.getBundleData(u, jobData, header)
body := fmt.Sprintf(`{"identifier":"http://hl7.org/fhir/sid/us-mbi|%s"}`, mbi)
return bbc.postBundleData(u, jobData, header, strings.NewReader(body))
}

func (bbc *BlueButtonClient) GetExplanationOfBenefit(jobData models.JobEnqueueArgs, patientID string, claimsWindow ClaimsWindow) (*fhirModels.Bundle, error) {
Expand Down Expand Up @@ -263,13 +249,21 @@ func (bbc *BlueButtonClient) GetMetadata() (string, error) {
}
jobData := models.JobEnqueueArgs{}

return bbc.getRawData(jobData, u)
return bbc.getRawData(jobData, "GET", u, nil)
}

func (bbc *BlueButtonClient) getBundleData(u *url.URL, jobData models.JobEnqueueArgs, headers http.Header) (*fhirModels.Bundle, error) {
return bbc.makeBundleDataRequest("GET", u, jobData, headers, nil)
}

func (bbc *BlueButtonClient) postBundleData(u *url.URL, jobData models.JobEnqueueArgs, headers http.Header, body io.Reader) (*fhirModels.Bundle, error) {
return bbc.makeBundleDataRequest("POST", u, jobData, headers, body)
}

func (bbc *BlueButtonClient) makeBundleDataRequest(method string, u *url.URL, jobData models.JobEnqueueArgs, headers http.Header, body io.Reader) (*fhirModels.Bundle, error) {
var b *fhirModels.Bundle
for ok := true; ok; {
result, nextURL, err := bbc.tryBundleRequest(u, jobData, headers)
result, nextURL, err := bbc.tryBundleRequest(method, u, jobData, headers, body)
if err != nil {
return nil, err
}
Expand All @@ -287,7 +281,7 @@ func (bbc *BlueButtonClient) getBundleData(u *url.URL, jobData models.JobEnqueue
return b, nil
}

func (bbc *BlueButtonClient) tryBundleRequest(u *url.URL, jobData models.JobEnqueueArgs, headers http.Header) (*fhirModels.Bundle, *url.URL, error) {
func (bbc *BlueButtonClient) tryBundleRequest(method string, u *url.URL, jobData models.JobEnqueueArgs, headers http.Header, body io.Reader) (*fhirModels.Bundle, *url.URL, error) {
m := monitoring.GetMonitor()
txn := m.Start(u.Path, nil, nil)
defer m.End(txn)
Expand All @@ -303,7 +297,7 @@ func (bbc *BlueButtonClient) tryBundleRequest(u *url.URL, jobData models.JobEnqu
b := backoff.WithMaxRetries(eb, bbc.maxTries)

err = backoff.RetryNotify(func() error {
req, err := http.NewRequest("GET", u.String(), nil)
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
logger.Error(err)
return err
Expand Down Expand Up @@ -337,7 +331,7 @@ func (bbc *BlueButtonClient) tryBundleRequest(u *url.URL, jobData models.JobEnqu
return result, nextURL, nil
}

func (bbc *BlueButtonClient) getRawData(jobData models.JobEnqueueArgs, u *url.URL) (string, error) {
func (bbc *BlueButtonClient) getRawData(jobData models.JobEnqueueArgs, method string, u *url.URL, body io.Reader) (string, error) {
m := monitoring.GetMonitor()
txn := m.Start(u.Path, nil, nil)
defer m.End(txn)
Expand All @@ -349,7 +343,7 @@ func (bbc *BlueButtonClient) getRawData(jobData models.JobEnqueueArgs, u *url.UR
var result string

err := backoff.RetryNotify(func() error {
req, err := http.NewRequest("GET", u.String(), nil)
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
logger.Error(err)
return err
Expand Down Expand Up @@ -417,19 +411,6 @@ func GetDefaultParams() (params url.Values) {
return params
}

func HashIdentifier(toHash string) (hashedValue string, err error) {
blueButtonPepper := conf.GetEnv("BB_HASH_PEPPER")
blueButtonIter := utils.GetEnvInt("BB_HASH_ITER", 1000)

pepper, err := hex.DecodeString(blueButtonPepper)

if err != nil {
return "", errors.Wrap(err, "Failed to decode bluebutton hash pepper")
}

return hex.EncodeToString(pbkdf2.Key([]byte(toHash), pepper, blueButtonIter, 32, sha256.New)), nil
}

func updateParamWithServiceDate(params *url.Values, claimsWindow ClaimsWindow) {
// ServiceDate only uses yyyy-mm-dd
const isoDate = "2006-01-02"
Expand Down
Loading

0 comments on commit bd692a5

Please sign in to comment.