diff --git a/src/main/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcService.java b/src/main/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcService.java index dccea1f32c20d3196e5921f628c8a392483edfc7..e07d5fc75492ba41cc2d3278aa3c975d275b39a2 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcService.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcService.java @@ -25,18 +25,23 @@ import de.ozgcloud.nachrichten.postfach.antragraum.*; import de.ozgcloud.vorgang.grpc.command.GrpcCommand; import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.server.service.GrpcService; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.NotImplementedException; -import org.springframework.stereotype.Service; -@Service +@GrpcService @RequiredArgsConstructor public class AntragsraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplBase { private final PersistPostfachNachrichtService postfachNachrichtService; private final NachrichtMapper mapper; + private final Saml2Verifier verifier; + private final Saml2Parser parser; + private final Saml2Decrypter decrypter; @Override public void findRueckfragen(GrpcFindRueckfragenRequest request, StreamObserver<GrpcFindRueckfragenResponse> streamObserver) { - String postfachId = verifyToken(request.getSamlToken(), request.getPostfachId()); + verifyToken(request.getSamlToken()); + String postfachId = decrypter.decryptPostfachId(parser.parse(request.getSamlToken())); var rueckfragen = postfachNachrichtService.findRueckfragen(postfachId).map(mapper::toGrpc).toList(); var response = GrpcFindRueckfragenResponse.newBuilder().addAllRueckfrage(rueckfragen).build(); @@ -63,13 +68,9 @@ public class AntragsraumGrpcService extends AntragraumServiceGrpc.AntragraumServ } void verifyToken(String token) { - throw new SecurityException("not validated yet"); + var errors = verifier.verify(token); + if (CollectionUtils.isNotEmpty(errors)) { + throw new SecurityException("SAML Token verification failed. Errors: %s".formatted(errors)); + } } - - String verifyToken(String token, String postfachId) { - verifyToken(token); - - return postfachId; - } - } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragsraum/BayernIdSamlConfiguration.java b/src/main/java/de/ozgcloud/nachrichten/antragsraum/BayernIdSamlConfiguration.java index 728c4bbc23878e8f7824de5c6b756f38b0336293..429b962d387a9d4671d3e403dc394e215db4a799 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragsraum/BayernIdSamlConfiguration.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragsraum/BayernIdSamlConfiguration.java @@ -22,9 +22,11 @@ package de.ozgcloud.nachrichten.antragsraum; import jakarta.annotation.PostConstruct; import lombok.extern.log4j.Log4j2; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.config.InitializationService; import org.opensaml.core.criterion.EntityIdCriterion; @@ -76,10 +78,6 @@ public class BayernIdSamlConfiguration { private String entityId; @Value("${ozgcloud.bayernid.saml.metadata-uri}") private Resource metadataUri; - @Value("${ozgcloud.bayernid.saml.signing.private-key-location}") - private Resource signingPrivateKey; - @Value("${ozgcloud.bayernid.saml.signing.certificate-location}") - private Resource signingCertificate; @Value("${ozgcloud.bayernid.saml.decryption.private-key-location}") private Resource decryptionPrivateKey; @Value("${ozgcloud.bayernid.saml.decryption.certificate-location}") @@ -113,6 +111,37 @@ public class BayernIdSamlConfiguration { } } + static ParserPool getParserPool() throws ComponentInitializationException { + BasicParserPool parserPool = new BasicParserPool(); + parserPool.setMaxPoolSize(100); + parserPool.setCoalescing(true); + parserPool.setIgnoreComments(true); + parserPool.setIgnoreElementContentWhitespace(true); + parserPool.setNamespaceAware(true); + parserPool.setExpandEntityReferences(false); + parserPool.setXincludeAware(false); + + final Map<String, Boolean> features = createFeatureMap(); + + parserPool.setBuilderFeatures(features); + + parserPool.setBuilderAttributes(new HashMap<>()); + + parserPool.initialize(); + + return parserPool; + } + + private static Map<String, Boolean> createFeatureMap() { + final Map<String, Boolean> features = new HashMap<>(); + features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE); + features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE); + features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE); + features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE); + features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE); + return features; + } + @Bean Saml2Verifier verifier() { return new Saml2Verifier(parser(), trustEngine(), verificationCriteria()); @@ -181,15 +210,16 @@ public class BayernIdSamlConfiguration { } } } catch (IOException e) { - LOG.error("Error reading idp metadata. ", e); - throw new Saml2Exception("Error reading medatadata.", e); + throw new Saml2Exception("Error reading idp metadata.", e); + } catch (ComponentInitializationException | XMLParserException e) { + throw new Saml2Exception("Error initializing parser pool.", e); } throw new Saml2Exception("No IDPSSO Descriptors found"); } - private XMLObject xmlObject(InputStream inputStream) { - Document document = document(inputStream); + private XMLObject xmlObject(InputStream inputStream) throws ComponentInitializationException, XMLParserException { + Document document = getParserPool().parse(inputStream); Element element = document.getDocumentElement(); Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element); if (unmarshaller == null) { @@ -202,45 +232,6 @@ public class BayernIdSamlConfiguration { } } - private Document document(InputStream inputStream) { - try { - return getParserPool().parse(inputStream); - } catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - static ParserPool getParserPool() throws Exception { - BasicParserPool parserPool = new BasicParserPool(); - parserPool.setMaxPoolSize(100); - parserPool.setCoalescing(true); - parserPool.setIgnoreComments(true); - parserPool.setIgnoreElementContentWhitespace(true); - parserPool.setNamespaceAware(true); - parserPool.setExpandEntityReferences(false); - parserPool.setXincludeAware(false); - - final Map<String, Boolean> features = createFeatureMap(); - - parserPool.setBuilderFeatures(features); - - parserPool.setBuilderAttributes(new HashMap<>()); - - parserPool.initialize(); - - return parserPool; - } - - private static Map<String, Boolean> createFeatureMap() { - final Map<String, Boolean> features = new HashMap<>(); - features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE); - features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE); - features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE); - features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE); - features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE); - return features; - } - List<Saml2X509Credential> getVerificationCertificates(EntityDescriptor descriptor) { IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); if (idpssoDescriptor == null) { diff --git a/src/test/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcServiceTest.java index 27e3f85376469e9657409e9fc6beaa2197d336e8..e9d257fff49c2cdb0f5bcd21edf27cbd8020ce9b 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragsraum/AntragsraumGrpcServiceTest.java @@ -20,11 +20,104 @@ package de.ozgcloud.nachrichten.antragsraum; +import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService; +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.antragraum.GrpcFindRueckfragenResponse; +import de.ozgcloud.nachrichten.postfach.antragraum.GrpcRueckfrage; +import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.opensaml.saml.saml2.core.Response; + +import java.util.stream.Stream; + +import static org.mockito.Mockito.*; class AntragsraumGrpcServiceTest { + @Spy + @InjectMocks + private AntragsraumGrpcService antragsraumGrpcService; + @Mock + private PersistPostfachNachrichtService postfachNachrichtService; + @Mock + private NachrichtMapper mapper; + @Mock + private Saml2Verifier verifier; + @Mock + private Saml2Parser parser; + @Mock + private Saml2Decrypter decrypter; + + @Nested + class TestFindRueckfragen { + @Mock + private StreamObserver<GrpcFindRueckfragenResponse> streamObserver; + + @Test + void shouldCallVerify() { + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(antragsraumGrpcService).verifyToken(anyString()); + } + + @Test + void shouldCallVerifier() { + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(verifier).verify(anyString()); + } + + @Test + void shouldCallDecrypt() { + when(parser.parse(anyString())).thenReturn(mock(Response.class)); + + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(decrypter).decryptPostfachId(any(Response.class)); + } + + @Test + void shouldCallPostfachService() { + when(decrypter.decryptPostfachId(any())).thenReturn(GrpcFindRueckfrageRequestTestFactory.POSTFACH_ID); + + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(postfachNachrichtService).findRueckfragen(anyString()); + } + + @Nested + class TestFindRueckfragenGrpc { + @BeforeEach + void setup() { + when(postfachNachrichtService.findRueckfragen(any())).thenReturn(Stream.of(PostfachNachrichtTestFactory.create())); + when(mapper.toGrpc(any(PostfachNachricht.class))).thenReturn(GrpcRueckfrage.getDefaultInstance()); + } + + @Test + void shouldCallMapper() { + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(mapper).toGrpc(any(PostfachNachricht.class)); + } + + @Test + void shouldCallOnNext() { + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); + + verify(streamObserver).onNext(any(GrpcFindRueckfragenResponse.class)); + } + + @Test + void shouldCallOnCompleted() { + antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); - @BeforeEach - void setUp() { + verify(streamObserver).onCompleted(); + } + } } } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/antragsraum/GrpcFindRueckfrageRequestTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/antragsraum/GrpcFindRueckfrageRequestTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..3e60f8a6769c653725d220ca05af6d1b4999c367 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/antragsraum/GrpcFindRueckfrageRequestTestFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024. + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.nachrichten.antragsraum; + +import de.ozgcloud.common.test.TestUtils; +import de.ozgcloud.nachrichten.postfach.antragraum.GrpcFindRueckfragenRequest; + +import java.util.UUID; + +public class GrpcFindRueckfrageRequestTestFactory { + static final String POSTFACH_ID = UUID.randomUUID().toString(); + static final String SAML_TOKEN = TestUtils.loadTextFile("SamlResponse.xml"); + + static GrpcFindRueckfragenRequest create() { + return createBuilder().build(); + } + + static GrpcFindRueckfragenRequest.Builder createBuilder() { + return GrpcFindRueckfragenRequest.newBuilder() + .setPostfachId(POSTFACH_ID) + .setSamlToken(SAML_TOKEN); + } +}