Skip to content

Commit

Permalink
433: BALP audit with oauth token support
Browse files Browse the repository at this point in the history
Boris Stanojevic committed Feb 1, 2024
1 parent 7df3541 commit 68b7dfa
Showing 56 changed files with 1,532 additions and 110 deletions.
4 changes: 4 additions & 0 deletions boot/ipf-atna-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
@@ -37,6 +37,10 @@
<groupId>org.openehealth.ipf.commons</groupId>
<artifactId>ipf-commons-audit</artifactId>
</dependency>
<dependency>
<groupId>org.openehealth.ipf.commons</groupId>
<artifactId>ipf-commons-ihe-fhir-r4-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
Original file line number Diff line number Diff line change
@@ -16,12 +16,20 @@

package org.openehealth.ipf.boot.atna;

import org.openehealth.ipf.commons.audit.*;
import org.openehealth.ipf.commons.audit.AuditContext;
import org.openehealth.ipf.commons.audit.AuditMessagePostProcessor;
import org.openehealth.ipf.commons.audit.AuditMetadataProvider;
import org.openehealth.ipf.commons.audit.DefaultAuditContext;
import org.openehealth.ipf.commons.audit.DefaultAuditMetadataProvider;
import org.openehealth.ipf.commons.audit.DefaultBalpAuditContext;
import org.openehealth.ipf.commons.audit.TlsParameters;
import org.openehealth.ipf.commons.audit.handler.AuditExceptionHandler;
import org.openehealth.ipf.commons.audit.handler.LoggingAuditExceptionHandler;
import org.openehealth.ipf.commons.audit.protocol.AuditTransmissionChannel;
import org.openehealth.ipf.commons.audit.protocol.AuditTransmissionProtocol;
import org.openehealth.ipf.commons.audit.queue.AuditMessageQueue;
import org.openehealth.ipf.commons.ihe.fhir.support.audit.marshal.BalpJsonSerializationStrategy;
import org.openehealth.ipf.commons.ihe.fhir.support.audit.marshal.BalpXmlSerializationStrategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -31,6 +39,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
*
*/
@@ -48,7 +58,27 @@ public AuditContext auditContext(IpfAtnaConfigurationProperties config,
AuditExceptionHandler auditExceptionHandler,
AuditMessagePostProcessor auditMessagePostProcessor,
@Value("${spring.application.name}") String appName) {
var auditContext = new DefaultAuditContext();
if (config.getBalp() != null) {
return balpConfiguration(defaultContextConfiguration(new DefaultBalpAuditContext(), config,
auditTransmissionProtocol, auditMessageQueue, tlsParameters, auditMetadataProvider,
auditExceptionHandler, auditMessagePostProcessor, appName), config);
} else {
return defaultContextConfiguration(new DefaultAuditContext(), config, auditTransmissionProtocol,
auditMessageQueue, tlsParameters, auditMetadataProvider, auditExceptionHandler,
auditMessagePostProcessor, appName);
}
}

