getParameters() {
76 | return requestDetails.getParameters();
77 | }
78 |
79 | public String getRequestPath() {
80 | return requestDetails.getRequestPath();
81 | }
82 |
83 | public RequestTypeEnum getRequestType() {
84 | return requestDetails.getRequestType();
85 | }
86 |
87 | public String getResourceName() {
88 | return requestDetails.getResourceName();
89 | }
90 |
91 | public RestOperationTypeEnum getRestOperationType() {
92 | return requestDetails.getRestOperationType();
93 | }
94 |
95 | public String getSecondaryOperation() {
96 | return requestDetails.getSecondaryOperation();
97 | }
98 |
99 | public boolean isRespondGzip() {
100 | return requestDetails.isRespondGzip();
101 | }
102 |
103 | public byte[] loadRequestContents() {
104 | return requestDetails.loadRequestContents();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/AccessChecker.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | /**
19 | * The main interface for deciding whether to grant access to a request or not. Implementations of
20 | * this do not have to be thread-safe as it is guaranteed by the server code not to call {@code
21 | * checkAccess} concurrently.
22 | */
23 | public interface AccessChecker {
24 |
25 | /**
26 | * Checks whether the current user has access to requested resources.
27 | *
28 | * @param requestDetails details about the resource and operation requested
29 | * @return the outcome of access checking
30 | */
31 | AccessDecision checkAccess(RequestDetailsReader requestDetails);
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import ca.uhn.fhir.context.FhirContext;
19 | import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
20 | import com.auth0.jwt.interfaces.DecodedJWT;
21 | import com.google.fhir.gateway.HttpFhirClient;
22 |
23 | /**
24 | * The factory for creating {@link AccessChecker} instances. A single instance of this might be used
25 | * for multiple queries; this is expected to be thread-safe.
26 | */
27 | public interface AccessCheckerFactory {
28 |
29 | /**
30 | * Creates an AccessChecker for a given FHIR store and JWT. Note the scope of this is for a single
31 | * access token, i.e., one instance is created for each request.
32 | *
33 | * @param jwt the access token in the JWT format; after being validated and decoded.
34 | * @param httpFhirClient the client to use for accessing the FHIR store.
35 | * @param fhirContext the FhirContext object that can be used for creating other HAPI FHIR
36 | * objects. This is an expensive object and should not be recreated for each access check.
37 | * @param patientFinder the utility class for finding patient IDs in query parameters/resources.
38 | * @return an AccessChecker; should never be {@code null}.
39 | * @throws AuthenticationException if an AccessChecker cannot be created for the given token; this
40 | * is where AccessChecker specific errors can be communicated to the user.
41 | */
42 | AccessChecker create(
43 | DecodedJWT jwt,
44 | HttpFhirClient httpFhirClient,
45 | FhirContext fhirContext,
46 | PatientFinder patientFinder)
47 | throws AuthenticationException;
48 | }
49 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import java.io.IOException;
19 | import javax.annotation.Nullable;
20 | import org.apache.http.HttpResponse;
21 |
22 | public interface AccessDecision {
23 |
24 | /**
25 | * @return true iff access was granted.
26 | */
27 | boolean canAccess();
28 |
29 | /**
30 | * Allows the incoming request mutation based on the access decision.
31 | *
32 | * Response is used to mutate the incoming request before executing the FHIR operation. We
33 | * currently only support query parameters update for GET Http method. This is expected to be
34 | * called after checking the access using @canAccess method. Mutating the request before checking
35 | * access can have side effect of wrong access check.
36 | *
37 | * @param requestDetailsReader details about the resource and operation requested
38 | * @return mutation to be applied on the incoming request or null if no mutation required
39 | */
40 | @Nullable
41 | RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader);
42 |
43 | /**
44 | * Depending on the outcome of the FHIR operations, this does any post-processing operations that
45 | * are related to access policies. This is expected to be called only if the actual FHIR operation
46 | * is finished successfully.
47 | *
48 | *
An example of this is when a new patient is created as the result of the query and that
49 | * patient ID should be added to some access lists.
50 | *
51 | * @param request the client to server request details
52 | * @param response the response returned from the FHIR store
53 | * @return the response entity content (with any post-processing modifications needed) if this
54 | * reads the response; otherwise null. Note that we should try to avoid reading the whole
55 | * content in memory whenever it is not needed for post-processing.
56 | */
57 | String postProcess(RequestDetailsReader request, HttpResponse response) throws IOException;
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import org.apache.http.HttpResponse;
19 |
20 | public final class NoOpAccessDecision implements AccessDecision {
21 |
22 | private final boolean accessGranted;
23 |
24 | public NoOpAccessDecision(boolean accessGranted) {
25 | this.accessGranted = accessGranted;
26 | }
27 |
28 | @Override
29 | public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) {
30 | return null;
31 | }
32 |
33 | @Override
34 | public boolean canAccess() {
35 | return accessGranted;
36 | }
37 |
38 | @Override
39 | public String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) {
40 | return null;
41 | }
42 |
43 | public static AccessDecision accessGranted() {
44 | return new NoOpAccessDecision(true);
45 | }
46 |
47 | public static AccessDecision accessDenied() {
48 | return new NoOpAccessDecision(false);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/PatientFinder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2024 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
19 | import com.google.fhir.gateway.BundlePatients;
20 | import java.util.Set;
21 | import org.hl7.fhir.r4.model.Bundle;
22 |
23 | public interface PatientFinder {
24 | /**
25 | * Finds the patient ID from the query if it is a direct Patient fetch (i.e., /Patient/PID) or the
26 | * patient can be inferred from query parameters.
27 | *
28 | * @param requestDetails the request
29 | * @return the ids of the patients that this query belongs to or an empty set if it cannot be
30 | * inferred (never null).
31 | * @throws InvalidRequestException for various reasons when unexpected parameters or content are
32 | * encountered. Callers are expected to deny access when this happens.
33 | */
34 | // TODO add @NotNull once we decide on null-check tooling.
35 | Set findPatientsFromParams(RequestDetailsReader requestDetails);
36 |
37 | /**
38 | * Find all patients referenced or updated in a Bundle.
39 | *
40 | * @param bundle bundle request to find patient references in.
41 | * @return the {@link BundlePatients} that wraps all found patients.
42 | * @throws InvalidRequestException for various reasons when unexpected content is encountered.
43 | * Callers are expected to deny access when this happens.
44 | */
45 | BundlePatients findPatientsInBundle(Bundle bundle);
46 |
47 | /**
48 | * Finds all patients in the content of a request.
49 | *
50 | * @param request that is expected to have a Bundle content.
51 | * @return the {@link BundlePatients} that wraps all found patients.
52 | * @throws InvalidRequestException for various reasons when unexpected content is encountered.
53 | * Callers are expected to deny access when this happens.
54 | */
55 | Set findPatientsInResource(RequestDetailsReader request);
56 |
57 | /**
58 | * Finds all patients in the body of a patch request
59 | *
60 | * @param request that is expected to have a body with a patch
61 | * @param resourceName the FHIR resource being patched
62 | * @return the set of patient ids in the patch
63 | * @throws InvalidRequestException for various reasons when unexpected content is encountered.
64 | * Callers are expected to deny access when this happens.
65 | */
66 | Set findPatientsInPatch(RequestDetailsReader request, String resourceName);
67 | }
68 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/RequestDetailsReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import ca.uhn.fhir.context.FhirContext;
19 | import ca.uhn.fhir.rest.api.RequestTypeEnum;
20 | import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
21 | import java.nio.charset.Charset;
22 | import java.util.List;
23 | import java.util.Map;
24 | import org.hl7.fhir.instance.model.api.IIdType;
25 |
26 | /**
27 | * This is mostly a wrapper for {@link ca.uhn.fhir.rest.api.server.RequestDetails} exposing an
28 | * immutable subset of its API; there are minor exceptions like {@code loadRequestContents}. The
29 | * method names are preserved; for documentation see {@code RequestDetails}.
30 | */
31 | public interface RequestDetailsReader {
32 |
33 | String getRequestId();
34 |
35 | Charset getCharset();
36 |
37 | String getCompleteUrl();
38 |
39 | FhirContext getFhirContext();
40 |
41 | String getFhirServerBase();
42 |
43 | String getHeader(String name);
44 |
45 | List getHeaders(String name);
46 |
47 | IIdType getId();
48 |
49 | String getOperation();
50 |
51 | Map getParameters();
52 |
53 | String getRequestPath();
54 |
55 | RequestTypeEnum getRequestType();
56 |
57 | String getResourceName();
58 |
59 | RestOperationTypeEnum getRestOperationType();
60 |
61 | String getSecondaryOperation();
62 |
63 | boolean isRespondGzip();
64 |
65 | byte[] loadRequestContents();
66 | }
67 |
--------------------------------------------------------------------------------
/server/src/main/java/com/google/fhir/gateway/interfaces/RequestMutation.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2024 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway.interfaces;
17 |
18 | import java.util.ArrayList;
19 | import java.util.HashMap;
20 | import java.util.List;
21 | import java.util.Map;
22 | import lombok.Builder;
23 | import lombok.Getter;
24 |
25 | /** Defines mutations that can be applied to the incoming request by an {@link AccessChecker}. */
26 | @Builder
27 | @Getter
28 | public class RequestMutation {
29 |
30 | // Additional query parameters and list of values for a parameter that should be added to the
31 | // outgoing FHIR request.
32 | // New values overwrites the old one if there is a conflict for a request param (i.e. a returned
33 | // parameter in RequestMutation is already present in the original request).
34 | // Old parameter values should be explicitly retained while mutating values for that parameter.
35 | @Builder.Default Map> additionalQueryParams = new HashMap<>();
36 |
37 | // Query parameters that are no longer needed when forwarding the request to the upstream server
38 | // Parameters with the keys in this list will be removed
39 | @Builder.Default List discardQueryParams = new ArrayList<>();
40 | }
41 |
--------------------------------------------------------------------------------
/server/src/main/resources/README.md:
--------------------------------------------------------------------------------
1 | # Description of resource files
2 |
3 | - `CompartmentDefinition-patient.json`: This is from the FHIR specification. It
4 | can be fetched on-the-fly when the proxy runs. However, given the importance
5 | of this resource and to make things simpler, it is downloaded and made
6 | available statically:
7 |
8 | ```shell
9 | $ curl -X GET -L -H "Accept: application/fhir+json" \
10 | http://hl7.org/fhir/CompartmentDefinition/patient \
11 | -o CompartmentDefinition-patient.json
12 | ```
13 |
14 | **NOTE**: We have also made changes to this file due to
15 | [b/215051963](b/215051963).
16 |
17 | - `patient_paths.json`: For each FHIR resource, this file has the corresponding
18 | mapping of the list of FHIR paths that should be searched for finding patients
19 | in that resource. This is used for access control of POST and PUT requests
20 | where a resource is provided by the client (see [b/209207333](b/209207333)).
21 |
22 | - `logback.xml`: The Logback configuration
23 |
--------------------------------------------------------------------------------
/server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 | INFO
23 |
24 |
25 | %d{HH:mm:ss.SSS} [%thread] %-5level %-40logger{40} [%file:%line] %msg%n
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/server/src/main/resources/patient_paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "Account": [
3 | "subject"
4 | ],
5 | "AdverseEvent": [
6 | "subject"
7 | ],
8 | "AllergyIntolerance": [
9 | "patient",
10 | "recorder",
11 | "asserter"
12 | ],
13 | "Appointment": [
14 | "participant.actor"
15 | ],
16 | "AppointmentResponse": [
17 | "actor"
18 | ],
19 | "AuditEvent": [
20 | "agent.who"
21 | ],
22 | "Basic": [
23 | "subject",
24 | "author"
25 | ],
26 | "BodyStructure": [
27 | "patient"
28 | ],
29 | "CarePlan": [
30 | "subject",
31 | "activity.detail.performer"
32 | ],
33 | "CareTeam": [
34 | "subject",
35 | "participant.member"
36 | ],
37 | "ChargeItem": [
38 | "subject"
39 | ],
40 | "Claim": [
41 | "patient",
42 | "payee.party"
43 | ],
44 | "ClaimResponse": [
45 | "patient"
46 | ],
47 | "ClinicalImpression": [
48 | "subject"
49 | ],
50 | "Communication": [
51 | "subject",
52 | "sender",
53 | "recipient"
54 | ],
55 | "CommunicationRequest": [
56 | "subject",
57 | "sender",
58 | "recipient",
59 | "requester"
60 | ],
61 | "Composition": [
62 | "subject",
63 | "author",
64 | "attester.party"
65 | ],
66 | "Condition": [
67 | "subject",
68 | "asserter"
69 | ],
70 | "Consent": [
71 | "patient"
72 | ],
73 | "Coverage": [
74 | "policyHolder",
75 | "subscriber",
76 | "beneficiary",
77 | "payor"
78 | ],
79 | "CoverageEligibilityRequest": [
80 | "patient"
81 | ],
82 | "CoverageEligibilityResponse": [
83 | "patient"
84 | ],
85 | "DetectedIssue": [
86 | "patient"
87 | ],
88 | "DeviceRequest": [
89 | "subject",
90 | "performer"
91 | ],
92 | "DeviceUseStatement": [
93 | "subject"
94 | ],
95 | "DiagnosticReport": [
96 | "subject"
97 | ],
98 | "DocumentManifest": [
99 | "subject",
100 | "author",
101 | "recipient"
102 | ],
103 | "DocumentReference": [
104 | "subject",
105 | "author"
106 | ],
107 | "Encounter": [
108 | "subject"
109 | ],
110 | "EnrollmentRequest": [
111 | "candidate"
112 | ],
113 | "EpisodeOfCare": [
114 | "patient"
115 | ],
116 | "ExplanationOfBenefit": [
117 | "patient",
118 | "payee.party"
119 | ],
120 | "FamilyMemberHistory": [
121 | "patient"
122 | ],
123 | "Flag": [
124 | "subject"
125 | ],
126 | "Goal": [
127 | "subject"
128 | ],
129 | "Group": [
130 | "member.entity"
131 | ],
132 | "ImagingStudy": [
133 | "subject"
134 | ],
135 | "Immunization": [
136 | "patient"
137 | ],
138 | "ImmunizationEvaluation": [
139 | "patient"
140 | ],
141 | "ImmunizationRecommendation": [
142 | "patient"
143 | ],
144 | "Invoice": [
145 | "subject",
146 | "recipient"
147 | ],
148 | "List": [
149 | "subject",
150 | "source"
151 | ],
152 | "MeasureReport": [
153 | "subject"
154 | ],
155 | "Media": [
156 | "subject"
157 | ],
158 | "MedicationAdministration": [
159 | "performer.actor",
160 | "subject"
161 | ],
162 | "MedicationDispense": [
163 | "subject",
164 | "receiver"
165 | ],
166 | "MedicationRequest": [
167 | "subject"
168 | ],
169 | "MedicationStatement": [
170 | "subject"
171 | ],
172 | "MolecularSequence": [
173 | "patient"
174 | ],
175 | "NutritionOrder": [
176 | "patient"
177 | ],
178 | "Observation": [
179 | "subject",
180 | "patient",
181 | "performer"
182 | ],
183 | "Person": [
184 | "link.target"
185 | ],
186 | "Procedure": [
187 | "subject",
188 | "performer.actor"
189 | ],
190 | "Provenance": [
191 | "target"
192 | ],
193 | "QuestionnaireResponse": [
194 | "subject",
195 | "author"
196 | ],
197 | "RelatedPerson": [
198 | "patient"
199 | ],
200 | "RequestGroup": [
201 | "subject",
202 | "action.participant"
203 | ],
204 | "ResearchSubject": [
205 | "individual"
206 | ],
207 | "RiskAssessment": [
208 | "subject"
209 | ],
210 | "Schedule": [
211 | "actor"
212 | ],
213 | "ServiceRequest": [
214 | "subject",
215 | "performer"
216 | ],
217 | "Specimen": [
218 | "subject"
219 | ],
220 | "SupplyDelivery": [
221 | "patient"
222 | ],
223 | "SupplyRequest": [
224 | "deliverTo"
225 | ],
226 | "VisionPrescription": [
227 | "patient"
228 | ]
229 | }
230 |
--------------------------------------------------------------------------------
/server/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
18 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/server/src/test/java/com/google/fhir/gateway/FhirUtilTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway;
17 |
18 | import static org.hamcrest.MatcherAssert.assertThat;
19 | import static org.hamcrest.Matchers.equalTo;
20 | import static org.mockito.Mockito.mock;
21 | import static org.mockito.Mockito.when;
22 |
23 | import ca.uhn.fhir.context.FhirContext;
24 | import com.google.common.io.Resources;
25 | import com.google.fhir.gateway.interfaces.RequestDetailsReader;
26 | import java.io.IOException;
27 | import java.net.URL;
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 | import org.mockito.junit.MockitoJUnitRunner;
31 |
32 | @RunWith(MockitoJUnitRunner.class)
33 | public class FhirUtilTest {
34 |
35 | private static final FhirContext fhirContext = FhirContext.forR4();
36 |
37 | @Test
38 | public void isValidIdPass() {
39 | assertThat(FhirUtil.isValidId("simple-id"), equalTo(true));
40 | }
41 |
42 | @Test
43 | public void isValidIdDotPass() {
44 | assertThat(FhirUtil.isValidId("id.with.dots"), equalTo(true));
45 | }
46 |
47 | @Test
48 | public void isValidIdUnderscoreFails() {
49 | assertThat(FhirUtil.isValidId("id_with_underscore"), equalTo(false));
50 | }
51 |
52 | @Test
53 | public void isValidIdPercentFails() {
54 | assertThat(FhirUtil.isValidId("id-with-%"), equalTo(false));
55 | }
56 |
57 | @Test
58 | public void isValidIdWithNumbersPass() {
59 | assertThat(FhirUtil.isValidId("id-with-numbers-892-12"), equalTo(true));
60 | }
61 |
62 | @Test
63 | public void isValidIdSlashFails() {
64 | assertThat(FhirUtil.isValidId("id-with-//"), equalTo(false));
65 | }
66 |
67 | @Test
68 | public void isValidIdTooShort() {
69 | assertThat(FhirUtil.isValidId(""), equalTo(false));
70 | }
71 |
72 | @Test
73 | public void isValidIdTooLong() {
74 | assertThat(
75 | FhirUtil.isValidId(
76 | "too-long-id-0123456789012345678901234567890123456789012345678901234567890123456789"),
77 | equalTo(false));
78 | }
79 |
80 | @Test
81 | public void isValidIdNull() {
82 | assertThat(FhirUtil.isValidId(""), equalTo(false));
83 | }
84 |
85 | @Test
86 | public void canParseValidBundle() throws IOException {
87 | URL bundleUrl = Resources.getResource("patient_id_search.json");
88 | byte[] bundleBytes = Resources.toByteArray(bundleUrl);
89 | RequestDetailsReader requestMock = mock(RequestDetailsReader.class);
90 | when(requestMock.loadRequestContents()).thenReturn(bundleBytes);
91 | assertThat(
92 | FhirUtil.parseRequestToBundle(fhirContext, requestMock).getEntry().size(), equalTo(10));
93 | }
94 |
95 | @Test(expected = IllegalArgumentException.class)
96 | public void throwExceptionOnNonBundleResource() throws IOException {
97 | URL bundleUrl = Resources.getResource("test_patient.json");
98 | byte[] bundleBytes = Resources.toByteArray(bundleUrl);
99 | RequestDetailsReader requestMock = mock(RequestDetailsReader.class);
100 | when(requestMock.loadRequestContents()).thenReturn(bundleBytes);
101 | FhirUtil.parseRequestToBundle(fhirContext, requestMock).getEntry().size();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/server/src/test/java/com/google/fhir/gateway/GcpFhirClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway;
17 |
18 | import static org.hamcrest.MatcherAssert.assertThat;
19 | import static org.hamcrest.Matchers.equalTo;
20 |
21 | import com.google.auth.oauth2.AccessToken;
22 | import com.google.auth.oauth2.GoogleCredentials;
23 | import java.io.IOException;
24 | import java.net.URI;
25 | import java.net.URISyntaxException;
26 | import java.util.List;
27 | import org.apache.http.Header;
28 | import org.apache.http.HttpResponse;
29 | import org.apache.http.message.BasicHeader;
30 | import org.junit.Before;
31 | import org.junit.Test;
32 | import org.junit.runner.RunWith;
33 | import org.mockito.Mock;
34 | import org.mockito.Mockito;
35 | import org.mockito.junit.MockitoJUnitRunner;
36 |
37 | @RunWith(MockitoJUnitRunner.class)
38 | public class GcpFhirClientTest {
39 |
40 | private GcpFhirClient gcpFhirClient;
41 |
42 | private final GoogleCredentials mockCredential =
43 | GoogleCredentials.create(new AccessToken("complicatedCode", null));
44 |
45 | @Mock private HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class);
46 |
47 | @Before
48 | public void setUp() throws IOException {
49 | gcpFhirClient = new GcpFhirClient("test", mockCredential);
50 | Header[] mockHeader = {
51 | new BasicHeader("LAST-MODIFIED", "today"),
52 | new BasicHeader("date", "yesterday"),
53 | new BasicHeader("keep", "no")
54 | };
55 | Mockito.when(fhirResponseMock.getAllHeaders()).thenReturn(mockHeader);
56 | }
57 |
58 | @Test
59 | public void getHeaderTest() {
60 | Header header = gcpFhirClient.getAuthHeader();
61 | assertThat(header.getElements().length, equalTo(1));
62 | assertThat(header.getElements()[0].getName(), equalTo("Bearer complicatedCode"));
63 | }
64 |
65 | @Test
66 | public void getUriForResourceTest() throws URISyntaxException {
67 | URI uri = gcpFhirClient.getUriForResource("hello/world");
68 | assertThat(uri.toString(), equalTo("test/hello/world"));
69 | }
70 |
71 | @Test
72 | public void responseHeadersToKeepTest() {
73 | List headersToKeep = gcpFhirClient.responseHeadersToKeep(fhirResponseMock);
74 | assertThat(headersToKeep.size(), equalTo(2));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/server/src/test/java/com/google/fhir/gateway/GenericFhirClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway;
17 |
18 | import static org.hamcrest.MatcherAssert.assertThat;
19 | import static org.hamcrest.Matchers.equalTo;
20 |
21 | import com.google.fhir.gateway.GenericFhirClient.GenericFhirClientBuilder;
22 | import java.net.URI;
23 | import java.net.URISyntaxException;
24 | import org.apache.http.Header;
25 | import org.junit.Test;
26 | import org.junit.runner.RunWith;
27 | import org.mockito.junit.MockitoJUnitRunner;
28 |
29 | @RunWith(MockitoJUnitRunner.class)
30 | public class GenericFhirClientTest {
31 |
32 | @Test(expected = IllegalArgumentException.class)
33 | public void buildGenericFhirClientFhirStoreNotSetTest() {
34 | new GenericFhirClientBuilder().build();
35 | }
36 |
37 | @Test(expected = IllegalArgumentException.class)
38 | public void buildGenericFhirClientNoFhirStoreBlankTest() {
39 | new GenericFhirClientBuilder().setFhirStore(" ").build();
40 | }
41 |
42 | @Test
43 | public void getAuthHeaderNoUsernamePasswordTest() {
44 | GenericFhirClient genericFhirClient =
45 | new GenericFhirClientBuilder().setFhirStore("random.fhir").build();
46 | Header header = genericFhirClient.getAuthHeader();
47 | assertThat(header.getName(), equalTo("Authorization"));
48 | assertThat(header.getValue(), equalTo(""));
49 | }
50 |
51 | @Test
52 | public void getUriForResourceTest() throws URISyntaxException {
53 | GenericFhirClient genericFhirClient =
54 | new GenericFhirClientBuilder().setFhirStore("random.fhir").build();
55 | URI uri = genericFhirClient.getUriForResource("hello/world");
56 | assertThat(uri.toString(), equalTo("random.fhir/hello/world"));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/src/test/java/com/google/fhir/gateway/HttpFhirClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.fhir.gateway;
17 |
18 | import static org.hamcrest.MatcherAssert.assertThat;
19 | import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
20 | import static org.hamcrest.Matchers.containsInAnyOrder;
21 | import static org.hamcrest.Matchers.empty;
22 | import static org.hamcrest.Matchers.nullValue;
23 | import static org.mockito.Mockito.when;
24 |
25 | import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
26 | import java.util.Arrays;
27 | import java.util.HashMap;
28 | import java.util.List;
29 | import java.util.Map;
30 | import org.apache.http.Header;
31 | import org.apache.http.HttpResponse;
32 | import org.apache.http.client.methods.RequestBuilder;
33 | import org.apache.http.message.BasicHeader;
34 | import org.junit.Test;
35 | import org.junit.runner.RunWith;
36 | import org.mockito.Mock;
37 | import org.mockito.Spy;
38 | import org.mockito.junit.MockitoJUnitRunner;
39 |
40 | @RunWith(MockitoJUnitRunner.class)
41 | public class HttpFhirClientTest {
42 |
43 | @Spy private HttpFhirClient fhirClient;
44 |
45 | @Mock private ServletRequestDetails requestMock;
46 |
47 | @Mock private HttpResponse httpResponse;
48 |
49 | @Test
50 | public void copyRequiredHeaders_passAllowedHeaders_addsToRequest() {
51 | Map> headers = new HashMap<>();
52 | String allowedRequestHeader = "etag";
53 | List headerValues = List.of("test-val-1", "test-val-2");
54 | headers.put(allowedRequestHeader, headerValues);
55 | when(requestMock.getHeaders()).thenReturn(headers);
56 | RequestBuilder requestBuilder = RequestBuilder.create("POST");
57 |
58 | fhirClient.copyRequiredHeaders(requestMock, requestBuilder);
59 |
60 | assertThat(
61 | Arrays.stream(requestBuilder.getHeaders(allowedRequestHeader))
62 | .map(header -> (BasicHeader) header)
63 | .map(BasicHeader::getValue)
64 | .toArray(),
65 | arrayContainingInAnyOrder("test-val-1", "test-val-2"));
66 | }
67 |
68 | @Test
69 | public void copyRequiredHeaders_passNotAllowedHeaders_emptyRequestHeaders() {
70 | Map> headers = new HashMap<>();
71 | String unsupportedHeader = "unsupported-header";
72 | headers.put(unsupportedHeader, List.of("test-val-1", "test-val-2"));
73 | when(requestMock.getHeaders()).thenReturn(headers);
74 | RequestBuilder requestBuilder = RequestBuilder.create("POST");
75 |
76 | fhirClient.copyRequiredHeaders(requestMock, requestBuilder);
77 |
78 | assertThat(requestBuilder.getHeaders(unsupportedHeader), nullValue());
79 | }
80 |
81 | @Test
82 | public void responseHeadersToKeep_addAllowedHeaders() {
83 | String allowedRequestHeader = "etag";
84 | Header[] headers = {
85 | new BasicHeader(allowedRequestHeader, "test-val-1"),
86 | new BasicHeader(allowedRequestHeader, "test-val-2")
87 | };
88 | when(httpResponse.getAllHeaders()).thenReturn(headers);
89 |
90 | List responseHeaders = fhirClient.responseHeadersToKeep(httpResponse);
91 |
92 | assertThat(responseHeaders, containsInAnyOrder(headers));
93 | }
94 |
95 | @Test
96 | public void responseHeadersToKeep_passNotAllowedHeaders_emptyRequestHeaders() {
97 | String unsupportedHeader = "unsupportedHeader";
98 | Header[] headers = {
99 | new BasicHeader(unsupportedHeader, "test-val-1"),
100 | new BasicHeader(unsupportedHeader, "test-val-2")
101 | };
102 | when(httpResponse.getAllHeaders()).thenReturn(headers);
103 |
104 | List