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 (#416)
Browse files Browse the repository at this point in the history
* feat: add tenantId attribute to Cognito user pool (#348)

* feat: remove unneeded scope checks in authorizer (#347)

* feat: update lambda state machine to accommodate tenantId (#367)

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

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

* fix: remove _id, _tenantId from bulk export results (#384)

* feat: Group export scripts (#389)

* fix: add multi-tenant metadata route (#392)

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

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

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

* docs: update postman collection and docs to use Id token  (#399)

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


Co-authored-by: Yanyu Zheng <yz2690@columbia.edu>

BREAKING CHANGE: The Cognito IdToken is now used instead of the accessToken to authorize requests.
  • Loading branch information
carvantes committed Aug 18, 2021
1 parent a2dbb2f commit a9aebcc
Show file tree
Hide file tree
Showing 31 changed files with 55,200 additions and 91,457 deletions.
67 changes: 52 additions & 15 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,15 @@ 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-west-2
- enableMultiTenancy: true
region: us-west-1
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -88,27 +95,42 @@ jobs:
run: |
cd javaHapiValidatorLambda
mvn --batch-mode --update-snapshots --no-transfer-progress clean install
serverless deploy --stage dev --region us-west-2 --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
cd ..
- name: Deploy FHIR Server and ddbToEs
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
serverless deploy --stage dev --region us-west-2 --useHapiValidator true --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --useHapiValidator true --enableMultiTenancy ${{ matrix.enableMultiTenancy }} --conceal
- name: Deploy auditLogMover
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
cd auditLogMover
yarn install
serverless deploy --stage dev --region us-west-2 --conceal
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
crucible-test:
needs: deploy
name: Run Crucible Tests
name: Run Crucible Tests - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
runs-on: ubuntu-18.04
strategy:
matrix:
include:
- enableMultiTenancy: false
region: us-west-2
serviceUrlSuffix: ''
serviceUrlSecretName: SERVICE_URL
cognitoClientIdSecretName: COGNITO_CLIENT_ID
apiKeySecretName: API_KEY
- enableMultiTenancy: true
region: us-west-1
serviceUrlSuffix: /tenant/tenant1
serviceUrlSecretName: MULTITENANCY_SERVICE_URL
cognitoClientIdSecretName: MULTITENANCY_COGNITO_CLIENT_ID
apiKeySecretName: MULTITENANCY_API_KEY
steps:
- uses: actions/checkout@v2
with:
Expand All @@ -123,22 +145,35 @@ jobs:
bundle install
- name: Execute tests
env:
SERVICE_URL: ${{ secrets.SERVICE_URL}}
API_KEY: ${{ secrets.API_KEY }}
COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID}}
SERVICE_URL: ${{ secrets[matrix.serviceUrlSecretName] }}${{ matrix.serviceUrlSuffix }}
API_KEY: ${{ secrets[matrix.apiKeySecretName] }}
COGNITO_CLIENT_ID: ${{ secrets[matrix.cognitoClientIdSecretName] }}
COGNITO_USERNAME: ${{ secrets.COGNITO_USERNAME_PRACTITIONER }}
COGNITO_PASSWORD: ${{ secrets.COGNITO_PASSWORD }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
ACCESS_TOKEN=$(aws cognito-idp initiate-auth --region us-west-2 --client-id $COGNITO_CLIENT_ID \
ACCESS_TOKEN=$(aws cognito-idp initiate-auth --region ${{ matrix.region }} --client-id $COGNITO_CLIENT_ID \
--auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=$COGNITO_USERNAME,PASSWORD=$COGNITO_PASSWORD | \
python -c 'import json,sys;obj=json.load(sys.stdin);print obj["AuthenticationResult"]["AccessToken"]')
python -c 'import json,sys;obj=json.load(sys.stdin);print obj["AuthenticationResult"]["IdToken"]')
bundle exec rake crucible:execute_hearth_tests[$SERVICE_URL,$API_KEY,$ACCESS_TOKEN]
custom-integration-tests:
needs: crucible-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-west-2
serviceUrlSecretName: SERVICE_URL
cognitoClientIdSecretName: COGNITO_CLIENT_ID
apiKeySecretName: API_KEY
- enableMultiTenancy: true
region: us-west-1
serviceUrlSecretName: MULTITENANCY_SERVICE_URL
cognitoClientIdSecretName: MULTITENANCY_COGNITO_CLIENT_ID
apiKeySecretName: MULTITENANCY_API_KEY
steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -151,13 +186,15 @@ jobs:
yarn install
- name: Execute tests
env:
API_URL: ${{ secrets.SERVICE_URL}}
API_KEY: ${{ secrets.API_KEY }}
API_AWS_REGION: ${{ secrets.API_AWS_REGION }}
COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID}}
API_URL: ${{ secrets[matrix.serviceUrlSecretName] }}
API_KEY: ${{ secrets[matrix.apiKeySecretName] }}
API_AWS_REGION: ${{ matrix.region }}
COGNITO_CLIENT_ID: ${{ secrets[matrix.cognitoClientIdSecretName] }}
COGNITO_USERNAME_PRACTITIONER: ${{ secrets.COGNITO_USERNAME_PRACTITIONER }}
COGNITO_USERNAME_AUDITOR: ${{ secrets.COGNITO_USERNAME_AUDITOR }}
COGNITO_USERNAME_PRACTITIONER_ANOTHER_TENANT: ${{ secrets.COGNITO_USERNAME_PRACTITIONER_ANOTHER_TENANT }}
COGNITO_PASSWORD: ${{ secrets.COGNITO_PASSWORD }}
MULTI_TENANCY_ENABLED: ${{ matrix.enableMultiTenancy }}
run: yarn int-test

merge-develop-to-mainline:
Expand Down
29 changes: 8 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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.

## Architecture

The system architecture consists of multiple layers of AWS serverless services. The endpoint is hosted using API Gateway. The database and storage layer consists of Amazon DynamoDB and S3, with Elasticsearch as the search index for the data written to DynamoDB. The endpoint is secured by API keys and Cognito for user-level authentication and user-group authorization. The diagram below shows the FHIR server’s system architecture components and how they are related.
Expand Down Expand Up @@ -103,23 +105,17 @@ Set up the following three environment variables:

For instructions on importing the environment JSON, click [here](https://thinkster.io/tutorials/testing-backend-apis-with-postman/managing-environments-in-postman).

The `COGNITO_AUTH_TOKEN` required for each of these files can be obtained by following the instructions under [Authorizing a user](#authorizing-a-user).

The following variables required in the Postman collection can be found in `Info_Output.log` or by running `serverless info --verbose`:
+ API_URL: from Service Information:endpoints: ANY
+ API_KEY: from Service Information: api keys: developer-key
+ CLIENT_ID: from Stack Outputs: UserPoolAppClientId
+ AUTH_URL: https://<CLIENT_ID>.auth.\<REGION\>.amazoncognito.com/oauth2/authorize

**Note:** You can also query Cognito openid "well-known" url to get the AUTH_URL
```
https://cognito-idp.[REGION].amazonaws.com/[from Stack Outputs: UserPoolId]/.well-known/openid-configuration
```


To find what FHIR Server supports, use the `GET Metadata` Postman request to retrieve the [Capability Statement](https://www.hl7.org/fhir/capabilitystatement.html)

**Authorizing a user**

FHIR Works on AWS uses Role-Based Access Control (RBAC) to determine what operations and what resource types a user can access. The default rule set can be found in [RBACRules.ts](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/src/RBACRules.ts). To access the API, you must use the OAuth access token. This access token must include scopes of either `openid`, `profile` or `aws.cognito.signin.user.admin`.
FHIR Works on AWS uses Role-Based Access Control (RBAC) to determine what operations and what resource types a user can access. The default rule set can be found in [RBACRules.ts](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/src/RBACRules.ts). To access the API, you must use the ID token. This ID token must include scopes of either `openid`, `profile` or `aws.cognito.signin.user.admin`.

Using either of these scopes provide information about users and their group. It helps determine what resources/records they can access.

Expand All @@ -129,18 +125,9 @@ Using either of these scopes provide information about users and their group. It

For more information, click [here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html).

**Retrieving access token via postman using the openid profile**

To access the FHIR API, an access token is required. This can be obtained by following these steps within Postman:

1. Open Postman and choose the operation (for example, `GET Patient`).
2. In the **Authorization** tab, choose **Get New Access Token**.
3. A sign in page appears. Enter the username and password (if you don't know it look at the [init-auth.py](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/scripts%5Cinit-auth.py) script).
4. After signing in, the access token is set and you have the access for approximately one hour.

**Retrieving an access token using aws.cognito.signin.user.admin**
**Retrieving an ID token using aws.cognito.signin.user.admin**

A Cognito OAuth access token can be obtained using the following command substituting all variables with their values from `Info_Output.log` or by using the `serverless info --verbose` command.
To access the FHIR API, an ID token is required. A Cognito ID token can be obtained using the following command substituting all variables with their values from `INFO_OUTPUT.log` or by using the `serverless info --verbose` command.
+ For Windows, enter:
```sh
scripts/init-auth.py <CLIENT_ID> <REGION>
Expand All @@ -149,7 +136,7 @@ scripts/init-auth.py <CLIENT_ID> <REGION>
```sh
python3 scripts/init-auth.py <CLIENT_ID> <REGION>
```
The return value is an access token that can be used to hit the FHIR API without accessing the Oauth Sign In page. In Postman, instead of clicking the Get New Access Token button, you can paste the `ACCESS_TOKEN` value into the **Available Tokens** field.
The return value is the `COGNITO_AUTH_TOKEN` (found in the postman collection) to be used for access to the FHIR APIs.

### Accessing binary resources

Expand Down
56 changes: 56 additions & 0 deletions USING_MULTI_TENANCY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 `custom: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.

The default deployment adds a custom claim `custom:tenantId` to the Cognito User Pool. You can manage the tenant Id value
for the different users on the AWS Cognito Console. The [provision-user.py](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/scripts/provision-user.py)
script will also create users with a set tenant Id.

## 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 `custom: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 a9aebcc

Please sign in to comment.