private <T extends DefaultAuditContext> T defaultContextConfiguration(T auditContext,
IpfAtnaConfigurationProperties config,
AuditTransmissionProtocol auditTransmissionProtocol,
AuditMessageQueue auditMessageQueue,
TlsParameters tlsParameters,
AuditMetadataProvider auditMetadataProvider,
AuditExceptionHandler auditExceptionHandler,
AuditMessagePostProcessor auditMessagePostProcessor,
@Value("${spring.application.name}") String appName) {

auditContext.setAuditEnabled(config.isAuditEnabled());

// Simple properties
@@ -59,7 +89,6 @@ public AuditContext auditContext(IpfAtnaConfigurationProperties config,
auditContext.setAuditSource(config.getAuditSourceType());
auditContext.setIncludeParticipantsFromResponse(config.isIncludeParticipantsFromResponse());
auditContext.setAuditValueIfMissing(config.getAuditValueIfMissing());
auditContext.setAuditRepositoryContextPath(config.getAuditRepositoryContextPath());

// Strategies and complex parameters; overrideable
auditContext.setTlsParameters(tlsParameters);
@@ -68,7 +97,63 @@ public AuditContext auditContext(IpfAtnaConfigurationProperties config,
auditContext.setAuditMessageQueue(auditMessageQueue);
auditContext.setAuditExceptionHandler(auditExceptionHandler);
auditContext.setAuditMessagePostProcessor(auditMessagePostProcessor);
return auditContext;
}

private DefaultBalpAuditContext balpConfiguration(DefaultBalpAuditContext auditContext, IpfAtnaConfigurationProperties config) {
if (config.getBalp() != null) {
auditContext.setAuditRepositoryContextPath(config.getBalp().getAuditRepositoryContextPath());

if (isNotBlank(config.getBalp().getAuditEventSerializationType())) {
auditContext.setSerializationStrategy(
config.getBalp().getAuditEventSerializationType().equalsIgnoreCase("json") ?
new BalpJsonSerializationStrategy() : new BalpXmlSerializationStrategy());
}
if (config.getBalp().getOauth() != null) {
if (config.getBalp().getOauth().getIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setIdPath(config.getBalp().getOauth().getIdPath());
}
if (config.getBalp().getOauth().getClientIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setClientIdPath(config.getBalp().getOauth().getClientIdPath());
}
if (config.getBalp().getOauth().getIssuerPath() != null) {
auditContext.getBalpJwtExtractorProperties().setIssuerPath(config.getBalp().getOauth().getIssuerPath());
}
if (config.getBalp().getOauth().getSubjectPath() != null) {
auditContext.getBalpJwtExtractorProperties().setSubjectPath(config.getBalp().getOauth().getSubjectPath());
}
if (config.getBalp().getOauth().getSubjectNamePath() != null) {
auditContext.getBalpJwtExtractorProperties().setSubjectNamePath(config.getBalp().getOauth().getSubjectNamePath());
}
if (config.getBalp().getOauth().getSubjectRolePath() != null) {
auditContext.getBalpJwtExtractorProperties().setSubjectRolePath(config.getBalp().getOauth().getSubjectRolePath());
}
if (config.getBalp().getOauth().getSubjectOrganizationIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setSubjectOrganizationIdPath(config.getBalp().getOauth().getSubjectOrganizationIdPath());
}
if (config.getBalp().getOauth().getPurposeOfUsePath() != null) {
auditContext.getBalpJwtExtractorProperties().setPurposeOfUsePath(config.getBalp().getOauth().getPurposeOfUsePath());
}
if (config.getBalp().getOauth().getHomeCommunityIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setHomeCommunityIdPath(config.getBalp().getOauth().getHomeCommunityIdPath());
}
if (config.getBalp().getOauth().getNationalProviderIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setNationalProviderIdPath(config.getBalp().getOauth().getNationalProviderIdPath());
}
if (config.getBalp().getOauth().getDocIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setDocIdPath(config.getBalp().getOauth().getDocIdPath());
}
if (config.getBalp().getOauth().getPatientIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setPatientIdPath(config.getBalp().getOauth().getPatientIdPath());
}
if (config.getBalp().getOauth().getPersonIdPath() != null) {
auditContext.getBalpJwtExtractorProperties().setPersonIdPath(config.getBalp().getOauth().getPersonIdPath());
}
if (config.getBalp().getOauth().getAcpPath() != null) {
auditContext.getBalpJwtExtractorProperties().setAcpPath(config.getBalp().getOauth().getAcpPath());
}
}
}
return auditContext;
}

Original file line number Diff line number Diff line change
@@ -64,12 +64,6 @@ public class IpfAtnaConfigurationProperties {
@Getter @Setter
private int auditRepositoryPort = 514;

/**
* Sets the context-path of the BALP audit record repository.
*/
@Getter @Setter
private String auditRepositoryContextPath = "";

/**
* Enterprise Site Id
*/
@@ -106,4 +100,88 @@ public class IpfAtnaConfigurationProperties {

@Getter @Setter
private String auditValueIfMissing = "UNKNOWN";

@Getter @Setter
private Balp balp;

public static class Balp {

/**
* Sets the context-path of the BALP audit record repository.
*/
@Getter
@Setter
private String auditRepositoryContextPath = "";

@Getter
@Setter
private String auditEventSerializationType = "json";

@Getter
@Setter
private OAuth oauth;

public static class OAuth {

@Getter
@Setter
private String[] idPath;

@Getter
@Setter
private String[] issuerPath;

@Getter
@Setter
private String[] clientIdPath;

@Getter
@Setter
private String[] subjectPath;

@Getter
@Setter
private String[] subjectNamePath;

@Getter
@Setter
private String[] subjectOrganizationPath;

@Getter
@Setter
private String[] subjectOrganizationIdPath;

@Getter
@Setter
private String[] subjectRolePath;

@Getter
@Setter
private String[] purposeOfUsePath;

@Getter
@Setter
private String[] homeCommunityIdPath;

@Getter
@Setter
private String[] nationalProviderIdPath;

@Getter
@Setter
private String[] personIdPath;

@Getter
@Setter
private String[] patientIdPath;

@Getter
@Setter
private String[] docIdPath;

@Getter
@Setter
private String[] acpPath;
}
}
}
Original file line number Diff line number Diff line change
@@ -44,7 +44,6 @@ public void testAtnaSettings() throws Exception {
assertEquals("mysite", auditContext.getAuditEnterpriseSiteId());
assertEquals("localhost", auditContext.getAuditRepositoryHostName());
assertEquals(1342, auditContext.getAuditRepositoryPort());
assertEquals("fhir", auditContext.getAuditRepositoryContextPath());
assertEquals("TLS", auditContext.getAuditTransmissionProtocol().getTransportName());
assertTrue(auditContext.getAuditMessageQueue() instanceof AsynchronousAuditMessageQueue);
}
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());
}

}
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:
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ ipf:
audit-enabled: false
audit-repository-host: localhost
audit-repository-port: 1342
audit-repository-context-path: fhir
audit-repository-transport: TLS
audit-enterprise-site-id: mysite
audit-queue-class: org.openehealth.ipf.commons.audit.queue.AsynchronousAuditMessageQueue
Original file line number Diff line number Diff line change
@@ -165,8 +165,6 @@ default String getAuditValueIfMissing() {
*/
boolean isIncludeParticipantsFromResponse();

String getAuditRepositoryContextPath();

static AuditContext noAudit() {
return DefaultAuditContext.NO_AUDIT;
}
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();
}
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"};
}
Original file line number Diff line number Diff line change
@@ -101,10 +101,6 @@ public class DefaultAuditContext implements AuditContext {
@Setter
private String auditValueIfMissing = "UNKNOWN";

@Getter
@Setter
private String auditRepositoryContextPath = "";

public String getAuditRepositoryTransport() {
return auditTransmissionProtocol.getTransportName();
}
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();
}

4 changes: 4 additions & 0 deletions commons/ihe/fhir/core/pom.xml
Original file line number Diff line number Diff line change
@@ -41,6 +41,10 @@
<groupId>org.openehealth.ipf.commons</groupId>
<artifactId>ipf-commons-map</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.github.mizosoft.methanol</groupId>
<artifactId>methanol</artifactId>
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ public interface Constants {
String FHIR_REQUEST_DETAILS = "FhirRequestDetails";

// Parameter information from the HttpServletRequest
String HTTP_AUTHORIZATION = "Authorization";
String HTTP_URI = "FhirHttpUri";
String HTTP_URL = "FhirHttpUrl";
String HTTP_SCHEME = "FhirHttpScheme";
Original file line number Diff line number Diff line change
@@ -25,7 +25,11 @@

import java.util.Map;

import static org.openehealth.ipf.commons.ihe.fhir.Constants.*;
import static org.openehealth.ipf.commons.ihe.fhir.Constants.FHIR_CONTEXT;
import static org.openehealth.ipf.commons.ihe.fhir.Constants.HTTP_AUTHORIZATION;
import static org.openehealth.ipf.commons.ihe.fhir.Constants.HTTP_CLIENT_IP_ADDRESS;
import static org.openehealth.ipf.commons.ihe.fhir.Constants.HTTP_URI;
import static org.openehealth.ipf.commons.ihe.fhir.Constants.HTTP_URL;

/**
* Generic Audit Strategy for FHIR transactions
@@ -55,6 +59,9 @@ public T enrichAuditDatasetFromRequest(T auditDataset, Object request, Map<Strin
if (parameters.get(FHIR_CONTEXT) != null) {
auditDataset.setFhirContext((FhirContext) parameters.get(FHIR_CONTEXT));
}
if (parameters.get(HTTP_AUTHORIZATION) != null) {
auditDataset.setAuthorization((String) parameters.get(HTTP_AUTHORIZATION));
}
return auditDataset;
}

Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
import lombok.Setter;
import org.openehealth.ipf.commons.audit.utils.AuditUtils;
import org.openehealth.ipf.commons.ihe.core.atna.AuditDataset;
import org.openehealth.ipf.commons.ihe.fhir.audit.auth.BalpJwtDataSet;

import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -77,6 +78,9 @@ public class FhirAuditDataset extends AuditDataset {
@Getter @Setter
private FhirContext fhirContext;

@Getter @Setter
private String authorization;

public FhirAuditDataset(boolean serverSide) {
super(serverSide);
}
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
import org.openehealth.ipf.commons.audit.AuditContext;
import org.openehealth.ipf.commons.audit.codes.EventOutcomeIndicator;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpJwtUtils;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.GenericFhirAuditMessageBuilder;

import java.util.Map;
@@ -161,13 +162,15 @@ public boolean enrichAuditDatasetFromResponse(GenericFhirAuditDataset auditDatas
public AuditMessage[] makeAuditMessage(AuditContext auditContext, GenericFhirAuditDataset auditDataset) {
var builder = new GenericFhirAuditMessageBuilder(auditContext, auditDataset)
.addPatients(auditDataset);

if (auditDataset.getQueryString() != null) {
builder.addQueryParticipantObject(auditDataset);
} else if (auditDataset.getResourceId() != null &&
auditDataset.getResourceId().hasResourceType() &&
auditDataset.getResourceId().hasIdPart()) {
builder.addResourceParticipantObject(auditDataset);
}
builder.addJwtParticipants(auditDataset);
return builder.getMessages();
}

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;
}
}

}
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;
}
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;
}
}
Original file line number Diff line number Diff line change
@@ -22,10 +22,15 @@ public class Constants {
public static final String EHS_SYSTEM_NAME = "e-health-suisse";

public static final String DCM_SYSTEM_NAME = "http://dicom.nema.org/resources/ontology/DCM";
public static final String DCM_OCLIENT_CODE = "110150";
public static final String SECURITY_SOURCE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/security-source-type";
public static final String AUDIT_ENTITY_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/audit-entity-type";
public static final String OBJECT_ROLE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/object-role";
public static final String AUDIT_LIFECYCLE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/dicom-audit-lifecycle";


public static final String OUSER_AGENT_TYPE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/v3-ParticipationType";
public static final String OUSER_AGENT_TYPE_CODE = "IRCP";
public static final String OUSER_AGENT_PURPOSE_OF_USE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/v3-ActReason";
public static final String OUSER_AGENT_ROLE_SYSTEM_NAME = "http://terminology.hl7.org/CodeSystem/v3-RoleClass";
public static final String OUSER_AGENT_TYPE_OPAQUE_SYSTEM_NAME = "https://profiles.ihe.net/ITI/BALP/CodeSystem/UserAgentTypes";
public static final String OUSER_AGENT_TYPE_OPAQUE_CODE = "UserOauthAgent";
}
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;
}

}
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());
}
}
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());
}
}
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());
}
}
Original file line number Diff line number Diff line change
@@ -132,6 +132,11 @@ public GenericFhirAuditMessageBuilder addResourceParticipantObject(GenericFhirAu
return self();
}

public GenericFhirAuditMessageBuilder addJwtParticipants(GenericFhirAuditDataset auditDataset) {
BalpJwtUtils.addJwtParticipant(delegate, auditDataset, getAuditContext());
return self();
}


private static EventActionCode eventActionCode(RestOperationTypeEnum operation) {
if (operation == null)
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
import ca.uhn.fhir.rest.client.impl.RestfulClientFactory;
import org.openehealth.ipf.commons.audit.AuditContext;
import org.openehealth.ipf.commons.audit.AuditMetadataProvider;
import org.openehealth.ipf.commons.audit.BalpAuditContext;
import org.openehealth.ipf.commons.audit.FhirContextHolder;
import org.openehealth.ipf.commons.audit.TlsParameters;
import org.openehealth.ipf.commons.audit.protocol.AuditTransmissionChannel;
@@ -78,7 +79,8 @@ public void send(AuditContext auditContext,
String baseUrl = String.format(BASE_URL_FORMAT,
auditContext.getAuditRepositoryHostName(),
auditContext.getAuditRepositoryPort(),
auditContext.getAuditRepositoryContextPath());
(auditContext instanceof BalpAuditContext)?
((BalpAuditContext)auditContext).getAuditRepositoryContextPath() : "");
createClient(baseUrl);
}
MethodOutcome outcome = client
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^^^&amp;1.2.840.113619.6.197&amp;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" +
"}";
}
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() + "^^^&amp;1.2.840.113619.6.197&amp;ISO");
bppcMap.put("doc_id", "urn:oid:1.2.3." + integers.next());
bppcMap.put("acp", "urn:oid:1.2.3." + integers.next());
return bppcMap;
}

}

Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openehealth.ipf.commons.audit.CustomTlsParameters;
import org.openehealth.ipf.commons.audit.DefaultAuditContext;
import org.openehealth.ipf.commons.audit.DefaultBalpAuditContext;
import org.openehealth.ipf.commons.audit.TlsParameters;
import org.openehealth.ipf.commons.audit.codes.EventOutcomeIndicator;
import org.openehealth.ipf.commons.audit.event.ApplicationActivityBuilder;
@@ -25,14 +25,15 @@
@ExtendWith(FhirAuditRepository.class)
public abstract class AbstractFhirRestTLSSenderIntegrationTest {

protected DefaultAuditContext auditContext;
protected DefaultBalpAuditContext auditContext;

private static final Logger LOG = LoggerFactory.getLogger(AbstractFhirRestTLSSenderIntegrationTest.class);

@BeforeEach
public void setup() {
this.auditContext = new DefaultAuditContext();
this.auditContext = new DefaultBalpAuditContext();
auditContext.setAuditRepositoryPort(FhirAuditRepository.getServerHttpsPort());
auditContext.setAuditRepositoryContextPath(FhirAuditRepository.getServerContextPath());
auditContext.setAuditRepositoryHost("localhost");
auditContext.setAuditEnabled(true);
auditContext.setSerializationStrategy((auditMessage, writer, pretty) -> writer.write("<AuditEvent />"));
@@ -57,7 +58,7 @@ TlsParameters setupDefaultTlsParameter() {
@AfterEach
public void tearDown() {
LOG.info("FhirAuditRepository size: " + FhirAuditRepository.getAuditEvents().size() + ". Cleanup....");
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
LOG.info("FhirAuditRepository size: " + FhirAuditRepository.getAuditEvents().size());
}

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;
}
}
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() {

}
};
}
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package org.openehealth.ipf.commons.ihe.fhir.extension;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
@@ -15,15 +9,14 @@
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.InstanceFactory;
import io.undertow.servlet.api.InstanceHandle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import net.java.quickcheck.generator.PrimitiveGenerators;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.ResourceType;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.openehealth.ipf.commons.audit.CustomTlsParameters;
import org.openehealth.ipf.commons.audit.TlsParameters;
import org.openehealth.ipf.commons.ihe.fhir.audit.server.FhirAuditServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
@@ -32,8 +25,6 @@
import java.net.URI;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import static io.undertow.servlet.Servlets.defaultContainer;
import static io.undertow.servlet.Servlets.deployment;
@@ -45,7 +36,8 @@ public class FhirAuditRepository implements BeforeAllCallback, BeforeEachCallbac
private ExtensionContext extensionContext;
private static FhirAuditServer fhirAuditServer;
private static final String STORE_KEY = "undertow";
private static int httpsPort ;
private static int httpsPort;
private static String contextPath;
static final String SERVER_KEY_STORE;
static final String SERVER_KEY_STORE_PASS = "init";
static final String TRUST_STORE;
@@ -80,6 +72,7 @@ public void beforeAll(ExtensionContext extensionContext) throws Exception {
if (hasStartedUndertow()) return;

httpsPort = freePort();
contextPath = PrimitiveGenerators.letterStrings(10, 10).next();
registerShutdownHook();
}

@@ -95,12 +88,12 @@ public void beforeEach(ExtensionContext extensionContext) throws Exception {
if (server == null) {
fhirAuditServer = new FhirAuditServer();
DeploymentInfo servletBuilder = deployment()
.setClassLoader(FhirAuditRepository.class.getClassLoader())
.setContextPath("/")
.setDeploymentName("FHIR-Deployment")
.addServlets(
servlet("FhirAuditServer", FhirAuditServer.class, new FhirServletInitiator(fhirAuditServer))
.addMapping("/*"));
.setClassLoader(FhirAuditRepository.class.getClassLoader())
.setContextPath("/" + contextPath)
.setDeploymentName("FHIR-Deployment")
.addServlets(
servlet("FhirAuditServer", FhirAuditServer.class, new FhirServletInitiator(fhirAuditServer))
.addMapping("/*"));

DeploymentManager manager = defaultContainer().addDeployment(servletBuilder);
manager.deploy();
@@ -110,11 +103,11 @@ public void beforeEach(ExtensionContext extensionContext) throws Exception {
.path(Handlers.redirect("/"))
.addPrefixPath("/", servletHandler);
server = Undertow.builder()
.setServerOption(UndertowOptions.ENABLE_HTTP2, true)
.addHttpsListener(
httpsPort,"localhost", setupDefaultTlsParameter().getSSLContext(true))
.setHandler(path)
.build();
.setServerOption(UndertowOptions.ENABLE_HTTP2, true)
.addHttpsListener(
httpsPort,"localhost", setupDefaultTlsParameter().getSSLContext(true))
.setHandler(path)
.build();
server.start();
}
}
@@ -131,39 +124,20 @@ private void registerShutdownHook() {
public static int getServerHttpsPort(){
return httpsPort;
}

public static String getServerContextPath(){
return contextPath;
}
public static List<AuditEvent> getAuditEvents() {
return fhirAuditServer.getAuditEvents();
}

private boolean hasStartedUndertow(){
return extensionContext.getRoot().getStore(GLOBAL).get(STORE_KEY) != null;
public static void clearAuditEvents() {
fhirAuditServer.clearAuditEvents();
}

static class FhirAuditServer extends RestfulServer implements IResourceProvider {

private final List<AuditEvent> auditEvents = new CopyOnWriteArrayList<>();

public FhirAuditServer() {
setFhirContext(FhirContext.forR4());
setResourceProviders(this);
}

public List<AuditEvent> getAuditEvents() {
return auditEvents;
}

@Create()
public MethodOutcome create(@ResourceParam AuditEvent auditEvent) {
auditEvents.add(auditEvent);
return new MethodOutcome(
new IdType(ResourceType.AuditEvent.name(),
UUID.randomUUID().toString()), true);
}

@Override
public Class<? extends IBaseResource> getResourceType() {
return AuditEvent.class;
}
private boolean hasStartedUndertow(){
return extensionContext.getRoot().getStore(GLOBAL).get(STORE_KEY) != null;
}

static class FhirServletInitiator implements InstanceFactory<FhirAuditServer> {
Original file line number Diff line number Diff line change
@@ -20,20 +20,34 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.Base64BinaryType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Reference;
import org.openehealth.ipf.commons.audit.AuditException;
import org.openehealth.ipf.commons.audit.codes.*;
import org.openehealth.ipf.commons.audit.codes.EventActionCode;
import org.openehealth.ipf.commons.audit.codes.EventOutcomeIndicator;
import org.openehealth.ipf.commons.audit.codes.NetworkAccessPointTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectDataLifeCycle;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCodeRole;
import org.openehealth.ipf.commons.audit.marshal.SerializationStrategy;
import org.openehealth.ipf.commons.audit.model.ActiveParticipantType;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.audit.model.AuditSourceIdentificationType;
import org.openehealth.ipf.commons.audit.model.ParticipantObjectIdentificationType;
import org.openehealth.ipf.commons.audit.types.ActiveParticipantRoleId;
import org.openehealth.ipf.commons.audit.types.CodedValueType;

import java.io.IOException;
import java.io.Writer;
import java.sql.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants.*;

@@ -104,7 +118,6 @@ protected AuditEvent.AuditEventEntityComponent participantObjectIdentificationTo
entity.setWhat(new Reference(poit.getParticipantObjectID()));
}
return entity;

}

protected AuditEvent.AuditEventSourceComponent auditSourceIdentificationToEventSource(AuditSourceIdentificationType asit) {
@@ -117,6 +130,40 @@ protected AuditEvent.AuditEventSourceComponent auditSourceIdentificationToEventS
}

protected AuditEvent.AuditEventAgentComponent activeParticipantToAgent(ActiveParticipantType ap) {
Optional<String> oUser = getOAuthAttrFromKnownRoleIdCode(ap.getRoleIDCodes(), OUSER_AGENT_TYPE_SYSTEM_NAME);
if (oUser.isPresent()) {
AuditEvent.AuditEventAgentComponent agent = new AuditEvent.AuditEventAgentComponent()
.setType(systemAndCodeToCodeableConcept(OUSER_AGENT_TYPE_SYSTEM_NAME, OUSER_AGENT_TYPE_CODE, "information recipient"))
.addPolicy(oUser.get())
.setName(ap.getUserName())
.setWho(
new Reference(ap.getUserID())
.setIdentifier(new Identifier().setSystem(ap.getAlternativeUserID()).setValue(ap.getUserID()))
.setDisplay(ap.getUserName()))
.setRequestor(ap.isUserIsRequestor());
getOAuthListAttrFromKnownRoleIdCode(ap.getRoleIDCodes(), OUSER_AGENT_PURPOSE_OF_USE_SYSTEM_NAME)
.forEach(purpose -> agent.getPurposeOfUse().add(
systemAndCodeToCodeableConcept(OUSER_AGENT_PURPOSE_OF_USE_SYSTEM_NAME, purpose, "")));
getOAuthListAttrFromKnownRoleIdCode(ap.getRoleIDCodes(), OUSER_AGENT_ROLE_SYSTEM_NAME)
.forEach(purpose -> agent.getRole().add(
systemAndCodeToCodeableConcept(OUSER_AGENT_ROLE_SYSTEM_NAME, purpose, "")));
return agent;
}
Optional<String> oClient = getOAuthAttrFromKnownRoleIdCode(ap.getRoleIDCodes(), DCM_SYSTEM_NAME);
if (oClient.isPresent()) {
return new AuditEvent.AuditEventAgentComponent()
.setType(systemAndCodeToCodeableConcept(DCM_SYSTEM_NAME, DCM_OCLIENT_CODE, "Application"))
.setRequestor(ap.isUserIsRequestor())
.setWho(new Reference().setIdentifier(new Identifier().setValue(oClient.get())));
}
Optional<String> opaqueToken = getOAuthAttrFromKnownRoleIdCode(ap.getRoleIDCodes(),
OUSER_AGENT_TYPE_OPAQUE_SYSTEM_NAME);
if (opaqueToken.isPresent()) {
return new AuditEvent.AuditEventAgentComponent()
.setType(new CodeableConcept(
new Coding(OUSER_AGENT_TYPE_OPAQUE_SYSTEM_NAME, OUSER_AGENT_TYPE_OPAQUE_CODE, "")))
.setRequestor(true);
}
return new AuditEvent.AuditEventAgentComponent()
.setType(codedValueTypeToCodeableConcept(ap.getRoleIDCodes().get(0), DCM_SYSTEM_NAME))
.setWho(new Reference().setDisplay(ap.getUserID()))
@@ -129,16 +176,29 @@ protected AuditEvent.AuditEventAgentComponent activeParticipantToAgent(ActivePar
.setType(auditEventNetworkType(ap.getNetworkAccessPointTypeCode())));
}

private Optional<String> getOAuthAttrFromKnownRoleIdCode(List<ActiveParticipantRoleId> roleCodes,
String knownCodeSystem) {
return roleCodes.stream().filter(p -> p.getCodeSystemName().equals(knownCodeSystem))
.findFirst()
.map(CodedValueType::getCode);
}

private List<String> getOAuthListAttrFromKnownRoleIdCode(List<ActiveParticipantRoleId> roleCodes,
String knownCodeSystem) {
return roleCodes.stream().filter(p -> p.getCodeSystemName().equals(knownCodeSystem))
.map(CodedValueType::getCode).collect(Collectors.toList());
}

protected AuditEvent.AuditEventAgentNetworkType auditEventNetworkType(NetworkAccessPointTypeCode naptc) {
try {
return AuditEvent.AuditEventAgentNetworkType.fromCode(String.valueOf(naptc.getValue()));
return naptc != null?
AuditEvent.AuditEventAgentNetworkType.fromCode(String.valueOf(naptc.getValue())) : null;
} catch (FHIRException e) {
// should never happen
throw new AuditException(e);
}
}


protected AuditEvent.AuditEventOutcome getAuditEventOutcome(EventOutcomeIndicator eventOutcomeIndicator) {
try {
return AuditEvent.AuditEventOutcome.fromCode(String.valueOf(eventOutcomeIndicator.getValue()));
@@ -190,4 +250,8 @@ protected CodeableConcept codedValueTypeToCodeableConcept(CodedValueType cvt, St
new CodeableConcept().addCoding(codedValueTypeToCoding(cvt, codeSystem)) :
null;
}

protected CodeableConcept systemAndCodeToCodeableConcept(String codeSystem, String code, String displayName) {
return new CodeableConcept().addCoding(new Coding(codeSystem, code, displayName));
}
}
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCodeRole;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIExportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIExportBuilder;

import java.util.Collections;

@@ -37,7 +37,7 @@ public Iti105ClientAuditStrategy() {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, Iti105AuditDataset auditDataset) {
return new PHIExportBuilder<>(auditContext, auditDataset, FhirEventTypeCode.SimplifiedPublish)
return new BalpPHIExportBuilder(auditContext, auditDataset, FhirEventTypeCode.SimplifiedPublish)
.setPatient(auditDataset.getPatientId())
.addExportedEntity(
auditDataset.getDocumentReferenceId() != null ?
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCodeRole;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIImportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIImportBuilder;

import java.util.Collections;

@@ -37,7 +37,7 @@ public Iti105ServerAuditStrategy() {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, Iti105AuditDataset auditDataset) {
return new PHIImportBuilder<>(auditContext, auditDataset, FhirEventTypeCode.SimplifiedPublish)
return new BalpPHIImportBuilder(auditContext, auditDataset, FhirEventTypeCode.SimplifiedPublish)
.setPatient(auditDataset.getPatientId())
.addImportedEntity(
auditDataset.getDocumentReferenceId(),
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCodeRole;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIExportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIExportBuilder;

import java.util.Collections;

@@ -37,7 +37,7 @@ public Iti65ClientAuditStrategy() {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, Iti65AuditDataset auditDataset) {
return new PHIExportBuilder<>(auditContext, auditDataset, FhirEventTypeCode.ProvideDocumentBundle)
return new BalpPHIExportBuilder(auditContext, auditDataset, FhirEventTypeCode.ProvideDocumentBundle)
.setPatient(auditDataset.getPatientId())
.addExportedEntity(
auditDataset.getSubmissionSetUuid() != null ?
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.audit.codes.ParticipantObjectTypeCodeRole;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIImportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIImportBuilder;

import java.util.Collections;

@@ -37,7 +37,7 @@ public Iti65ServerAuditStrategy() {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, Iti65AuditDataset auditDataset) {
return new PHIImportBuilder<>(auditContext, auditDataset, FhirEventTypeCode.ProvideDocumentBundle)
return new BalpPHIImportBuilder(auditContext, auditDataset, FhirEventTypeCode.ProvideDocumentBundle)
.setPatient(auditDataset.getPatientId())
.addImportedEntity(
auditDataset.getSubmissionSetUuid(),
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditStrategy;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpQueryInformationBuilder;


/**
@@ -37,7 +38,7 @@ public Iti66AuditStrategy(boolean serverSide) {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, FhirQueryAuditDataset auditDataset) {
return new QueryInformationBuilder<>(auditContext, auditDataset, FhirEventTypeCode.MobileDocumentManifestQuery)
return new BalpQueryInformationBuilder(auditContext, auditDataset, FhirEventTypeCode.MobileDocumentManifestQuery)
.addPatients(auditDataset.getPatientIds())
.setQueryParameters("MobileDocumentManifestQuery",
FhirParticipantObjectIdTypeCode.MobileDocumentManifestQuery,
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditStrategy;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpQueryInformationBuilder;

/**
* @author Christian Ohr
@@ -35,7 +36,7 @@ public Iti67AuditStrategy(boolean serverSide) {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, FhirQueryAuditDataset auditDataset) {
return new QueryInformationBuilder(auditContext, auditDataset, FhirEventTypeCode.MobileDocumentReferenceQuery)
return new BalpQueryInformationBuilder(auditContext, auditDataset, FhirEventTypeCode.MobileDocumentReferenceQuery)
.addPatients(auditDataset.getPatientIds())
.setQueryParameters("MobileDocumentReferenceQuery",
FhirParticipantObjectIdTypeCode.MobileDocumentReferenceQuery,
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@
import org.openehealth.ipf.commons.ihe.core.atna.AuditStrategySupport;
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIExportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIExportBuilder;

import java.util.Map;

@@ -45,7 +46,7 @@ public Iti68AuditDataset enrichAuditDatasetFromRequest(Iti68AuditDataset auditDa

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, Iti68AuditDataset auditDataset) {
PHIExportBuilder builder = new PHIExportBuilder<>(auditContext, auditDataset,
BalpPHIExportBuilder builder = new BalpPHIExportBuilder(auditContext, auditDataset,
EventActionCode.Create,
FhirEventTypeCode.MobileDocumentRetrieval)
.setPatient(auditDataset.getPatientId());
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@
import org.openehealth.ipf.commons.ihe.core.atna.event.PHIExportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirAuditDataset;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpPHIExportBuilder;
import org.openehealth.ipf.commons.ihe.fhir.iti68.Iti68AuditDataset;

import java.util.Map;
@@ -45,7 +46,7 @@ public FhirAuditDataset enrichAuditDatasetFromRequest(FhirAuditDataset auditData

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, FhirAuditDataset auditDataset) {
PHIExportBuilder builder = new PHIExportBuilder<>(auditContext, auditDataset,
BalpPHIExportBuilder builder = new BalpPHIExportBuilder(auditContext, auditDataset,
EventActionCode.Create,
FhirEventTypeCode.MobileDocumentRetrieval)
.setPatient(auditDataset.getPatientId());
Original file line number Diff line number Diff line change
@@ -18,11 +18,11 @@
import org.hl7.fhir.r4.model.IdType;
import org.openehealth.ipf.commons.audit.AuditContext;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.QueryInformationBuilder;
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditDataset;
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditStrategy;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpQueryInformationBuilder;

import java.util.Map;

@@ -40,7 +40,7 @@ protected Iti78AuditStrategy(boolean serverSide) {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, FhirQueryAuditDataset auditDataset) {
return new QueryInformationBuilder<>(auditContext, auditDataset, FhirEventTypeCode.MobilePatientDemographicsQuery)
return new BalpQueryInformationBuilder(auditContext, auditDataset, FhirEventTypeCode.MobilePatientDemographicsQuery)
.addPatients(auditDataset.getPatientIds())
.setQueryParameters(
"MobilePatientDemographicsQuery",
Original file line number Diff line number Diff line change
@@ -20,12 +20,12 @@
import org.hl7.fhir.r4.model.StringType;
import org.openehealth.ipf.commons.audit.AuditContext;
import org.openehealth.ipf.commons.audit.model.AuditMessage;
import org.openehealth.ipf.commons.ihe.core.atna.event.QueryInformationBuilder;
import org.openehealth.ipf.commons.ihe.fhir.Constants;
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditDataset;
import org.openehealth.ipf.commons.ihe.fhir.audit.FhirQueryAuditStrategy;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirEventTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.FhirParticipantObjectIdTypeCode;
import org.openehealth.ipf.commons.ihe.fhir.audit.events.BalpQueryInformationBuilder;

import java.util.Map;

@@ -43,7 +43,7 @@ public Iti83AuditStrategy(boolean serverSide) {

@Override
public AuditMessage[] makeAuditMessage(AuditContext auditContext, FhirQueryAuditDataset auditDataset) {
return new QueryInformationBuilder<>(auditContext, auditDataset, FhirEventTypeCode.MobilePatientIdentifierCrossReferenceQuery)
return new BalpQueryInformationBuilder(auditContext, auditDataset, FhirEventTypeCode.MobilePatientIdentifierCrossReferenceQuery)
.addPatients(auditDataset.getPatientIds())
.setQueryParameters(
"PIXmQuery",
8 changes: 7 additions & 1 deletion dependencies/pom.xml
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
<jakarta-jwsapi-version>2.1.0</jakarta-jwsapi-version>
<jdom-version>2.0.6.1</jdom-version>
<methanol-version>1.7.0</methanol-version>
<nimbus-jose-jwt-version>9.37.3</nimbus-jose-jwt-version>
<ph-schematron-version>5.6.5</ph-schematron-version>
<ph-commons-version>9.5.5</ph-commons-version>
<saxon-he-version>11.4</saxon-he-version>
@@ -100,7 +101,7 @@
<artifactId>jaxb2-basics-tools</artifactId>
</exclusion>
<exclusion>
<!-- Hapi introduce beanutils to override a old version used in jaxb2-basics-tools,
<!-- Hapi introduce beanutils to override a old version used in jaxb2-basics-tools,
but since the tools are not needed, the fix also not -->
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
@@ -305,6 +306,11 @@
<artifactId>methanol</artifactId>
<version>${methanol-version}</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt-version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
Original file line number Diff line number Diff line change
@@ -24,6 +24,10 @@

import javax.naming.ldap.LdapName;
import javax.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* @author Christian Ohr
@@ -51,4 +55,24 @@ public static void extractClientCertificateCommonName(Exchange exchange, AuditDa
}
}
}

public static Optional<String> extractAuthorizationHeader(Exchange exchange) {
if (exchange.getIn().getHeader(Constants.HTTP_INCOMING_HEADERS) != null) {
Map<String, List<String>> httpHeaders = exchange.getIn().getHeader(Constants.HTTP_INCOMING_HEADERS, Map.class);
if (!httpHeaders.isEmpty()
&& httpHeaders.keySet().stream().anyMatch(Constants.HTTP_AUTHORIZATION::equalsIgnoreCase)) {

List<String> values = httpHeaders.entrySet().stream()
.filter(entry -> Constants.HTTP_AUTHORIZATION.equalsIgnoreCase(entry.getKey()))
.findFirst()
.map(Map.Entry::getValue)
.orElse(new ArrayList<>());

if (!values.isEmpty()) {
return Optional.of(values.get(0));
}
}
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -28,6 +28,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static java.util.Objects.requireNonNull;


@@ -113,14 +117,12 @@ private AuditDatasetType createAndEnrichAuditDatasetFromRequest(AuditStrategy<Au

// TODO Also extract basic auth user?
AuditInterceptorUtils.extractClientCertificateCommonName(exchange, auditDataset);

AuditInterceptorUtils.extractAuthorizationHeader(exchange).ifPresent(auditDataset::setAuthorization);
return strategy.enrichAuditDatasetFromRequest(auditDataset, msg, exchange.getIn().getHeaders());
} catch (Exception e) {
LOG.error("Exception when enriching audit dataset from request", e);
return null;
}
}



}
4 changes: 4 additions & 0 deletions platform-camel/ihe/fhir/r4/mhd/pom.xml
Original file line number Diff line number Diff line change
@@ -130,6 +130,10 @@
<artifactId>undertow-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.java.quickcheck</groupId>
<artifactId>quickcheck</artifactId>
</dependency>
</dependencies>

<build>
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ public static void startServer(String contextDescriptor) {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
Original file line number Diff line number Diff line change
@@ -18,13 +18,16 @@

import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import org.apache.camel.Exchange;
import org.apache.camel.component.http.HttpConstants;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
import org.ietf.jgss.Oid;
import org.openehealth.ipf.commons.core.URN;
import org.openehealth.ipf.commons.ihe.fhir.Constants;
import org.openehealth.ipf.commons.ihe.fhir.IpfFhirServlet;
import org.openehealth.ipf.commons.ihe.fhir.SslAwareMethanolRestfulClientFactory;
import org.openehealth.ipf.commons.ihe.fhir.audit.auth.BalpJwtGenerator;
import org.openehealth.ipf.commons.ihe.fhir.mhd.MhdProfile;
import org.openehealth.ipf.commons.ihe.fhir.mhd.model.ComprehensiveDocumentReference;
import org.openehealth.ipf.commons.ihe.fhir.mhd.model.ComprehensiveProvideDocumentBundle;
@@ -34,6 +37,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.http.HttpHeaders;
import java.security.MessageDigest;
import java.time.LocalDate;
import java.time.LocalTime;
@@ -144,13 +148,27 @@ protected Bundle thisSucks() {
}

protected Bundle sendManually(Bundle bundle) {
return client.transaction().withBundle(bundle).encodedXml().execute();
return client.transaction().withBundle(bundle)
.encodedXml().execute();
}

protected Bundle sendManuallyWithJwt(Bundle bundle) {
var headerValue = "Bearer " + new BalpJwtGenerator().next();
return client.transaction().withBundle(bundle)
.withAdditionalHeader("Authorization", headerValue)
.encodedXml().execute();
}

protected Bundle sendViaProducer(Bundle bundle) {
return producerTemplate.requestBody("direct:input", bundle, Bundle.class);
}

protected Bundle sendViaProducerWithJwtAuthorization(Bundle bundle) {
var headerValue = "Bearer " + new BalpJwtGenerator().next();
return producerTemplate.requestBodyAndHeader("direct:input", bundle, "Authorization",
headerValue, Bundle.class);
}

protected void printAsXML(IBaseResource resource) {
LOG.info(clientFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(resource));
}
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openehealth.ipf.commons.ihe.fhir.audit.codes.Constants;
import org.openehealth.ipf.commons.ihe.fhir.extension.FhirAuditRepository;

import java.util.Optional;
@@ -44,12 +45,12 @@ public static void setUpClass() {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
public void testSendManualMhd() throws Exception {
sendManually(provideAndRegister());
sendManuallyWithJwt(provideAndRegister());

// Check ATNA Audit
var auditEvents = FhirAuditRepository.getAuditEvents();
@@ -69,12 +70,16 @@ public void testSendManualMhd() throws Exception {
assertEquals(1, auditEvent.getEntity().stream()
.filter(event -> event.getType().getCode().equals("2") && event.getRole().getCode().equals("20"))
.count());
assertTrue(auditEvent.getAgent().stream()
.anyMatch(p -> p.getType().getCodingFirstRep().getCode().equals(Constants.OUSER_AGENT_TYPE_CODE)
&& p.getType().getCodingFirstRep().getSystem().equals(Constants.OUSER_AGENT_TYPE_SYSTEM_NAME)
&& p.hasPolicy()));
}

@Test
public void testSendEndpointMhd() throws Exception {
var bundle = provideAndRegister();
sendViaProducer(bundle);
sendViaProducerWithJwtAuthorization(bundle);

// Check ATNA Audit
var auditEvents = FhirAuditRepository.getAuditEvents();
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ public static void setUpClass() {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ public static void setUpClass() {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ public static void setUpClass() {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ public static void setUpClass() {

@BeforeEach
public void beforeEach() {
FhirAuditRepository.getAuditEvents().clear();
FhirAuditRepository.clearAuditEvents();
}

@Test
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ http://openehealth.org/schema/ipf-commons-core.xsd">

<ipf:globalContext id="globalContext"/>

<bean id="auditContext" class="org.openehealth.ipf.commons.audit.DefaultAuditContext">
<bean id="auditContext" class="org.openehealth.ipf.commons.audit.DefaultBalpAuditContext">
<property name="auditEnabled" value="true"/>
<property name="tlsParameters" ref="tlsParameters" />
<property name="auditMessageQueue" ref="auditMessageQueue"/>
@@ -44,6 +44,7 @@ http://openehealth.org/schema/ipf-commons-core.xsd">
<property name="auditSourceId" value="IPF"/>
<property name="auditEnterpriseSiteId" value="IPF"/>
<property name="auditRepositoryPort" ref="auditRepositoryPort" />
<property name="auditRepositoryContextPath" ref="auditRepositoryContextPath" />
</bean>

<bean id="tlsParameters" class="org.openehealth.ipf.commons.audit.CustomTlsParameters">
@@ -75,6 +76,10 @@ http://openehealth.org/schema/ipf-commons-core.xsd">
class="org.openehealth.ipf.commons.ihe.fhir.extension.FhirAuditRepository"
factory-method="getServerHttpsPort" />

<bean id="auditRepositoryContextPath"
class="org.openehealth.ipf.commons.ihe.fhir.extension.FhirAuditRepository"
factory-method="getServerContextPath" />

<bean id="auditMessageQueue"
class="org.openehealth.ipf.commons.ihe.fhir.audit.queue.FhirDelegateMockedMessageQueue"/>

0 comments on commit 68b7dfa

Please sign in to comment.