diff --git a/etp-backend/deps.edn b/etp-backend/deps.edn index 41e5634da..2f791c428 100644 --- a/etp-backend/deps.edn +++ b/etp-backend/deps.edn @@ -1,6 +1,7 @@ {:paths ["src/main/clj" "src/main/sql" "src/main/resources"] + :mvn/repos {"shibboleth" {:url "https://build.shibboleth.net/maven/releases/"}} :deps {org.clojure/clojure {:mvn/version "1.10.1"} ch.qos.logback/logback-classic {:mvn/version "1.4.8"} org.slf4j/log4j-over-slf4j {:mvn/version "1.7.30"} @@ -71,7 +72,9 @@ com.jcraft/jsch {:mvn/version "0.1.55"} com.sun.mail/javax.mail {:mvn/version "1.6.2"} - org.apache.ws.security/wss4j {:mvn/version "1.6.19"} + org.apache.wss4j/wss4j-ws-security-dom {:mvn/version "3.0.0"} + org.apache.wss4j/wss4j-ws-security-common {:mvn/version "3.0.0"} + com.sun.xml.messaging.saaj/saaj-impl {:mvn/version "3.0.2"} org.apache.axis/axis {:mvn/version "1.4"} commons-io/commons-io {:mvn/version "2.11.0"} ;; commons-discovery is needed by some other library dynamically at runtime @@ -82,7 +85,8 @@ "src/test/resources"] :extra-deps {eftest/eftest {:mvn/version "0.5.9"} prismatic/schema-generators {:mvn/version "0.1.3"} - ring/ring-mock {:mvn/version "0.4.0"}} + ring/ring-mock {:mvn/version "0.4.0"} + org.xmlunit/xmlunit-core {:mvn/version "2.9.1"}} :jvm-opts ["-Djava.awt.headless=true"]} :test {:main-opts ["-e" "(run-tests-and-exit!)" "-A:dev"]} :test-ci {:main-opts ["-e" "(run-tests-with-junit-reporter-and-exit!)" "-A:dev"]} diff --git a/etp-backend/src/main/clj/solita/etp/service/suomifi_viestit.clj b/etp-backend/src/main/clj/solita/etp/service/suomifi_viestit.clj index 361884e3b..6f44f97a5 100644 --- a/etp-backend/src/main/clj/solita/etp/service/suomifi_viestit.clj +++ b/etp-backend/src/main/clj/solita/etp/service/suomifi_viestit.clj @@ -9,12 +9,14 @@ [clj-http.conn-mgr :as conn-mgr] [clojure.string :as str] [solita.etp.exception :as exception]) - (:import (org.apache.ws.security WSConstants WSEncryptionPart) - (org.apache.ws.security.components.crypto CryptoFactory) - (org.apache.ws.security.message WSSecSignature WSSecHeader WSSecTimestamp) - (java.util Properties) - (java.io ByteArrayInputStream) + (:import (java.util Properties) + (java.io ByteArrayInputStream ByteArrayOutputStream) + (org.apache.xml.security Init) (org.apache.axis.soap MessageFactoryImpl) + (org.apache.wss4j.common WSEncryptionPart) + (org.apache.wss4j.common.crypto CryptoFactory) + (org.apache.wss4j.dom WSConstants) + (org.apache.wss4j.dom.message WSSecHeader WSSecSignature WSSecTimestamp) (org.apache.xml.security.c14n Canonicalizer) (org.apache.axis.configuration NullProvider) (org.apache.axis.client AxisClient) @@ -22,6 +24,7 @@ ;; register default algorithms (Canonicalizer/registerDefaultAlgorithms) +(Init/init) (defn- request-create-xml [data] (clostache/render-resource (str "suomifi/viesti.xml") data)) @@ -30,7 +33,7 @@ (log/debug request) (if config/suomifi-viestit-endpoint-url (http/post config/suomifi-viestit-endpoint-url - (cond-> {:body request + (cond-> {:body request :throw-exceptions false} config/suomifi-viestit-proxy? (assoc @@ -49,11 +52,11 @@ (-> axisMessage .getSOAPEnvelope .getAsDocument)))) (defn- document->signed-request [document] - (let [c14n (Canonicalizer/getInstance Canonicalizer/ALGO_ID_C14N_WITH_COMMENTS) - canonicalMessage (.canonicalizeSubtree c14n document) - in (ByteArrayInputStream. canonicalMessage)] - (-> (.createMessage (MessageFactoryImpl.) nil in) - .getSOAPEnvelope .getAsString))) + (let [c14n (Canonicalizer/getInstance Canonicalizer/ALGO_ID_C14N_WITH_COMMENTS)] + (with-open [os (ByteArrayOutputStream.)] + (.canonicalizeSubtree c14n document os) + (-> (.createMessage (MessageFactoryImpl.) nil (ByteArrayInputStream. (.toByteArray os))) + .getSOAPEnvelope .getAsString)))) (defn- signSOAPEnvelope [request keystore-file keystore-password keystore-alias] (let [properties (doto (Properties.) @@ -63,21 +66,21 @@ (.setProperty "signer.username" keystore-alias) (.setProperty "signer.password" keystore-password)) crypto (CryptoFactory/getInstance properties) - signer (doto (WSSecSignature.) + doc (raw-request->document request) + header (doto (WSSecHeader. doc) + (.setMustUnderstand true) + (.insertSecurityHeader)) + signer (doto (WSSecSignature. header) (.setUserInfo keystore-alias keystore-password) (.setKeyIdentifierType WSConstants/BST_DIRECT_REFERENCE) (.setUseSingleCertificate true)) - doc (raw-request->document request) - header (doto (WSSecHeader.) - (.setMustUnderstand true) - (.insertSecurityHeader doc)) - timestamp (doto (WSSecTimestamp.) - (.setTimeToLive 60)) - build-doc (.build timestamp doc header) timestampPart (WSEncryptionPart. "Timestamp", WSConstants/WSU_NS, "") bodyPart (WSEncryptionPart. WSConstants/ELEM_BODY, WSConstants/URI_SOAP11_ENV, "")] - (.setParts signer [timestampPart bodyPart]) - (document->signed-request (.build signer build-doc crypto header)))) + (doto (WSSecTimestamp. header) + (.setTimeToLive 60) + (.build)) + (.addAll (.getParts signer) (list timestampPart bodyPart)) + (document->signed-request (.build signer crypto)))) (defn- read-response->xml [response] (-> response xml/string->xml xml/without-soap-envelope first xml/with-kebab-case-tags)) @@ -105,12 +108,12 @@ (throw (ex-info (str "Sending suomifi message " (-> request :sanoma :tunniste) " failed.") - {:type type + {:type type :endpoint-url config/suomifi-viestit-endpoint-url - :request (select-keys* request [[:sanoma :tunniste] - [:kysely :kohteet :nimike]]) - :response response - :cause (ex-data cause)} + :request (select-keys* request [[:sanoma :tunniste] + [:kysely :kohteet :nimike]]) + :response response + :cause (ex-data cause)} cause))) (defn- assert-status! [response] @@ -121,8 +124,8 @@ (defn- read-response [request response] (try (some-> response assert-status! :body response-parser coerce-response!) - (catch Throwable t - (throw-ex-info! :suomifi-viestit-invalid-response request response t)))) + (catch Throwable t + (throw-ex-info! :suomifi-viestit-invalid-response request response t)))) (defn- handle-request! [request keystore-file keystore-password keystore-alias] (try diff --git a/etp-backend/src/test/clj/solita/etp/service/suomifi_viestit_test.clj b/etp-backend/src/test/clj/solita/etp/service/suomifi_viestit_test.clj index dbf4d1631..b470ba665 100644 --- a/etp-backend/src/test/clj/solita/etp/service/suomifi_viestit_test.clj +++ b/etp-backend/src/test/clj/solita/etp/service/suomifi_viestit_test.clj @@ -2,18 +2,72 @@ (:require [clojure.test :as t] [solita.etp.test-system :as ts] [clojure.java.io :as io] - [solita.etp.service.valvonta-kaytto.suomifi-viestit :as suomifi-viestit] + [solita.etp.service.valvonta-kaytto.suomifi-viestit :as valvonta-kaytto.suomifi-viestit] + [solita.etp.service.suomifi-viestit :as service.suomifi-viestit] [clojure.string :as str] [solita.etp.service.pdf :as pdf]) - (:import (java.time LocalDate))) + (:import (java.time LocalDate) + (org.w3c.dom Node) + (org.xmlunit.diff Comparison$Detail ComparisonListener ComparisonResult ComparisonType DOMDifferenceEngine DifferenceEvaluator DifferenceEvaluators) + (org.xmlunit.builder Input) + (org.xmlunit.util Predicate))) (t/use-fixtures :each ts/fixture) (defn- handle-request [request-resource response-resource response-status] (fn [request] (t/is (= (-> request str/trim) (-> request-resource io/resource slurp str/trim))) + {:body (-> response-resource io/resource slurp) + :status response-status})) + +(defn- in-tree? [node-name ^Node node] + (cond + (= node-name (.getNodeName node)) true + (nil? (.getParentNode node)) false + :else (in-tree? node-name (.getParentNode node)))) + +(defn- in-tree-filter [interesting-tree] + (reify Predicate + (test [_ node] + (if (= "soapenv:Envelope" (->> node (cast Node) .getNodeName)) ; Can't skip the top level element + true + (in-tree? interesting-tree node))))) + +(defn- difference-listener [] + (reify ComparisonListener (comparisonPerformed [_ comparison _] + (t/is false comparison)))) ; ComparisonListener is called when comparison fails. Fail the test + +(defn- empty-node? [^Comparison$Detail target] + (str/blank? (-> target .getTarget .getTextContent))) + +(defn- handle-request-with-xml-compare [request-resource response-resource response-status] + (fn [request] + (let [wanted (-> request-resource io/resource Input/fromURL .build) + actual (-> request str Input/fromString .build)] + ;; Header contains attributes and text elements that are generated by encryption + ;; Check that they're in place, but ignore values + (doto (DOMDifferenceEngine.) + (.setNodeFilter (in-tree-filter "soapenv:Header")) + (.setDifferenceEvaluator (reify DifferenceEvaluator + (evaluate [_ comparison outcome] + (if (and (contains? #{ComparisonType/ATTR_VALUE ComparisonType/TEXT_VALUE} (-> comparison .getType)) + (= (-> comparison .getTestDetails empty-node?) (-> comparison .getControlDetails empty-node?))) + ComparisonResult/EQUAL + (.evaluate DifferenceEvaluators/Default comparison outcome))))) + (.addDifferenceListener (difference-listener)) + (.compare wanted actual)) + ;; Check the body but ignore the wsu:Id + (doto (DOMDifferenceEngine.) + (.setNodeFilter (in-tree-filter "soapenv:Body")) + (.setDifferenceEvaluator (reify DifferenceEvaluator + (evaluate [_ comparison outcome] + (if (= "wsu:Id" (-> comparison .getTestDetails .getTarget .getNodeName)) + ComparisonResult/EQUAL + (.evaluate DifferenceEvaluators/Default comparison outcome))))) + (.addDifferenceListener (difference-listener)) + (.compare wanted actual)) {:body (-> response-resource io/resource slurp) - :status response-status})) + :status response-status}))) (def valvonta {:id 1 :rakennustunnus "103515074X" @@ -50,24 +104,36 @@ :laskutus-salasana "0000"}) (t/deftest send-message-to-osapuoli-test - (with-bindings {#'solita.etp.service.suomifi-viestit/post! (handle-request "suomifi/viesti-request.xml" + (with-bindings {#'service.suomifi-viestit/post! (handle-request "suomifi/viesti-request.xml" "suomifi/viesti-response.xml" 202) - #'suomifi-viestit/now (fn [] "2021-09-08T06:21:03.625667Z") - #'suomifi-viestit/bytes->base64 (fn [_] "dGVzdGk=")} + #'valvonta-kaytto.suomifi-viestit/now (fn [] "2021-09-08T06:21:03.625667Z") + #'valvonta-kaytto.suomifi-viestit/bytes->base64 (fn [_] "dGVzdGk=")} - (t/is (= (:sanoma-tunniste (suomifi-viestit/send-message-to-osapuoli! valvonta toimenpide osapuoli document config)) + (t/is (= (:sanoma-tunniste (valvonta-kaytto.suomifi-viestit/send-message-to-osapuoli! valvonta toimenpide osapuoli document config)) "ARA-05.03.02-2021-31-ETP-KV-1-2-PERSON-1")))) (t/deftest send-message-to-osapuoli-id-already-exists-test - (with-bindings {#'solita.etp.service.suomifi-viestit/post! (handle-request "suomifi/viesti-request.xml" + (with-bindings {#'service.suomifi-viestit/post! (handle-request "suomifi/viesti-request.xml" "suomifi/viesti-id-already-exists-response.xml" 200) - #'suomifi-viestit/now (fn [] - "2021-09-08T06:21:03.625667Z") - #'suomifi-viestit/bytes->base64 (fn [_] - "dGVzdGk=")} + #'valvonta-kaytto.suomifi-viestit/now (fn [] + "2021-09-08T06:21:03.625667Z") + #'valvonta-kaytto.suomifi-viestit/bytes->base64 (fn [_] + "dGVzdGk=")} (t/is (thrown-with-msg? clojure.lang.ExceptionInfo #"Sending suomifi message ARA-05.03.02-2021-31-ETP-KV-1-2-PERSON-1 failed." - (suomifi-viestit/send-message-to-osapuoli! valvonta toimenpide osapuoli document config))))) \ No newline at end of file + (valvonta-kaytto.suomifi-viestit/send-message-to-osapuoli! valvonta toimenpide osapuoli document config))))) + +(t/deftest send-message-to-osapuoli-with-signing-test + (with-bindings {#'service.suomifi-viestit/post! (handle-request-with-xml-compare "suomifi/viesti-request-signed.xml" + "suomifi/viesti-response.xml" + 202) + #'valvonta-kaytto.suomifi-viestit/now (fn [] "2021-09-08T06:21:03.625667Z") + #'valvonta-kaytto.suomifi-viestit/bytes->base64 (fn [_] "dGVzdGk=")} + (let [config-with-keystore (merge config {:keystore-file (.getPath (io/resource "suomifi/store.jks")) + :keystore-password "password" + :keystore-alias "default"})] + (t/is (= (:sanoma-tunniste (valvonta-kaytto.suomifi-viestit/send-message-to-osapuoli! valvonta toimenpide osapuoli document config-with-keystore)) + "ARA-05.03.02-2021-31-ETP-KV-1-2-PERSON-1"))))) diff --git a/etp-backend/src/test/resources/suomifi/store.jks b/etp-backend/src/test/resources/suomifi/store.jks new file mode 100644 index 000000000..fbcf2f119 Binary files /dev/null and b/etp-backend/src/test/resources/suomifi/store.jks differ diff --git a/etp-backend/src/test/resources/suomifi/viesti-request-signed.xml b/etp-backend/src/test/resources/suomifi/viesti-request-signed.xml new file mode 100644 index 000000000..6f10d3e24 --- /dev/null +++ b/etp-backend/src/test/resources/suomifi/viesti-request-signed.xml @@ -0,0 +1,54 @@ +MIICqjCCAZICCQCWuHuS7T9O8DANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtUZXN0aW5nIGV0cDAgFw0yMzA3MDQwOTM3MTBaGA8yMTIzMDYxMDA5MzcxMFowFjEUMBIGA1UEAwwLVGVzdGluZyBldHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrUBLhTr17mVcaLrGDcO6i+WyF81pwO76gF0p+9a0z7bwhkRz+SC4dcr9cikRWZcI5t6KVeyLJlSmpQW2lZwf3Y36776lEWs6U1xSdJrc+g/SF4XAyy2kpil73AkGUxhVKdkR1UKLnvXZB7ik/RiAkkYvS/y5enUBQ8jBF5tV6fA8koDht/SPgF/sZJObfj/XBANX6Zs4yS1jqNn74c6DWwvUkO+ieiZtyXeRynnFbNebXv0gszrmLuaDojZqXmu5RMUi47tcmE/F5fiueeDMImGiht2AidgThlZspK84mV2MZfEEyAtkL3JEeU6ouhRKTK9bX+j5XqYXtFeQ4dj/hAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAC4t2ZW2A+UyCI6AOhRZ9DGzmkHEYFkcoE+q2hFBUm+uNnnK0IrJzmC6Sb4dnmB4xdEA/HqmE3IjL/8ITTuyE8bwx7kYlqcvzL6wLpChuLNFSPR8FCuBA9DKjgJpJGlkaoPbLnQuSMuQ7u+DZpzmVMIQLtB+8Ih3dQZk7J8J92cuy2YO/jj3nMCLtLaqTGE1dxc34M/C9WQjQGln03W0M7NkVcCMC8VffjIpkM7L/bcq9aI0EiBnqpT5n5F7yGMHCJ2aHFeGJMtjKZqcjKTKnQRAwDJJgCxygQurGLnswt5xEHZQgEUlxxF/iO/cGuW4Gp2ASL27fRtTKiicskCvlrs=c8ZscSKhEa7QDMi64ilTt1qfBc8=vGechQ2SqaMU/hHXvIsypJYDet4=LHDGSI/n9PGQnOdZhkp6G/zPcQLOmfiFHzaNrTYd2Wfq1WMYzT1Sj24Rdviw0Kg+2dLmB9Z0TkPX +98b3/k6TC+Ol1mEd8zoa9zQO6ZfHl9zsETfUJqUhxLkdwcv4/CqwaAhjB+TM7Esq+F9FaVOUiYZ+ +ZiBXRiZsJMJ9A9avl+OHXc1elZ/VquPL/UVZj9slhrTKiyFdzhDdO9fTQb0v57z9oCXXW5r91P4j +zMFOhQFs3WnTu8LBit2cP7tC9mH4hz6+1vmYvaCdG/b3lA5Dtj0hXYIJalyi7bpcHRzz1lJN40LG +IEYsd+Mzl5iR7qUHMPkRgqZyCEF6IAFy6+l2og==2023-07-04T12:31:05.742Z2023-07-04T12:32:05.742Z + + Organisaatio + OR + + ARA-05.03.02-2021-31-ETP-KV-1-2-PERSON-1 + 1.0 + OR + + + false + + + + + Testi Vastaanottaja + Testitie 1 A + 00000 + Kaupunki + FI + + + ARA-05.03.02-2021-31-ETP-KV-1-2-PERSON-1 + Tietopyyntö + 2021-09-08T06:21:03.625667Z + Tämän viestin liitteenä on tietopyyntö koskien rakennustasi: 103515074X +Hämeenkatu 10, 333100 Tampere +Tietopyyntöön on vastattava 01.01.2022 mennessä. + +Som bilaga till detta meddelande finns en begäran om information som gäller din byggnad: 103515074X +Hämeenkatu 10, 333100 Tammerfors +Begäran om information ska besvaras senast den 01.01.2022. + + + Tietopyyntö liitteenä + dGVzdGk= + application/pdf + tietopyynto.pdf + + + + + Edita + false + + 0000 + 0000 + + + \ No newline at end of file