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

Commit a9aebcc

Browse files
authoredAug 18, 2021
feat: implement multi-tenancy and group export (#416)
* 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 <[email protected]> BREAKING CHANGE: The Cognito IdToken is now used instead of the accessToken to authorize requests.
1 parent a2dbb2f commit a9aebcc

31 files changed

+55200
-91457
lines changed
 

‎.github/workflows/deploy.yaml

+52-15
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,15 @@ jobs:
5454
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5555
deploy:
5656
needs: pre-deployment-check
57-
name: Deploy to Dev
57+
name: Deploy to Dev - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
5858
runs-on: ubuntu-18.04
59+
strategy:
60+
matrix:
61+
include:
62+
- enableMultiTenancy: false
63+
region: us-west-2
64+
- enableMultiTenancy: true
65+
region: us-west-1
5966
steps:
6067
- name: Checkout
6168
uses: actions/checkout@v2
@@ -88,27 +95,42 @@ jobs:
8895
run: |
8996
cd javaHapiValidatorLambda
9097
mvn --batch-mode --update-snapshots --no-transfer-progress clean install
91-
serverless deploy --stage dev --region us-west-2 --conceal
98+
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
9299
cd ..
93100
- name: Deploy FHIR Server and ddbToEs
94101
env:
95102
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
96103
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
97104
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
98105
run: |
99-
serverless deploy --stage dev --region us-west-2 --useHapiValidator true --conceal
106+
serverless deploy --stage dev --region ${{ matrix.region }} --useHapiValidator true --enableMultiTenancy ${{ matrix.enableMultiTenancy }} --conceal
100107
- name: Deploy auditLogMover
101108
env:
102109
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
103110
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
104111
run: |
105112
cd auditLogMover
106113
yarn install
107-
serverless deploy --stage dev --region us-west-2 --conceal
114+
serverless deploy --stage dev --region ${{ matrix.region }} --conceal
108115
crucible-test:
109116
needs: deploy
110-
name: Run Crucible Tests
117+
name: Run Crucible Tests - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
111118
runs-on: ubuntu-18.04
119+
strategy:
120+
matrix:
121+
include:
122+
- enableMultiTenancy: false
123+
region: us-west-2
124+
serviceUrlSuffix: ''
125+
serviceUrlSecretName: SERVICE_URL
126+
cognitoClientIdSecretName: COGNITO_CLIENT_ID
127+
apiKeySecretName: API_KEY
128+
- enableMultiTenancy: true
129+
region: us-west-1
130+
serviceUrlSuffix: /tenant/tenant1
131+
serviceUrlSecretName: MULTITENANCY_SERVICE_URL
132+
cognitoClientIdSecretName: MULTITENANCY_COGNITO_CLIENT_ID
133+
apiKeySecretName: MULTITENANCY_API_KEY
112134
steps:
113135
- uses: actions/checkout@v2
114136
with:
@@ -123,22 +145,35 @@ jobs:
123145
bundle install
124146
- name: Execute tests
125147
env:
126-
SERVICE_URL: ${{ secrets.SERVICE_URL}}
127-
API_KEY: ${{ secrets.API_KEY }}
128-
COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID}}
148+
SERVICE_URL: ${{ secrets[matrix.serviceUrlSecretName] }}${{ matrix.serviceUrlSuffix }}
149+
API_KEY: ${{ secrets[matrix.apiKeySecretName] }}
150+
COGNITO_CLIENT_ID: ${{ secrets[matrix.cognitoClientIdSecretName] }}
129151
COGNITO_USERNAME: ${{ secrets.COGNITO_USERNAME_PRACTITIONER }}
130152
COGNITO_PASSWORD: ${{ secrets.COGNITO_PASSWORD }}
131153
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID}}
132154
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
133155
run: |
134-
ACCESS_TOKEN=$(aws cognito-idp initiate-auth --region us-west-2 --client-id $COGNITO_CLIENT_ID \
156+
ACCESS_TOKEN=$(aws cognito-idp initiate-auth --region ${{ matrix.region }} --client-id $COGNITO_CLIENT_ID \
135157
--auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=$COGNITO_USERNAME,PASSWORD=$COGNITO_PASSWORD | \
136-
python -c 'import json,sys;obj=json.load(sys.stdin);print obj["AuthenticationResult"]["AccessToken"]')
158+
python -c 'import json,sys;obj=json.load(sys.stdin);print obj["AuthenticationResult"]["IdToken"]')
137159
bundle exec rake crucible:execute_hearth_tests[$SERVICE_URL,$API_KEY,$ACCESS_TOKEN]
138160
custom-integration-tests:
139161
needs: crucible-test
140-
name: Run custom integration tests
162+
name: Run custom integration tests - enableMultiTenancy=${{ matrix.enableMultiTenancy }}
141163
runs-on: ubuntu-18.04
164+
strategy:
165+
matrix:
166+
include:
167+
- enableMultiTenancy: false
168+
region: us-west-2
169+
serviceUrlSecretName: SERVICE_URL
170+
cognitoClientIdSecretName: COGNITO_CLIENT_ID
171+
apiKeySecretName: API_KEY
172+
- enableMultiTenancy: true
173+
region: us-west-1
174+
serviceUrlSecretName: MULTITENANCY_SERVICE_URL
175+
cognitoClientIdSecretName: MULTITENANCY_COGNITO_CLIENT_ID
176+
apiKeySecretName: MULTITENANCY_API_KEY
142177
steps:
143178
- name: Checkout
144179
uses: actions/checkout@v2
@@ -151,13 +186,15 @@ jobs:
151186
yarn install
152187
- name: Execute tests
153188
env:
154-
API_URL: ${{ secrets.SERVICE_URL}}
155-
API_KEY: ${{ secrets.API_KEY }}
156-
API_AWS_REGION: ${{ secrets.API_AWS_REGION }}
157-
COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID}}
189+
API_URL: ${{ secrets[matrix.serviceUrlSecretName] }}
190+
API_KEY: ${{ secrets[matrix.apiKeySecretName] }}
191+
API_AWS_REGION: ${{ matrix.region }}
192+
COGNITO_CLIENT_ID: ${{ secrets[matrix.cognitoClientIdSecretName] }}
158193
COGNITO_USERNAME_PRACTITIONER: ${{ secrets.COGNITO_USERNAME_PRACTITIONER }}
159194
COGNITO_USERNAME_AUDITOR: ${{ secrets.COGNITO_USERNAME_AUDITOR }}
195+
COGNITO_USERNAME_PRACTITIONER_ANOTHER_TENANT: ${{ secrets.COGNITO_USERNAME_PRACTITIONER_ANOTHER_TENANT }}
160196
COGNITO_PASSWORD: ${{ secrets.COGNITO_PASSWORD }}
197+
MULTI_TENANCY_ENABLED: ${{ matrix.enableMultiTenancy }}
161198
run: yarn int-test
162199

