From 5392abf75b5a39eb8b21616715d172766467d819 Mon Sep 17 00:00:00 2001 From: Thomas Richner Date: Sat, 30 Mar 2024 13:19:27 +0100 Subject: [PATCH] EPA-81: Added authorization related operations --- .../oviva/epa/client/KonnektorService.java | 21 ++++- .../ExceptionMappedKonnektorService.java | 13 +++ .../client/internal/KonnektorServiceImpl.java | 86 ++++++++++++++++++- .../svc/PhrManagementServiceClient.java | 41 +++++++-- .../svc/phr/ContextHeaderBuilder.java | 8 +- .../epa/client/internal/svc/utils/Models.java | 12 +++ .../epa/client/model/AuthorizationEntry.java | 5 ++ .../client/model/AuthorizedApplication.java | 9 ++ .../KonnektorServiceAcceptanceTest.java | 24 ++++++ 9 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/utils/Models.java create mode 100644 diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizationEntry.java create mode 100644 diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizedApplication.java diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/KonnektorService.java b/diga-epa-client/src/main/java/com/oviva/epa/client/KonnektorService.java index 1ddc4a8..06336d8 100644 --- a/diga-epa-client/src/main/java/com/oviva/epa/client/KonnektorService.java +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/KonnektorService.java @@ -1,9 +1,6 @@ package com.oviva.epa.client; -import com.oviva.epa.client.model.Card; -import com.oviva.epa.client.model.PinStatus; -import com.oviva.epa.client.model.RecordIdentifier; -import com.oviva.epa.client.model.WriteDocumentResponse; +import com.oviva.epa.client.model.*; import de.gematik.epa.ihe.model.document.Document; import de.gematik.epa.ihe.model.simple.AuthorInstitution; import edu.umd.cs.findbugs.annotations.NonNull; @@ -20,6 +17,22 @@ public interface KonnektorService { @NonNull PinStatus verifySmcPin(@NonNull String cardHandle); + /** Determines the authorization state for a given electronic health record. */ + @NonNull + List getAuthorizationState(@NonNull RecordIdentifier recordIdentifier); + + /** + * Returns a list of all electronic health records the Konnektor has access to.
+ * IMPORTANT: There is strict rate-limiting on this endpoint. Currently once per day. + * + *

See also gemILF_PS_ePA_V1.4.3 + * + *

"A_19008-01 - Einschränkung der Häufigkeit der Abfrage getAuthorizationList Das PS DARF + * den Request getAuthorizationList NICHT öfter als einmal pro Tag stellen" + */ + @NonNull + List getAuthorizationList(); + @NonNull WriteDocumentResponse writeDocument( @NonNull RecordIdentifier recordIdentifier, @NonNull Document document); diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/ExceptionMappedKonnektorService.java b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/ExceptionMappedKonnektorService.java index c912435..6ef8515 100644 --- a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/ExceptionMappedKonnektorService.java +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/ExceptionMappedKonnektorService.java @@ -38,6 +38,19 @@ public List getCardsInfo() { return wrap(() -> delegate.verifySmcPin(cardHandle)); } + @NonNull + @Override + public List getAuthorizationState( + @NonNull RecordIdentifier recordIdentifier) { + return wrap(() -> delegate.getAuthorizationState(recordIdentifier)); + } + + @NonNull + @Override + public List getAuthorizationList() { + return wrap(delegate::getAuthorizationList); + } + @Override public @NonNull WriteDocumentResponse writeDocument( @NonNull RecordIdentifier recordIdentifier, @NonNull Document document) { diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/KonnektorServiceImpl.java b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/KonnektorServiceImpl.java index 7ce2f64..316a7aa 100644 --- a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/KonnektorServiceImpl.java +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/KonnektorServiceImpl.java @@ -21,16 +21,25 @@ import de.gematik.epa.ihe.model.simple.SubmissionSetMetadata; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; +import javax.xml.datatype.XMLGregorianCalendar; import oasis.names.tc.ebxml_regrep.xsd.rs._3.RegistryErrorList; import oasis.names.tc.ebxml_regrep.xsd.rs._3.RegistryResponseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import telematik.ws.conn.cardservicecommon.xsd.v2_0.CardTypeType; +import telematik.ws.conn.connectorcommon.xsd.v5_0.ResultEnum; +import telematik.ws.conn.connectorcommon.xsd.v5_0.Status; import telematik.ws.conn.exception.FaultMessageException; +import telematik.ws.conn.phrs.phrmanagementservice.xsd.v2_5.GetAuthorizationListResponse; +import telematik.ws.conn.phrs.phrmanagementservice.xsd.v2_5.GetAuthorizationStateResponse; public class KonnektorServiceImpl implements KonnektorService { - private static final String STATUS_SUCCESS = + private static Logger log = LoggerFactory.getLogger(KonnektorServiceImpl.class); + private static final String REGISTRY_STATUS_SUCCESS = "urn:oasis:names:tc:ebxml-regrep:ResponseStatusType:Success"; private final EventServiceClient eventServiceClient; private final CardServiceClient cardServiceClient; @@ -67,7 +76,6 @@ public List getAuthorInstitutions() { @NonNull @Override public List getCardsInfo() { - // don't be fooled by the name, this returns a list of cards... return eventServiceClient.getSmbInfo().getCards().getCard().stream() .map(c -> new Card(c.getCardHandle(), c.getCardHolderName(), mapCardType(c.getCardType()))) .toList(); @@ -87,6 +95,78 @@ private Card.CardType mapCardType(CardTypeType t) { return PinStatus.valueOf(response.getPinStatus().name()); } + @Override + @NonNull + public List getAuthorizationState( + @NonNull RecordIdentifier recordIdentifier) { + + var res = + phrManagementServiceClient.getAuthorizationState( + recordIdentifier.kvnr(), recordIdentifier.homeCommunityId()); + validateAuthorizationStateResponse(res); + + return res.getAuthorizationStatusList().getAuthorizedApplication().stream() + .map(e -> new AuthorizedApplication(e.getApplicationName(), parseDate(e.getValidTo()))) + .toList(); + } + + private void validateAuthorizationStateResponse(GetAuthorizationStateResponse res) { + var result = parseResult(res.getStatus()); + if (ResultEnum.OK.equals(result)) { + return; + } + + if (ResultEnum.WARNING.equals(result)) { + log.atDebug().addKeyValue("response", res::toString).log("warning response from Konnektor"); + return; + } + + throw new KonnektorException( + "bad GetAuthorizationStateResponse: " + res.getStatus().toString()); + } + + @Override + @NonNull + public List getAuthorizationList() { + var res = phrManagementServiceClient.getAuthorizationList(); + + validateAuthorizationListResponse(res); + + return res.getAuthorizationList().getAuthorizationEntry().stream() + .map( + e -> + new AuthorizationEntry( + new RecordIdentifier( + e.getRecordIdentifier().getInsurantId().getExtension(), + e.getRecordIdentifier().getHomeCommunityId()), + parseDate(e.getValidTo()))) + .toList(); + } + + private LocalDate parseDate(XMLGregorianCalendar encoded) { + return LocalDate.of(encoded.getYear(), encoded.getMonth(), encoded.getDay()); + } + + private void validateAuthorizationListResponse(GetAuthorizationListResponse res) { + var result = parseResult(res.getStatus()); + if (ResultEnum.OK.equals(result)) { + return; + } + + throw new KonnektorException("bad GetAuthorizationListResponse: " + res.getStatus().toString()); + } + + private ResultEnum parseResult(Status status) { + var str = status.getResult().toUpperCase(Locale.ROOT); + try { + // upper case before parsing, the Result allows 'Warning' while the enum does not + return ResultEnum.valueOf(str); + } catch (IllegalArgumentException e) { + // let's not fail on an unknown enum, response might still be useful + return ResultEnum.WARNING; + } + } + @Override public @NonNull WriteDocumentResponse writeDocument( @NonNull RecordIdentifier recordIdentifier, @NonNull Document document) { @@ -146,7 +226,7 @@ private Card.CardType mapCardType(CardTypeType t) { private void validateResponse(RegistryResponseType res) { - if (STATUS_SUCCESS.equals(res.getStatus())) { + if (REGISTRY_STATUS_SUCCESS.equals(res.getStatus())) { return; } var errors = diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/PhrManagementServiceClient.java b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/PhrManagementServiceClient.java index b831c00..ea3b742 100644 --- a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/PhrManagementServiceClient.java +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/PhrManagementServiceClient.java @@ -1,10 +1,12 @@ package com.oviva.epa.client.internal.svc; import com.oviva.epa.client.internal.svc.model.KonnektorContext; +import com.oviva.epa.client.internal.svc.utils.Models; import telematik.ws.conn.connectorcontext.xsd.v2_0.ContextType; import telematik.ws.conn.phrs.phrmanagementservice.wsdl.v2_5.PHRManagementServicePortType; -import telematik.ws.conn.phrs.phrmanagementservice.xsd.v2_5.ObjectFactory; +import telematik.ws.conn.phrs.phrmanagementservice.xsd.v2_5.*; import telematik.ws.fd.phr.phrcommon.xsd.v1_1.InsurantIdType; +import telematik.ws.fd.phr.phrcommon.xsd.v1_1.RecordIdentifierType; public class PhrManagementServiceClient { @@ -17,14 +19,31 @@ public PhrManagementServiceClient( this.konnektorContext = konnektorContext; } + public GetAuthorizationStateResponse getAuthorizationState(String knvr, String homeCommunityId) { + var context = getContext(); + + var req = + new GetAuthorizationState() + .withContext(context) + .withUserAgent("PS_123/V1.1.0/gematik") // TODO where does that come from? + .withRecordIdentifier( + new RecordIdentifierType() + .withInsurantId(Models.fromKvnr(knvr)) + .withHomeCommunityId(homeCommunityId)); + + return phrManagementService.getAuthorizationState(req); + } + + public GetAuthorizationListResponse getAuthorizationList() { + + var context = getContext(); + var req = new GetAuthorizationList().withContext(context); + return phrManagementService.getAuthorizationList(req); + } + public String getHomeCommunityID(String kvnr) { - var context = - new ContextType() - .withClientSystemId(konnektorContext.clientSystemId()) - .withMandantId(konnektorContext.mandantId()) - .withUserId(konnektorContext.userId()) - .withWorkplaceId(konnektorContext.workplaceId()); + var context = getContext(); var insurantId = new InsurantIdType().withExtension(kvnr).withRoot(new InsurantIdType().getRoot()); @@ -37,4 +56,12 @@ public String getHomeCommunityID(String kvnr) { return phrManagementService.getHomeCommunityID(getHomeCommunityIDRequest).getHomeCommunityID(); } + + private ContextType getContext() { + return new ContextType() + .withClientSystemId(konnektorContext.clientSystemId()) + .withMandantId(konnektorContext.mandantId()) + .withUserId(konnektorContext.userId()) + .withWorkplaceId(konnektorContext.workplaceId()); + } } diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/phr/ContextHeaderBuilder.java b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/phr/ContextHeaderBuilder.java index 544807e..5617fbc 100644 --- a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/phr/ContextHeaderBuilder.java +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/phr/ContextHeaderBuilder.java @@ -2,9 +2,9 @@ import com.oviva.epa.client.internal.svc.model.KonnektorContext; import com.oviva.epa.client.internal.svc.phr.model.RecordIdentifier; +import com.oviva.epa.client.internal.svc.utils.Models; import telematik.ws.conn.connectorcontext.xsd.v2_0.ContextType; import telematik.ws.conn.phrs.phrservice.xsd.v2_0.ContextHeader; -import telematik.ws.fd.phr.phrcommon.xsd.v1_1.InsurantIdType; import telematik.ws.fd.phr.phrcommon.xsd.v1_1.RecordIdentifierType; public class ContextHeaderBuilder { @@ -20,10 +20,6 @@ public static ContextHeaderBuilder fromKonnektorContext(KonnektorContext ctx) { return new ContextHeaderBuilder(ctx); } - private static InsurantIdType fromKvnr(String kvnr) { - return new InsurantIdType().withExtension(kvnr).withRoot(new InsurantIdType().getRoot()); - } - private static ContextType buildKonnektorContext(KonnektorContext ctx) { return new ContextType() .withClientSystemId(ctx.clientSystemId()) @@ -52,7 +48,7 @@ public ContextHeader build() { RecordIdentifierType buildRecordIdentifier(RecordIdentifier ctx) { return new RecordIdentifierType() - .withInsurantId(fromKvnr(ctx.kvnr())) + .withInsurantId(Models.fromKvnr(ctx.kvnr())) .withHomeCommunityId(ctx.homeCommunityId()); } } diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/utils/Models.java b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/utils/Models.java new file mode 100644 index 0000000..c574014 --- /dev/null +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/internal/svc/utils/Models.java @@ -0,0 +1,12 @@ +package com.oviva.epa.client.internal.svc.utils; + +import telematik.ws.fd.phr.phrcommon.xsd.v1_1.InsurantIdType; + +public class Models { + + private Models() {} + + public static InsurantIdType fromKvnr(String kvnr) { + return new InsurantIdType().withExtension(kvnr).withRoot(new InsurantIdType().getRoot()); + } +} diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizationEntry.java b/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizationEntry.java new file mode 100644 index 0000000..0e03af2 --- /dev/null +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizationEntry.java @@ -0,0 +1,5 @@ +package com.oviva.epa.client.model; + +import java.time.LocalDate; + +public record AuthorizationEntry(RecordIdentifier recordIdentifier, LocalDate validTo) {} diff --git a/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizedApplication.java b/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizedApplication.java new file mode 100644 index 0000000..596523c --- /dev/null +++ b/diga-epa-client/src/main/java/com/oviva/epa/client/model/AuthorizedApplication.java @@ -0,0 +1,9 @@ +package com.oviva.epa.client.model; + +import java.time.LocalDate; + +/** + * @param name the name of the application, e.g. 'ePA' + * @param validTo the date until when this authorization is valid + */ +public record AuthorizedApplication(String name, LocalDate validTo) {} diff --git a/diga-epa-client/src/test/java/com/oviva/epa/client/KonnektorServiceAcceptanceTest.java b/diga-epa-client/src/test/java/com/oviva/epa/client/KonnektorServiceAcceptanceTest.java index 5c58259..ce95251 100644 --- a/diga-epa-client/src/test/java/com/oviva/epa/client/KonnektorServiceAcceptanceTest.java +++ b/diga-epa-client/src/test/java/com/oviva/epa/client/KonnektorServiceAcceptanceTest.java @@ -76,6 +76,30 @@ void testGetCardsInfo() throws Exception { assertThat(pinStatus, equalTo(PinStatus.VERIFIED)); } + @Test + void getAuthorizationList() { + + // IMPORTANT: This is strictly rate-limited to once a day! + var authorizations = konnektorService.getAuthorizationList(); + + // check whether our test KVNR is among them + assertTrue(authorizations.stream().anyMatch(a -> a.recordIdentifier().kvnr().equals(KVNR))); + } + + @Test + void getAuthorizationState() { + + // 1) get home community + var hcid = konnektorService.getHomeCommunityID(KVNR); + var recordIdentifier = new RecordIdentifier(KVNR, hcid); + + // 2) get the authorization state + var authorizedApplications = konnektorService.getAuthorizationState(recordIdentifier); + + // 3) check whether we're authorized for the ePA + assertTrue(authorizedApplications.stream().anyMatch(a -> "ePA".equals(a.name()))); + } + @Test void writeDocument() {