-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
433: BALP audit with oauth token support
Boris Stanojevic
committed
Feb 1, 2024
1 parent
7df3541
commit 68b7dfa
Showing
56 changed files
with
1,532 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
...starter/src/test/java/org/openehealth/ipf/boot/atna/IpfAtnaBalpAutoConfigurationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright 2017 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.openehealth.ipf.boot.atna; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.openehealth.ipf.commons.audit.AuditContext; | ||
import org.openehealth.ipf.commons.audit.BalpAuditContext; | ||
import org.openehealth.ipf.commons.audit.queue.AsynchronousAuditMessageQueue; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.test.context.ActiveProfiles; | ||
import org.springframework.test.context.junit.jupiter.SpringExtension; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertArrayEquals; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
|
||
/** | ||
* | ||
*/ | ||
@ExtendWith(SpringExtension.class) | ||
@SpringBootTest(classes = { TestApplication.class }) | ||
@ActiveProfiles("balp") | ||
public class IpfAtnaBalpAutoConfigurationTest { | ||
|
||
@Autowired | ||
private AuditContext auditContext; | ||
|
||
@Test | ||
public void testAtnaWithBalpSettings() throws Exception { | ||
assertTrue(auditContext instanceof BalpAuditContext); | ||
|
||
assertEquals("atna-test", auditContext.getAuditSourceId()); | ||
assertEquals("mysite", auditContext.getAuditEnterpriseSiteId()); | ||
assertEquals("localhost", auditContext.getAuditRepositoryHostName()); | ||
assertEquals(1342, auditContext.getAuditRepositoryPort()); | ||
assertEquals("FHIR-REST-TLS", auditContext.getAuditTransmissionProtocol().getTransportName()); | ||
assertTrue(auditContext.getAuditMessageQueue() instanceof AsynchronousAuditMessageQueue); | ||
|
||
assertEquals("fhir", ((BalpAuditContext)auditContext).getAuditRepositoryContextPath()); | ||
assertArrayEquals(new String[]{"cid","client-id","my-client-id-path"}, | ||
((BalpAuditContext) auditContext).getBalpJwtExtractorProperties().getClientIdPath()); | ||
assertArrayEquals(new String[]{}, | ||
((BalpAuditContext) auditContext).getBalpJwtExtractorProperties().getIdPath()); | ||
} | ||
|
||
} |
8 changes: 8 additions & 0 deletions
8
boot/ipf-atna-spring-boot-starter/src/test/resources/application-balp.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
ipf: | ||
atna: | ||
audit-repository-transport: FHIR-REST-TLS | ||
balp: | ||
audit-repository-context-path: fhir | ||
oauth: | ||
clientIdPath: cid,client-id,my-client-id-path | ||
idPath: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
commons/audit/src/main/java/org/openehealth/ipf/commons/audit/BalpAuditContext.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.audit; | ||
|
||
/** | ||
* @author Boris Stanojevic | ||
* @since 4.8 | ||
*/ | ||
public interface BalpAuditContext extends AuditContext { | ||
|
||
String getAuditRepositoryContextPath(); | ||
|
||
BalpJwtExtractorProperties getBalpJwtExtractorProperties(); | ||
} |
68 changes: 68 additions & 0 deletions
68
...ons/audit/src/main/java/org/openehealth/ipf/commons/audit/BalpJwtExtractorProperties.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.audit; | ||
|
||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
public class BalpJwtExtractorProperties { | ||
|
||
@Getter | ||
@Setter | ||
private String[] idPath = new String[]{"jti"}; | ||
|
||
@Getter @Setter | ||
private String[] issuerPath = new String[]{"iss"}; | ||
|
||
@Getter @Setter | ||
private String[] clientIdPath = new String[]{"client_id", "cid"}; | ||
|
||
@Getter @Setter | ||
private String[] subjectPath = new String[]{"sub"}; | ||
|
||
@Getter @Setter | ||
private String[] subjectNamePath = new String[]{"extensions:ihe_iua:subject_name"}; | ||
|
||
@Getter @Setter | ||
private String[] subjectOrganizationPath = new String[]{"extensions:ihe_iua:subject_organization"}; | ||
|
||
@Getter @Setter | ||
private String[] subjectOrganizationIdPath = new String[]{"extensions:ihe_iua:subject_organization_id"}; | ||
|
||
@Getter @Setter | ||
private String[] subjectRolePath = new String[]{"extensions:ihe_iua:subject_role"}; | ||
|
||
@Getter @Setter | ||
private String[] purposeOfUsePath = new String[]{"extensions:ihe_iua:purpose_of_use"}; | ||
|
||
@Getter @Setter | ||
private String[] homeCommunityIdPath = new String[]{"extensions:ihe_iua:home_community_id"}; | ||
|
||
@Getter @Setter | ||
private String[] nationalProviderIdPath = new String[]{"extensions:ihe_iua:national_provider_identifier"}; | ||
|
||
@Getter @Setter | ||
private String[] personIdPath = new String[]{"extensions:ihe_iua:person_id"}; | ||
|
||
@Getter @Setter | ||
private String[] patientIdPath = new String[]{"extensions:ihe_bppc:patient_id"}; | ||
|
||
@Getter @Setter | ||
private String[] docIdPath = new String[]{"extensions:ihe_bppc:doc_id"}; | ||
|
||
@Getter @Setter | ||
private String[] acpPath = new String[]{"extensions:ihe_bppc:acp"}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
commons/audit/src/main/java/org/openehealth/ipf/commons/audit/DefaultBalpAuditContext.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.audit; | ||
|
||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
public class DefaultBalpAuditContext extends DefaultAuditContext implements BalpAuditContext { | ||
|
||
@Getter | ||
@Setter | ||
private String auditRepositoryContextPath; | ||
|
||
@Getter | ||
@Setter | ||
private BalpJwtExtractorProperties balpJwtExtractorProperties = new BalpJwtExtractorProperties(); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
...src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/auth/BalpJwtClaimsExtractor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.auth; | ||
|
||
import com.nimbusds.jwt.JWT; | ||
import com.nimbusds.jwt.JWTClaimsSet; | ||
import lombok.Getter; | ||
import org.openehealth.ipf.commons.audit.BalpJwtExtractorProperties; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.text.ParseException; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.apache.commons.lang3.StringUtils.isNotBlank; | ||
|
||
public class BalpJwtClaimsExtractor { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(BalpJwtClaimsExtractor.class); | ||
|
||
public Optional<String> extractId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getIdPath())); | ||
} | ||
|
||
public Optional<String> extractClientId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getClientIdPath())); | ||
} | ||
|
||
public Optional<String> extractSubject(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getSubjectPath())); | ||
} | ||
|
||
public Optional<String> extractIssuer(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getIssuerPath())); | ||
} | ||
|
||
public Optional<String> extractSubjectName(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getSubjectNamePath())); | ||
} | ||
|
||
public Optional<String> extractSubjectOrganization(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getSubjectOrganizationPath())); | ||
} | ||
|
||
public Optional<String> extractSubjectOrganizationId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getSubjectOrganizationIdPath())); | ||
} | ||
|
||
public Optional<Set<String>> extractSubjectRole(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractListClaimFromJWT(jwt, balpJwtExtractorProperties.getSubjectRolePath())); | ||
} | ||
|
||
public Optional<Set<String>> extractPurposeOfUse(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractListClaimFromJWT(jwt, balpJwtExtractorProperties.getPurposeOfUsePath())); | ||
} | ||
|
||
public Optional<String> extractHomeCommunityId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getHomeCommunityIdPath())); | ||
} | ||
|
||
public Optional<String> extractNationalProviderIdentifier(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getNationalProviderIdPath())); | ||
} | ||
|
||
public Optional<String> extractPersonId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getPersonIdPath())); | ||
} | ||
|
||
public Optional<String> extractBppcPatientId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getPatientIdPath())); | ||
} | ||
|
||
public Optional<String> extractBppcDocId(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getDocIdPath())); | ||
} | ||
|
||
public Optional<String> extractBppcAcp(JWT jwt, BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
return Optional.ofNullable(extractStringClaimFromJWT(jwt, balpJwtExtractorProperties.getAcpPath())); | ||
} | ||
|
||
private String extractStringClaimFromJWT(JWT jwt, String[] expressions){ | ||
Optional<ClaimSetPair> finalClaimForExpression = getFinalClaimSet(jwt, expressions); | ||
if (finalClaimForExpression.isPresent()) { | ||
JWTClaimsSet claimsSet = finalClaimForExpression.get().getJwtClaimsSet(); | ||
String expression = finalClaimForExpression.get().getExpression(); | ||
try { | ||
return claimsSet.getStringClaim(expression); | ||
} catch (ParseException pe) { | ||
LOG.warn("Not string claims present for expression key '" + expression + "'", pe); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private Set<String> extractListClaimFromJWT(JWT jwt, String[] expressions){ | ||
Optional<ClaimSetPair> finalClaimForExpression = getFinalClaimSet(jwt, expressions); | ||
if (finalClaimForExpression.isPresent()) { | ||
JWTClaimsSet claimsSet = finalClaimForExpression.get().getJwtClaimsSet(); | ||
String expression = finalClaimForExpression.get().getExpression(); | ||
try { | ||
List<Object> values = claimsSet.getListClaim(expression); | ||
if (values != null && !values.isEmpty()) { | ||
return values.stream().map(Objects::toString).collect(Collectors.toSet()); | ||
} | ||
} catch (ParseException pe) { | ||
LOG.warn("Not list claims present for expression key '" + expression + "'", pe); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private Optional<ClaimSetPair> getFinalClaimSet(JWT jwt, String[] expressions) { | ||
if (expressions == null) { | ||
return Optional.empty(); | ||
} | ||
for (var expression: expressions) { | ||
try { | ||
if (expression.contains(":")) { | ||
JWTClaimsSet extracted = jwt.getJWTClaimsSet(); | ||
List<String> structure = List.of(expression.split("\\:")); | ||
Iterator<String> structureIterator = structure.listIterator(); | ||
String subExpression = null; | ||
while (structureIterator.hasNext()) { | ||
subExpression = structureIterator.next(); | ||
if (structureIterator.hasNext()) { | ||
if (!containsClaim(extracted, subExpression)) { | ||
break; | ||
} | ||
extracted = JWTClaimsSet.parse(extracted.getJSONObjectClaim(subExpression)); | ||
} | ||
} | ||
if (extracted != null && isNotBlank(subExpression) && | ||
extracted.getClaims().containsKey(subExpression)) { | ||
return Optional.of(new ClaimSetPair(subExpression, extracted)); | ||
} | ||
} else { | ||
if (jwt.getJWTClaimsSet().getClaims().containsKey(expression)) { | ||
return Optional.of(new ClaimSetPair(expression, jwt.getJWTClaimsSet())); | ||
} | ||
} | ||
} catch (ParseException pe) { | ||
LOG.debug("Not claimset present for expression key: " + pe.getMessage()); | ||
} | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
private boolean containsClaim(JWTClaimsSet claimsSet, String name) { | ||
return claimsSet.getClaim(name) != null; | ||
} | ||
|
||
private static final class ClaimSetPair { | ||
@Getter | ||
private final String expression; | ||
@Getter | ||
private final JWTClaimsSet jwtClaimsSet; | ||
|
||
public ClaimSetPair(String expression, JWTClaimsSet jwtClaimsSet) { | ||
this.expression = expression; | ||
this.jwtClaimsSet = jwtClaimsSet; | ||
} | ||
} | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
...ir/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/auth/BalpJwtDataSet.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.auth; | ||
|
||
import lombok.Data; | ||
|
||
import java.util.Set; | ||
|
||
@Data | ||
public class BalpJwtDataSet { | ||
|
||
String opaqueJwt; | ||
String issuer; | ||
String subject; | ||
String audience; | ||
String jwtId; | ||
String clientId; | ||
String iheIuaSubjectName; | ||
String iheIuaSubjectOrganization; | ||
String iheIuaSubjectOrganizationId; | ||
Set<String> iheIuaSubjectRole; | ||
Set<String> iheIuaPurposeOfUse; | ||
String iheIuaHomeCommunityId; | ||
String iheIuaNationalProviderIdentifier; | ||
String iheIuaPersonId; | ||
String iheBppcPatientId; | ||
String iheBppcDocId; | ||
String iheBppcAcp; | ||
} |
68 changes: 68 additions & 0 deletions
68
...hir/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/auth/BalpJwtParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.auth; | ||
|
||
import com.nimbusds.jwt.JWT; | ||
import com.nimbusds.jwt.JWTParser; | ||
import org.openehealth.ipf.commons.audit.BalpJwtExtractorProperties; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.text.ParseException; | ||
import java.util.Optional; | ||
|
||
import static org.apache.commons.lang3.StringUtils.isBlank; | ||
|
||
public class BalpJwtParser { | ||
|
||
private static final BalpJwtClaimsExtractor claimsExtractor = new BalpJwtClaimsExtractor(); | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(BalpJwtParser.class); | ||
|
||
public static Optional<BalpJwtDataSet> parseAuthorizationToBalpDataSet(String authenticationHeader, | ||
BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
Optional<JWT> jwt = parseAuthenticationToJWT(authenticationHeader); | ||
return jwt.map(value -> { | ||
BalpJwtDataSet balpJwtDataSet = parseJwtToBalpDataSet(value, claimsExtractor, balpJwtExtractorProperties); | ||
balpJwtDataSet.setOpaqueJwt(authenticationHeader.substring(authenticationHeader.length() - 32)); | ||
return balpJwtDataSet; | ||
}); | ||
} | ||
|
||
public static Optional<JWT> parseAuthenticationToJWT(String authenticationHeader) { | ||
if (isBlank(authenticationHeader) || | ||
!authenticationHeader.toLowerCase().startsWith("bearer ")) return Optional.empty(); | ||
|
||
String bearer = authenticationHeader.replaceAll("^[Bb][Ee][Aa][Rr][Ee][Rr][ ]+", ""); | ||
try { | ||
return Optional.of(JWTParser.parse(bearer)); | ||
} catch (ParseException pe) { | ||
LOG.debug("Invalid JWT token", pe); | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
public static BalpJwtDataSet parseJwtToBalpDataSet(JWT jwt, | ||
BalpJwtClaimsExtractor claimsExtractor, | ||
BalpJwtExtractorProperties balpJwtExtractorProperties) { | ||
BalpJwtDataSet balpJwtDataSet = new BalpJwtDataSet(); | ||
|
||
claimsExtractor.extractIssuer(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIssuer); | ||
claimsExtractor.extractId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setJwtId); | ||
claimsExtractor.extractClientId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setClientId); | ||
claimsExtractor.extractSubject(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setSubject); | ||
|
||
claimsExtractor.extractBppcAcp(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheBppcAcp); | ||
claimsExtractor.extractBppcDocId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheBppcDocId); | ||
claimsExtractor.extractBppcPatientId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheBppcPatientId); | ||
|
||
claimsExtractor.extractSubjectName(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaSubjectName); | ||
claimsExtractor.extractSubjectOrganization(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaSubjectOrganization); | ||
claimsExtractor.extractSubjectOrganizationId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaSubjectOrganizationId); | ||
claimsExtractor.extractSubjectRole(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaSubjectRole); | ||
claimsExtractor.extractHomeCommunityId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaHomeCommunityId); | ||
claimsExtractor.extractPurposeOfUse(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaPurposeOfUse); | ||
claimsExtractor.extractNationalProviderIdentifier(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaNationalProviderIdentifier); | ||
claimsExtractor.extractPersonId(jwt, balpJwtExtractorProperties).ifPresent(balpJwtDataSet::setIheIuaPersonId); | ||
|
||
return balpJwtDataSet; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
...ir/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/events/BalpJwtUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.events; | ||
|
||
import org.openehealth.ipf.commons.audit.AuditContext; | ||
import org.openehealth.ipf.commons.audit.BalpAuditContext; | ||
import org.openehealth.ipf.commons.audit.BalpJwtExtractorProperties; | ||
import org.openehealth.ipf.commons.audit.DefaultBalpAuditContext; | ||
import org.openehealth.ipf.commons.audit.event.BaseAuditMessageBuilder; | ||
import org.openehealth.ipf.commons.audit.model.ActiveParticipantType; | ||
import org.openehealth.ipf.commons.audit.types.ActiveParticipantRoleId; | ||
import org.openehealth.ipf.commons.audit.types.CodedValueType; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirAuditDataset; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.auth.BalpJwtDataSet; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.auth.BalpJwtParser; | ||
|
||
import java.util.Optional; | ||
|
||
import static org.apache.commons.lang3.StringUtils.isNotBlank; | ||
import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.DCM_SYSTEM_NAME; | ||
import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.OUSER_AGENT_PURPOSE_OF_USE_SYSTEM_NAME; | ||
import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.OUSER_AGENT_ROLE_SYSTEM_NAME; | ||
import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.OUSER_AGENT_TYPE_OPAQUE_SYSTEM_NAME; | ||
import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.OUSER_AGENT_TYPE_SYSTEM_NAME; | ||
|
||
public class BalpJwtUtils { | ||
|
||
private static final BalpJwtExtractorProperties DEFAULT_BALP_JWT_EXTRACTOR_PROPERTIES = new BalpJwtExtractorProperties(); | ||
|
||
public static <D extends BaseAuditMessageBuilder<D>> void addJwtParticipant(D delegate, | ||
FhirAuditDataset auditDataset, | ||
AuditContext auditContext) { | ||
BalpJwtExtractorProperties balpJwtExtractorProperties = (auditContext instanceof BalpAuditContext)? | ||
((BalpAuditContext)auditContext).getBalpJwtExtractorProperties() : DEFAULT_BALP_JWT_EXTRACTOR_PROPERTIES; | ||
Optional<BalpJwtDataSet> balpDataSet = BalpJwtParser.parseAuthorizationToBalpDataSet( | ||
auditDataset.getAuthorization(), balpJwtExtractorProperties); | ||
balpDataSet.ifPresent(dataSet -> { | ||
if (isNotBlank(dataSet.getJwtId())) { | ||
ActiveParticipantType ap = new ActiveParticipantType(dataSet.getSubject(), true); | ||
ap.getRoleIDCodes().add( | ||
ActiveParticipantRoleId.of(CodedValueType.of(dataSet.getJwtId(), | ||
OUSER_AGENT_TYPE_SYSTEM_NAME, "oAuth Token ID"))); | ||
ap.setUserName(dataSet.getIheIuaSubjectName()); | ||
if (isNotBlank(dataSet.getIssuer())) { | ||
ap.setAlternativeUserID(dataSet.getIssuer()); | ||
} | ||
if (dataSet.getIheIuaPurposeOfUse() != null && !dataSet.getIheIuaPurposeOfUse().isEmpty()) { | ||
dataSet.getIheIuaPurposeOfUse().forEach(purpose -> ap.getRoleIDCodes().add( | ||
ActiveParticipantRoleId.of(CodedValueType.of(purpose, | ||
OUSER_AGENT_PURPOSE_OF_USE_SYSTEM_NAME, "oAuth Token Purpose of Use")))); | ||
} | ||
if (dataSet.getIheIuaSubjectRole() != null && !dataSet.getIheIuaSubjectRole().isEmpty()) { | ||
dataSet.getIheIuaSubjectRole().forEach(role -> ap.getRoleIDCodes().add( | ||
ActiveParticipantRoleId.of(CodedValueType.of(role, | ||
OUSER_AGENT_ROLE_SYSTEM_NAME, "oAuth Token User Role")))); | ||
} | ||
delegate.addActiveParticipant(ap); | ||
if (isNotBlank(dataSet.getClientId())) { | ||
ActiveParticipantType clientAp = new ActiveParticipantType( | ||
dataSet.getClientId(), !auditDataset.isServerSide()); | ||
clientAp.getRoleIDCodes().add( | ||
ActiveParticipantRoleId.of(CodedValueType.of(dataSet.getClientId(), | ||
DCM_SYSTEM_NAME, "oAuth Token Client ID"))); | ||
delegate.addActiveParticipant(clientAp); | ||
} | ||
} else if (isNotBlank(dataSet.getOpaqueJwt())) { | ||
ActiveParticipantType ap = new ActiveParticipantType(dataSet.getSubject(), true); | ||
ap.getRoleIDCodes().add( | ||
ActiveParticipantRoleId.of(CodedValueType.of(dataSet.getOpaqueJwt(), | ||
OUSER_AGENT_TYPE_OPAQUE_SYSTEM_NAME, "oAuth Opaque Token"))); | ||
} | ||
}); | ||
} | ||
|
||
private BalpAuditContext balpAuditContext(AuditContext auditContext) { | ||
return auditContext instanceof BalpAuditContext? (BalpAuditContext) auditContext : null; | ||
} | ||
|
||
} |
47 changes: 47 additions & 0 deletions
47
...src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/events/BalpPHIExportBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.events; | ||
|
||
import org.openehealth.ipf.commons.audit.AuditContext; | ||
import org.openehealth.ipf.commons.audit.BalpAuditContext; | ||
import org.openehealth.ipf.commons.audit.codes.EventActionCode; | ||
import org.openehealth.ipf.commons.audit.codes.EventOutcomeIndicator; | ||
import org.openehealth.ipf.commons.audit.types.EventType; | ||
import org.openehealth.ipf.commons.audit.types.PurposeOfUse; | ||
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIExportBuilder; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirAuditDataset; | ||
|
||
import static org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpJwtUtils.addJwtParticipant; | ||
|
||
public class BalpPHIExportBuilder extends PHIExportBuilder<BalpPHIExportBuilder> { | ||
|
||
public BalpPHIExportBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public BalpPHIExportBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventActionCode eventActionCode, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventActionCode, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public BalpPHIExportBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventOutcomeIndicator eventOutcomeIndicator, | ||
String eventOutcomeDescription, | ||
EventActionCode eventActionCode, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventOutcomeIndicator, eventOutcomeDescription, eventActionCode, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public void addJwtId(FhirAuditDataset auditDataset) { | ||
addJwtParticipant(delegate, auditDataset, getAuditContext()); | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...src/main/java/org/openehealth/ipf/commons/ihe/fhir/audit/events/BalpPHIImportBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.events; | ||
|
||
import org.openehealth.ipf.commons.audit.AuditContext; | ||
import org.openehealth.ipf.commons.audit.codes.EventActionCode; | ||
import org.openehealth.ipf.commons.audit.codes.EventOutcomeIndicator; | ||
import org.openehealth.ipf.commons.audit.types.EventType; | ||
import org.openehealth.ipf.commons.audit.types.PurposeOfUse; | ||
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIImportBuilder; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirAuditDataset; | ||
|
||
import static org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpJwtUtils.addJwtParticipant; | ||
|
||
public class BalpPHIImportBuilder extends PHIImportBuilder<BalpPHIImportBuilder> { | ||
|
||
|
||
public BalpPHIImportBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public BalpPHIImportBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventActionCode eventActionCode, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventActionCode, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public BalpPHIImportBuilder(AuditContext auditContext, FhirAuditDataset auditDataset, EventOutcomeIndicator eventOutcomeIndicator, String eventOutcomeDescription, EventActionCode eventActionCode, EventType eventType, PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventOutcomeIndicator, eventOutcomeDescription, eventActionCode, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public void addJwtId(FhirAuditDataset auditDataset) { | ||
addJwtParticipant(delegate, auditDataset, getAuditContext()); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...n/java/org/openehealth/ipf/commons/ihe/fhir/audit/events/BalpQueryInformationBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.events; | ||
|
||
import org.openehealth.ipf.commons.audit.AuditContext; | ||
import org.openehealth.ipf.commons.audit.types.EventType; | ||
import org.openehealth.ipf.commons.audit.types.PurposeOfUse; | ||
import org.openehealth.ipf.commons.ihe.core.atna.event.QueryInformationBuilder; | ||
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirAuditDataset; | ||
|
||
import static org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpJwtUtils.addJwtParticipant; | ||
|
||
public class BalpQueryInformationBuilder extends QueryInformationBuilder<BalpQueryInformationBuilder> { | ||
|
||
public BalpQueryInformationBuilder(AuditContext auditContext, | ||
FhirAuditDataset auditDataset, | ||
EventType eventType, | ||
PurposeOfUse... purposesOfUse) { | ||
super(auditContext, auditDataset, eventType, purposesOfUse); | ||
addJwtId(auditDataset); | ||
} | ||
|
||
public void addJwtId(FhirAuditDataset auditDataset) { | ||
addJwtParticipant(delegate, auditDataset, getAuditContext()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...test/java/org/openehealth/ipf/commons/ihe/fhir/audit/auth/BalpJwtClaimsExtractorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.auth; | ||
|
||
import com.nimbusds.jwt.JWT; | ||
import com.nimbusds.jwt.JWTParser; | ||
import org.junit.jupiter.api.Test; | ||
import org.openehealth.ipf.commons.audit.BalpJwtExtractorProperties; | ||
|
||
import java.text.ParseException; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
public class BalpJwtClaimsExtractorTest { | ||
|
||
private final BalpJwtClaimsExtractor balpJwtClaimsExtractor = new BalpJwtClaimsExtractor(); | ||
private final BalpJwtExtractorProperties balpJwtExtractorProperties = new BalpJwtExtractorProperties(); | ||
|
||
private final BalpJwtGenerator balpJwtGenerator = new BalpJwtGenerator(); | ||
|
||
@Test | ||
void testExtractor() throws Exception { | ||
String generatedJwt = balpJwtGenerator.next(); | ||
|
||
JWT jwt = parseJWT(generatedJwt); | ||
assertNotNull(jwt); | ||
|
||
assertTrue(balpJwtClaimsExtractor.extractIssuer(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertEquals("https://localhost:8443/auth/realms/master", balpJwtClaimsExtractor.extractIssuer(jwt, balpJwtExtractorProperties).get()); | ||
|
||
assertTrue(balpJwtClaimsExtractor.extractId(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractSubject(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractClientId(jwt, balpJwtExtractorProperties).isPresent()); | ||
|
||
assertTrue(balpJwtClaimsExtractor.extractPersonId(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractHomeCommunityId(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractNationalProviderIdentifier(jwt, balpJwtExtractorProperties).isPresent()); | ||
|
||
assertTrue(balpJwtClaimsExtractor.extractSubjectName(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertEquals("Dr. John Smith", balpJwtClaimsExtractor.extractSubjectName(jwt, balpJwtExtractorProperties).get()); | ||
assertTrue(balpJwtClaimsExtractor.extractSubjectOrganization(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractSubjectOrganizationId(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractSubjectRole(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertEquals(2, balpJwtClaimsExtractor.extractSubjectRole(jwt, balpJwtExtractorProperties).get().size()); | ||
assertTrue(balpJwtClaimsExtractor.extractSubjectRole(jwt, balpJwtExtractorProperties).get().contains("my-role-1")); | ||
assertTrue(balpJwtClaimsExtractor.extractSubjectRole(jwt, balpJwtExtractorProperties).get().contains("my-role-2")); | ||
assertTrue(balpJwtClaimsExtractor.extractPurposeOfUse(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertEquals(2, balpJwtClaimsExtractor.extractPurposeOfUse(jwt, balpJwtExtractorProperties).get().size()); | ||
|
||
assertTrue(balpJwtClaimsExtractor.extractBppcAcp(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractBppcDocId(jwt, balpJwtExtractorProperties).isPresent()); | ||
assertTrue(balpJwtClaimsExtractor.extractBppcPatientId(jwt, balpJwtExtractorProperties).isPresent()); | ||
|
||
balpJwtExtractorProperties.setIssuerPath(new String[]{"blah"}); | ||
balpJwtExtractorProperties.setAcpPath(new String[]{"extensions:ihe_blah"}); | ||
assertTrue(balpJwtClaimsExtractor.extractIssuer(jwt, balpJwtExtractorProperties).isEmpty()); | ||
assertTrue(balpJwtClaimsExtractor.extractBppcAcp(jwt, balpJwtExtractorProperties).isEmpty()); | ||
|
||
balpJwtExtractorProperties.setAcpPath(null); | ||
assertTrue(balpJwtClaimsExtractor.extractBppcAcp(jwt, balpJwtExtractorProperties).isEmpty()); | ||
|
||
balpJwtExtractorProperties.setAcpPath(new String[]{""}); | ||
assertTrue(balpJwtClaimsExtractor.extractBppcAcp(jwt, balpJwtExtractorProperties).isEmpty()); | ||
|
||
} | ||
|
||
private JWT parseJWT(String jwt) { | ||
try { | ||
return JWTParser.parse(jwt); | ||
} catch (ParseException pe) { | ||
return null; | ||
} | ||
} | ||
|
||
private static final String jwtAsString = "{\n" + | ||
" \"aud\": \"master-realm\",\n" + | ||
" \"sub\": \"f7fc9091-7b8a-42e0-a829-6c4ba22d38b2\",\n" + | ||
" \"extensions\": {\n" + | ||
" \"ihe_iua\": {\n" + | ||
" \"subject_organization_id\": \"urn:oid:1.2.3.19161\",\n" + | ||
" \"home_community_id\": \"urn:oid:1.2.3.43740\",\n" + | ||
" \"national_provider_identifier\": \"urn:oid:1.2.3.48200\",\n" + | ||
" \"subject_role\": [\n" + | ||
" \"my-role-1\",\n" + | ||
" \"my-role-2\"\n" + | ||
" ],\n" + | ||
" \"purpose_of_use\": [\n" + | ||
" \"1.0.14265.1\",\n" + | ||
" \"1.0.14265.2\"\n" + | ||
" ],\n" + | ||
" \"subject_name\": \"Dr. John Smith\",\n" + | ||
" \"subject_organization\": \"Central Hospital\",\n" + | ||
" \"person_id\": \"ABC9586\"\n" + | ||
" },\n" + | ||
" \"ihe_bppc\": {\n" + | ||
" \"patient_id\": \"31494^^^&1.2.840.113619.6.197&ISO\",\n" + | ||
" \"doc_id\": \"urn:oid:1.2.3.29380\",\n" + | ||
" \"acp\": \"urn:oid:1.2.3.32574\"\n" + | ||
" }\n" + | ||
" },\n" + | ||
" \"nbf\": 1706531233,\n" + | ||
" \"iss\": \"https://localhost:8443/auth/realms/master\",\n" + | ||
" \"typ\": \"Bearer\",\n" + | ||
" \"exp\": 1706531353,\n" + | ||
" \"jti\": \"e2093a98-9dcd-4947-b5cb-ee5b47c089c5\",\n" + | ||
" \"client_id\": \"pbrBkyXksp\"\n" + | ||
"}"; | ||
} |
105 changes: 105 additions & 0 deletions
105
.../core/src/test/java/org/openehealth/ipf/commons/ihe/fhir/audit/auth/BalpJwtGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.auth; | ||
|
||
import com.nimbusds.jose.JOSEObjectType; | ||
import com.nimbusds.jose.JWSAlgorithm; | ||
import com.nimbusds.jose.JWSHeader; | ||
import com.nimbusds.jose.crypto.RSASSASigner; | ||
import com.nimbusds.jose.jwk.KeyUse; | ||
import com.nimbusds.jose.jwk.RSAKey; | ||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; | ||
import com.nimbusds.jwt.JWTClaimsSet; | ||
import com.nimbusds.jwt.SignedJWT; | ||
import net.java.quickcheck.Generator; | ||
import net.java.quickcheck.generator.PrimitiveGenerators; | ||
|
||
import java.time.Instant; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
|
||
public class BalpJwtGenerator implements Generator<String> { | ||
|
||
private static final Generator<String> strings = PrimitiveGenerators.letterStrings(10, 10); | ||
|
||
private static final Generator<Integer> integers = PrimitiveGenerators.integers(1000, 99999); | ||
|
||
@Override | ||
public String next() { | ||
try { | ||
RSAKey jwk = new RSAKeyGenerator(2048) | ||
.keyUse(KeyUse.SIGNATURE) | ||
.keyID(UUID.randomUUID().toString()) | ||
.generate(); | ||
|
||
SignedJWT signedJWT = new SignedJWT(jwsHeader(jwk), jwsClaimsSet()); | ||
signedJWT.sign(new RSASSASigner(jwk.toPrivateKey())); | ||
return signedJWT.serialize(); | ||
} catch (Exception e) { | ||
return null; | ||
} | ||
} | ||
|
||
public static String anyValidJwtWithRSAKey(RSAKey rsaKey){ | ||
try { | ||
SignedJWT signedJWT = new SignedJWT(jwsHeader(rsaKey), jwsClaimsSet()); | ||
signedJWT.sign(new RSASSASigner(rsaKey.toPrivateKey())); | ||
return signedJWT.serialize(); | ||
} catch (Exception e) { | ||
return null; | ||
} | ||
} | ||
|
||
private static JWSHeader jwsHeader(RSAKey rsaKey) { | ||
return new JWSHeader.Builder(JWSAlgorithm.RS256) | ||
.type(JOSEObjectType.JWT) | ||
.keyID(rsaKey.getKeyID()) | ||
.build(); | ||
} | ||
|
||
private static JWTClaimsSet jwsClaimsSet(){ | ||
return new JWTClaimsSet.Builder() | ||
.issuer("https://localhost:8443/auth/realms/master") | ||
.audience("master-realm") | ||
.subject(UUID.randomUUID().toString()) | ||
.jwtID(UUID.randomUUID().toString()) | ||
.claim("client_id", strings.next()) | ||
.claim("typ", "Bearer") | ||
.claim("extensions", jwsIheExtensions()) | ||
.notBeforeTime(Date.from(Instant.now())) | ||
.expirationTime(Date.from(Instant.now().plusSeconds(120))) | ||
.build(); | ||
} | ||
|
||
private static Map<String, Map<String, Object>> jwsIheExtensions() { | ||
Map<String, Map<String, Object>> extensions = new HashMap<>(); | ||
extensions.put("ihe_iua", jwsIheIuaExtensions()); | ||
extensions.put("ihe_bppc", jwsIheBppcExtensions()); | ||
|
||
return extensions; | ||
} | ||
|
||
private static Map<String, Object> jwsIheIuaExtensions() { | ||
Map<String, Object> iheIuaMap = new HashMap<>(); | ||
iheIuaMap.put("subject_name", "Dr. John Smith"); | ||
iheIuaMap.put("subject_organization", "Central Hospital"); | ||
iheIuaMap.put("subject_organization_id", "urn:oid:1.2.3." + integers.next()); | ||
iheIuaMap.put("subject_role", List.of("my-role-1", "my-role-2")); | ||
iheIuaMap.put("purpose_of_use", List.of("1.0.14265.1", "1.0.14265.2")); | ||
iheIuaMap.put("home_community_id", "urn:oid:1.2.3." + integers.next()); | ||
iheIuaMap.put("national_provider_identifier", "urn:oid:1.2.3." + integers.next()); | ||
iheIuaMap.put("person_id", "ABC" + integers.next()); | ||
return iheIuaMap; | ||
} | ||
|
||
private static Map<String, Object> jwsIheBppcExtensions() { | ||
Map<String, Object> bppcMap = new HashMap<>(); | ||
bppcMap.put("patient_id", integers.next() + "^^^&1.2.840.113619.6.197&ISO"); | ||
bppcMap.put("doc_id", "urn:oid:1.2.3." + integers.next()); | ||
bppcMap.put("acp", "urn:oid:1.2.3." + integers.next()); | ||
return bppcMap; | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
...core/src/test/java/org/openehealth/ipf/commons/ihe/fhir/audit/server/FhirAuditServer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.server; | ||
|
||
import ca.uhn.fhir.context.FhirContext; | ||
import ca.uhn.fhir.rest.annotation.Create; | ||
import ca.uhn.fhir.rest.annotation.Delete; | ||
import ca.uhn.fhir.rest.annotation.IdParam; | ||
import ca.uhn.fhir.rest.annotation.OptionalParam; | ||
import ca.uhn.fhir.rest.annotation.Read; | ||
import ca.uhn.fhir.rest.annotation.ResourceParam; | ||
import ca.uhn.fhir.rest.annotation.Search; | ||
import ca.uhn.fhir.rest.api.MethodOutcome; | ||
import ca.uhn.fhir.rest.param.TokenParam; | ||
import ca.uhn.fhir.rest.server.IResourceProvider; | ||
import ca.uhn.fhir.rest.server.RestfulServer; | ||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; | ||
import org.hl7.fhir.dstu3.model.Organization; | ||
import org.hl7.fhir.instance.model.api.IBaseResource; | ||
import org.hl7.fhir.r4.model.AuditEvent; | ||
import org.hl7.fhir.r4.model.IdType; | ||
import org.hl7.fhir.r4.model.ResourceType; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static org.apache.commons.lang3.StringUtils.isBlank; | ||
import static org.apache.commons.lang3.StringUtils.isNotBlank; | ||
|
||
public class FhirAuditServer extends RestfulServer implements IResourceProvider { | ||
|
||
private final Map<String, AuditEvent> auditEvents = Collections.synchronizedMap(new HashMap<>()); | ||
|
||
public FhirAuditServer() { | ||
setFhirContext(FhirContext.forR4()); | ||
setResourceProviders(this); | ||
} | ||
|
||
public List<AuditEvent> getAuditEvents() { | ||
return new ArrayList<>(auditEvents.values()); | ||
} | ||
|
||
public void clearAuditEvents() { | ||
auditEvents.clear(); | ||
} | ||
|
||
@Read() | ||
public AuditEvent read(@IdParam IdType theId) { | ||
AuditEvent auditEvent = auditEvents.get(theId.getIdPart()); | ||
if (auditEvent == null) { | ||
throw new ResourceNotFoundException(theId); | ||
} | ||
return auditEvent; | ||
} | ||
|
||
@Delete() | ||
public MethodOutcome delete(@IdParam IdType theId) { | ||
AuditEvent auditEvent = auditEvents.remove(theId.getIdPart()); | ||
if (auditEvent == null) { | ||
throw new ResourceNotFoundException(theId); | ||
} | ||
return new MethodOutcome(theId); | ||
} | ||
|
||
@Create() | ||
public MethodOutcome create(@ResourceParam AuditEvent auditEvent) { | ||
String id = UUID.randomUUID().toString(); | ||
IdType idType = new IdType(ResourceType.AuditEvent.name(), id); | ||
auditEvent.setId(idType); | ||
auditEvents.put(id, auditEvent); | ||
return new MethodOutcome(idType, true); | ||
} | ||
|
||
@Search | ||
public List<AuditEvent> list(@OptionalParam(name= AuditEvent.SP_TYPE) TokenParam type, | ||
@OptionalParam(name= AuditEvent.SP_SUBTYPE) TokenParam subtype) { | ||
Stream<AuditEvent> allAuditEvents = getAuditEvents().stream(); | ||
if (type != null) { | ||
if (isNotBlank(type.getSystem()) && isNotBlank(type.getValue())) { | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getType().hasSystem() && auditEvent.getType().getSystem().equals(type.getSystem()) && | ||
auditEvent.getType().hasCode() && auditEvent.getType().getCode().equals(type.getValue())); | ||
} else if (isNotBlank(type.getSystem()) && isBlank(type.getValue())){ | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getType().hasSystem() && auditEvent.getType().getSystem().equals(type.getSystem())); | ||
} else if (isBlank(type.getSystem()) && isNotBlank(type.getValue())){ | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getType().hasCode() && auditEvent.getType().getCode().equals(type.getValue())); | ||
} | ||
} | ||
if (subtype != null) { | ||
if (isNotBlank(subtype.getSystem()) && isNotBlank(subtype.getValue())) { | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getSubtypeFirstRep().hasSystem() && auditEvent.getSubtypeFirstRep().getSystem().equals(subtype.getSystem()) && | ||
auditEvent.getSubtypeFirstRep().hasCode() && auditEvent.getSubtypeFirstRep().getCode().equals(subtype.getValue())); | ||
} else if (isNotBlank(subtype.getSystem()) && isBlank(subtype.getValue())){ | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getSubtypeFirstRep().hasSystem() && auditEvent.getSubtypeFirstRep().getSystem().equals(subtype.getSystem())); | ||
} else if (isBlank(subtype.getSystem()) && isNotBlank(subtype.getValue())){ | ||
allAuditEvents = allAuditEvents.filter(auditEvent -> | ||
auditEvent.getSubtypeFirstRep().hasCode() && auditEvent.getSubtypeFirstRep().getCode().equals(subtype.getValue())); | ||
} | ||
} | ||
return allAuditEvents.collect(Collectors.toList()); | ||
} | ||
|
||
@Override | ||
public Class<? extends IBaseResource> getResourceType() { | ||
return AuditEvent.class; | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
...re/src/test/java/org/openehealth/ipf/commons/ihe/fhir/audit/server/TLSBalpRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openehealth.ipf.commons.ihe.fhir.audit.server; | ||
|
||
import io.undertow.Handlers; | ||
import io.undertow.Undertow; | ||
import io.undertow.UndertowOptions; | ||
import io.undertow.server.HttpHandler; | ||
import io.undertow.server.handlers.PathHandler; | ||
import io.undertow.servlet.api.DeploymentInfo; | ||
import io.undertow.servlet.api.DeploymentManager; | ||
import io.undertow.servlet.api.InstanceFactory; | ||
import io.undertow.servlet.api.InstanceHandle; | ||
import org.openehealth.ipf.commons.audit.TlsParameters; | ||
import org.openehealth.ipf.commons.ihe.fhir.extension.FhirAuditRepository; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.servlet.ServletException; | ||
|
||
import java.io.Closeable; | ||
import java.io.IOException; | ||
|
||
import static io.undertow.servlet.Servlets.defaultContainer; | ||
import static io.undertow.servlet.Servlets.deployment; | ||
import static io.undertow.servlet.Servlets.servlet; | ||
|
||
public class TLSBalpRepository implements Closeable { | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(TLSBalpRepository.class); | ||
protected final TlsParameters tlsParameters; | ||
private Undertow server; | ||
private final int httpsPort; | ||
|
||
public TLSBalpRepository(TlsParameters tlsParameters, int httpsPort) { | ||
this.tlsParameters = tlsParameters; | ||
this.httpsPort = httpsPort; | ||
} | ||
|
||
public TLSBalpRepository(int httpsPort) { | ||
this.tlsParameters = TlsParameters.getDefault(); | ||
this.httpsPort = httpsPort; | ||
} | ||
|
||
@Override | ||
public void close() throws IOException { | ||
stop(); | ||
} | ||
|
||
public void stop() { | ||
if (server != null) server.stop(); | ||
LOG.info("successfully stopped FHIR Audit Server"); | ||
} | ||
|
||
public Undertow start() throws ServletException { | ||
DeploymentInfo servletBuilder = deployment() | ||
.setClassLoader(FhirAuditRepository.class.getClassLoader()) | ||
.setContextPath("/fhir") | ||
.setDeploymentName("FHIR-Deployment") | ||
.addServlets( | ||
servlet("FhirAuditServer", FhirAuditServer.class, new FhirServletInitiator(new FhirAuditServer())) | ||
.addMapping("/*")); | ||
|
||
DeploymentManager manager = defaultContainer().addDeployment(servletBuilder); | ||
manager.deploy(); | ||
|
||
HttpHandler servletHandler = manager.start(); | ||
PathHandler path = Handlers | ||
.path(Handlers.redirect("/")) | ||
.addPrefixPath("/", servletHandler); | ||
server = Undertow.builder() | ||
.setServerOption(UndertowOptions.ENABLE_HTTP2, true) | ||
.addHttpsListener( | ||
httpsPort,"localhost", tlsParameters.getSSLContext(true)) | ||
.setHandler(path) | ||
.build(); | ||
server.start(); | ||
LOG.info("successfully started FHIR Audit Server on port {}", httpsPort); | ||
return server; | ||
} | ||
static class FhirServletInitiator implements InstanceFactory<FhirAuditServer> { | ||
|
||
private final FhirAuditServer fhirAuditServer; | ||
|
||
public FhirServletInitiator(FhirAuditServer fhirAuditServer) { | ||
this.fhirAuditServer = fhirAuditServer; | ||
} | ||
|
||
@Override | ||
public InstanceHandle<FhirAuditServer> createInstance() throws InstantiationException { | ||
return new InstanceHandle<>() { | ||
@Override | ||
public FhirAuditServer getInstance() { | ||
return fhirAuditServer; | ||
} | ||
|
||
@Override | ||
public void release() { | ||
|
||
} | ||
}; | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters