From 6c3eec08ead0d0cb845c36f3659e5e26baa51715 Mon Sep 17 00:00:00 2001
From: chris-cwa <69595386+chris-cwa@users.noreply.github.com>
Date: Fri, 4 Jun 2021 15:49:40 +0200
Subject: [PATCH] Dcc Server (EXPOSUREAPP-7504) (#3347)

* Extend corona test data structures with digital covid certificate related properties.
+Some additional wiring, plumbing and tests for future PRs.

* LINTs

* Adjust TestRegistrationRequest to supply dcc consent and DOB on test registration.

* Remove explicit assignment, defaults are sufficient.

* A few additional unit tests to check defaults.

* DateOfBirthKey calculation, draft 1

* Fix date parser pattern.

* wip

* Adjust padding calculation to take the new dobHash into account.
Some refactoring to make it less complicated to adjust for future changes.

* klint, ofc.

* TestCertificate repo, draft1.

* DGC -> dcc

* TestCertificateRepository, draft 2

* TestCertificateRepository, draft 3

* TestCertificateRepository, draft 4

* Unit tests, draft 1.

* Add new app config parameters and implement delay mechanism.

* Unit tests, draft 2

* dcc server

* merge 2.4
add di module

* clean up

* fix exception

* add server environment

* fix test

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com>
Co-authored-by: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
---
 .../server/CovidCertificateApiV1.kt           | 33 ++++++++++++++++++
 .../server/CovidCertificateModule.kt          | 26 ++++++++++++++
 .../server/CovidCertificateServer.kt          | 34 ++++++++++++++++---
 .../covidcertificate/server/DccException.kt   |  3 ++
 .../environment/EnvironmentModule.kt          |  4 ++-
 .../environment/EnvironmentSetup.kt           |  7 +++-
 .../covidcertificate/DCCHttpClient.kt         |  8 +++++
 .../environment/covidcertificate/DCCModule.kt | 28 +++++++++++++++
 .../covidcertificate/DCCServerUrl.kt          |  8 +++++
 .../util/di/ApplicationComponent.kt           |  2 ++
 .../environment/EnvironmentSetupTest.kt       | 28 ++++++++++-----
 11 files changed, 165 insertions(+), 16 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateApiV1.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/DccException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCHttpClient.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCServerUrl.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateApiV1.kt
new file mode 100644
index 000000000..393236da3
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateApiV1.kt
@@ -0,0 +1,33 @@
+package de.rki.coronawarnapp.covidcertificate.server
+
+import com.google.gson.annotations.SerializedName
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface CovidCertificateApiV1 {
+
+    data class PublicKeyUploadRequest(
+        @SerializedName("registrationToken") val registrationToken: String,
+        @SerializedName("publicKey") val publicKey: String
+    )
+
+    @POST("/version/v1/publicKey")
+    suspend fun sendPublicKey(
+        @Body requestBody: PublicKeyUploadRequest
+    )
+
+    data class ComponentsRequest(
+        @SerializedName("registrationToken") val registrationToken: String,
+    )
+
+    data class ComponentsResponse(
+        @SerializedName("dek") val dek: String,
+        @SerializedName("dcc") val dcc: String
+    )
+
+    @POST("/version/v1/publicKey")
+    suspend fun getComponents(
+        @Body requestBody: ComponentsRequest
+    ): Response<ComponentsResponse>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateModule.kt
new file mode 100644
index 000000000..47ca09e5f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateModule.kt
@@ -0,0 +1,26 @@
+package de.rki.coronawarnapp.covidcertificate.server
+
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+import de.rki.coronawarnapp.environment.covidcertificate.DCCHttpClient
+import de.rki.coronawarnapp.environment.covidcertificate.DCCServerUrl
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+
+@Module
+class CovidCertificateModule {
+
+    @Reusable
+    @Provides
+    fun apiV1(
+        @DCCHttpClient httpClient: OkHttpClient,
+        @DCCServerUrl url: String,
+    ): CovidCertificateApiV1 {
+        return Retrofit.Builder()
+            .client(httpClient)
+            .baseUrl(url)
+            .build()
+            .create(CovidCertificateApiV1::class.java)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt
index 912482395..9e4eed79c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/CovidCertificateServer.kt
@@ -1,27 +1,51 @@
 package de.rki.coronawarnapp.covidcertificate.server
 
+import dagger.Lazy
 import dagger.Reusable
 import de.rki.coronawarnapp.coronatest.type.RegistrationToken
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.encryption.rsa.RSAKey
+import kotlinx.coroutines.withContext
 import timber.log.Timber
 import javax.inject.Inject
 
 @Reusable
-class CovidCertificateServer @Inject constructor() {
+class CovidCertificateServer @Inject constructor(
+    private val dccApi: Lazy<CovidCertificateApiV1>,
+    private val dispatcherProvider: DispatcherProvider
+) {
+
+    private val api: CovidCertificateApiV1
+        get() = dccApi.get()
 
     suspend fun registerPublicKeyForTest(
         testRegistrationToken: RegistrationToken,
         publicKey: RSAKey.Public,
-    ) {
+    ): Unit = withContext(dispatcherProvider.IO) {
         Timber.tag(TAG).v("registerPublicKeyForTest(token=%s, key=%s)", testRegistrationToken, publicKey)
-        throw NotImplementedError()
+        api.sendPublicKey(
+            requestBody = CovidCertificateApiV1.PublicKeyUploadRequest(
+                registrationToken = testRegistrationToken,
+                publicKey = publicKey.base64
+            )
+        )
     }
 
+    @Throws(DccException::class)
     suspend fun requestCertificateForTest(
         testRegistrationToken: RegistrationToken,
-    ): TestCertificateComponents {
+    ): TestCertificateComponents = withContext(dispatcherProvider.IO) {
         Timber.tag(TAG).v("requestCertificateForTest(token=%s)", testRegistrationToken)
-        throw NotImplementedError()
+        val response = api.getComponents(
+            requestBody = CovidCertificateApiV1.ComponentsRequest(testRegistrationToken)
+        )
+        // TODO replace with InvalidTestCertificateException + correct error codes
+        if (response.code() == 202) throw DccException()
+        val result = response.body() ?: throw DccException()
+        TestCertificateComponents(
+            dataEncryptionKeyBase64 = result.dek,
+            encryptedCoseTestCertificateBase64 = result.dcc
+        )
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/DccException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/DccException.kt
new file mode 100644
index 000000000..058cb8292
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/server/DccException.kt
@@ -0,0 +1,3 @@
+package de.rki.coronawarnapp.covidcertificate.server
+
+class DccException : Exception()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
index b99710e6d..10fa9b978 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.environment
 
 import dagger.Module
 import de.rki.coronawarnapp.environment.bugreporting.BugReportingServerModule
+import de.rki.coronawarnapp.environment.covidcertificate.DCCModule
 import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNModule
 import de.rki.coronawarnapp.environment.download.DownloadCDNModule
 import de.rki.coronawarnapp.environment.submission.SubmissionCDNModule
@@ -13,7 +14,8 @@ import de.rki.coronawarnapp.environment.verification.VerificationCDNModule
         SubmissionCDNModule::class,
         VerificationCDNModule::class,
         DataDonationCDNModule::class,
-        BugReportingServerModule::class
+        BugReportingServerModule::class,
+        DCCModule::class
     ]
 )
 class EnvironmentModule
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
index d44e8e88e..bc62e8bab 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
@@ -7,6 +7,7 @@ import com.google.gson.JsonObject
 import com.google.gson.JsonPrimitive
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.CROWD_NOTIFIER_PUBLIC_KEY
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DATA_DONATION
+import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DCC
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DOWNLOAD
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.LOG_UPLOAD
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SAFETYNET_API_KEY
@@ -37,7 +38,8 @@ class EnvironmentSetup @Inject constructor(
         DATA_DONATION("DATA_DONATION_CDN_URL"),
         LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"),
         SAFETYNET_API_KEY("SAFETYNET_API_KEY"),
-        CROWD_NOTIFIER_PUBLIC_KEY("CROWD_NOTIFIER_PUBLIC_KEY")
+        CROWD_NOTIFIER_PUBLIC_KEY("CROWD_NOTIFIER_PUBLIC_KEY"),
+        DCC("DCC_SERVER_URL"),
     }
 
     enum class Type(val rawKey: String) {
@@ -135,6 +137,9 @@ class EnvironmentSetup @Inject constructor(
     val logUploadServerUrl: String
         get() = getEnvironmentValue(LOG_UPLOAD).asString
 
+    val dccServerUrl: String
+        get() = getEnvironmentValue(DCC).asString
+
     companion object {
         private const val PKEY_CURRENT_ENVINROMENT = "environment.current"
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCHttpClient.kt
new file mode 100644
index 000000000..d34bb8687
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCHttpClient.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.environment.covidcertificate
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DCCHttpClient
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCModule.kt
new file mode 100644
index 000000000..47821bb16
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCModule.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.environment.covidcertificate
+
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+import de.rki.coronawarnapp.environment.BaseEnvironmentModule
+import de.rki.coronawarnapp.environment.EnvironmentSetup
+import de.rki.coronawarnapp.http.HttpClientDefault
+import okhttp3.OkHttpClient
+import javax.inject.Singleton
+
+@Module
+class DCCModule : BaseEnvironmentModule() {
+
+    @Reusable
+    @DCCHttpClient
+    @Provides
+    fun dccHttpClient(@HttpClientDefault defaultHttpClient: OkHttpClient): OkHttpClient =
+        defaultHttpClient.newBuilder().build()
+
+    @Singleton
+    @DCCServerUrl
+    @Provides
+    fun dccServerUrl(environment: EnvironmentSetup): String {
+        val url = environment.dccServerUrl
+        return requireValidUrl(url)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCServerUrl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCServerUrl.kt
new file mode 100644
index 000000000..8c9cffcad
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/covidcertificate/DCCServerUrl.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.environment.covidcertificate
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class DCCServerUrl
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
index 471f0e586..9cb1bf7c2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
@@ -14,6 +14,7 @@ import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule
 import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
 import de.rki.coronawarnapp.coronatest.CoronaTestModule
 import de.rki.coronawarnapp.coronatest.server.VerificationModule
+import de.rki.coronawarnapp.covidcertificate.server.CovidCertificateModule
 import de.rki.coronawarnapp.datadonation.DataDonationModule
 import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule
 import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule
@@ -81,6 +82,7 @@ import javax.inject.Singleton
         PresenceTracingModule::class,
         CoronaTestModule::class,
         VaccinationModule::class,
+        CovidCertificateModule::class,
     ]
 )
 interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
index 12b178e9d..b1a4b64e4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
@@ -68,6 +68,7 @@ class EnvironmentSetupTest : BaseTest() {
                 dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}"
                 logUploadServerUrl shouldBe "https://logupload-${env.rawKey}"
                 crowdNotifierPublicKey shouldBe "123_abc-${env.rawKey}"
+                dccServerUrl shouldBe "https://dcc-${env.rawKey}"
             }
         }
     }
@@ -127,7 +128,8 @@ class EnvironmentSetupTest : BaseTest() {
         EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL"
         EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY"
         EnvironmentSetup.EnvKey.CROWD_NOTIFIER_PUBLIC_KEY.rawKey shouldBe "CROWD_NOTIFIER_PUBLIC_KEY"
-        EnvironmentSetup.EnvKey.values().size shouldBe 9
+        EnvironmentSetup.EnvKey.DCC.rawKey shouldBe "DCC_SERVER_URL"
+        EnvironmentSetup.EnvKey.values().size shouldBe 10
     }
 
     companion object {
@@ -152,7 +154,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-PROD",
                     "SAFETYNET_API_KEY": "placeholder-PROD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-PROD"                    
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-PROD",
+                    "DCC_SERVER_URL": "https://dcc-PROD"
                 },
                 "DEV": {
                     "USE_EUR_KEY_PKGS" : false,
@@ -164,7 +167,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-DEV",
                     "SAFETYNET_API_KEY": "placeholder-DEV",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-DEV"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-DEV",
+                    "DCC_SERVER_URL": "https://dcc-DEV"
                },
                 "INT": {
                     "USE_EUR_KEY_PKGS" : false,
@@ -176,7 +180,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-INT",
                     "SAFETYNET_API_KEY": "placeholder-INT",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-INT"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-INT",
+                    "DCC_SERVER_URL": "https://dcc-INT"
                 },
                 "WRU": {
                     "USE_EUR_KEY_PKGS" : false,
@@ -189,7 +194,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SAFETYNET_API_KEY": "placeholder-WRU",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU",
                     "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU",
+                    "DCC_SERVER_URL": "https://dcc-WRU"
                 },
                 "WRU-XD": {
                     "USE_EUR_KEY_PKGS" : true,
@@ -201,7 +207,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-WRU-XD",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XD"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XD",
+                    "DCC_SERVER_URL": "https://dcc-WRU-XD"
                 },
                 "WRU-XA": {
                     "USE_EUR_KEY_PKGS" : true,
@@ -213,7 +220,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-WRU-XA",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XA",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XA"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XA",
+                    "DCC_SERVER_URL": "https://dcc-WRU-XA"
                 },
                 "TESTER-MOCK": {
                     "USE_EUR_KEY_PKGS" : true,
@@ -225,7 +233,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-TESTER-MOCK",
                     "SAFETYNET_API_KEY": "placeholder-TESTER-MOCK",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-TESTER-MOCK",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-TESTER-MOCK"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-TESTER-MOCK",
+                    "DCC_SERVER_URL": "https://dcc-TESTER-MOCK"
                 },
                 "LOCAL": {
                     "USE_EUR_KEY_PKGS" : true,
@@ -237,7 +246,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "VACCINATION_CDN_URL": "https://vaccination-LOCAL",
                     "SAFETYNET_API_KEY": "placeholder-LOCAL",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL",
-                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-LOCAL"
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-LOCAL",
+                    "DCC_SERVER_URL": "https://dcc-LOCAL"
                 }
             }
         """
-- 
GitLab