163200
merge-develop-to-mainline:

‎README.md

+8-21
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ git clone https://github.com/awslabs/fhir-works-on-aws-deployment.git
4040

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

43+
If you intend to do a multi-tenant deployment read the [Using Multi-Tenancy](./USING_MULTI_TENANCY.md) documentation first.
44+
4345
## Architecture
4446

4547
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.
@@ -103,23 +105,17 @@ Set up the following three environment variables:
103105

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

108+
The `COGNITO_AUTH_TOKEN` required for each of these files can be obtained by following the instructions under [Authorizing a user](#authorizing-a-user).
109+
106110
The following variables required in the Postman collection can be found in `Info_Output.log` or by running `serverless info --verbose`:
107111
+ API_URL: from Service Information:endpoints: ANY
108112
+ API_KEY: from Service Information: api keys: developer-key
109-
+ CLIENT_ID: from Stack Outputs: UserPoolAppClientId
110-
+ AUTH_URL: https://<CLIENT_ID>.auth.\<REGION\>.amazoncognito.com/oauth2/authorize
111-
112-
**Note:** You can also query Cognito openid "well-known" url to get the AUTH_URL
113-
```
114-
https://cognito-idp.[REGION].amazonaws.com/[from Stack Outputs: UserPoolId]/.well-known/openid-configuration
115-
```
116-
117113

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

120116
**Authorizing a user**
121117

122-
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`.
118+
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`.
123119

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

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

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

132-
**Retrieving access token via postman using the openid profile**
133-
134-
To access the FHIR API, an access token is required. This can be obtained by following these steps within Postman:
135-
136-
1. Open Postman and choose the operation (for example, `GET Patient`).
137-
2. In the **Authorization** tab, choose **Get New Access Token**.
138-
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).
139-
4. After signing in, the access token is set and you have the access for approximately one hour.
140-
141-
**Retrieving an access token using aws.cognito.signin.user.admin**
128+
**Retrieving an ID token using aws.cognito.signin.user.admin**
142129

