Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
feat: implement multi-tenancy and group export (#421)
Browse files Browse the repository at this point in the history
* feat: update lambda state machine to accommodate tenantId (#367)

* feat: add "enableMultiTenancy" CFN parameter (#382)

* fix: pass enableMultiTenancy to ES

* fix: remove _id, _tenantId from bulk export results

* feat: Group export scripts (#389)

* chore: script generating patient compartment search params

* feat: update Glue script for group export

* Upload patient compartment jsons to S3

* fix: allow more concurrent export jobs for multi-tenant deployments (#397)

* feat: add ES hard delete config value (#398)

* docs: add multi-tenancy docs (#400)

* fix: pass enableMultiTenancy flag to s3DataService

* test: add multi-tenancy integ tests (#387)

* test: integ tests for Group export (#393)

* chore: upgrade dependencies

* add public multi-tenant routes

* add system/read and user/delete permissions to defaults

* test: fix tests for smart multi-tenancy

* test: update gh actions to also test multi-tenant environment

* docs: update bulk export docs to mention group export

Co-authored-by: Yanyu Zheng <yz2690@columbia.edu>
  • Loading branch information
carvantes and Bingjiling committed Aug 24, 2021
1 parent d8644eb commit 5335807
Show file tree
Hide file tree
Showing 24 changed files with 2,257 additions and 147 deletions.
68 changes: 56 additions & 12 deletions .github/workflows/deploy-smart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,21 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
deploy:
needs: pre-deployment-check
name: Deploy to Dev
name: Deploy to Dev - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
runs-on: ubuntu-18.04
strategy:
matrix:
include:
- enableMultiTenancy: false
region: us-east-2
issuerEndpointSecretName: SMART_ISSUER_ENDPOINT
oAuth2ApiEndpointSecretName: SMART_OAUTH2_API_ENDPOINT
patientPickerEndpointSecretName: SMART_PATIENT_PICKER_ENDPOINT
- enableMultiTenancy: true
region: us-west-1
issuerEndpointSecretName: MULTITENANCY_SMART_ISSUER_ENDPOINT
oAuth2ApiEndpointSecretName: MULTITENANCY_SMART_OAUTH2_API_ENDPOINT
patientPickerEndpointSecretName: MULTITENANCY_SMART_PATIENT_PICKER_ENDPOINT
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -77,27 +90,39 @@ jobs:
run: |
cd javaHapiValidatorLambda
mvn --batch-mode --update-snapshots --no-transfer-progress clean install
serverless deploy --stage dev --region us-east-2 --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
cd ..
- name: Deploy FHIR Server and ddbToEs
env:
AWS_ACCESS_KEY_ID: ${{ secrets.SMART_AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SMART_AWS_SECRET_ACCESS_KEY }}
run: |
yarn install
serverless deploy --stage dev --region us-east-2 --issuerEndpoint ${{ secrets.SMART_ISSUER_ENDPOINT }} --oAuth2ApiEndpoint ${{ secrets.SMART_OAUTH2_API_ENDPOINT }} --patientPickerEndpoint ${{ secrets.SMART_PATIENT_PICKER_ENDPOINT }} --useHapiValidator true --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --issuerEndpoint ${{ secrets[matrix.issuerEndpointSecretName] }} --oAuth2ApiEndpoint ${{ secrets[matrix.oAuth2ApiEndpointSecretName] }} --patientPickerEndpoint ${{ secrets[matrix.patientPickerEndpointSecretName] }} --useHapiValidator true --enableMultiTenancy ${{ matrix.enableMultiTenancy }} --conceal
- name: Deploy auditLogMover
env:
AWS_ACCESS_KEY_ID: ${{ secrets.SMART_AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SMART_AWS_SECRET_ACCESS_KEY }}
run: |
cd auditLogMover
yarn install
serverless deploy --stage dev --region us-east-2 --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
inferno-test:
needs: deploy
name: Run Inferno Tests
name: Run Inferno Tests - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
include:
- enableMultiTenancy: false
region: us-east-2
serviceUrlSuffix: ''
smartServiceURLSecretName: SMART_SERVICE_URL
- enableMultiTenancy: true
region: us-west-1
serviceUrlSuffix: /tenant/tenant1
smartServiceURLSecretName: MULTITENANCY_SMART_SERVICE_URL
steps:
- uses: actions/checkout@v2
with:
Expand All @@ -112,7 +137,7 @@ jobs:
bundle install
- name: Execute tests
env:
SERVICE_URL: ${{ secrets.SMART_SERVICE_URL}}
SERVICE_URL: ${{ secrets[matrix.smartServiceURLSecretName]}}${{ matrix.serviceUrlSuffix }}
CLIENT_ID: ${{ secrets.SMART_AUTH_CLIENT_ID}}
CLIENT_SECRET: ${{ secrets.SMART_AUTH_CLIENT_SECRET }}
AUTH_ENDPOINT: ${{ secrets.SMART_AUTH_ENDPOINT }}
Expand All @@ -132,8 +157,25 @@ jobs:
bundle exec rake inferno:execute_batch[fhir-works.json]
custom-integration-tests:
needs: inferno-test
name: Run custom integration tests
name: Run custom integration tests - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
runs-on: ubuntu-18.04
strategy:
matrix:
include:
- enableMultiTenancy: false
region: us-east-2
smartOauth2ApiEndpointSecretName: SMART_OAUTH2_API_ENDPOINT
smartAuthUsernameSecretName: SMART_AUTH_USERNAME
smartAuthAdminUsernameSecretName: SMART_AUTH_ADMIN_USERNAME
smartServiceURLSecretName: SMART_SERVICE_URL
smartApiKeySecretName: SMART_API_KEY
- enableMultiTenancy: true
region: us-west-1
smartOauth2ApiEndpointSecretName: MULTITENANCY_SMART_OAUTH2_API_ENDPOINT
smartAuthUsernameSecretName: MULTITENANCY_SMART_AUTH_USERNAME
smartAuthAdminUsernameSecretName: MULTITENANCY_SMART_AUTH_ADMIN_USERNAME
smartServiceURLSecretName: MULTITENANCY_SMART_SERVICE_URL
smartApiKeySecretName: MULTITENANCY_SMART_API_KEY
steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -146,14 +188,16 @@ jobs:
yarn install
- name: Execute tests
env:
SMART_OAUTH2_API_ENDPOINT: ${{ secrets.SMART_OAUTH2_API_ENDPOINT}}
SMART_OAUTH2_API_ENDPOINT: ${{ secrets[matrix.smartOauth2ApiEndpointSecretName] }}
SMART_INTEGRATION_TEST_CLIENT_ID: ${{ secrets.SMART_INTEGRATION_TEST_CLIENT_ID}}
SMART_INTEGRATION_TEST_CLIENT_PW: ${{ secrets.SMART_INTEGRATION_TEST_CLIENT_PW}}
SMART_AUTH_USERNAME: ${{ secrets.SMART_AUTH_USERNAME}}
SMART_AUTH_ADMIN_USERNAME: ${{ secrets.SMART_AUTH_ADMIN_USERNAME}}
SMART_AUTH_USERNAME: ${{ secrets[matrix.smartAuthUsernameSecretName] }}
SMART_AUTH_ADMIN_USERNAME: ${{ secrets[matrix.smartAuthAdminUsernameSecretName] }}
SMART_AUTH_ADMIN_ANOTHER_TENANT_USERNAME: ${{ secrets.SMART_AUTH_ADMIN_ANOTHER_TENANT_USERNAME}}
SMART_AUTH_PASSWORD: ${{ secrets.SMART_AUTH_PASSWORD}}
SMART_SERVICE_URL: ${{ secrets.SMART_SERVICE_URL}}
SMART_API_KEY: ${{ secrets.SMART_API_KEY}}
SMART_SERVICE_URL: ${{ secrets[matrix.smartServiceURLSecretName] }}
SMART_API_KEY: ${{ secrets[matrix.smartApiKeySecretName] }}
MULTI_TENANCY_ENABLED: ${{ matrix.enableMultiTenancy }}
run: yarn int-test
merge-develop-to-mainline:
needs: custom-integration-tests
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ git clone https://github.com/awslabs/fhir-works-on-aws-deployment.git

If you intend to use FHIR Implementation Guides read the [Using Implementation Guides](./USING_IMPLEMENTATION_GUIDES.md) documentation first.

If you intend to do a multi-tenant deployment read the [Using Multi-Tenancy](./USING_MULTI_TENANCY.md) documentation first.

### Post installation

After your installation of FHIR Works on AWS you will need to update your OAuth2 authorization server to set the FHIR Works API Gateway endpoint as the audience of the access token.
Expand Down Expand Up @@ -135,12 +137,14 @@ Binary resources are FHIR resources that consist of binary/unstructured data of

### Testing Bulk Data Export

Bulk Export allows you to export all of your data from DDB to S3. We currently only support [System Level](https://hl7.org/fhir/uv/bulkdata/export/index.html#endpoint---system-level-export) export.
Bulk Export allows you to export all of your data from DDB to S3. We currently support [System Level](https://hl7.org/fhir/uv/bulkdata/export/index.html#endpoint---system-level-export) and [Group](https://hl7.org/fhir/uv/bulkdata/export/index.html#endpoint---group-of-patients) export.
The `system/*.read` scope is required for Group export. System export works with either `system/*.read` or `user/*.read`

For more information about Bulk Export, please refer to this [implementation guide](https://hl7.org/fhir/uv/bulkdata/export/index.html).

The easiest way to test this feature on FHIR Works on AWS is to make API requests using the provided [FHIR_SMART.postman_collection.json](./postman/FHIR_SMART.postman_collection.json).

1. In the collection, under the "Export" folder, use `GET System Export` request to initiate an Export request.
1. In the collection, under the "Export" folder, use `GET System Export` or `GET Group export` request to initiate an Export request.
2. In the response, check the header field `Content-Location` for a URL. The url should be in the format `<base-url>/$export/<jobId>`.
3. To get the status of the export job, in the "Export" folder used the `GET System Job Status` request. That request will ask for the `jobId` value from step 2.
4. Check the response that is returned from `GET System Job Status`. If the job is in progress you will see a header with the field `x-progress: in-progress`. Keep polling that URL until the job is complete. Once the job is complete you'll get a JSON body with presigned S3 URLs of your exported data. You can download the exported data using those URLs.
Expand Down
52 changes: 52 additions & 0 deletions USING_MULTI_TENANCY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Multi-Tenancy

Multi-tenancy allows a single `fhir-works-on-aws` stack to serve as multiple FHIR servers for different tenants.

`fhir-works-on-aws` uses a pooled infrastructure model for multi-tenancy. This means that all tenants share the
same infrastructure (DynamoDB tables, S3 Buckets, Elasticsearch cluster, etc.), but the data
is logically partitioned to ensure that tenants are prevented from accessing another tenant’s resources.

## Enabling multi-tenancy

Use the `enableMultiTenancy` option when deploying the stack:

```bash
serverless deploy --enableMultiTenancy true
```

**Note:** Updating an existing (single-tenant) stack to enable multi-tenancy is a breaking change. Multi-tenant
deployments use a different data partitioning strategy that renders the old, single-tenant, data inaccessible.
If you wish to switch from single-tenant to a multi-tenant model, it is recommended to create a new multi-tenant stack
and then migrate the data from the old stack. Switching from multi-tenant to a single-tenant model is also a breaking change.

## Tenant identifiers

Tenants are identified by a tenant Id in the auth token. A tenant Id is a string that can contain alphanumeric characters,
dashes, and underscores and have a maximum length of 64 characters.

There are 2 ways to include a tenant Id in the auth token:

1. Add the tenant Id in a custom claim. This is the recommended approach.
The default configuration adds the tenant Id on the `tenantId` claim

1. Encode the tenant Id in the `aud` claim by providing an URL that matches `<baseUrl>/tenant/<tenantId>`.
This can be useful when using an IDP that does not support custom claims.

If a token has a tenant Id in a custom claim and in the aud claim, then both claims must have the same tenant Id value,
otherwise an Unauthorized error is thrown.

## Additional Configuration

Additional configuration values can be set on [config.ts](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/src/config.ts)

- `enableMultiTenancy`: Whether or not to enable multi-tenancy.
- `useTenantSpecificUrl`: When enabled, `/tenant/<tenantId>/` is appended to the FHIR server url.

e.g. A client with `tennatId=tenantA` would use the following url to search for Patients:
```
GET <serverUrl>/tenant/<tenantId>/Patient
GET https://1234567890.execute-api.us-west-2.amazonaws.com/dev/tenant/tenantA/Patient
```
Enabling this setting is useful to give each tenant a unique FHIR server base URL.

- `tenantIdClaimPath`: Path to the tenant Id claim in the auth token JSON. Defaults to `tenantId`
61 changes: 61 additions & 0 deletions bulkExport/extractPatientCompartmentSearchParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
This scripts generates the two patientCompartmentSearchParams JSON files from compartment definition files and save them at bulkExport/schema.
Run the script:
> node extractPatientCompartmentSearchParams.js
The compartment definition files are downloaded from the following URL and saved in folder bulkExport/schema:
compartmentdefinition-patient.4.0.1.json: https://www.hl7.org/fhir/compartmentdefinition-patient.json.html
compartmentdefinition-patient.3.0.2.json: http://hl7.org/fhir/stu3/compartmentdefinition-patient.json.html (Note the AuditEvent and Provenance fields in this file are updated to remove dotted path)
*/

const fs = require('fs');
const compartmentPatientV3 = require('./schema/compartmentdefinition-patient.3.0.2.json');
const compartmentPatientV4 = require('./schema/compartmentdefinition-patient.4.0.1.json');
const baseSearchParamsV3 = require('../../fhir-works-on-aws-search-es/src/schema/compiledSearchParameters.3.0.1.json');
const baseSearchParamsV4 = require('../../fhir-works-on-aws-search-es/src/schema/compiledSearchParameters.4.0.1.json');

// Create a dictionary of search params
function extractPatientCompartmentSearchParams(baseSearchParams, compartmentPatient) {
const baseSearchParamsDict = {};
// example of an item in baseSearchParamsDict: Account-identifier: {resourceType: "Account", path: "identifier"}
baseSearchParams.forEach(param => {
baseSearchParamsDict[`${param.base}-${param.name}`] = param.compiled;
});

// Find the search params needed for patient compartment
const patientCompartmentSearchParams = {};
compartmentPatient.resource.forEach(resource => {
if (resource.param) {
let compiledPaths = [];
resource.param.forEach(param => {
const pathsForThisParam = baseSearchParamsDict[`${resource.code}-${param}`].map(item => item.path);
compiledPaths = compiledPaths.concat(pathsForThisParam);
});
patientCompartmentSearchParams[resource.code] = compiledPaths;
}
});
return patientCompartmentSearchParams;
}

const patientCompartmentSearchParamsV4 = extractPatientCompartmentSearchParams(
baseSearchParamsV4,
compartmentPatientV4,
);
const patientCompartmentSearchParamsV3 = extractPatientCompartmentSearchParams(
baseSearchParamsV3,
compartmentPatientV3,
);

fs.writeFileSync(
'./schema/patientCompartmentSearchParams.3.0.2.json',
JSON.stringify(patientCompartmentSearchParamsV3),
);
fs.writeFileSync(
'./schema/patientCompartmentSearchParams.4.0.1.json',
JSON.stringify(patientCompartmentSearchParamsV4),
);

console.log(patientCompartmentSearchParamsV4);
console.log(patientCompartmentSearchParamsV3);
Loading

0 comments on commit 5335807

Please sign in to comment.