143-
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.
130+
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.
144131
+ For Windows, enter:
145132
```sh
146133
scripts/init-auth.py <CLIENT_ID> <REGION>
@@ -149,7 +136,7 @@ scripts/init-auth.py <CLIENT_ID> <REGION>
149136
```sh
150137
python3 scripts/init-auth.py <CLIENT_ID> <REGION>
151138
```
152-
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.
139+
The return value is the `COGNITO_AUTH_TOKEN` (found in the postman collection) to be used for access to the FHIR APIs.
153140

154141
### Accessing binary resources
155142

‎USING_MULTI_TENANCY.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Multi-Tenancy
2+
3+
Multi-tenancy allows a single `fhir-works-on-aws` stack to serve as multiple FHIR servers for different tenants.
4+
5+
`fhir-works-on-aws` uses a pooled infrastructure model for multi-tenancy. This means that all tenants share the
6+
same infrastructure (DynamoDB tables, S3 Buckets, Elasticsearch cluster, etc.), but the data
7+
is logically partitioned to ensure that tenants are prevented from accessing another tenant’s resources.
8+
9+
## Enabling multi-tenancy
10+
11+
Use the `enableMultiTenancy` option when deploying the stack:
12+
13+
```bash
14+
serverless deploy --enableMultiTenancy true
15+
```
16+
17+
**Note:** Updating an existing (single-tenant) stack to enable multi-tenancy is a breaking change. Multi-tenant
18+
deployments use a different data partitioning strategy that renders the old, single-tenant, data inaccessible.
19+
If you wish to switch from single-tenant to a multi-tenant model, it is recommended to create a new multi-tenant stack
20+
and then migrate the data from the old stack. Switching from multi-tenant to a single-tenant model is also a breaking change.
21+
22+
## Tenant identifiers
23+
24+
Tenants are identified by a tenant Id in the auth token. A tenant Id is a string that can contain alphanumeric characters,
25+
dashes, and underscores and have a maximum length of 64 characters.
26+
27+
There are 2 ways to include a tenant Id in the auth token:
28+
29+
1. Add the tenant Id in a custom claim. This is the recommended approach.
30+
The default configuration adds the tenant Id on the `custom:tenantId` claim
31+
32+
1. Encode the tenant Id in the `aud` claim by providing an URL that matches `<baseUrl>/tenant/<tenantId>`.
33+
This can be useful when using an IDP that does not support custom claims.
34+
35+
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,
36+
otherwise an Unauthorized error is thrown.
37+
38+
The default deployment adds a custom claim `custom:tenantId` to the Cognito User Pool. You can manage the tenant Id value
39+
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)
40+
script will also create users with a set tenant Id.
41+
42+
## Additional Configuration
43+
44+
Additional configuration values can be set on [config.ts](https://github.com/awslabs/fhir-works-on-aws-deployment/blob/mainline/src/config.ts)
45+
46+
- `enableMultiTenancy`: Whether or not to enable multi-tenancy.
47+
- `useTenantSpecificUrl`: When enabled, `/tenant/<tenantId>/` is appended to the FHIR server url.
48+
49+
e.g. A client with `tennatId=tenantA` would use the following url to search for Patients:
50+
```
51+
GET <serverUrl>/tenant/<tenantId>/Patient
52+
GET https://1234567890.execute-api.us-west-2.amazonaws.com/dev/tenant/tenantA/Patient
53+
```
54+
Enabling this setting is useful to give each tenant a unique FHIR server base URL.
55+
56+
- `tenantIdClaimPath`: Path to the tenant Id claim in the auth token JSON. Defaults to `custom:tenantId`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
This scripts generates the two patientCompartmentSearchParams JSON files from compartment definition files and save them at bulkExport/schema.
3+
4+
Run the script:
5+
> node extractPatientCompartmentSearchParams.js
6+
7+
The compartment definition files are downloaded from the following URL and saved in folder bulkExport/schema:
8+
9+
compartmentdefinition-patient.4.0.1.json: https://www.hl7.org/fhir/compartmentdefinition-patient.json.html
10+
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)
11+
*/
12+
13+
const fs = require('fs');
14+
const compartmentPatientV3 = require('./schema/compartmentdefinition-patient.3.0.2.json');
15+
const compartmentPatientV4 = require('./schema/compartmentdefinition-patient.4.0.1.json');
16+
const baseSearchParamsV3 = require('../../fhir-works-on-aws-search-es/src/schema/compiledSearchParameters.3.0.1.json');
17+
const baseSearchParamsV4 = require('../../fhir-works-on-aws-search-es/src/schema/compiledSearchParameters.4.0.1.json');
18+
19+
// Create a dictionary of search params
20+
function extractPatientCompartmentSearchParams(baseSearchParams, compartmentPatient) {
21+
const baseSearchParamsDict = {};
22+
// example of an item in baseSearchParamsDict: Account-identifier: {resourceType: "Account", path: "identifier"}
23+
baseSearchParams.forEach(param => {
24+
baseSearchParamsDict[`${param.base}-${param.name}`] = param.compiled;
25+
});
26+
27+
// Find the search params needed for patient compartment
28+
const patientCompartmentSearchParams = {};
29+
compartmentPatient.resource.forEach(resource => {
30+
if (resource.param) {
31+
let compiledPaths = [];
32+
resource.param.forEach(param => {
33+
const pathsForThisParam = baseSearchParamsDict[`${resource.code}-${param}`].map(item => item.path);
34+
compiledPaths = compiledPaths.concat(pathsForThisParam);
35+
});
36+
patientCompartmentSearchParams[resource.code] = compiledPaths;
37+
}
38+
});
39+
return patientCompartmentSearchParams;
40+
}
41+
42+
const patientCompartmentSearchParamsV4 = extractPatientCompartmentSearchParams(
43+
baseSearchParamsV4,
44+
compartmentPatientV4,
45+
);
46+
const patientCompartmentSearchParamsV3 = extractPatientCompartmentSearchParams(
47+
baseSearchParamsV3,
48+
compartmentPatientV3,
49+
);
50+
51+
fs.writeFileSync(
52+
'./schema/patientCompartmentSearchParams.3.0.2.json',
53+
JSON.stringify(patientCompartmentSearchParamsV3),
54+
);
55+
fs.writeFileSync(
56+
'./schema/patientCompartmentSearchParams.4.0.1.json',
57+
JSON.stringify(patientCompartmentSearchParamsV4),
58+
);
59+
60+
console.log(patientCompartmentSearchParamsV4);
61+
console.log(patientCompartmentSearchParamsV3);

0 commit comments

Comments
 (0)
This repository has been archived.