diff --git a/.gitignore b/.gitignore index fa6f9af7..ac541123 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .gradle -/build/ +build !gradle/wrapper/gradle-wrapper.jar ### STS ### @@ -32,4 +32,3 @@ src/main/resources/iexec-sms-aes.key src/main/resources/boot/sms-palaemon-conf.yml -src/main/java/com/iexec/sms/utils/version/Version.java diff --git a/Jenkinsfile b/Jenkinsfile index 401e0898..96ce042b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,44 +1,13 @@ -pipeline { - - agent { - label 'jenkins-agent-machine-1' - } - - stages { - - stage('Build') { - steps { - sh './gradlew build --refresh-dependencies --no-daemon' - } - } - - stage('Upload Jars') { - when { - anyOf{ - branch 'master' - branch 'develop' - } - } - steps { - withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'nexus', usernameVariable: 'NEXUS_USER', passwordVariable: 'NEXUS_PASSWORD']]) { - sh './gradlew -PnexusUser=$NEXUS_USER -PnexusPassword=$NEXUS_PASSWORD uploadArchives --no-daemon' - } - } - } - - stage('Build/Upload Docker image') { - when { - anyOf{ - branch 'master' - branch 'develop' - } - } - steps { - withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'nexus', usernameVariable: 'NEXUS_USER', passwordVariable: 'NEXUS_PASSWORD']]) { - sh './gradlew -PnexusUser=$NEXUS_USER -PnexusPassword=$NEXUS_PASSWORD pushImage --no-daemon' - } - } - } - } - -} +@Library('global-jenkins-library@2.0.1') _ +buildJavaProject( + buildInfo: getBuildInfo(), + integrationTestsEnvVars: [], + shouldPublishJars: true, + shouldPublishDockerImages: true, + dockerfileDir: 'build/resources/main', + dockerfileFilename: 'Dockerfile.untrusted', + buildContext: '.', + preDevelopVisibility: 'iex.ec', + developVisibility: 'iex.ec', + preProductionVisibility: 'docker.io', + productionVisibility: 'docker.io') diff --git a/build.gradle b/build.gradle index 56a4393f..be62b520 100644 --- a/build.gradle +++ b/build.gradle @@ -2,34 +2,42 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'java' - id 'maven' - id 'jacoco' - id 'org.springframework.boot' version '2.4.3' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'io.freefair.lombok' version '5.3.0' + id 'org.springframework.boot' version '2.6.2' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'jacoco' + id 'org.sonarqube' version '3.3' + id 'maven-publish' } -group = 'com.iexec.sms' -sourceCompatibility = 11 -targetCompatibility = 11 +ext { + springCloudVersion = '2021.0.0' + openFeignVersion = '11.7' + gitBranch = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() +} -repositories { - mavenCentral() - jcenter() - maven { - url "https://nexus.iex.ec/repository/maven-public/" +allprojects { + group = 'com.iexec.sms' + sourceCompatibility = 11 + targetCompatibility = 11 + if (gitBranch != 'main' && gitBranch != 'master' && !(gitBranch ==~ '(release|hotfix|support)/.*')) { + version += '-NEXT-SNAPSHOT' } - maven { - url "https://jitpack.io" + repositories { + mavenLocal() + mavenCentral() + maven { + url "https://nexus.intra.iex.ec/repository/maven-public/" + } + maven { + url "https://jitpack.io" + } } } configurations { - deployerJars -} - -ext { - springCloudVersion = '2020.0.1' + integrationTestImplementation.extendsFrom testImplementation + integrationTestRuntimeOnly.extendsFrom runtimeOnly } dependencyManagement { @@ -41,7 +49,7 @@ dependencyManagement { dependencies { // iexec implementation "com.iexec.common:iexec-common:$iexecCommonVersion" - //implementation files("../iexec-common/build/libs/iexec-common-${iexecCommonVersion}.jar") + implementation project(':iexec-sms-library') // spring implementation "org.springframework.boot:spring-boot-starter-web" @@ -51,11 +59,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' // H2 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - runtime 'com.h2database:h2:1.4.200' + runtimeOnly 'com.h2database:h2:1.4.200' - // swagger - implementation "io.springfox:springfox-swagger2:2.9.2" - implementation "io.springfox:springfox-swagger-ui:2.9.2" + // Spring Doc + implementation 'org.springdoc:springdoc-openapi-ui:1.6.3' //ssl implementation 'org.apache.httpcomponents:httpclient:4.5.9' @@ -78,84 +85,81 @@ dependencies { } testImplementation 'org.springframework.security:spring-security-test' // testImplementation 'org.mockito:mockito-inline:2.13.0' // activates mocking final classes/methods + + // test containers + testImplementation 'org.testcontainers:junit-jupiter:1.16.0' + testImplementation 'org.testcontainers:testcontainers:1.16.0' + testImplementation 'org.testcontainers:mongodb:1.16.0' +} + +sourceSets { + integrationTest { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + srcDir 'src/itest/java' + } + resources.srcDir 'src/itest/resources' + } +} + +springBoot { + buildInfo() } test { useJUnitPlatform() } -jacoco { - toolVersion = "0.8.3" -} -build.dependsOn jacocoTestReport - -def gitBranch = 'git name-rev --name-only HEAD'.execute().text.trim() -def isMasterBranch = gitBranch == "master" -def isDevelopBranch = gitBranch == "develop" -def canUploadArchives = (isMasterBranch || isDevelopBranch ) && project.hasProperty("nexusUser") && project.hasProperty("nexusPassword") -def gitShortCommit = 'git rev-parse --short HEAD'.execute().text.trim() -def isSnapshotVersion = project.version.contains("SNAPSHOT") - -project.ext.getNexusMaven = { - def nexusMavenBase = "https://nexus.iex.ec/repository" - if (isSnapshotVersion) { - return nexusMavenBase + "/maven-snapshots/" - } else { - return nexusMavenBase + "/maven-releases/" - } +task itest(type:Test) { + group 'Verification' + description 'Runs the integration tests.' + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + outputs.upToDateWhen { false } // run always + useJUnitPlatform() } -uploadArchives { - repositories.mavenDeployer { - configuration = configurations.deployerJars - repository(url: getNexusMaven()) { - authentication(userName: project.nexusUser, password: project.nexusPassword) - } +jacoco { + toolVersion = "0.8.7" +} +// sonarqube code coverage requires jacoco XML report +jacocoTestReport { + reports { + xml.enabled true } } -uploadArchives.enabled = canUploadArchives +tasks.test.finalizedBy tasks.jacocoTestReport +tasks.sonarqube.dependsOn tasks.jacocoTestReport -// create the version controller for the core -task createVersion(type: Copy) { - // delete old one - delete 'src/main/java/com/iexec/sms/utils/version/Version.java' - // use and copy template to the new location - from 'src/main/resources/Version.java.template' - into 'src/main/java/com/iexec/sms/utils/version/' - - rename { String fileName -> - fileName.replace('.template', '') +publishing { + publications { + maven(MavenPublication) { + artifact bootJar + from components.java + } } - // replace tokens in the template file - filter(ReplaceTokens, tokens: [projectversion: "${version}".toString()]) -} -// the createVersion task should be called before compileJava or the version service will not work -compileJava.dependsOn createVersion - -def imageName = "nexus.iex.ec/iexec-sms" -def trustedDockerfileName = "Dockerfile" -def untrustedDockerfileName = "Dockerfile.untrusted" -def jarName = "iexec-sms-${version}.jar" - -project.ext.getDockerImageNameFull = { - def imageNameWithVersion = imageName + ":${version}" - if (isSnapshotVersion) { - return imageNameWithVersion + "-" + gitShortCommit - } else { - return imageNameWithVersion + repositories { + maven { + credentials { + username project.hasProperty('nexusUser') ? nexusUser : '' + password project.hasProperty('nexusPassword') ? nexusPassword : '' + } + url project.hasProperty('nexusUrl') ? nexusUrl : '' + } } } -project.ext.getDockerImageNameShortCommit = { - return imageName + ":" + gitShortCommit -} +ext.jarPathForOCI = relativePath(tasks.bootJar.outputs.files.singleFile) +ext.gitShortCommit = 'git rev-parse --short=8 HEAD'.execute().text.trim() +ext.ociImageName = 'local/' + ['bash', '-c', 'basename $(git config --get remote.origin.url) .git'].execute().text.trim() task buildImage(type: Exec) { - description 'Building iexec-sms Docker image' - commandLine("sh", "-c", - "docker image build -f build/resources/main/${untrustedDockerfileName}" + - " -t ${getDockerImageNameFull()} --build-arg JAR_NAME=${jarName} . &&" + - "docker tag ${getDockerImageNameFull()} ${imageName}:dev") + group 'Build' + description 'Builds an OCI image from a Dockerfile.' + dependsOn bootJar + commandLine ("sh", "-c", "docker build -f build/resources/main/Dockerfile.untrusted --build-arg jar=$jarPathForOCI" + + " -t $ociImageName:$gitShortCommit . && docker tag $ociImageName:$gitShortCommit $ociImageName:dev") standardOutput = new ByteArrayOutputStream() ext.output = { @@ -165,12 +169,11 @@ task buildImage(type: Exec) { } task buildTrustedImage(type: Exec) { - def trustedImageName = getDockerImageNameFull() + "-trusted" - description 'Building iexec-sms Docker image' - commandLine("sh", "-c", - "docker image build -f build/resources/main/${trustedDockerfileName} " + - "-t ${trustedImageName} --build-arg JAR_NAME=${jarName} --no-cache . && " + - "docker tag ${trustedImageName} ${imageName}:dev-trusted") + group 'Build' + description 'Builds a trusted OCI image from a trusted Dockerfile.' + dependsOn bootJar + commandLine ("sh", "-c", "docker image build -f build/resources/main/Dockerfile --build-arg jar=$jarPathForOCI" + + " -t $ociImageName:$gitShortCommit-trusted --no-cache . && docker tag $ociImageName:$gitShortCommit-trusted $ociImageName:dev-trusted") } task templatePalaemon { @@ -219,17 +222,3 @@ task templatePalaemon { } //templatePalaemon.dependsOn buildImage //buildImage.finalizedBy templatePalaemon - -task pushImage(type: Exec) { - if (project.hasProperty("nexusUser") && project.hasProperty("nexusPassword")) { - commandLine("sh", "-c", "docker login -u " + project.nexusUser + " -p " + project.nexusPassword + " nexus.iex.ec && " + - "docker push " + getDockerImageNameFull() + " && " + - "docker tag " + getDockerImageNameFull() + " " + getDockerImageNameShortCommit() + " && " + - "docker push " + getDockerImageNameShortCommit() + " && " + - "docker logout") - } else { - println "Credentials for DockerHub are missing, the images cannot be pushed" - } -} -pushImage.dependsOn buildImage -pushImage.enabled = canUploadArchives diff --git a/gradle.properties b/gradle.properties index d5cfd117..3a9e90d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=7.0.0 -iexecCommonVersion=5.9.0 +version=7.1.0 +iexecCommonVersion=6.0.0 nexusUser= -nexusPassword= \ No newline at end of file +nexusPassword= diff --git a/iexec-sms-library/build.gradle b/iexec-sms-library/build.gradle new file mode 100644 index 00000000..99fbdcd9 --- /dev/null +++ b/iexec-sms-library/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'java-library' + id 'jacoco' + id 'maven-publish' +} + +dependencies { + implementation "com.iexec.common:iexec-common:$iexecCommonVersion" + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' +} + +java { + withJavadocJar() + withSourcesJar() +} + +test { + useJUnitPlatform() +} + +jacoco { + toolVersion = "0.8.7" +} +// sonarqube code coverage requires jacoco XML report +jacocoTestReport { + reports { + xml.enabled true + } +} +tasks.test.finalizedBy tasks.jacocoTestReport + +publishing { + publications { + maven(MavenPublication) { + from components.java + } + } + repositories { + maven { + credentials { + username project.hasProperty("nexusUser")? project.nexusUser: '' + password project.hasProperty("nexusPassword")? project.nexusPassword: '' + } + url = project.hasProperty("nexusUrl")? project.nexusUrl: '' + } + } +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java new file mode 100644 index 00000000..d4c8f147 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java @@ -0,0 +1,107 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.api; + +import com.iexec.common.chain.WorkerpoolAuthorization; +import com.iexec.common.sms.secret.SmsSecretResponse; +import com.iexec.common.tee.TeeWorkflowSharedConfiguration; +import com.iexec.common.web.ApiResponseBody; +import feign.Headers; +import feign.Param; +import feign.RequestLine; + +import java.util.List; + +/** + * Interface allowing to instantiate a Feign client targeting SMS REST endpoints. + *

+ * To create the client, see the related builder. + * @see SmsClientBuilder + */ +public interface SmsClient { + + @RequestLine("POST /apps/{appAddress}/secrets/1") + @Headers("Authorization: {authorization}") + ApiResponseBody> addAppDeveloperAppComputeSecret( + @Param("authorization") String authorization, + @Param("appAddress") String appAddress, + //@Param("secretIndex") String secretIndex, + String secretValue + ); + + @RequestLine("HEAD /apps/{appAddress}/secrets/{secretIndex}") + ApiResponseBody> isAppDeveloperAppComputeSecretPresent( + @Param("appAddress") String appAddress, + @Param("secretIndex") String secretIndex + ); + + @RequestLine("GET /cas/url") + String getSconeCasUrl(); + + @RequestLine("POST /requesters/{requesterAddress}/secrets/{secretKey}") + @Headers("Authorization: {authorization}") + ApiResponseBody> addRequesterAppComputeSecret( + @Param("authorization") String authorization, + @Param("requesterAddress") String requesterAddress, + @Param("secretKey") String secretKey, + String secretValue + ); + + @RequestLine("HEAD /requesters/{requesterAddress}/secrets/{secretKey}") + ApiResponseBody> isRequesterAppComputeSecretPresent( + @Param("requesterAddress") String requesterAddress, + @Param("secretKey") String secretKey + ); + + @RequestLine("POST /secrets/web2?ownerAddress={ownerAddress}&secretName={secretName}") + @Headers("Authorization: {authorization}") + String setWeb2Secret( + @Param("authorization") String authorization, + @Param("ownerAddress") String ownerAddress, + @Param("secretName") String secretName, + String secretValue + ); + + @RequestLine("POST /secrets/web3?secretAddress={secretAddress}") + @Headers("Authorization: {authorization}") + String setWeb3Secret( + @Param("authorization") String authorization, + @Param("secretAddress") String secretAddress, + String secretValue + ); + + @RequestLine("POST /tee/challenges/{chainTaskId}") + String generateTeeChallenge(@Param("chainTaskId") String chainTaskId); + + @RequestLine("POST /tee/sessions") + @Headers("Authorization: {authorization}") + ApiResponseBody generateTeeSession( + @Param("authorization") String authorization, + WorkerpoolAuthorization workerpoolAuthorization + ); + + @RequestLine("GET /tee/workflow/config") + TeeWorkflowSharedConfiguration getTeeWorkflowConfiguration(); + + @RequestLine("POST /untee/secrets") + @Headers("Authorization: {authorization}") + SmsSecretResponse getUnTeeSecrets( + @Param("authorization") String authorization, + WorkerpoolAuthorization workerpoolAuthorization + ); + +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientBuilder.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientBuilder.java new file mode 100644 index 00000000..304bb737 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.api; + +import com.iexec.common.utils.FeignBuilder; +import feign.Logger; + +/** + * Creates Feign client instances to query REST endpoints described in {@link SmsClient}. + * @see FeignBuilder + */ +public class SmsClientBuilder { + + private SmsClientBuilder() {} + + /** + * Create an unauthenticated feign client to query apis described in {@link SmsClient}. + * @param logLevel Feign logging level to configure. + * @param url Url targeted by the client. + * @return Feign client for {@link SmsClient} apis. + */ + public static SmsClient getInstance(Logger.Level logLevel, String url) { + return FeignBuilder.createBuilder(logLevel) + .target(SmsClient.class, url); + } + +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java new file mode 100644 index 00000000..77f001c1 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.api; + +public enum TeeSessionGenerationError { + // region Authorization + INVALID_AUTHORIZATION, + EXECUTION_NOT_AUTHORIZED_EMPTY_PARAMS_UNAUTHORIZED, + EXECUTION_NOT_AUTHORIZED_NO_MATCH_ONCHAIN_TYPE, + EXECUTION_NOT_AUTHORIZED_GET_CHAIN_TASK_FAILED, + EXECUTION_NOT_AUTHORIZED_TASK_NOT_ACTIVE, + EXECUTION_NOT_AUTHORIZED_GET_CHAIN_DEAL_FAILED, + EXECUTION_NOT_AUTHORIZED_INVALID_SIGNATURE, + // endregion + + // region Pre-compute + PRE_COMPUTE_GET_DATASET_SECRET_FAILED, + // endregion + + // region App-compute + APP_COMPUTE_NO_ENCLAVE_CONFIG, + APP_COMPUTE_INVALID_ENCLAVE_CONFIG, + // endregion + + // region Post-compute + POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, + POST_COMPUTE_GET_STORAGE_TOKENS_FAILED, + + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS, + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE, + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE, + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS, + // endregion + + // region Secure session generation + SECURE_SESSION_CAS_CALL_FAILED, + SECURE_SESSION_GENERATION_FAILED, + // endregion + + // region Miscellaneous + GET_TASK_DESCRIPTION_FAILED, + NO_SESSION_REQUEST, + NO_TASK_DESCRIPTION, + GET_SESSION_YML_FAILED, + + UNKNOWN_ISSUE + // endregion +} diff --git a/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java new file mode 100644 index 00000000..3cd6a2e2 --- /dev/null +++ b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.api; + +import feign.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SmsClientTest { + + @Test + void instantiationTest() { + Assertions.assertNotNull(SmsClientBuilder.getInstance(Logger.Level.FULL, "localhost")); + } + +} diff --git a/settings.gradle b/settings.gradle index a8c81501..26918f1d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ rootProject.name = 'iexec-sms' +include 'iexec-sms-library' diff --git a/src/itest/java/com/iexec/sms/CommonTestSetup.java b/src/itest/java/com/iexec/sms/CommonTestSetup.java new file mode 100644 index 00000000..ea333f8c --- /dev/null +++ b/src/itest/java/com/iexec/sms/CommonTestSetup.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms; + +import com.iexec.sms.blockchain.IexecHubService; +import com.iexec.sms.blockchain.Web3jService; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@TestPropertySource(properties = {"spring.config.location = classpath:/application.yml, classpath:/application-test.yml"}) +public abstract class CommonTestSetup { + + @LocalServerPort + protected int randomServerPort; + + // region Following beans are mocked as they use the blockchain + @MockBean + protected IexecHubService iexecHubService; + + @MockBean + protected Web3jService web3jService; + // endregion +} diff --git a/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java b/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java new file mode 100644 index 00000000..92a806c6 --- /dev/null +++ b/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java @@ -0,0 +1,307 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +import com.iexec.common.contract.generated.Ownable; +import com.iexec.common.utils.HashUtils; +import com.iexec.sms.CommonTestSetup; +import com.iexec.sms.api.SmsClient; +import com.iexec.sms.api.SmsClientBuilder; +import com.iexec.sms.encryption.EncryptionService; +import feign.FeignException; +import feign.Logger; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.http.HttpStatus; +import org.web3j.crypto.Hash; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; + +import static com.iexec.common.utils.SignatureUtils.signMessageHashAndGetSignature; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Slf4j +public class TeeTaskComputeSecretIntegrationTests extends CommonTestSetup { + private static final String APP_ADDRESS = "0xabcd1339ec7e762e639f4887e2bfe5ee8023e23e"; + private static final String UPPER_CASE_APP_ADDRESS = "0xABCD1339EC7E762E639F4887E2BFE5EE8023E23E"; + private static final String SECRET_VALUE = generateRandomAscii(4096); + private static final String OWNER_ADDRESS = "0xabcd1339ec7e762e639f4887e2bfe5ee8023e23e"; + private static final String REQUESTER_ADDRESS = "0x123790ae4E14865B972ee04a5f9FD5fB153Cd5e7"; + private static final String DOMAIN = "IEXEC_SMS_DOMAIN"; + private static final String APP_DEVELOPER_PRIVATE_KEY = "0x2fac4d263f1b20bfc33ea2bcb1cbe1521322dbde81d04b0c454ffff1218f0ed6"; + private static final String REQUESTER_PRIVATE_KEY = "0xb8e97e9e217a50dedbe3c0c4c37b85a85a10d4eb23fca6dbad55162cfbb1c450"; + + private SmsClient apiClient; + + @Autowired + private EncryptionService encryptionService; + + @Autowired + private TeeTaskComputeSecretRepository repository; + + /* + * Generate random ASCII from seed for re-testability. + * See also {@link org.apache.commons.lang3.RandomStringUtils#randomAscii(int)} + * */ + private static String generateRandomAscii(int count) { + long seed = new Date().getTime(); + log.info("Generating random ascii from seed: {}", seed); + return RandomStringUtils.random(count, + 32, + 127, + false, + false, + null, + new Random(seed)); + } + + @BeforeEach + private void setUp() { + apiClient = SmsClientBuilder.getInstance(Logger.Level.FULL, "http://localhost:" + randomServerPort); + final Ownable appContract = mock(Ownable.class); + when(appContract.getContractAddress()).thenReturn(APP_ADDRESS); + when(iexecHubService.getOwnableContract(APP_ADDRESS)) + .thenReturn(appContract); + repository.deleteAll(); + } + + @Test + void shouldAddNewComputeSecrets() { + final String appDeveloperSecretIndex = "1"; + final String requesterSecretKey = "secret-key"; + final String requesterAddress = REQUESTER_ADDRESS; + final String appAddress = APP_ADDRESS; + final String secretValue = SECRET_VALUE; + final String ownerAddress = OWNER_ADDRESS; + + addNewAppDeveloperSecret(appAddress, appDeveloperSecretIndex, secretValue, ownerAddress); + addNewRequesterSecret(requesterAddress, requesterSecretKey, secretValue); + + // Check the new secrets exists for the API + try { + apiClient.isAppDeveloperAppComputeSecretPresent(appAddress, appDeveloperSecretIndex); + } catch (FeignException e) { + Assertions.assertThat(e.status()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + try { + apiClient.isRequesterAppComputeSecretPresent(requesterAddress, requesterSecretKey); + } catch (FeignException e) { + Assertions.assertThat(e.status()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + // We check the secrets have been added to the database + final ExampleMatcher exampleMatcher = ExampleMatcher.matching() + .withIgnorePaths("value"); + final Optional appDeveloperSecret = repository.findOne( + Example.of(TeeTaskComputeSecret + .builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress(appAddress) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .key(appDeveloperSecretIndex) + .build(), + exampleMatcher + ) + ); + if (appDeveloperSecret.isEmpty()) { + // Could be something like `Assertions.assertThat(appDeveloperSecret).isPresent()` + // but Sonar needs a call to `appDeveloperSecret.isEmpty()` to avoid triggering a warning. + Assertions.fail("An app developer secret was expected but none has been retrieved."); + return; + } + Assertions.assertThat(appDeveloperSecret.get().getId()).isNotBlank(); + Assertions.assertThat(appDeveloperSecret.get().getOnChainObjectAddress()).isEqualToIgnoringCase(appAddress); + Assertions.assertThat(appDeveloperSecret.get().getKey()).isEqualTo(appDeveloperSecretIndex); + Assertions.assertThat(appDeveloperSecret.get().getValue()).isNotEqualTo(secretValue); + Assertions.assertThat(appDeveloperSecret.get().getValue()).isEqualTo(encryptionService.encrypt(secretValue)); + + final Optional requesterSecret = repository.findOne( + Example.of(TeeTaskComputeSecret + .builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress("") + .secretOwnerRole(SecretOwnerRole.REQUESTER) + .fixedSecretOwner(requesterAddress.toLowerCase()) + .key(requesterSecretKey) + .build(), + exampleMatcher) + ); + if (requesterSecret.isEmpty()) { + // Could be something like `Assertions.assertThat(requesterSecret).isPresent()` + // but Sonar needs a call to `requesterSecret.isEmpty()` to avoid triggering a warning. + Assertions.fail("An app requester secret was expected but none has been retrieved."); + return; + } + Assertions.assertThat(requesterSecret.get().getId()).isNotBlank(); + Assertions.assertThat(requesterSecret.get().getOnChainObjectAddress()).isEqualToIgnoringCase(""); + Assertions.assertThat(requesterSecret.get().getKey()).isEqualTo(requesterSecretKey); + Assertions.assertThat(requesterSecret.get().getValue()).isNotEqualTo(secretValue); + Assertions.assertThat(requesterSecret.get().getValue()).isEqualTo(encryptionService.encrypt(secretValue)); + + // We shouldn't be able to add a new secrets to the database with the same IDs + try { + final String authorization = getAuthorizationForAppDeveloper(appAddress, appDeveloperSecretIndex, secretValue); + apiClient.addAppDeveloperAppComputeSecret(authorization, appAddress, secretValue); + Assertions.fail("A second app developer secret with the same app address and index should be rejected."); + } catch (FeignException.Conflict ignored) { + // Having a Conflict exception is what we expect there. + } + try { + final String authorization = getAuthorizationForRequester(requesterAddress, requesterSecretKey, secretValue); + apiClient.addRequesterAppComputeSecret(authorization, requesterAddress, requesterSecretKey, secretValue); + Assertions.fail("A second app requester secret with the same app address and index should be rejected."); + } catch (FeignException.Conflict ignored) { + // Having a Conflict exception is what we expect there. + } + + // We shouldn't be able to add a new secret to the database with the same index + // and an appAddress whose only difference is the case. + try { + when(iexecHubService.getOwner(UPPER_CASE_APP_ADDRESS)).thenReturn(ownerAddress); + + final String authorization = getAuthorizationForAppDeveloper(UPPER_CASE_APP_ADDRESS, appDeveloperSecretIndex, secretValue); + apiClient.addAppDeveloperAppComputeSecret(authorization, UPPER_CASE_APP_ADDRESS, secretValue); + Assertions.fail("A second app developer secret with the same index " + + "and an app address whose only difference is the case should be rejected."); + } catch (FeignException.Conflict ignored) { + // Having a Conflict exception is what we expect there. + } + } + + @Test + void addMultipleRequesterSecrets() { + List keys = List.of("secret-key-1", "secret-key-2", "secret-key-3"); + for (String key : keys) { + addNewRequesterSecret(REQUESTER_ADDRESS, key, SECRET_VALUE); + } + Assertions.assertThat(repository.count()).isEqualTo(keys.size()); + List secrets = repository.findAll(); + Assertions.assertThat(secrets.stream().map(TeeTaskComputeSecret::getKey).collect(Collectors.toList())) + .containsExactlyInAnyOrder("secret-key-1", "secret-key-2", "secret-key-3"); + + } + + @ParameterizedTest + @ValueSource(strings = { + "this-is-a-really-long-key-with-far-too-many-characters-in-its-name", + "this-is-a-key-with-invalid-characters:!*~" + }) + void checkInvalidRequesterSecretKey(String secretKey) { + Assertions.assertThatThrownBy(() -> addNewRequesterSecret(REQUESTER_ADDRESS, secretKey, SECRET_VALUE)) + .isInstanceOf(FeignException.BadRequest.class); + Assertions.assertThat(repository.count()).isZero(); + } + + /** + * Checks no application developer secret already exists with given appAddress/index couple + * and adds a new application developer secret to the database + */ + @SuppressWarnings("SameParameterValue") + private void addNewAppDeveloperSecret(String appAddress, String secretIndex, String secretValue, String ownerAddress) { + when(iexecHubService.getOwner(appAddress)).thenReturn(ownerAddress); + + final String authorization = getAuthorizationForAppDeveloper(appAddress, secretIndex, secretValue); + + // At first, no secret should be in the database + try { + apiClient.isAppDeveloperAppComputeSecretPresent(appAddress, secretIndex); + Assertions.fail("No application developer secret was expected but one has been retrieved."); + } catch (FeignException.NotFound ignored) { + // Having a Not Found exception is what we expect there. + } + + // Add a new secret to the database + try { + apiClient.addAppDeveloperAppComputeSecret(authorization, appAddress, secretValue); + } catch (FeignException e) { + Assertions.assertThat(e.status()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + } + + /** + * Checks no requester secret already exists with given appAddress/index couple + * and adds a new requester secret to the database + */ + @SuppressWarnings("SameParameterValue") + private void addNewRequesterSecret(String requesterAddress, + String secretKey, + String secretValue) { + final String authorization = getAuthorizationForRequester(requesterAddress, secretKey, secretValue); + + // At first, no secret should be in the database + try { + apiClient.isRequesterAppComputeSecretPresent(requesterAddress, secretKey); + Assertions.fail("No application requester secret was expected but one has been retrieved."); + } catch (FeignException.NotFound ignored) { + // Having a Not Found exception is what we expect there. + } + + // Add a new secret to the database + try { + apiClient.addRequesterAppComputeSecret(authorization, requesterAddress, secretKey, secretValue); + } catch (FeignException e) { + Assertions.assertThat(e.status()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + } + + /** + * Forges an authorization that'll permit adding + * given application developer secret to database. + */ + private String getAuthorizationForAppDeveloper( + String appAddress, + String secretIndex, + String secretValue) { + final String challenge = HashUtils.concatenateAndHash( + Hash.sha3String(DOMAIN), + appAddress, + Hash.sha3String(secretIndex), + Hash.sha3String(secretValue)); + return signMessageHashAndGetSignature(challenge, APP_DEVELOPER_PRIVATE_KEY).getValue(); + } + + /** + * Forges an authorization that'll permit adding + * given requester secret to database. + */ + private String getAuthorizationForRequester( + String requesterAddress, + String secretKey, + String secretValue) { + + final String challenge = HashUtils.concatenateAndHash( + Hash.sha3String(DOMAIN), + requesterAddress, + Hash.sha3String(secretKey), + Hash.sha3String(secretValue)); + return signMessageHashAndGetSignature(challenge, REQUESTER_PRIVATE_KEY).getValue(); + } +} diff --git a/src/itest/resources/application-test.yml b/src/itest/resources/application-test.yml new file mode 100644 index 00000000..775bcdc0 --- /dev/null +++ b/src/itest/resources/application-test.yml @@ -0,0 +1,21 @@ +# Those `server` properties are needed so that a random port is chosen for the HTTP listener +server: + http: + enabled: false + ssl: + enabled: false + +# Run database in-mem +spring: + datasource: + url: jdbc:h2:mem:db + +# `TeeWorkflowConfiguration` needs some dummy value to be instantiated +tee.workflow: + las-image: "none" + pre-compute: + image: "none" + fingerprint: "none" + post-compute: + image: "none" + fingerprint: "none" diff --git a/src/main/java/com/iexec/sms/AppController.java b/src/main/java/com/iexec/sms/AppController.java index b98e656e..ba2b2583 100644 --- a/src/main/java/com/iexec/sms/AppController.java +++ b/src/main/java/com/iexec/sms/AppController.java @@ -26,11 +26,8 @@ @RestController public class AppController { - public AppController() { - } - @GetMapping(value = "/up") - public static ResponseEntity isUp() { + public ResponseEntity isUp() { String message = String.format("Up! (%s)", new Date()); return ResponseEntity.ok(message); } diff --git a/src/main/java/com/iexec/sms/authorization/AuthorizationError.java b/src/main/java/com/iexec/sms/authorization/AuthorizationError.java new file mode 100644 index 00000000..025fd3b2 --- /dev/null +++ b/src/main/java/com/iexec/sms/authorization/AuthorizationError.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.authorization; + +public enum AuthorizationError { + EMPTY_PARAMS_UNAUTHORIZED, + NO_MATCH_ONCHAIN_TYPE, + GET_CHAIN_TASK_FAILED, + TASK_NOT_ACTIVE, + GET_CHAIN_DEAL_FAILED, + INVALID_SIGNATURE; +} diff --git a/src/main/java/com/iexec/sms/authorization/AuthorizationService.java b/src/main/java/com/iexec/sms/authorization/AuthorizationService.java index f0330f68..31399575 100644 --- a/src/main/java/com/iexec/sms/authorization/AuthorizationService.java +++ b/src/main/java/com/iexec/sms/authorization/AuthorizationService.java @@ -17,10 +17,6 @@ package com.iexec.sms.authorization; -import static com.iexec.sms.App.DOMAIN; - -import java.util.Optional; - import com.iexec.common.chain.ChainDeal; import com.iexec.common.chain.ChainTask; import com.iexec.common.chain.ChainTaskStatus; @@ -30,27 +26,38 @@ import com.iexec.common.utils.HashUtils; import com.iexec.common.utils.SignatureUtils; import com.iexec.sms.blockchain.IexecHubService; - -import org.springframework.stereotype.Service; - import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; import org.web3j.crypto.Hash; +import java.util.Optional; + +import static com.iexec.sms.App.DOMAIN; +import static com.iexec.sms.authorization.AuthorizationError.*; + @Slf4j @Service public class AuthorizationService { - - private IexecHubService iexecHubService; + private final IexecHubService iexecHubService; public AuthorizationService(IexecHubService iexecHubService) { this.iexecHubService = iexecHubService; } public boolean isAuthorizedOnExecution(WorkerpoolAuthorization workerpoolAuthorization, boolean isTeeTask) { + return isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, isTeeTask).isEmpty(); + } + + /** + * Checks whether this execution is authorized. + * If not authorized, return the reason. + * Otherwise, returns an empty {@link Optional}. + */ + public Optional isAuthorizedOnExecutionWithDetailedIssue(WorkerpoolAuthorization workerpoolAuthorization, boolean isTeeTask) { if (workerpoolAuthorization == null || workerpoolAuthorization.getChainTaskId().isEmpty()) { log.error("Not authorized with empty params"); - return false; + return Optional.of(EMPTY_PARAMS_UNAUTHORIZED); } String chainTaskId = workerpoolAuthorization.getChainTaskId(); @@ -59,13 +66,13 @@ public boolean isAuthorizedOnExecution(WorkerpoolAuthorization workerpoolAuthori log.error("Could not match onchain task type [isTeeTask:{}, isTeeTaskOnchain:{}," + "chainTaskId:{}, walletAddress:{}]",isTeeTask, isTeeTaskOnchain, chainTaskId, workerpoolAuthorization.getWorkerWallet()); - return false; + return Optional.of(NO_MATCH_ONCHAIN_TYPE); } Optional optionalChainTask = iexecHubService.getChainTask(chainTaskId); - if (!optionalChainTask.isPresent()) { + if (optionalChainTask.isEmpty()) { log.error("Could not get chainTask [chainTaskId:{}]", chainTaskId); - return false; + return Optional.of(GET_CHAIN_TASK_FAILED); } ChainTask chainTask = optionalChainTask.get(); ChainTaskStatus taskStatus = chainTask.getStatus(); @@ -74,13 +81,13 @@ public boolean isAuthorizedOnExecution(WorkerpoolAuthorization workerpoolAuthori if (!taskStatus.equals(ChainTaskStatus.ACTIVE)) { log.error("Task not active onchain [chainTaskId:{}, status:{}]", chainTaskId, taskStatus); - return false; + return Optional.of(TASK_NOT_ACTIVE); } Optional optionalChainDeal = iexecHubService.getChainDeal(chainDealId); - if (!optionalChainDeal.isPresent()) { + if (optionalChainDeal.isEmpty()) { log.error("isAuthorizedOnExecution failed (getChainDeal failed) [chainTaskId:{}]", chainTaskId); - return false; + return Optional.of(GET_CHAIN_DEAL_FAILED); } ChainDeal chainDeal = optionalChainDeal.get(); String workerpoolAddress = chainDeal.getPoolOwner(); @@ -90,10 +97,10 @@ public boolean isAuthorizedOnExecution(WorkerpoolAuthorization workerpoolAuthori if (!isSignerByWorkerpool) { log.error("isAuthorizedOnExecution failed (invalid signature) [chainTaskId:{}, isWorkerpoolSignatureValid:{}]", chainTaskId, isSignerByWorkerpool); - return false; + return Optional.of(INVALID_SIGNATURE); } - return true; + return Optional.empty(); } public boolean isSignedByHimself(String message, String signature, String address) { @@ -132,6 +139,26 @@ public String getChallengeForSetWeb3Secret(String secretAddress, Hash.sha3String(secretValue)); } + public String getChallengeForSetAppDeveloperAppComputeSecret(String appAddress, + String secretIndex, + String secretValue) { + return HashUtils.concatenateAndHash( + Hash.sha3String(DOMAIN), + appAddress, + Hash.sha3String(secretIndex), + Hash.sha3String(secretValue)); + } + + public String getChallengeForSetRequesterAppComputeSecret( + String requesterAddress, + String secretKey, + String secretValue) { + return HashUtils.concatenateAndHash( + Hash.sha3String(DOMAIN), + requesterAddress, + Hash.sha3String(secretKey), + Hash.sha3String(secretValue)); + } public String getChallengeForSetWeb2Secret(String ownerAddress, String secretKey, diff --git a/src/main/java/com/iexec/sms/config/SwaggerConfig.java b/src/main/java/com/iexec/sms/config/OpenApiConfig.java similarity index 54% rename from src/main/java/com/iexec/sms/config/SwaggerConfig.java rename to src/main/java/com/iexec/sms/config/OpenApiConfig.java index 156e5a99..68975e9a 100644 --- a/src/main/java/com/iexec/sms/config/SwaggerConfig.java +++ b/src/main/java/com/iexec/sms/config/OpenApiConfig.java @@ -16,29 +16,29 @@ package com.iexec.sms.config; +import com.iexec.sms.utils.version.VersionService; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration -@EnableSwagger2 -public class SwaggerConfig { +public class OpenApiConfig { + + private final VersionService versionService; + + public OpenApiConfig(VersionService versionService) { + this.versionService = versionService; + } /* - * - * Swagger link: - * http://localhost:13300/swagger-ui.html - * */ + * Swagger URI: /swagger-ui/index.html + */ @Bean - public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.any()) - .build(); - } -} \ No newline at end of file + public OpenAPI api() { + return new OpenAPI().info( + new Info() + .title("iExec SMS") + .version(versionService.getVersion()) + ); + }} \ No newline at end of file diff --git a/src/main/java/com/iexec/sms/secret/AbstractSecretService.java b/src/main/java/com/iexec/sms/secret/AbstractSecretService.java index 2e4669f8..2a1271ee 100644 --- a/src/main/java/com/iexec/sms/secret/AbstractSecretService.java +++ b/src/main/java/com/iexec/sms/secret/AbstractSecretService.java @@ -18,14 +18,13 @@ import com.iexec.sms.encryption.EncryptionService; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor public abstract class AbstractSecretService { - public EncryptionService encryptionService; + private final EncryptionService encryptionService; + + public AbstractSecretService(EncryptionService encryptionService) { + this.encryptionService = encryptionService; + } public Secret encryptSecret(Secret secret) { if (!secret.isEncryptedValue()) { diff --git a/src/main/java/com/iexec/sms/secret/Secret.java b/src/main/java/com/iexec/sms/secret/Secret.java index 9cd184c7..096f238d 100644 --- a/src/main/java/com/iexec/sms/secret/Secret.java +++ b/src/main/java/com/iexec/sms/secret/Secret.java @@ -26,7 +26,6 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; - import java.util.Objects; @Data @@ -72,5 +71,15 @@ public String getTrimmedValue() { Objects.requireNonNull(this.value, "Secret value must not be null"); return this.value.trim(); } + + @Override + public String toString() { + return "Secret{" + + "id='" + id + '\'' + + ", address='" + address + '\'' + + ", value='" + (isEncryptedValue ? value : "") + '\'' + + ", isEncryptedValue=" + isEncryptedValue + + '}'; + } } diff --git a/src/main/java/com/iexec/sms/secret/SecretController.java b/src/main/java/com/iexec/sms/secret/SecretController.java index 1f45ca86..f61af5b0 100644 --- a/src/main/java/com/iexec/sms/secret/SecretController.java +++ b/src/main/java/com/iexec/sms/secret/SecretController.java @@ -22,7 +22,6 @@ import com.iexec.sms.secret.web2.Web2SecretsService; import com.iexec.sms.secret.web3.Web3Secret; import com.iexec.sms.secret.web3.Web3SecretService; -import com.iexec.sms.utils.version.VersionService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -38,16 +37,13 @@ @RequestMapping("/secrets") public class SecretController { - private AuthorizationService authorizationService; - private Web3SecretService web3SecretService; - private VersionService versionService; - private Web2SecretsService web2SecretsService; + private final AuthorizationService authorizationService; + private final Web3SecretService web3SecretService; + private final Web2SecretsService web2SecretsService; - public SecretController(VersionService versionService, - AuthorizationService authorizationService, + public SecretController(AuthorizationService authorizationService, Web2SecretsService web2SecretsService, Web3SecretService web3SecretService) { - this.versionService = versionService; this.web2SecretsService = web2SecretsService; this.authorizationService = authorizationService; this.web3SecretService = web3SecretService; @@ -65,14 +61,12 @@ public ResponseEntity isWeb3SecretSet(@RequestParam String secretAddress) { public ResponseEntity getWeb3Secret(@RequestHeader("Authorization") String authorization, @RequestParam String secretAddress, @RequestParam(required = false, defaultValue = "false") boolean shouldDecryptSecret) { - if (isInProduction(authorization)) { - String challenge = authorizationService.getChallengeForGetWeb3Secret(secretAddress); - - //TODO: also isAuthorizedOnExecution(..) - if (!authorizationService.isSignedByOwner(challenge, authorization, secretAddress)) { - log.error("Unauthorized to getWeb3Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + String challenge = authorizationService.getChallengeForGetWeb3Secret(secretAddress); + + //TODO: also isAuthorizedOnExecution(..) + if (!authorizationService.isSignedByOwner(challenge, authorization, secretAddress)) { + log.error("Unauthorized to getWeb3Secret [expectedChallenge:{}]", challenge); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } Optional secret = web3SecretService.getSecret(secretAddress, shouldDecryptSecret); @@ -83,13 +77,15 @@ public ResponseEntity getWeb3Secret(@RequestHeader("Authorization") public ResponseEntity addWeb3Secret(@RequestHeader("Authorization") String authorization, @RequestParam String secretAddress, @RequestBody String secretValue) { - if (isInProduction(authorization)) { - String challenge = authorizationService.getChallengeForSetWeb3Secret(secretAddress, secretValue); + if (!SecretUtils.isSecretSizeValid(secretValue)) { + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build(); + } - if (!authorizationService.isSignedByOwner(challenge, authorization, secretAddress)) { - log.error("Unauthorized to addWeb3Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + String challenge = authorizationService.getChallengeForSetWeb3Secret(secretAddress, secretValue); + + if (!authorizationService.isSignedByOwner(challenge, authorization, secretAddress)) { + log.error("Unauthorized to addWeb3Secret [expectedChallenge:{}]", challenge); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } if (web3SecretService.getSecret(secretAddress).isPresent()) { @@ -114,13 +110,11 @@ public ResponseEntity getWeb2Secret(@RequestHeader("Authorization") Stri @RequestParam String ownerAddress, @RequestParam String secretName, @RequestParam(required = false, defaultValue = "false") boolean shouldDecryptSecret) { - if (isInProduction(authorization)) { - String challenge = authorizationService.getChallengeForGetWeb2Secret(ownerAddress, secretName); + String challenge = authorizationService.getChallengeForGetWeb2Secret(ownerAddress, secretName); - if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { - log.error("Unauthorized to getWeb2Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { + log.error("Unauthorized to getWeb2Secret [expectedChallenge:{}]", challenge); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } Optional secret = web2SecretsService.getSecret(ownerAddress, secretName, shouldDecryptSecret); @@ -132,13 +126,15 @@ public ResponseEntity addWeb2Secret(@RequestHeader("Authorization") Stri @RequestParam String ownerAddress, @RequestParam String secretName, @RequestBody String secretValue) { - if (isInProduction(authorization)) { - String challenge = authorizationService.getChallengeForSetWeb2Secret(ownerAddress, secretName, secretValue); + if (!SecretUtils.isSecretSizeValid(secretValue)) { + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build(); + } - if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { - log.error("Unauthorized to addWeb2Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + String challenge = authorizationService.getChallengeForSetWeb2Secret(ownerAddress, secretName, secretValue); + + if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { + log.error("Unauthorized to addWeb2Secret [expectedChallenge:{}]", challenge); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } if (web2SecretsService.getSecret(ownerAddress, secretName).isPresent()) { @@ -154,13 +150,11 @@ public ResponseEntity updateWeb2Secret(@RequestHeader("Authorization") S @RequestParam String ownerAddress, @RequestParam String secretName, @RequestBody String newSecretValue) { - if (isInProduction(authorization)) { - String challenge = authorizationService.getChallengeForSetWeb2Secret(ownerAddress, secretName, newSecretValue); + String challenge = authorizationService.getChallengeForSetWeb2Secret(ownerAddress, secretName, newSecretValue); - if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { - log.error("Unauthorized to updateWeb2Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { + log.error("Unauthorized to updateWeb2Secret [expectedChallenge:{}]", challenge); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } if (web2SecretsService.getSecret(ownerAddress, secretName).isEmpty()) { @@ -175,7 +169,7 @@ public ResponseEntity updateWeb2Secret(@RequestHeader("Authorization") S * Server-side signature of a messageHash * */ @PostMapping("/delegate/signature") - private ResponseEntity signMessageHashOnServerSide(@RequestParam String messageHash, + public ResponseEntity signMessageHashOnServerSide(@RequestParam String messageHash, @RequestBody String privateKey) { Signature signature = signMessageHashAndGetSignature(messageHash, privateKey); @@ -185,11 +179,5 @@ private ResponseEntity signMessageHashOnServerSide(@RequestParam String return ResponseEntity.ok(signature.getValue()); } - - private boolean isInProduction(String authorization) { - boolean canAvoidAuthorization = versionService.isSnapshot() && authorization.equals("*"); - return !canAvoidAuthorization; - } - } diff --git a/src/main/java/com/iexec/sms/secret/SecretUtils.java b/src/main/java/com/iexec/sms/secret/SecretUtils.java new file mode 100644 index 00000000..660b98e3 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/SecretUtils.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret; + +import java.nio.charset.StandardCharsets; + +public abstract class SecretUtils { + /** + * Max size (in kBs) a secret can have. + */ + public static final int SECRET_MAX_SIZE = 4096; + + public static boolean isSecretSizeValid(String secretValue) { + return secretValue.getBytes(StandardCharsets.UTF_8).length <= SECRET_MAX_SIZE; + } +} diff --git a/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java b/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java new file mode 100644 index 00000000..f52a3810 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java @@ -0,0 +1,278 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.authorization.AuthorizationService; +import com.iexec.sms.secret.SecretUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +@Slf4j +@CrossOrigin +@RestController +public class AppComputeSecretController { + private final AuthorizationService authorizationService; + private final TeeTaskComputeSecretService teeTaskComputeSecretService; + + private static final ApiResponseBody> invalidAuthorizationPayload = createErrorPayload("Invalid authorization"); + + static final String INVALID_SECRET_INDEX_FORMAT_MSG = "Secret index should be a positive number"; + static final String INVALID_SECRET_KEY_FORMAT_MSG = "Secret key should contain at most 64 characters from [0-9A-Za-z-_]"; + + private static final Pattern secretKeyPattern = Pattern.compile("^[\\p{Alnum}-_]{" + + TeeTaskComputeSecret.SECRET_KEY_MIN_LENGTH + "," + + TeeTaskComputeSecret.SECRET_KEY_MAX_LENGTH + "}$"); + + public AppComputeSecretController(AuthorizationService authorizationService, + TeeTaskComputeSecretService teeTaskComputeSecretService) { + this.authorizationService = authorizationService; + this.teeTaskComputeSecretService = teeTaskComputeSecretService; + } + + // region App developer endpoints + @PostMapping("/apps/{appAddress}/secrets/1") + public ResponseEntity>> addAppDeveloperAppComputeSecret(@RequestHeader("Authorization") String authorization, + @PathVariable String appAddress, + @RequestBody String secretValue) { + appAddress = appAddress.toLowerCase(); + String secretIndex = "1"; + + try { + checkSecretIndex(secretIndex); + } catch (NumberFormatException e) { + log.error(INVALID_SECRET_INDEX_FORMAT_MSG, e); + return ResponseEntity + .badRequest() + .body(createErrorPayload(INVALID_SECRET_INDEX_FORMAT_MSG)); + } + + if (!SecretUtils.isSecretSizeValid(secretValue)) { + return ResponseEntity + .status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(createErrorPayload("Secret size should not exceed 4 Kb")); + } + + String challenge = authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue); + + if (!authorizationService.isSignedByOwner(challenge, authorization, appAddress)) { + log.error("Unauthorized to addAppDeveloperComputeComputeSecret" + + " [appAddress: {}, expectedChallenge: {}]", + appAddress, challenge); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(invalidAuthorizationPayload); + } + + if (teeTaskComputeSecretService.isSecretPresent( + OnChainObjectType.APPLICATION, + appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + secretIndex)) { + log.error("Can't add app developer secret as it already exists" + + " [appAddress:{}, secretIndex:{}]", + appAddress, secretIndex); + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(createErrorPayload("Secret already exists")); + } + + teeTaskComputeSecretService.encryptAndSaveSecret( + OnChainObjectType.APPLICATION, + appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + secretIndex, + secretValue + ); + return ResponseEntity.noContent().build(); + } + + @RequestMapping(method = RequestMethod.HEAD, path = "/apps/{appAddress}/secrets/{secretIndex}") + public ResponseEntity>> isAppDeveloperAppComputeSecretPresent(@PathVariable String appAddress, + @PathVariable String secretIndex) { + appAddress = appAddress.toLowerCase(); + try { + checkSecretIndex(secretIndex); + } catch (NumberFormatException e) { + log.error(INVALID_SECRET_INDEX_FORMAT_MSG, e); + return ResponseEntity + .badRequest() + .body(createErrorPayload(INVALID_SECRET_INDEX_FORMAT_MSG)); + } + + final boolean isSecretPresent = teeTaskComputeSecretService.isSecretPresent( + OnChainObjectType.APPLICATION, + appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + secretIndex + ); + if (isSecretPresent) { + log.debug("App developer secret found [appAddress: {}, secretIndex: {}]", appAddress, secretIndex); + return ResponseEntity.noContent().build(); + } + + log.debug("App developer secret not found [appAddress: {}, secretIndex: {}]", appAddress, secretIndex); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(createErrorPayload("Secret not found")); + } + + /** + * Checks provided application developer index is in a valid range. + * A valid index is a positive number. + * @param secretIndex Secret index value to check. + */ + private void checkSecretIndex(String secretIndex) { + int idx = Integer.parseInt(secretIndex); + if (idx <= 0) { + throw new NumberFormatException(); + } + } + // endregion + + // region App requester endpoint + @PostMapping("/requesters/{requesterAddress}/secrets/{secretKey}") + public ResponseEntity>> addRequesterAppComputeSecret(@RequestHeader("Authorization") String authorization, + @PathVariable String requesterAddress, + @PathVariable String secretKey, + @RequestBody String secretValue) { + requesterAddress = requesterAddress.toLowerCase(); + if (!secretKeyPattern.matcher(secretKey).matches()) { + return ResponseEntity + .badRequest() + .body(createErrorPayload(INVALID_SECRET_KEY_FORMAT_MSG)); + } + + String challenge = authorizationService.getChallengeForSetRequesterAppComputeSecret( + requesterAddress, + secretKey, + secretValue + ); + + if (!authorizationService.isSignedByHimself(challenge, authorization, requesterAddress)) { + log.error("Unauthorized to addRequesterAppComputeSecret" + + " [requesterAddress:{}, expectedChallenge:{}]", + requesterAddress, challenge); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(invalidAuthorizationPayload); + } + + final List badRequestErrors = validateRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + if (!badRequestErrors.isEmpty()) { + return ResponseEntity + .badRequest() + .body(createErrorPayload(badRequestErrors)); + } + + if (teeTaskComputeSecretService.isSecretPresent( + OnChainObjectType.APPLICATION, + "", + SecretOwnerRole.REQUESTER, + requesterAddress, + secretKey)) { + log.debug("Can't add requester secret as it already exists" + + " [requesterAddress:{}, secretIndex:{}]", + requesterAddress, secretKey); + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(createErrorPayload("Secret already exists")); + } + + teeTaskComputeSecretService.encryptAndSaveSecret( + OnChainObjectType.APPLICATION, + "", + SecretOwnerRole.REQUESTER, + requesterAddress, + secretKey, + secretValue + ); + return ResponseEntity.noContent().build(); + } + + private List validateRequesterAppComputeSecret( + String requesterAddress, + String secretKey, + String secretValue) { + List errors = new ArrayList<>(); + + if (!SecretUtils.isSecretSizeValid(secretValue)) { + final String errorMessage = "Secret size should not exceed 4 Kb"; + log.debug("{} [requesterAddress:{}, secretIndex:{}, secretLength:{}]", + errorMessage, requesterAddress, secretKey, secretValue.length() + ); + errors.add(errorMessage); + } + + return errors; + } + + @RequestMapping(method = RequestMethod.HEAD, path = "/requesters/{requesterAddress}/secrets/{secretKey}") + public ResponseEntity>> isRequesterAppComputeSecretPresent( + @PathVariable String requesterAddress, + @PathVariable String secretKey) { + requesterAddress = requesterAddress.toLowerCase(); + + if (!secretKeyPattern.matcher(secretKey).matches()) { + return ResponseEntity + .badRequest() + .body(createErrorPayload(INVALID_SECRET_KEY_FORMAT_MSG)); + } + + final boolean isSecretPresent = teeTaskComputeSecretService.isSecretPresent( + OnChainObjectType.APPLICATION, + "", + SecretOwnerRole.REQUESTER, + requesterAddress, + secretKey + ); + + String messageDetails = MessageFormat.format("[requester: {0}, secretIndex: {1}]", + requesterAddress, secretKey); + if (isSecretPresent) { + log.debug("App requester secret found {}", messageDetails); + return ResponseEntity.noContent().build(); + } + + log.debug("App requester secret not found {}", messageDetails); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(createErrorPayload("Secret not found")); + } + // endregion + + private static ApiResponseBody> createErrorPayload(String errorMessage) { + return createErrorPayload(List.of(errorMessage)); + } + + private static ApiResponseBody> createErrorPayload(List errors) { + return ApiResponseBody + .>builder() + .error(errors) + .build(); + } +} diff --git a/src/main/java/com/iexec/sms/secret/compute/OnChainObjectType.java b/src/main/java/com/iexec/sms/secret/compute/OnChainObjectType.java new file mode 100644 index 00000000..55782b1a --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/OnChainObjectType.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +public enum OnChainObjectType { + APPLICATION +} diff --git a/src/main/java/com/iexec/sms/secret/compute/SecretOwnerRole.java b/src/main/java/com/iexec/sms/secret/compute/SecretOwnerRole.java new file mode 100644 index 00000000..c0415dcf --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/SecretOwnerRole.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +/** + * Defines which role a tee task compute secret owner can have. + */ +public enum SecretOwnerRole { + APPLICATION_DEVELOPER, + REQUESTER +} diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java new file mode 100644 index 00000000..7658abb1 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +import com.iexec.sms.secret.SecretUtils; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * Define a secret that can be used during the execution of a TEE task. + * Currently, only secrets for application developers and requesters are supported. + *

+ * In this implementation, a unique constraint has been added on onChainObjectAddress, + * fixedSecretOwner and key columns. + * This constraint has been defined in such a way because: + *

+ *

+ * The key parameter must be a String compliant with the following constraints: + *

+ */ +@Data +@NoArgsConstructor +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = {"onChainObjectAddress", "fixedSecretOwner", "key"}) }) +public class TeeTaskComputeSecret implements Serializable { + + public static final int SECRET_KEY_MIN_LENGTH = 1; + public static final int SECRET_KEY_MAX_LENGTH = 64; + + @Id + @GeneratedValue(generator = "system-uuid") + @GenericGenerator(name = "system-uuid", strategy = "uuid") + private String id; + + /** + * Represents the blockchain address of the deployed object + * (0xapplication, 0xdataset, 0xworkerpool) + *

+ * In a future release, it should also handle ENS names. + */ + @NotNull + private String onChainObjectAddress; // Will be empty for a secret belonging to a requester + @NotNull + private OnChainObjectType onChainObjectType; + @NotNull + private SecretOwnerRole secretOwnerRole; + @NotNull + private String fixedSecretOwner; // Will be empty for a secret belonging to an application developer + @NotNull + @Size(min = SECRET_KEY_MIN_LENGTH, max = SECRET_KEY_MAX_LENGTH) + private String key; + @NotNull + /* + * Expected behavior of AES encryption is to not expand the data very much. + * Final size might be padded to the next block, plus another padding might + * be necessary for the IV (https://stackoverflow.com/a/93463). + * In addition to that, it is worth mentioning that current implementation + * encrypts the input and produces a Base64 result (stored as-is in + * database) which causes an overhead of ~33% + * (https://en.wikipedia.org/wiki/Base64). + *

+ * For these reasons and for simplicity purposes, we reserve twice the size + * of `SECRET_MAX_SIZE` in storage. + */ + @Column(length = SecretUtils.SECRET_MAX_SIZE * 2) + private String value; + + @Builder + public TeeTaskComputeSecret( + OnChainObjectType onChainObjectType, + String onChainObjectAddress, + SecretOwnerRole secretOwnerRole, + String fixedSecretOwner, + String key, + String value) { + this.onChainObjectType = onChainObjectType; + this.onChainObjectAddress = onChainObjectAddress; + this.secretOwnerRole = secretOwnerRole; + this.fixedSecretOwner = fixedSecretOwner; + this.key = key; + this.value = value; + } +} diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java new file mode 100644 index 00000000..88180feb --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TeeTaskComputeSecretRepository extends JpaRepository { +} diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java new file mode 100644 index 00000000..0c321b04 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.secret.compute; + +import com.iexec.sms.encryption.EncryptionService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.SerializationUtils; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Slf4j +@Service +public class TeeTaskComputeSecretService { + private final TeeTaskComputeSecretRepository teeTaskComputeSecretRepository; + private final EncryptionService encryptionService; + + protected TeeTaskComputeSecretService( + TeeTaskComputeSecretRepository teeTaskComputeSecretRepository, + EncryptionService encryptionService) { + this.teeTaskComputeSecretRepository = teeTaskComputeSecretRepository; + this.encryptionService = encryptionService; + } + + /** + * Retrieve a secret. + * Decrypt if required. + */ + public Optional getSecret( + OnChainObjectType onChainObjectType, + String onChainObjectAddress, + SecretOwnerRole secretOwnerRole, + String secretOwner, + String secretKey) { + onChainObjectAddress = onChainObjectAddress.toLowerCase(); + final TeeTaskComputeSecret wantedSecret = TeeTaskComputeSecret + .builder() + .onChainObjectType(onChainObjectType) + .onChainObjectAddress(onChainObjectAddress) + .secretOwnerRole(secretOwnerRole) + .fixedSecretOwner(secretOwner) + .key(secretKey) + .build(); + final ExampleMatcher exampleMatcher = ExampleMatcher.matching() + .withIgnorePaths("value"); + final Optional oSecret = teeTaskComputeSecretRepository + .findOne(Example.of(wantedSecret, exampleMatcher)); + if (oSecret.isEmpty()) { + return Optional.empty(); + } + final TeeTaskComputeSecret secret = oSecret.get(); + final String decryptedValue = encryptionService.decrypt(secret.getValue()); + // deep copy to avoid altering original object + //TODO: Improve this out-of-the box cloning to get better performances + TeeTaskComputeSecret decryptedSecret = SerializationUtils.clone(secret); + decryptedSecret.setValue(decryptedValue); + return Optional.of(decryptedSecret); + } + + /** + * Check whether a secret exists. + * + * @return {@code true} if the secret exists in the database, {@code false} otherwise. + */ + public boolean isSecretPresent(OnChainObjectType onChainObjectType, + String deployedObjectAddress, + SecretOwnerRole secretOwnerRole, + String secretOwner, + String secretKey) { + return getSecret( + onChainObjectType, + deployedObjectAddress, + secretOwnerRole, + secretOwner, + secretKey + ).isPresent(); + } + + /** + * Encrypt a secret and store it if it doesn't already exist. + * + * @return {@code false} if the secret already exists, {@code true} otherwise. + */ + public boolean encryptAndSaveSecret(OnChainObjectType onChainObjectType, + String onChainObjectAddress, + SecretOwnerRole secretOwnerRole, + String secretOwner, + String secretKey, + String secretValue) { + if (isSecretPresent(onChainObjectType, onChainObjectAddress, secretOwnerRole, secretOwner, secretKey)) { + final TeeTaskComputeSecret secret = TeeTaskComputeSecret + .builder() + .onChainObjectType(onChainObjectType) + .onChainObjectAddress(onChainObjectAddress) + .secretOwnerRole(secretOwnerRole) + .fixedSecretOwner(secretOwner) + .key(secretKey) + .build(); + log.info("Tee task compute secret already exists, can't update it." + + " [secret:{}]", secret); + return false; + } + onChainObjectAddress = onChainObjectAddress.toLowerCase(); + final TeeTaskComputeSecret secret = TeeTaskComputeSecret + .builder() + .onChainObjectType(onChainObjectType) + .onChainObjectAddress(onChainObjectAddress) + .secretOwnerRole(secretOwnerRole) + .fixedSecretOwner(secretOwner) + .key(secretKey) + .value(encryptionService.encrypt(secretValue)) + .build(); + log.info("Adding new tee task compute secret" + + " [secret:{}]", secret); + teeTaskComputeSecretRepository.save(secret); + return true; + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java b/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java index 438d39ee..4122451f 100644 --- a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java +++ b/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java @@ -29,7 +29,7 @@ @Service public class Web2SecretsService extends AbstractSecretService { - private Web2SecretsRepository web2SecretsRepository; + private final Web2SecretsRepository web2SecretsRepository; public Web2SecretsService(Web2SecretsRepository web2SecretsRepository, EncryptionService encryptionService) { @@ -43,14 +43,13 @@ public Optional getWeb2Secrets(String ownerAddress) { } public Optional getSecret(String ownerAddress, String secretAddress) { - ownerAddress = ownerAddress.toLowerCase(); return getSecret(ownerAddress, secretAddress, false); } public Optional getSecret(String ownerAddress, String secretAddress, boolean shouldDecryptValue) { ownerAddress = ownerAddress.toLowerCase(); Optional web2Secrets = getWeb2Secrets(ownerAddress); - if (!web2Secrets.isPresent()) { + if (web2Secrets.isEmpty()) { return Optional.empty(); } Secret secret = web2Secrets.get().getSecret(secretAddress); @@ -73,7 +72,7 @@ public void addSecret(String ownerAddress, String secretAddress, String secretVa Secret secret = new Secret(secretAddress, secretValue); encryptSecret(secret); - log.info("Adding new secret [ownerAddress:{}, secretAddress:{}, secretValueHash:{}]", + log.info("Adding new secret [ownerAddress:{}, secretAddress:{}, encryptedSecretValue:{}]", ownerAddress, secretAddress, secret.getValue()); web2Secrets.getSecrets().add(secret); web2SecretsRepository.save(web2Secrets); @@ -91,7 +90,7 @@ public void updateSecret(String ownerAddress, String secretAddress, String newSe return; } - log.info("Updating secret [ownerAddress:{}, secretAddress:{}, oldSecretValueHash:{}, newSecretValueHash:{}]", + log.info("Updating secret [ownerAddress:{}, secretAddress:{}, oldEncryptedSecretValue:{}, newEncryptedSecretValue:{}]", ownerAddress, secretAddress, existingSecret.getValue(), newSecret.getValue()); existingSecret.setValue(newSecret.getValue(), true); web2SecretsRepository.save(web2Secrets.get()); diff --git a/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java b/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java index 8f42599a..ecf197b8 100644 --- a/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java +++ b/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java @@ -28,7 +28,7 @@ @Service public class Web3SecretService extends AbstractSecretService { - private Web3SecretRepository web3SecretRepository; + private final Web3SecretRepository web3SecretRepository; public Web3SecretService(Web3SecretRepository web3SecretRepository, EncryptionService encryptionService) { @@ -45,11 +45,10 @@ public Optional getSecret(String secretAddress, boolean shouldDecryp if (shouldDecryptValue) { decryptSecret(secret.get()); } - return Optional.of(secret.get()); + return secret; } public Optional getSecret(String secretAddress) { - secretAddress = secretAddress.toLowerCase(); return getSecret(secretAddress, false); } @@ -61,7 +60,7 @@ public void addSecret(String secretAddress, String secretValue) { secretAddress = secretAddress.toLowerCase(); Web3Secret web3Secret = new Web3Secret(secretAddress, secretValue); encryptSecret(web3Secret); - log.info("Adding new web3 secret [secretAddress:{}, secretValueHash:{}]", + log.info("Adding new web3 secret [secretAddress:{}, encryptedSecretValue:{}]", secretAddress, web3Secret.getValue()); web3SecretRepository.save(web3Secret); } diff --git a/src/main/java/com/iexec/sms/tee/TeeController.java b/src/main/java/com/iexec/sms/tee/TeeController.java index 9e6c4211..917eb875 100644 --- a/src/main/java/com/iexec/sms/tee/TeeController.java +++ b/src/main/java/com/iexec/sms/tee/TeeController.java @@ -17,26 +17,42 @@ package com.iexec.sms.tee; -import java.util.Optional; - import com.iexec.common.chain.WorkerpoolAuthorization; +import com.iexec.sms.api.TeeSessionGenerationError; import com.iexec.common.tee.TeeWorkflowSharedConfiguration; +import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.authorization.AuthorizationError; import com.iexec.sms.authorization.AuthorizationService; import com.iexec.sms.tee.challenge.TeeChallenge; import com.iexec.sms.tee.challenge.TeeChallengeService; +import com.iexec.sms.tee.session.TeeSessionGenerationException; import com.iexec.sms.tee.session.TeeSessionService; import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.web3j.crypto.Keys; -import lombok.extern.slf4j.Slf4j; +import java.util.Map; +import java.util.Optional; + +import static com.iexec.sms.api.TeeSessionGenerationError.*; +import static com.iexec.sms.authorization.AuthorizationError.*; @Slf4j @RestController @RequestMapping("/tee") public class TeeController { + private static final Map authorizationToGenerationError = + Map.of( + EMPTY_PARAMS_UNAUTHORIZED, EXECUTION_NOT_AUTHORIZED_EMPTY_PARAMS_UNAUTHORIZED, + NO_MATCH_ONCHAIN_TYPE, EXECUTION_NOT_AUTHORIZED_NO_MATCH_ONCHAIN_TYPE, + GET_CHAIN_TASK_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_TASK_FAILED, + TASK_NOT_ACTIVE, EXECUTION_NOT_AUTHORIZED_TASK_NOT_ACTIVE, + GET_CHAIN_DEAL_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_DEAL_FAILED, + INVALID_SIGNATURE, EXECUTION_NOT_AUTHORIZED_INVALID_SIGNATURE + ); private final AuthorizationService authorizationService; private final TeeChallengeService teeChallengeService; @@ -95,16 +111,35 @@ public ResponseEntity generateTeeChallenge(@PathVariable String chainTas * 500 INTERNAL_SERVER_ERROR otherwise. */ @PostMapping("/sessions") - public ResponseEntity generateTeeSession( + public ResponseEntity> generateTeeSession( @RequestHeader("Authorization") String authorization, @RequestBody WorkerpoolAuthorization workerpoolAuthorization) { String workerAddress = workerpoolAuthorization.getWorkerWallet(); String challenge = authorizationService.getChallengeForWorker(workerpoolAuthorization); if (!authorizationService.isSignedByHimself(challenge, authorization, workerAddress)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + final ApiResponseBody body = + ApiResponseBody.builder() + .error(INVALID_AUTHORIZATION) + .build(); + + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(body); } - if (!authorizationService.isAuthorizedOnExecution(workerpoolAuthorization, true)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + final Optional authorizationError = + authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true); + if (authorizationError.isPresent()) { + final TeeSessionGenerationError teeSessionGenerationError = + authorizationToGenerationError.get(authorizationError.get()); + + final ApiResponseBody body = + ApiResponseBody.builder() + .error(teeSessionGenerationError) + .build(); + + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(body); } String taskId = workerpoolAuthorization.getChainTaskId(); workerAddress = Keys.toChecksumAddress(workerAddress); @@ -114,13 +149,32 @@ public ResponseEntity generateTeeSession( try { String sessionId = teeSessionService .generateTeeSession(taskId, workerAddress, attestingEnclave); - return sessionId.isEmpty() - ? ResponseEntity.notFound().build() - : ResponseEntity.ok(sessionId); - } catch(Exception e) { + + if (sessionId.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(ApiResponseBody.builder().data(sessionId).build()); + } catch(TeeSessionGenerationException e) { log.error("Failed to generate secure session [taskId:{}, workerAddress:{}]", taskId, workerAddress, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + final ApiResponseBody body = + ApiResponseBody.builder() + .error(e.getError()) + .build(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(body); + } catch (Exception e) { + log.error("Failed to generate secure session with unknown reason [taskId:{}, workerAddress:{}]", + taskId, workerAddress, e); + final ApiResponseBody body = + ApiResponseBody.builder() + .error(SECURE_SESSION_GENERATION_FAILED) + .build(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(body); } } } diff --git a/src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java b/src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java new file mode 100644 index 00000000..04fa9761 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.tee.session; + +import com.iexec.sms.api.TeeSessionGenerationError; + +public class TeeSessionGenerationException extends Exception { + private final TeeSessionGenerationError error; + + public TeeSessionGenerationException(TeeSessionGenerationError error, String message) { + super(message); + this.error = error; + } + + public TeeSessionGenerationError getError() { + return error; + } +} diff --git a/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java b/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java index 019b53bd..22f552ea 100644 --- a/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java +++ b/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java @@ -26,7 +26,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import static java.util.Objects.requireNonNull; +import static com.iexec.sms.api.TeeSessionGenerationError.*; @Slf4j @Service @@ -52,12 +52,16 @@ public TeeSessionService( public String generateTeeSession( String taskId, String workerAddress, - String teeChallenge) throws Exception { + String teeChallenge) throws TeeSessionGenerationException { String sessionId = createSessionId(taskId); TaskDescription taskDescription = iexecHubService.getTaskDescription(taskId); - requireNonNull(taskDescription, - "Failed to get task description - taskId: " + taskId); + if (taskDescription == null) { + throw new TeeSessionGenerationException( + GET_TASK_DESCRIPTION_FAILED, + String.format("Failed to get task description [taskId:%s]", taskId) + ); + } PalaemonSessionRequest request = PalaemonSessionRequest.builder() .sessionId(sessionId) .taskDescription(taskDescription) @@ -66,8 +70,9 @@ public String generateTeeSession( .build(); String sessionYmlAsString = palaemonSessionService.getSessionYml(request); if (sessionYmlAsString.isEmpty()) { - throw new Exception("Failed to get session yml [taskId:" + taskId + "," + - " workerAddress:" + workerAddress); + throw new TeeSessionGenerationException( + GET_SESSION_YML_FAILED, + String.format("Failed to get session yml [taskId:%s, workerAddress:%s]", taskId, workerAddress)); } log.info("Session yml is ready [taskId:{}]", taskId); if (shouldDisplayDebugSession){ @@ -78,7 +83,13 @@ public String generateTeeSession( .generateSecureSession(sessionYmlAsString.getBytes()) .getStatusCode() .is2xxSuccessful(); - return isSessionGenerated ? sessionId : ""; + if (!isSessionGenerated) { + throw new TeeSessionGenerationException( + SECURE_SESSION_CAS_CALL_FAILED, + String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", taskId, workerAddress) + ); + } + return sessionId; } private String createSessionId(String taskId) { diff --git a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java b/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java index 7d41c778..d273976e 100644 --- a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java +++ b/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java @@ -22,10 +22,15 @@ import com.iexec.common.utils.FileHelper; import com.iexec.common.utils.IexecEnvUtils; import com.iexec.sms.secret.Secret; +import com.iexec.sms.secret.compute.OnChainObjectType; +import com.iexec.sms.secret.compute.SecretOwnerRole; +import com.iexec.sms.secret.compute.TeeTaskComputeSecret; +import com.iexec.sms.secret.compute.TeeTaskComputeSecretService; import com.iexec.sms.secret.web2.Web2SecretsService; import com.iexec.sms.secret.web3.Web3SecretService; import com.iexec.sms.tee.challenge.TeeChallenge; import com.iexec.sms.tee.challenge.TeeChallengeService; +import com.iexec.sms.tee.session.TeeSessionGenerationException; import com.iexec.sms.tee.session.attestation.AttestationSecurityConfig; import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; import com.iexec.sms.utils.EthereumCredentials; @@ -38,9 +43,9 @@ import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; - import java.io.FileNotFoundException; import java.io.StringWriter; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -52,7 +57,7 @@ import static com.iexec.common.sms.secret.ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY; import static com.iexec.common.tee.TeeUtils.booleanToYesNo; import static com.iexec.common.worker.result.ResultUtils.*; -import static java.util.Objects.requireNonNull; +import static com.iexec.sms.api.TeeSessionGenerationError.*; @Slf4j @Service @@ -77,6 +82,8 @@ public class PalaemonSessionService { // PostCompute static final String POST_COMPUTE_MRENCLAVE = "POST_COMPUTE_MRENCLAVE"; static final String POST_COMPUTE_ENTRYPOINT = "POST_COMPUTE_ENTRYPOINT"; + // Secrets + static final String REQUESTER_SECRETS = "REQUESTER_SECRETS"; // Env private static final String ENV_PROPERTY = "env"; @@ -85,6 +92,7 @@ public class PalaemonSessionService { private final TeeChallengeService teeChallengeService; private final TeeWorkflowConfiguration teeWorkflowConfig; private final AttestationSecurityConfig attestationSecurityConfig; + private final TeeTaskComputeSecretService teeTaskComputeSecretService; @Value("${scone.cas.palaemon}") private String palaemonTemplateFilePath; @@ -94,16 +102,18 @@ public PalaemonSessionService( Web2SecretsService web2SecretsService, TeeChallengeService teeChallengeService, TeeWorkflowConfiguration teeWorkflowConfig, - AttestationSecurityConfig attestationSecurityConfig) { + AttestationSecurityConfig attestationSecurityConfig, + TeeTaskComputeSecretService teeTaskComputeSecretService) { this.web3SecretService = web3SecretService; this.web2SecretsService = web2SecretsService; this.teeChallengeService = teeChallengeService; this.teeWorkflowConfig = teeWorkflowConfig; this.attestationSecurityConfig = attestationSecurityConfig; + this.teeTaskComputeSecretService = teeTaskComputeSecretService; } @PostConstruct - void postConstruct() throws Exception { + void postConstruct() throws FileNotFoundException { if (StringUtils.isEmpty(palaemonTemplateFilePath)) { throw new IllegalArgumentException("Missing palaemon template filepath"); } @@ -119,14 +129,24 @@ void postConstruct() throws Exception { * TODO: Read onchain available infos from enclave instead of copying * public vars to palaemon.yml. It needs ssl call from enclave to eth * node (only ethereum node address required inside palaemon.yml) - * + * * @param request session request details * @return session config in yaml string format - * @throws Exception */ - public String getSessionYml(PalaemonSessionRequest request) throws Exception { - requireNonNull(request, "Session request must not be null"); - requireNonNull(request.getTaskDescription(), "Task description must not be null"); + public String getSessionYml(PalaemonSessionRequest request) throws TeeSessionGenerationException { + if (request == null) { + throw new TeeSessionGenerationException( + NO_SESSION_REQUEST, + "Session request must not be null" + ); + } + if (request.getTaskDescription() == null) { + throw new TeeSessionGenerationException( + NO_TASK_DESCRIPTION, + "Task description must not be null" + ); + } + TaskDescription taskDescription = request.getTaskDescription(); Map palaemonTokens = new HashMap<>(); palaemonTokens.put(SESSION_ID, request.getSessionId()); @@ -159,13 +179,12 @@ public String getSessionYml(PalaemonSessionRequest request) throws Exception { /** * Get tokens to be injected in the pre-compute enclave. - * - * @param request + * * @return map of pre-compute tokens - * @throws Exception if dataset secret is not found. + * @throws TeeSessionGenerationException if dataset secret is not found. */ Map getPreComputePalaemonTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); Map tokens = new HashMap<>(); @@ -178,7 +197,10 @@ Map getPreComputePalaemonTokens(PalaemonSessionRequest request) if (taskDescription.containsDataset()) { String datasetKey = web3SecretService .getSecret(taskDescription.getDatasetAddress(), true) - .orElseThrow(() -> new Exception("Empty dataset secret - taskId: " + taskId)) + .orElseThrow(() -> new TeeSessionGenerationException( + PRE_COMPUTE_GET_DATASET_SECRET_FAILED, + "Empty dataset secret - taskId: " + taskId + )) .getTrimmedValue(); tokens.put(IEXEC_DATASET_KEY, datasetKey); } else { @@ -190,7 +212,7 @@ Map getPreComputePalaemonTokens(PalaemonSessionRequest request) .entrySet() .stream() .filter(e -> e.getKey().contains(IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX)) - .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); tokens.put(INPUT_FILE_URLS, inputFileUrls); return tokens; } @@ -199,16 +221,31 @@ Map getPreComputePalaemonTokens(PalaemonSessionRequest request) * Compute (App) */ Map getAppPalaemonTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException{ TaskDescription taskDescription = request.getTaskDescription(); - requireNonNull(taskDescription, "Task description must no be null"); + if (taskDescription == null) { + throw new TeeSessionGenerationException( + NO_TASK_DESCRIPTION, + "Task description must no be null" + ); + } + Map tokens = new HashMap<>(); TeeEnclaveConfiguration enclaveConfig = taskDescription.getAppEnclaveConfiguration(); - requireNonNull(enclaveConfig, "Enclave configuration must no be null"); + if (enclaveConfig == null) { + throw new TeeSessionGenerationException( + APP_COMPUTE_NO_ENCLAVE_CONFIG, + "Enclave configuration must no be null" + ); + } if (!enclaveConfig.getValidator().isValid()){ - throw new IllegalArgumentException("Invalid enclave configuration: " + - enclaveConfig.getValidator().validate().toString()); + throw new TeeSessionGenerationException( + APP_COMPUTE_INVALID_ENCLAVE_CONFIG, + "Invalid enclave configuration: " + + enclaveConfig.getValidator().validate().toString() + ); } + tokens.put(APP_MRENCLAVE, enclaveConfig.getFingerprint()); String appArgs = enclaveConfig.getEntrypoint(); if (!StringUtils.isEmpty(taskDescription.getCmd())) { @@ -221,8 +258,64 @@ Map getAppPalaemonTokens(PalaemonSessionRequest request) .entrySet() .stream() .filter(e -> e.getKey().contains(IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX)) - .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); tokens.put(INPUT_FILE_NAMES, inputFileNames); + + final Map computeSecrets = getApplicationComputeSecrets(taskDescription); + tokens.putAll(computeSecrets); + + return tokens; + } + + private Map getApplicationComputeSecrets(TaskDescription taskDescription) { + final Map tokens = new HashMap<>(); + final String applicationAddress = taskDescription.getAppAddress(); + + if (applicationAddress != null) { + final String secretIndex = "1"; + String appDeveloperSecret = + teeTaskComputeSecretService.getSecret( + OnChainObjectType.APPLICATION, + applicationAddress.toLowerCase(), + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + secretIndex) + .map(TeeTaskComputeSecret::getValue) + .orElse(EMPTY_YML_VALUE); + tokens.put(IexecEnvUtils.IEXEC_APP_DEVELOPER_SECRET_PREFIX + secretIndex, appDeveloperSecret); + } + + if (taskDescription.getSecrets() == null || taskDescription.getRequester() == null) { + tokens.put(REQUESTER_SECRETS, Collections.emptyMap()); + return tokens; + } + + final HashMap requesterSecrets = new HashMap<>(); + for (Map.Entry secretEntry: taskDescription.getSecrets().entrySet()) { + try { + int requesterSecretIndex = Integer.parseInt(secretEntry.getKey()); + if (requesterSecretIndex <= 0) { + String message = "Application secret indices provided in the deal parameters must be positive numbers" + + " [providedApplicationSecretIndex:" + requesterSecretIndex + "]"; + log.warn(message); + throw new NumberFormatException(message); + } + } catch(NumberFormatException e) { + log.warn("Invalid entry found in deal parameters secrets map", e); + continue; + } + String requesterSecret = teeTaskComputeSecretService.getSecret( + OnChainObjectType.APPLICATION, + "", + SecretOwnerRole.REQUESTER, + taskDescription.getRequester().toLowerCase(), + secretEntry.getValue()) + .map(TeeTaskComputeSecret::getValue) + .orElse(EMPTY_YML_VALUE); + requesterSecrets.put(IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + secretEntry.getKey(), requesterSecret); + } + tokens.put(REQUESTER_SECRETS, requesterSecrets); + return tokens; } @@ -230,10 +323,12 @@ Map getAppPalaemonTokens(PalaemonSessionRequest request) * Post-Compute (Result) */ Map getPostComputePalaemonTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); - requireNonNull(taskDescription, "Task description must no be null"); - String taskId = taskDescription.getChainTaskId(); + if (taskDescription == null) { + throw new TeeSessionGenerationException(NO_TASK_DESCRIPTION, "Task description must not be null"); + } + Map tokens = new HashMap<>(); String teePostComputeFingerprint = teeWorkflowConfig.getPostComputeFingerprint(); // ############################################################################### @@ -250,27 +345,18 @@ Map getPostComputePalaemonTokens(PalaemonSessionRequest request) tokens.put(POST_COMPUTE_ENTRYPOINT, entrypoint); // encryption Map encryptionTokens = getPostComputeEncryptionTokens(request); - if (encryptionTokens.isEmpty()) { - throw new Exception("Failed to get post-compute encryption tokens - taskId: " + taskId); - } tokens.putAll(encryptionTokens); // storage Map storageTokens = getPostComputeStorageTokens(request); - if (storageTokens.isEmpty()) { - throw new Exception("Failed to get post-compute storage tokens - taskId: " + taskId); - } tokens.putAll(storageTokens); // enclave signature Map signTokens = getPostComputeSignTokens(request); - if (signTokens.isEmpty()) { - throw new Exception("Failed to get post-compute signature tokens - taskId: " + taskId); - } tokens.putAll(signTokens); return tokens; } Map getPostComputeEncryptionTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); Map tokens = new HashMap<>(); @@ -286,7 +372,10 @@ Map getPostComputeEncryptionTokens(PalaemonSessionRequest reques IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, true); if (beneficiaryResultEncryptionKeySecret.isEmpty()) { - throw new Exception("Empty beneficiary encryption key - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, + "Empty beneficiary encryption key - taskId: " + taskId + ); } String publicKeyValue = beneficiaryResultEncryptionKeySecret.get().getTrimmedValue(); tokens.put(RESULT_ENCRYPTION_PUBLIC_KEY, publicKeyValue); // base64 encoded by client @@ -298,7 +387,7 @@ Map getPostComputeEncryptionTokens(PalaemonSessionRequest reques // that feature we only allow to push to the requester // private storage space Map getPostComputeStorageTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); Map tokens = new HashMap<>(); @@ -315,12 +404,14 @@ Map getPostComputeStorageTokens(PalaemonSessionRequest request) String keyName = storageProvider.equals(DROPBOX_RESULT_STORAGE_PROVIDER) ? ReservedSecretKeyName.IEXEC_RESULT_DROPBOX_TOKEN : ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN; - Optional requesterStorageTokenSecret = + Optional requesterStorageTokenSecret = web2SecretsService.getSecret(taskDescription.getRequester(), keyName, true); if (requesterStorageTokenSecret.isEmpty()) { log.error("Failed to get storage token [taskId:{}, storageProvider:{}, requester:{}]", taskId, storageProvider, taskDescription.getRequester()); - throw new Exception("Empty requester storage token - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_STORAGE_TOKENS_FAILED, + "Empty requester storage token - taskId: " + taskId); } String requesterStorageToken = requesterStorageTokenSecret.get().getTrimmedValue(); tokens.put(RESULT_STORAGE_PROVIDER, storageProvider); @@ -330,23 +421,35 @@ Map getPostComputeStorageTokens(PalaemonSessionRequest request) } Map getPostComputeSignTokens(PalaemonSessionRequest request) - throws Exception { + throws TeeSessionGenerationException { String taskId = request.getTaskDescription().getChainTaskId(); String workerAddress = request.getWorkerAddress(); Map tokens = new HashMap<>(); if (StringUtils.isEmpty(workerAddress)) { - throw new Exception("Empty worker address - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS, + "Empty worker address - taskId: " + taskId + ); } if (StringUtils.isEmpty(request.getEnclaveChallenge())) { - throw new Exception("Empty public enclave challenge - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE, + "Empty public enclave challenge - taskId: " + taskId + ); } Optional teeChallenge = teeChallengeService.getOrCreate(taskId, true); if (teeChallenge.isEmpty()) { - throw new Exception("Empty TEE challenge - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE, + "Empty TEE challenge - taskId: " + taskId + ); } EthereumCredentials enclaveCredentials = teeChallenge.get().getCredentials(); if (enclaveCredentials == null || enclaveCredentials.getPrivateKey().isEmpty()) { - throw new Exception("Empty TEE challenge credentials - taskId: " + taskId); + throw new TeeSessionGenerationException( + POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS, + "Empty TEE challenge credentials - taskId: " + taskId + ); } tokens.put(RESULT_TASK_ID, taskId); tokens.put(RESULT_SIGN_WORKER_ADDRESS, workerAddress); diff --git a/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java b/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java index 62e558f5..794872f7 100644 --- a/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java +++ b/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java @@ -60,13 +60,13 @@ public Optional getUnTeeTaskSecrets(String chainTaskId) { // TODO use taskDescription instead of chainDeal Optional oChainTask = iexecHubService.getChainTask(chainTaskId); - if (!oChainTask.isPresent()) { + if (oChainTask.isEmpty()) { log.error("getUnTeeTaskSecrets failed (getChainTask failed) [chainTaskId:{}]", chainTaskId); return Optional.empty(); } ChainTask chainTask = oChainTask.get(); Optional oChainDeal = iexecHubService.getChainDeal(chainTask.getDealid()); - if (!oChainDeal.isPresent()) { + if (oChainDeal.isEmpty()) { log.error("getUnTeeTaskSecrets failed (getChainDeal failed) [chainTaskId:{}]", chainTaskId); return Optional.empty(); } @@ -74,7 +74,7 @@ public Optional getUnTeeTaskSecrets(String chainTaskId) { String chainDatasetId = chainDeal.getChainDataset().getChainDatasetId(); Optional datasetSecret = web3SecretService.getSecret(chainDatasetId, true); - if (!datasetSecret.isPresent()) { + if (datasetSecret.isEmpty()) { log.error("getUnTeeTaskSecrets failed (datasetSecret failed) [chainTaskId:{}]", chainTaskId); return Optional.empty(); } diff --git a/src/main/java/com/iexec/sms/utils/version/VersionController.java b/src/main/java/com/iexec/sms/utils/version/VersionController.java index cfde5fcd..d27e650a 100644 --- a/src/main/java/com/iexec/sms/utils/version/VersionController.java +++ b/src/main/java/com/iexec/sms/utils/version/VersionController.java @@ -20,11 +20,10 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; - @RestController public class VersionController { - private VersionService versionService; + private final VersionService versionService; public VersionController(VersionService versionService) { this.versionService = versionService; diff --git a/src/main/java/com/iexec/sms/utils/version/VersionService.java b/src/main/java/com/iexec/sms/utils/version/VersionService.java index 2171afe6..574e2f27 100644 --- a/src/main/java/com/iexec/sms/utils/version/VersionService.java +++ b/src/main/java/com/iexec/sms/utils/version/VersionService.java @@ -17,19 +17,24 @@ package com.iexec.sms.utils.version; import com.iexec.common.utils.VersionUtils; +import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Service; @Service public class VersionService { - private String version = Version.PROJECT_VERSION; + private final BuildProperties buildProperties; + + VersionService(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } public String getVersion() { - return version; + return buildProperties.getVersion(); } public boolean isSnapshot() { - return VersionUtils.isSnapshot(version); + return VersionUtils.isSnapshot(buildProperties.getVersion()); } } diff --git a/src/main/resources/Dockerfile b/src/main/resources/Dockerfile index a9ddca04..7327ed5c 100644 --- a/src/main/resources/Dockerfile +++ b/src/main/resources/Dockerfile @@ -1,10 +1,12 @@ FROM nexus.iex.ec/sconecuratedimages-apps-java:jdk-alpine-scone3.0 -ARG JAR_NAME +ARG jar -COPY build/libs/$JAR_NAME /app/iexec-sms.jar -COPY build/resources/main/palaemonTemplate.vm /app/palaemonTemplate.vm -COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 +RUN test -n "$jar" + +COPY $jar /app/iexec-sms.jar +COPY build/resources/main/palaemonTemplate.vm /app/palaemonTemplate.vm +COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 # #### Docker ENV vars should be placed in palaemon conf for Scone ### diff --git a/src/main/resources/Dockerfile.untrusted b/src/main/resources/Dockerfile.untrusted index fc331c03..76063921 100644 --- a/src/main/resources/Dockerfile.untrusted +++ b/src/main/resources/Dockerfile.untrusted @@ -1,8 +1,10 @@ -FROM openjdk:11.0.7-jre-slim +FROM openjdk:11.0.15-jre-slim -ARG JAR_NAME +ARG jar -COPY build/libs/$JAR_NAME /app/iexec-sms.jar +RUN test -n "$jar" + +COPY $jar /app/iexec-sms.jar COPY build/resources/main/palaemonTemplate.vm /app/palaemonTemplate.vm COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 @@ -13,4 +15,4 @@ ENV IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS=http://chain:8545 ENV IEXEC_SCONE_CAS_HOST=iexec-cas ENV IEXEC_SMS_H2_URL=jdbc:h2:file:/scone/sms-h2 -ENTRYPOINT [ "/bin/sh", "-c", "java -jar /app/iexec-sms.jar" ] \ No newline at end of file +ENTRYPOINT [ "/bin/sh", "-c", "java -jar /app/iexec-sms.jar" ] diff --git a/src/main/resources/Version.java.template b/src/main/resources/Version.java.template deleted file mode 100644 index 28ee11e7..00000000 --- a/src/main/resources/Version.java.template +++ /dev/null @@ -1,5 +0,0 @@ -package com.iexec.sms.utils.version; - -class Version { - static final String PROJECT_VERSION = "@projectversion@"; -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b52a5d65..610ac599 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -79,3 +79,7 @@ logging: # level: # org.springframework: DEBUG # org.apache.http: DEBUG + +springdoc: + packagesToScan: com.iexec.sms + pathsToMatch: /** \ No newline at end of file diff --git a/src/main/resources/palaemonTemplate.vm b/src/main/resources/palaemonTemplate.vm index 175f4740..645c75fe 100644 --- a/src/main/resources/palaemonTemplate.vm +++ b/src/main/resources/palaemonTemplate.vm @@ -73,6 +73,11 @@ services: #foreach($key in $INPUT_FILE_NAMES.keySet()) $key: '$INPUT_FILE_NAMES.get($key)' #end + IEXEC_APP_DEVELOPER_SECRET: '$IEXEC_APP_DEVELOPER_SECRET_1' + IEXEC_APP_DEVELOPER_SECRET_1: '$IEXEC_APP_DEVELOPER_SECRET_1' + #foreach($key in $REQUESTER_SECRETS.keySet()) + $key: '$REQUESTER_SECRETS.get($key)' + #end #* Post-compute enclave diff --git a/src/test/java/com/iexec/sms/Web3jUtils.java b/src/test/java/com/iexec/sms/Web3jUtils.java new file mode 100644 index 00000000..3757a53c --- /dev/null +++ b/src/test/java/com/iexec/sms/Web3jUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms; + +import org.web3j.crypto.Credentials; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Keys; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public class Web3jUtils { + + private Web3jUtils() {} + + /** + * Create an Ethereum address after generating a new ECKeyPair. + * @return A valid Ethereum address + */ + public static String createEthereumAddress() { + try { + ECKeyPair ecKeyPair = Keys.createEcKeyPair(); + return Credentials.create(ecKeyPair).getAddress(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new IllegalStateException("Failed to create ECKeyPair and to return a valid Ethereum address", e); + } + } + +} diff --git a/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java b/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java index 54bf020e..67322d33 100644 --- a/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java +++ b/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java @@ -18,6 +18,7 @@ import static com.iexec.common.chain.ChainTaskStatus.ACTIVE; import static com.iexec.common.chain.ChainTaskStatus.UNSET; +import static com.iexec.sms.authorization.AuthorizationError.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -37,7 +38,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -public class AuthorizationServiceTests { +class AuthorizationServiceTests { @Mock IexecHubService iexecHubService; @@ -46,12 +47,13 @@ public class AuthorizationServiceTests { private AuthorizationService authorizationService; @BeforeEach - public void beforeEach() { + void beforeEach() { MockitoAnnotations.openMocks(this); } + // region isAuthorizedOnExecution @Test - public void shouldBeAuthorizedOnExecutionOfTeeTask() { + void shouldBeAuthorizedOnExecutionOfTeeTask() { ChainDeal chainDeal = TestUtils.getChainDeal(); ChainTask chainTask = TestUtils.getChainTask(ACTIVE); WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); @@ -64,13 +66,13 @@ public void shouldBeAuthorizedOnExecutionOfTeeTask() { } @Test - public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWithNullAuthorization() { + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWithNullAuthorization() { boolean isAuth = authorizationService.isAuthorizedOnExecution(null, true); assertThat(isAuth).isFalse(); } @Test - public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskTypeNotMatchedOnchain() { + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskTypeNotMatchedOnchain() { WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); @@ -80,7 +82,7 @@ public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskTypeNotMatchedOncha } @Test - public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskNotActive() { + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskNotActive() { WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); ChainTask chainTask = TestUtils.getChainTask(UNSET); when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); @@ -91,7 +93,7 @@ public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskNotActive() { } @Test - public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenPoolSignatureIsNotValid() { + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenPoolSignatureIsNotValid() { ChainDeal chainDeal = TestUtils.getChainDeal(); ChainTask chainTask = TestUtils.getChainTask(ACTIVE); WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); @@ -104,6 +106,109 @@ public void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenPoolSignatureIsNotValid boolean isAuth = authorizationService.isAuthorizedOnExecution(auth, true); assertThat(isAuth).isFalse(); } + // endregion + + // region isAuthorizedOnExecutionWithDetailedIssue + @Test + void shouldBeAuthorizedOnExecutionOfTeeTaskWithDetails() { + ChainDeal chainDeal = TestUtils.getChainDeal(); + ChainTask chainTask = TestUtils.getChainTask(ACTIVE); + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask)); + when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.of(chainDeal)); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, true); + assertThat(isAuth).isEmpty(); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWithNullAuthorizationWithDetails() { + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(null, true); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(EMPTY_PARAMS_UNAUTHORIZED); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskTypeNotMatchedOnchainWithDetails() { + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, false); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(NO_MATCH_ONCHAIN_TYPE); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenGetTaskFailedWithDetails() { + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.empty()); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, true); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(GET_CHAIN_TASK_FAILED); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskNotActiveWithDetails() { + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + ChainTask chainTask = TestUtils.getChainTask(UNSET); + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask)); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, true); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(TASK_NOT_ACTIVE); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenGetDealFailedWithDetails() { + ChainDeal chainDeal = TestUtils.getChainDeal(); + ChainTask chainTask = TestUtils.getChainTask(ACTIVE); + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + auth.setSignature(new Signature(TestUtils.POOL_WRONG_SIGNATURE)); + + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask)); + when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.empty()); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, true); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(GET_CHAIN_DEAL_FAILED); + } + + @Test + void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenPoolSignatureIsNotValidWithDetails() { + ChainDeal chainDeal = TestUtils.getChainDeal(); + ChainTask chainTask = TestUtils.getChainTask(ACTIVE); + WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); + auth.setSignature(new Signature(TestUtils.POOL_WRONG_SIGNATURE)); + + when(iexecHubService.isTeeTask(auth.getChainTaskId())).thenReturn(true); + when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask)); + when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.of(chainDeal)); + + Optional isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth, true); + assertThat(isAuth).isNotEmpty() + .get() + .isEqualTo(INVALID_SIGNATURE); + } + // endregion + + @Test + void getChallengeForSetRequesterAppComputeSecret() { + String challenge = authorizationService.getChallengeForSetRequesterAppComputeSecret( + "", "0", ""); + Assertions.assertEquals("0x31991eefc2731228bdd25dbc5a242722eda3869f9b06536dbd96a774e5228509", + challenge); + } @Test void getChallengeForSetWeb3Secret() { diff --git a/src/test/java/com/iexec/sms/encryption/EncryptionServiceTests.java b/src/test/java/com/iexec/sms/encryption/EncryptionServiceTests.java index 6a58b18a..776d6f41 100644 --- a/src/test/java/com/iexec/sms/encryption/EncryptionServiceTests.java +++ b/src/test/java/com/iexec/sms/encryption/EncryptionServiceTests.java @@ -25,7 +25,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; -public class EncryptionServiceTests { +class EncryptionServiceTests { @TempDir public File tempDir; @@ -37,7 +37,7 @@ public class EncryptionServiceTests { private EncryptionService encryptionService; @Test - public void shouldCreateAesKey() { + void shouldCreateAesKey() { String data = "data mock"; // File createdFile = new File(tempDir, "aesKey"); String aesKeyPath = tempDir.getAbsolutePath() + "aesKey"; diff --git a/src/test/java/com/iexec/sms/secret/compute/AppComputeSecretControllerTest.java b/src/test/java/com/iexec/sms/secret/compute/AppComputeSecretControllerTest.java new file mode 100644 index 00000000..65326e01 --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/compute/AppComputeSecretControllerTest.java @@ -0,0 +1,506 @@ +package com.iexec.sms.secret.compute; + +import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.authorization.AuthorizationService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Arrays; +import java.util.List; + +import static com.iexec.sms.Web3jUtils.createEthereumAddress; +import static org.mockito.Mockito.*; + +class AppComputeSecretControllerTest { + private static final String AUTHORIZATION = "authorization"; + private static final String COMMON_SECRET_VALUE = "I'm a secret."; + private static final String EXACT_MAX_SIZE_SECRET_VALUE = new String(new byte[4096]); + private static final String TOO_LONG_SECRET_VALUE = new String(new byte[4097]); + private static final String CHALLENGE = "challenge"; + private static final ApiResponseBody> INVALID_AUTHORIZATION_PAYLOAD = createErrorResponse("Invalid authorization"); + + @Mock + TeeTaskComputeSecretService teeTaskComputeSecretService; + + @Mock + AuthorizationService authorizationService; + + @InjectMocks + AppComputeSecretController appComputeSecretController; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + } + + // region addAppDeveloperAppComputeSecret + + @Test + void shouldAddAppDeveloperSecret() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, appAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(false); + doReturn(true).when(teeTaskComputeSecretService) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex, secretValue); + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex); + verify(teeTaskComputeSecretService, times(1)) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex, secretValue); + } + + @Test + void shouldNotAddAppDeveloperSecretSinceNotSignedByOwner() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, appAddress)) + .thenReturn(false); + + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(INVALID_AUTHORIZATION_PAYLOAD)); + + verify(teeTaskComputeSecretService, never()) + .isSecretPresent(any(), any(), any(), any(), any()); + verify(teeTaskComputeSecretService, never()) + .encryptAndSaveSecret(any(), any(), any(), any(), any(), any()); + } + + @Test + void shouldNotAddAppDeveloperSecretSinceSecretAlreadyExists() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, appAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(true); + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.CONFLICT).body(createErrorResponse("Secret already exists"))); + + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex); + verify(teeTaskComputeSecretService, never()) + .encryptAndSaveSecret(any(), any(), any(), any(), any(), any()); + } + + @Test + void shouldNotAddAppDeveloperSecretSinceSecretValueTooLong() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + final String secretValue = TOO_LONG_SECRET_VALUE; + + when(authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, appAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(false); + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(createErrorResponse("Secret size should not exceed 4 Kb"))); + + verifyNoInteractions(authorizationService, teeTaskComputeSecretService); + } + + // TODO enable this test when supporting multiple application developer secrets + @Test + @Disabled + void shouldNotAddAppDeveloperSecretSinceBadSecretIndexFormat() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "bad-secret-index"; + final String secretValue = COMMON_SECRET_VALUE; + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + //secretIndex, // TODO uncomment this when supporting multiple application developer secrets + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_INDEX_FORMAT_MSG))); + verifyNoInteractions(authorizationService, teeTaskComputeSecretService); + } + + // TODO enable this test when supporting multiple application developer secrets + @Test + @Disabled + void shouldNotAddAppDeveloperSecretSinceBadSecretValue() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "-10"; + final String secretValue = COMMON_SECRET_VALUE; + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + //secretIndex, // TODO uncomment this when supporting multiple application developer secrets + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_INDEX_FORMAT_MSG))); + verifyNoInteractions(authorizationService, teeTaskComputeSecretService); + } + + @Test + void shouldAddMaxSizeAppDeveloperSecret() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + final String secretValue = EXACT_MAX_SIZE_SECRET_VALUE; + + when(authorizationService.getChallengeForSetAppDeveloperAppComputeSecret(appAddress, secretIndex, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, appAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(false); + doReturn(true).when(teeTaskComputeSecretService) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex, secretValue); + + ResponseEntity>> result = appComputeSecretController.addAppDeveloperAppComputeSecret( + AUTHORIZATION, + appAddress, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex); + verify(teeTaskComputeSecretService, times(1)) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex, secretValue); + } + + // endregion + + // region isAppDeveloperAppComputeSecretPresent + @Test + void appDeveloperSecretShouldExist() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(true); + + ResponseEntity>> result = + appComputeSecretController.isAppDeveloperAppComputeSecretPresent(appAddress, secretIndex); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex); + verifyNoInteractions(authorizationService); + } + + @Test + void appDeveloperSecretShouldNotExist() { + final String appAddress = createEthereumAddress(); + final String secretIndex = "1"; + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex)) + .thenReturn(false); + + ResponseEntity>> result = + appComputeSecretController.isAppDeveloperAppComputeSecretPresent(appAddress, secretIndex); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.NOT_FOUND).body(createErrorResponse("Secret not found"))); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", secretIndex); + verifyNoInteractions(authorizationService); + } + + @Test + void isAppDeveloperAppComputeSecretPresentShouldFailWhenIndexNotANumber() { + final String secretIndex = "bad-secret-index"; + final String appAddress = createEthereumAddress(); + ResponseEntity>> result = + appComputeSecretController.isAppDeveloperAppComputeSecretPresent(appAddress, secretIndex); + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_INDEX_FORMAT_MSG))); + verifyNoInteractions(authorizationService, teeTaskComputeSecretService); + } + + @Test + void isAppDeveloperAppComputeSecretPresentShouldFailWhenIndexLowerThanZero() { + final String secretIndex = "-1"; + final String appAddress = createEthereumAddress(); + ResponseEntity>> result = + appComputeSecretController.isAppDeveloperAppComputeSecretPresent(appAddress, secretIndex); + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_INDEX_FORMAT_MSG))); + verifyNoInteractions(authorizationService, teeTaskComputeSecretService); + } + // endregion + + // region addRequesterAppComputeSecret + @Test + void shouldAddRequesterSecret() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "valid-requester-secret"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(false); + when(teeTaskComputeSecretService.encryptAndSaveSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey, secretValue)) + .thenReturn(true); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(authorizationService) + .getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + verify(authorizationService) + .isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress); + verify(teeTaskComputeSecretService) + .isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey); + verify(teeTaskComputeSecretService) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey, secretValue); + } + + @Test + void shouldNotAddRequesterSecretSinceNotSignedByRequester() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "not-signed-secret"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress)) + .thenReturn(false); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(INVALID_AUTHORIZATION_PAYLOAD)); + + verify(authorizationService) + .getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + verify(authorizationService) + .isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress); + verifyNoInteractions(teeTaskComputeSecretService); + } + + @Test + void shouldNotAddRequesterSecretSinceSecretAlreadyExists() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "secret-already-exists"; + final String secretValue = COMMON_SECRET_VALUE; + + when(authorizationService.getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(true); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.CONFLICT).body(createErrorResponse("Secret already exists"))); + + verify(authorizationService) + .getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + verify(authorizationService) + .isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress); + verify(teeTaskComputeSecretService) + .isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey); + verify(teeTaskComputeSecretService, never()) + .encryptAndSaveSecret(any(), any(), any(), any(), any(), any()); + } + + @ParameterizedTest + @ValueSource(strings = { + "this-is-a-really-long-key-with-far-too-many-characters-in-its-name", + "this-is-a-key-with-invalid-characters:!*~" + }) + void shouldNotAddRequesterSecretSinceInvalidSecretKey(String secretKey) { + final String requesterAddress = createEthereumAddress(); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + COMMON_SECRET_VALUE + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_KEY_FORMAT_MSG))); + verifyNoInteractions(teeTaskComputeSecretService); + } + + @Test + void shouldNotAddRequesterSecretSinceSecretValueTooLong() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "too-long-secret-value"; + final String secretValue = TOO_LONG_SECRET_VALUE; + + when(authorizationService.getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress)) + .thenReturn(true); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest().body(createErrorResponse("Secret size should not exceed 4 Kb"))); + + verify(authorizationService) + .getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + verify(authorizationService) + .isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress); + verifyNoInteractions(teeTaskComputeSecretService); + } + + @Test + void shouldAddMaxSizeRequesterSecret() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "max-size-secret-value"; + final String secretValue = EXACT_MAX_SIZE_SECRET_VALUE; + + when(authorizationService.getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress)) + .thenReturn(true); + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(false); + when(teeTaskComputeSecretService + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey, secretValue)) + .thenReturn(true); + + ResponseEntity>> result = appComputeSecretController.addRequesterAppComputeSecret( + AUTHORIZATION, + requesterAddress, + secretKey, + secretValue + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(authorizationService) + .getChallengeForSetRequesterAppComputeSecret(requesterAddress, secretKey, secretValue); + verify(authorizationService) + .isSignedByHimself(CHALLENGE, AUTHORIZATION, requesterAddress); + verify(teeTaskComputeSecretService) + .isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey); + verify(teeTaskComputeSecretService) + .encryptAndSaveSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey, secretValue); + } + // endregion + + // region isRequesterAppComputeSecretPresent + @Test + void requesterSecretShouldExist() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "exist"; + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(true); + + ResponseEntity>> result = + appComputeSecretController.isRequesterAppComputeSecretPresent(requesterAddress, secretKey); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.noContent().build()); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey); + } + + @Test + void requesterSecretShouldNotExist() { + final String requesterAddress = createEthereumAddress(); + final String secretKey = "empty"; + when(teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(false); + + ResponseEntity>> result = + appComputeSecretController.isRequesterAppComputeSecretPresent(requesterAddress, secretKey); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.status(HttpStatus.NOT_FOUND).body(createErrorResponse("Secret not found"))); + verify(teeTaskComputeSecretService, times(1)) + .isSecretPresent(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey); + } + + @ParameterizedTest + @ValueSource(strings = { + "this-is-a-really-long-key-with-far-too-many-characters-in-its-name", + "this-is-a-key-with-invalid-characters:!*~" + }) + void shouldNotReadRequesterSecretSinceInvalidSecretKey(String secretKey) { + final String requesterAddress = createEthereumAddress(); + ResponseEntity>> result = appComputeSecretController.isRequesterAppComputeSecretPresent( + requesterAddress, + secretKey + ); + + Assertions.assertThat(result).isEqualTo(ResponseEntity.badRequest() + .body(createErrorResponse(AppComputeSecretController.INVALID_SECRET_KEY_FORMAT_MSG))); + verifyNoInteractions(teeTaskComputeSecretService); + } + // endregion + + private static ApiResponseBody> createErrorResponse(String... errorMessages) { + return ApiResponseBody.>builder().error(Arrays.asList(errorMessages)).build(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java new file mode 100644 index 00000000..f586952a --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java @@ -0,0 +1,110 @@ +package com.iexec.sms.secret.compute; + +import com.iexec.sms.encryption.EncryptionService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +class TeeTaskComputeSecretServiceTest { + private static final String APP_ADDRESS = "appAddress"; + private static final String DECRYPTED_SECRET_VALUE = "I'm a secret."; + private static final String ENCRYPTED_SECRET_VALUE = "I'm an encrypted secret."; + private static final TeeTaskComputeSecret COMPUTE_SECRET = TeeTaskComputeSecret + .builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress(APP_ADDRESS.toLowerCase()) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .key("0") + .value(ENCRYPTED_SECRET_VALUE) + .build(); + + @Mock + TeeTaskComputeSecretRepository teeTaskComputeSecretRepository; + + @Mock + EncryptionService encryptionService; + + @InjectMocks + @Spy + TeeTaskComputeSecretService teeTaskComputeSecretService; + + @Captor + ArgumentCaptor computeSecretCaptor; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + } + + // region encryptAndSaveSecret + @Test + void shouldAddSecret() { + doReturn(false).when(teeTaskComputeSecretService) + .isSecretPresent(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); + when(encryptionService.encrypt(DECRYPTED_SECRET_VALUE)) + .thenReturn(ENCRYPTED_SECRET_VALUE); + + teeTaskComputeSecretService.encryptAndSaveSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0", DECRYPTED_SECRET_VALUE); + + verify(teeTaskComputeSecretRepository, times(1)).save(computeSecretCaptor.capture()); + final TeeTaskComputeSecret savedTeeTaskComputeSecret = computeSecretCaptor.getValue(); + Assertions.assertThat(savedTeeTaskComputeSecret.getKey()).isEqualTo("0"); + Assertions.assertThat(savedTeeTaskComputeSecret.getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); + Assertions.assertThat(savedTeeTaskComputeSecret.getValue()).isEqualTo(ENCRYPTED_SECRET_VALUE); + } + + @Test + void shouldNotAddSecretSinceAlreadyExist() { + doReturn(true).when(teeTaskComputeSecretService) + .isSecretPresent(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); + + teeTaskComputeSecretService.encryptAndSaveSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0", DECRYPTED_SECRET_VALUE); + + verify(teeTaskComputeSecretRepository, times(0)).save(computeSecretCaptor.capture()); + } + // endregion + + // region getSecret + @Test + void shouldGetSecret() { + when(teeTaskComputeSecretRepository.findOne(any())) + .thenReturn(Optional.of(COMPUTE_SECRET)); + when(encryptionService.decrypt(ENCRYPTED_SECRET_VALUE)) + .thenReturn(DECRYPTED_SECRET_VALUE); + + Optional decryptedSecret = teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); + Assertions.assertThat(decryptedSecret).isPresent(); + Assertions.assertThat(decryptedSecret.get().getKey()).isEqualTo("0"); + Assertions.assertThat(decryptedSecret.get().getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); + Assertions.assertThat(decryptedSecret.get().getValue()).isEqualTo(DECRYPTED_SECRET_VALUE); + verify(encryptionService, Mockito.times(1)).decrypt(any()); + } + // endregion + + // region isSecretPresent + @Test + void secretShouldExist() { + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0")) + .thenReturn(Optional.of(COMPUTE_SECRET)); + + final boolean isSecretPresent = teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); + + Assertions.assertThat(isSecretPresent).isTrue(); + } + + @Test + void secretShouldNotExist() { + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0")) + .thenReturn(Optional.empty()); + + final boolean isSecretPresent = teeTaskComputeSecretService.isSecretPresent(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); + + Assertions.assertThat(isSecretPresent).isFalse(); + } + // endregion +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java new file mode 100644 index 00000000..8fff237f --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java @@ -0,0 +1,170 @@ +package com.iexec.sms.secret.compute; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.dao.DataIntegrityViolationException; + +import javax.validation.ConstraintViolationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Slf4j +@DataJpaTest +public class TeeTaskComputeSecretTest { + + private final TeeTaskComputeSecretRepository teeTaskComputeSecretRepository; + + TeeTaskComputeSecretTest(@Autowired TeeTaskComputeSecretRepository teeTaskComputeSecretRepository) { + this.teeTaskComputeSecretRepository = teeTaskComputeSecretRepository; + } + + private TeeTaskComputeSecret getAppDeveloperSecret() { + return TeeTaskComputeSecret.builder() + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .onChainObjectAddress("0x1") + .onChainObjectType(OnChainObjectType.APPLICATION) + .fixedSecretOwner("") + .key("0") + .value("secretValue") + .build(); + } + + private TeeTaskComputeSecret getRequesterSecret() { + return TeeTaskComputeSecret.builder() + .secretOwnerRole(SecretOwnerRole.REQUESTER) + .onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + .fixedSecretOwner("0x1") + .key("secret-key") + .value("secretValue") + .build(); + } + + @Test + void shouldNotSaveSecretFromDefaultBuilder() { + log.info("shouldNotSaveEntity"); + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder().build(); + assertThat(secret).hasAllNullFieldsOrProperties(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenOnChainObjectAddressIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + //.onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + .key("") + .value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenOnChainObjectTypeIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + .onChainObjectAddress("") + //.onChainObjectType(OnChainObjectType.APPLICATION) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + .key("") + .value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenSecretOwnerRoleIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + .onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + //.secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + .key("") + .value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenFixedSecretOwnerIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + .onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + //.fixedSecretOwner("") + .key("") + .value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenKeyIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + .onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + //.key("") + .value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldNotSaveSecretWhenValueIsNull() { + TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() + .onChainObjectAddress("") + .onChainObjectType(OnChainObjectType.APPLICATION) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + .key("") + //.value("") + .build(); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) + .isInstanceOf(ConstraintViolationException.class); + } + + @Test + void shouldFailToSaveSameAppDeveloperSecretTwice() { + log.info("AppDeveloperSecret"); + teeTaskComputeSecretRepository.saveAndFlush(getAppDeveloperSecret()); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(getAppDeveloperSecret())) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void shouldFailToSaveSameRequesterSecretTwice() { + log.info("RequesterSecret"); + teeTaskComputeSecretRepository.saveAndFlush(getRequesterSecret()); + assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(getRequesterSecret())) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void shouldSaveAppDeveloperAndRequesterSecrets() { + TeeTaskComputeSecret appDeveloperSecret = getAppDeveloperSecret(); + teeTaskComputeSecretRepository.save(appDeveloperSecret); + String appDeveloperSecretId = appDeveloperSecret.getId(); + TeeTaskComputeSecret requesterSecret = getRequesterSecret(); + teeTaskComputeSecretRepository.save(requesterSecret); + String requesterSecretId = requesterSecret.getId(); + assertThat(teeTaskComputeSecretRepository.count()).isEqualTo(2); + assertThat(teeTaskComputeSecretRepository.getById(appDeveloperSecretId)) + .isEqualTo(appDeveloperSecret); + assertThat(teeTaskComputeSecretRepository.getById(requesterSecretId)) + .isEqualTo(requesterSecret); + } + +} diff --git a/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java b/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java index 0e71fed9..1d17bec1 100644 --- a/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java +++ b/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java @@ -28,10 +28,9 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class Web2SecretsServiceTests { +class Web2SecretsServiceTests { String ownerAddress = "ownerAddress"; String secretAddress = "secretAddress"; @@ -48,12 +47,12 @@ public class Web2SecretsServiceTests { private Web2SecretsService web2SecretsService; @BeforeEach - public void beforeEach() { + void beforeEach() { MockitoAnnotations.openMocks(this); } @Test - public void shouldGetAndDecryptWeb2Secrets() { + void shouldGetAndDecryptWeb2Secrets() { ownerAddress = ownerAddress.toLowerCase(); Secret encryptedSecret = new Secret(secretAddress, encryptedSecretValue); encryptedSecret.setEncryptedValue(true); @@ -71,14 +70,14 @@ public void shouldGetAndDecryptWeb2Secrets() { } @Test - public void shouldAddSecret() { + void shouldAddSecret() { ownerAddress = ownerAddress.toLowerCase(); web2SecretsService.addSecret(ownerAddress, secretAddress, plainSecretValue); verify(web2SecretsRepository, times(1)).save(any()); } @Test - public void shouldUpdateSecret() { + void shouldUpdateSecret() { ownerAddress = ownerAddress.toLowerCase(); Secret encryptedSecret = new Secret(secretAddress, encryptedSecretValue); encryptedSecret.setEncryptedValue(true); diff --git a/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java b/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java index cf9114de..15135ea0 100644 --- a/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java +++ b/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java @@ -16,6 +16,6 @@ package com.iexec.sms.secret.web3; -public class Web3SecretsServiceTests { +class Web3SecretsServiceTests { } \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/TeeControllerTests.java b/src/test/java/com/iexec/sms/tee/TeeControllerTests.java new file mode 100644 index 00000000..2b2d09d6 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/TeeControllerTests.java @@ -0,0 +1,197 @@ +package com.iexec.sms.tee; + +import com.iexec.common.chain.WorkerpoolAuthorization; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.authorization.AuthorizationError; +import com.iexec.sms.authorization.AuthorizationService; +import com.iexec.sms.tee.challenge.TeeChallengeService; +import com.iexec.sms.tee.session.TeeSessionGenerationException; +import com.iexec.sms.tee.session.TeeSessionService; +import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.web3j.crypto.Keys; + +import java.util.Optional; +import java.util.stream.Stream; + +import static com.iexec.sms.api.TeeSessionGenerationError.*; +import static com.iexec.sms.authorization.AuthorizationError.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class TeeControllerTests { + private final static String TASK_ID = "0x0"; + private final static String WORKER_ADDRESS = "0x1"; + private final static String ENCLAVE_CHALLENGE = "0x2"; + private final static String AUTHORIZATION = "0x2"; + private final static String CHALLENGE = "CHALLENGE"; + private final static String SESSION_ID = "SESSION_ID"; + + @Mock + AuthorizationService authorizationService; + @Mock + TeeChallengeService teeChallengeService; + @Mock + TeeSessionService teeSessionService; + @Mock + TeeWorkflowConfiguration teeWorkflowConfig; + + @InjectMocks + TeeController teeController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + // region generateTeeSession + @Test + void shouldGenerateTeeSession() throws TeeSessionGenerationException { + final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization + .builder() + .chainTaskId(TASK_ID) + .workerWallet(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .build(); + + when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenReturn(SESSION_ID); + + final ResponseEntity> response = + teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotEquals(null, response.getBody()); + assertNotEquals(null, response.getBody().getData()); + assertNotEquals("", response.getBody().getData()); + assertNull(response.getBody().getError()); + } + + @Test + void shouldNotGenerateTeeSessionSinceNotSignedByHimself() { + final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization + .builder() + .chainTaskId(TASK_ID) + .workerWallet(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .build(); + + when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(false); + + final ResponseEntity> response = + teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertNotEquals(null, response.getBody()); + assertNull(response.getBody().getData()); + assertNotEquals(null, response.getBody().getError()); + assertEquals(TeeSessionGenerationError.INVALID_AUTHORIZATION, response.getBody().getError()); + } + + private static Stream notAuthorizedParams() { + return Stream.of( + Arguments.of(EMPTY_PARAMS_UNAUTHORIZED, EXECUTION_NOT_AUTHORIZED_EMPTY_PARAMS_UNAUTHORIZED), + Arguments.of(NO_MATCH_ONCHAIN_TYPE, EXECUTION_NOT_AUTHORIZED_NO_MATCH_ONCHAIN_TYPE), + Arguments.of(GET_CHAIN_TASK_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_TASK_FAILED), + Arguments.of(TASK_NOT_ACTIVE, EXECUTION_NOT_AUTHORIZED_TASK_NOT_ACTIVE), + Arguments.of(GET_CHAIN_DEAL_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_DEAL_FAILED), + Arguments.of(INVALID_SIGNATURE, EXECUTION_NOT_AUTHORIZED_INVALID_SIGNATURE) + ); + } + + @ParameterizedTest + @MethodSource("notAuthorizedParams") + void shouldNotGenerateTeeSessionSinceNotAuthorized(AuthorizationError cause, TeeSessionGenerationError consequence) { + final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization + .builder() + .chainTaskId(TASK_ID) + .workerWallet(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .build(); + + when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.of(cause)); + + final ResponseEntity> response = + teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertNotEquals(null, response.getBody()); + assertNull(response.getBody().getData()); + assertNotEquals(null, response.getBody().getError()); + assertEquals(consequence, response.getBody().getError()); + } + + @Test + void shouldNotGenerateTeeSessionSinceEmptySessionId() throws TeeSessionGenerationException { + final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization + .builder() + .chainTaskId(TASK_ID) + .workerWallet(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .build(); + + when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenReturn(""); + + final ResponseEntity> response = + teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + assertNull(response.getBody()); + } + + private static Stream exceptionOnSessionIdGeneration() { + return Stream.of( + Arguments.of( + new TeeSessionGenerationException( + SECURE_SESSION_GENERATION_FAILED, + String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS) + ) + ), + Arguments.of(new RuntimeException()) + ); + } + + /** + * {@link TeeController#generateTeeSession(String, WorkerpoolAuthorization)} + * should catch every error thrown + * by {@link TeeSessionService#generateTeeSession(String, String, String)}. + */ + @ParameterizedTest + @MethodSource("exceptionOnSessionIdGeneration") + void shouldNotGenerateTeeSessionSinceSessionIdGenerationFailed(Exception exception) throws TeeSessionGenerationException { + final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization + .builder() + .chainTaskId(TASK_ID) + .workerWallet(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .build(); + + when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenThrow(exception); + + final ResponseEntity> response = + teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertNotEquals(null, response.getBody()); + assertNull(response.getBody().getData()); + assertNotEquals(null, response.getBody().getError()); + assertEquals(SECURE_SESSION_GENERATION_FAILED, response.getBody().getError()); + } + // endregion +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java b/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java index 50f96a84..6cc471b4 100644 --- a/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java +++ b/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java @@ -34,7 +34,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -public class TeeChallengeServiceTests { +class TeeChallengeServiceTests { private final static String TASK_ID = "0x123"; private final static String PLAIN_PRIVATE = "plainPrivate"; @@ -50,7 +50,7 @@ public class TeeChallengeServiceTests { private TeeChallengeService teeChallengeService; @BeforeEach - public void beforeEach() { + void beforeEach() { MockitoAnnotations.openMocks(this); } @@ -61,7 +61,7 @@ private TeeChallenge getEncryptedTeeChallengeStub() throws Exception { } @Test - public void shouldGetExistingChallengeWithoutDecryptingKeys() throws Exception { + void shouldGetExistingChallengeWithoutDecryptingKeys() throws Exception { TeeChallenge encryptedTeeChallengeStub = getEncryptedTeeChallengeStub(); when(teeChallengeRepository.findByTaskId(TASK_ID)).thenReturn(Optional.of(encryptedTeeChallengeStub)); @@ -72,7 +72,7 @@ public void shouldGetExistingChallengeWithoutDecryptingKeys() throws Exception { } @Test - public void shouldGetExistingChallengeAndDecryptKeys() throws Exception { + void shouldGetExistingChallengeAndDecryptKeys() throws Exception { TeeChallenge encryptedTeeChallengeStub = getEncryptedTeeChallengeStub(); when(teeChallengeRepository.findByTaskId(TASK_ID)).thenReturn(Optional.of(encryptedTeeChallengeStub)); when(encryptionService.decrypt(anyString())).thenReturn(PLAIN_PRIVATE); @@ -84,7 +84,7 @@ public void shouldGetExistingChallengeAndDecryptKeys() throws Exception { } @Test - public void shouldCreateNewChallengeWithoutDecryptingKeys() throws Exception { + void shouldCreateNewChallengeWithoutDecryptingKeys() throws Exception { TeeChallenge encryptedTeeChallengeStub = getEncryptedTeeChallengeStub(); when(teeChallengeRepository.findByTaskId(TASK_ID)).thenReturn(Optional.empty()); when(encryptionService.encrypt(anyString())).thenReturn(ENC_PRIVATE); @@ -97,7 +97,7 @@ public void shouldCreateNewChallengeWithoutDecryptingKeys() throws Exception { } @Test - public void shouldCreateNewChallengeAndDecryptKeys() throws Exception { + void shouldCreateNewChallengeAndDecryptKeys() throws Exception { TeeChallenge encryptedTeeChallengeStub = getEncryptedTeeChallengeStub(); when(teeChallengeRepository.findByTaskId(TASK_ID)).thenReturn(Optional.empty()); when(encryptionService.encrypt(anyString())).thenReturn(ENC_PRIVATE); @@ -110,7 +110,7 @@ public void shouldCreateNewChallengeAndDecryptKeys() throws Exception { } @Test - public void shouldEncryptChallengeKeys() throws Exception { + void shouldEncryptChallengeKeys() throws Exception { TeeChallenge teeChallenge = new TeeChallenge(TASK_ID); when(encryptionService.encrypt(anyString())).thenReturn(ENC_PRIVATE); teeChallengeService.encryptChallengeKeys(teeChallenge); @@ -119,7 +119,7 @@ public void shouldEncryptChallengeKeys() throws Exception { } @Test - public void shouldDecryptChallengeKeys() throws Exception { + void shouldDecryptChallengeKeys() throws Exception { TeeChallenge teeChallenge = new TeeChallenge(TASK_ID); teeChallenge.getCredentials().setEncrypted(true); when(encryptionService.decrypt(anyString())).thenReturn(PLAIN_PRIVATE); diff --git a/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java b/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java new file mode 100644 index 00000000..30225d59 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java @@ -0,0 +1,87 @@ +package com.iexec.sms.tee.session; + +import com.iexec.common.task.TaskDescription; +import com.iexec.sms.blockchain.IexecHubService; +import com.iexec.sms.tee.session.cas.CasClient; +import com.iexec.sms.tee.session.palaemon.PalaemonSessionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; + +import static com.iexec.sms.api.TeeSessionGenerationError.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class TeeSessionServiceTests { + private final static String TASK_ID = "0x0"; + private final static String WORKER_ADDRESS = "0x1"; + private final static String TEE_CHALLENGE = "0x2"; + + @Mock + IexecHubService iexecHubService; + + @Mock + CasClient casClient; + + @Mock + PalaemonSessionService palaemonSessionService; + + TeeSessionService teeSessionService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + teeSessionService = new TeeSessionService(iexecHubService, palaemonSessionService, casClient, false); + } + + @Test + void shouldGenerateTeeSession() throws TeeSessionGenerationException { + final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); + final String sessionYmlAsString = "YML session"; + + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); + when(palaemonSessionService.getSessionYml(any())).thenReturn(sessionYmlAsString); + when(casClient.generateSecureSession(sessionYmlAsString.getBytes())).thenReturn(ResponseEntity.ok(null)); + + final String teeSession = assertDoesNotThrow(() -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertNotNull(teeSession); + } + + @Test + void shouldNotGenerateTeeSessionSinceCantGetTaskDescription() { + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(null); + + final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertEquals(GET_TASK_DESCRIPTION_FAILED, teeSessionGenerationException.getError()); + assertEquals(String.format("Failed to get task description [taskId:%s]", TASK_ID), teeSessionGenerationException.getMessage()); + } + + @Test + void shouldNotGenerateTeeSessionSinceCantGetSessionYml() throws TeeSessionGenerationException { + final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); + + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); + when(palaemonSessionService.getSessionYml(any())).thenReturn(""); + + final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertEquals(GET_SESSION_YML_FAILED, teeSessionGenerationException.getError()); + assertEquals(String.format("Failed to get session yml [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS), teeSessionGenerationException.getMessage()); + } + + @Test + void shouldNotGenerateTeeSessionSinceCantGenerateSecureSession() throws TeeSessionGenerationException { + final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); + final String sessionYmlAsString = "YML session"; + + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); + when(palaemonSessionService.getSessionYml(any())).thenReturn(sessionYmlAsString); + when(casClient.generateSecureSession(sessionYmlAsString.getBytes())).thenReturn(ResponseEntity.notFound().build()); + + final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertEquals(SECURE_SESSION_CAS_CALL_FAILED, teeSessionGenerationException.getError()); + assertEquals(String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS), teeSessionGenerationException.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java b/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java index 51ffb39a..feb11146 100644 --- a/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java +++ b/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java @@ -24,13 +24,18 @@ import com.iexec.common.utils.FileHelper; import com.iexec.common.utils.IexecEnvUtils; import com.iexec.common.worker.result.ResultUtils; -import com.iexec.sms.blockchain.IexecHubService; +import com.iexec.sms.api.TeeSessionGenerationError; import com.iexec.sms.secret.Secret; +import com.iexec.sms.secret.compute.OnChainObjectType; +import com.iexec.sms.secret.compute.SecretOwnerRole; +import com.iexec.sms.secret.compute.TeeTaskComputeSecret; +import com.iexec.sms.secret.compute.TeeTaskComputeSecretService; import com.iexec.sms.secret.web2.Web2SecretsService; import com.iexec.sms.secret.web3.Web3Secret; import com.iexec.sms.secret.web3.Web3SecretService; import com.iexec.sms.tee.challenge.TeeChallenge; import com.iexec.sms.tee.challenge.TeeChallengeService; +import com.iexec.sms.tee.session.TeeSessionGenerationException; import com.iexec.sms.tee.session.attestation.AttestationSecurityConfig; import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; import com.iexec.sms.utils.EthereumCredentials; @@ -39,25 +44,30 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.util.ReflectionTestUtils; import org.yaml.snakeyaml.Yaml; +import java.security.GeneralSecurityException; import java.util.*; -import static com.iexec.common.precompute.PreComputeUtils.IEXEC_DATASET_KEY; -import static com.iexec.common.precompute.PreComputeUtils.INPUT_FILE_URLS; -import static com.iexec.common.precompute.PreComputeUtils.IS_DATASET_REQUIRED; +import static com.iexec.common.chain.DealParams.DROPBOX_RESULT_STORAGE_PROVIDER; +import static com.iexec.common.sms.secret.ReservedSecretKeyName.IEXEC_RESULT_DROPBOX_TOKEN; +import static com.iexec.common.sms.secret.ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY; import static com.iexec.common.worker.result.ResultUtils.*; +import static com.iexec.sms.Web3jUtils.createEthereumAddress; +import static com.iexec.sms.api.TeeSessionGenerationError.*; import static com.iexec.sms.tee.session.palaemon.PalaemonSessionService.*; -import static com.iexec.sms.tee.session.palaemon.PalaemonSessionService.INPUT_FILE_NAMES; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @Slf4j -public class PalaemonSessionServiceTests { +class PalaemonSessionServiceTests { private static final String TEMPLATE_SESSION_FILE = "src/main/resources/palaemonTemplate.vm"; private static final String EXPECTED_SESSION_FILE = "src/test/resources/tee-session.yml"; @@ -66,7 +76,6 @@ public class PalaemonSessionServiceTests { private static final String SESSION_ID = "sessionId"; private static final String WORKER_ADDRESS = "workerAddress"; private static final String ENCLAVE_CHALLENGE = "enclaveChallenge"; - private static final String REQUESTER = "requester"; // pre-compute private static final String PRE_COMPUTE_FINGERPRINT = "mrEnclave1"; private static final String PRE_COMPUTE_ENTRYPOINT = "entrypoint1"; @@ -77,16 +86,22 @@ public class PalaemonSessionServiceTests { // keys with leading/trailing \n should not break the workflow private static final String DATASET_KEY = "\ndatasetKey\n"; // app + private static final String APP_DEVELOPER_SECRET_INDEX = "1"; + private static final String APP_DEVELOPER_SECRET_VALUE = "appDeveloperSecretValue"; + private static final String REQUESTER_SECRET_KEY_1 = "requesterSecretKey1"; + private static final String REQUESTER_SECRET_VALUE_1 = "requesterSecretValue1"; + private static final String REQUESTER_SECRET_KEY_2 = "requesterSecretKey2"; + private static final String REQUESTER_SECRET_VALUE_2 = "requesterSecretValue2"; private static final String APP_URI = "appUri"; private static final String APP_FINGERPRINT = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"; private static final String APP_ENTRYPOINT = "appEntrypoint"; private static final TeeEnclaveConfiguration enclaveConfig = mock(TeeEnclaveConfiguration.class); private static final String ARGS = "args"; + private static final String IEXEC_APP_DEVELOPER_SECRET_1 = "IEXEC_APP_DEVELOPER_SECRET_1"; // post-compute private static final String POST_COMPUTE_FINGERPRINT = "mrEnclave3"; private static final String POST_COMPUTE_ENTRYPOINT = "entrypoint3"; - private static final String POST_COMPUTE_IMAGE = "postComputeImage"; private static final String STORAGE_PROVIDER = "ipfs"; private static final String STORAGE_PROXY = "storageProxy"; private static final String STORAGE_TOKEN = "storageToken"; @@ -98,8 +113,9 @@ public class PalaemonSessionServiceTests { private static final String INPUT_FILE_URL_2 = "http://host/file2"; private static final String INPUT_FILE_NAME_2 = "file2"; - @Mock - private IexecHubService iexecHubService; + private String appAddress; + private String requesterAddress; + @Mock private Web3SecretService web3SecretService; @Mock @@ -110,11 +126,12 @@ public class PalaemonSessionServiceTests { private TeeWorkflowConfiguration teeWorkflowConfig; @Mock private AttestationSecurityConfig attestationSecurityConfig; + @Mock private TeeTaskComputeSecretService teeTaskComputeSecretService; private PalaemonSessionService palaemonSessionService; @BeforeEach - void beforeEach() throws Exception { + void beforeEach() { MockitoAnnotations.openMocks(this); // spy is needed to mock some internal calls of the tested // class when relevant @@ -123,15 +140,18 @@ void beforeEach() throws Exception { web2SecretsService, teeChallengeService, teeWorkflowConfig, - attestationSecurityConfig)); + attestationSecurityConfig, + teeTaskComputeSecretService + )); ReflectionTestUtils.setField(palaemonSessionService, "palaemonTemplateFilePath", TEMPLATE_SESSION_FILE); when(enclaveConfig.getFingerprint()).thenReturn(APP_FINGERPRINT); when(enclaveConfig.getEntrypoint()).thenReturn(APP_ENTRYPOINT); } + //region getSessionYml @Test - public void shouldGetSessionYml() throws Exception { - PalaemonSessionRequest request = createSessionRequest(); + void shouldGetSessionYml() throws Exception { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); doReturn(getPreComputeTokens()).when(palaemonSessionService) .getPreComputePalaemonTokens(request); doReturn(getAppTokens()).when(palaemonSessionService) @@ -150,11 +170,33 @@ public void shouldGetSessionYml() throws Exception { assertRecursively(expectedYmlMap, actualYmlMap); } - // pre-compute + @Test + void shouldNotGetSessionYmlSinceRequestIsNull() { + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getSessionYml(null) + ); + assertEquals(NO_SESSION_REQUEST, exception.getError()); + assertEquals("Session request must not be null", exception.getMessage()); + } @Test - public void shouldGetPreComputePalaemonTokens() throws Exception { - PalaemonSessionRequest request = createSessionRequest(); + void shouldNotGetSessionYmlSinceTaskDescriptionIsMissing() { + PalaemonSessionRequest request = PalaemonSessionRequest.builder().build(); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getSessionYml(request) + ); + assertEquals(NO_TASK_DESCRIPTION, exception.getError()); + assertEquals("Task description must not be null", exception.getMessage()); + } + //endregion + + //region getPreComputePalaemonTokens + @Test + void shouldGetPreComputePalaemonTokens() throws Exception { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); when(teeWorkflowConfig.getPreComputeFingerprint()) .thenReturn(PRE_COMPUTE_FINGERPRINT); when(teeWorkflowConfig.getPreComputeEntrypoint()) @@ -165,57 +207,225 @@ public void shouldGetPreComputePalaemonTokens() throws Exception { Map tokens = palaemonSessionService.getPreComputePalaemonTokens(request); - assertThat(tokens).isNotEmpty(); - assertThat(tokens.get(PalaemonSessionService.PRE_COMPUTE_MRENCLAVE)) - .isEqualTo(PRE_COMPUTE_FINGERPRINT); - assertThat(tokens.get(PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT)) - .isEqualTo(PRE_COMPUTE_ENTRYPOINT); - assertThat(tokens.get(PreComputeUtils.IEXEC_DATASET_KEY)) - .isEqualTo(secret.getTrimmedValue()); - assertThat(tokens.get(PalaemonSessionService.INPUT_FILE_URLS)) - .isEqualTo(Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2)); + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + PalaemonSessionService.PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, + PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, + PreComputeUtils.IEXEC_DATASET_KEY, secret.getTrimmedValue(), + PreComputeUtils.IS_DATASET_REQUIRED, true, + PalaemonSessionService.INPUT_FILE_URLS, + Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2) + + ) + ); } - // app + @Test + void shouldGetPreComputePalaemonTokensWithoutDataset() throws Exception { + PalaemonSessionRequest request = PalaemonSessionRequest.builder() + .sessionId(SESSION_ID) + .workerAddress(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .taskDescription(TaskDescription.builder() + .chainTaskId(TASK_ID) + .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) + .build()) + .build(); + when(teeWorkflowConfig.getPreComputeFingerprint()) + .thenReturn(PRE_COMPUTE_FINGERPRINT); + when(teeWorkflowConfig.getPreComputeEntrypoint()) + .thenReturn(PRE_COMPUTE_ENTRYPOINT); + Map tokens = + palaemonSessionService.getPreComputePalaemonTokens(request); + assertThat(tokens).isNotEmpty() + .containsExactlyInAnyOrderEntriesOf( + Map.of( + PalaemonSessionService.PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, + PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, + PreComputeUtils.IEXEC_DATASET_KEY, "", + PreComputeUtils.IS_DATASET_REQUIRED, false, + PalaemonSessionService.INPUT_FILE_URLS, + Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2) + + ) + ); + } + //endregion + + //region getAppPalaemonTokens @Test - public void shouldGetAppPalaemonTokens() throws Exception { - PalaemonSessionRequest request = createSessionRequest(); + void shouldGetAppPalaemonTokensForAdvancedTaskDescription() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); when(enclaveConfig.getValidator()).thenReturn(validator); when(validator.isValid()).thenReturn(true); - Map tokens = - palaemonSessionService.getAppPalaemonTokens(request); - assertThat(tokens).isNotEmpty(); - assertThat(tokens.get(PalaemonSessionService.APP_MRENCLAVE)) - .isEqualTo(APP_FINGERPRINT); - assertThat(tokens.get(PalaemonSessionService.APP_ARGS)) - .isEqualTo(APP_ENTRYPOINT + " " + ARGS); - assertThat(tokens.get(PalaemonSessionService.INPUT_FILE_NAMES)) - .isEqualTo(Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", "file1", - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", "file2")); + addApplicationDeveloperSecret(); + addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + addRequesterSecret(REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); + + Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); + + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_1); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_2); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + PalaemonSessionService.APP_MRENCLAVE, APP_FINGERPRINT, + PalaemonSessionService.APP_ARGS, APP_ENTRYPOINT + " " + ARGS, + PalaemonSessionService.INPUT_FILE_NAMES, + Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", "file1", + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", "file2" + ), + IEXEC_APP_DEVELOPER_SECRET_1, APP_DEVELOPER_SECRET_VALUE, + REQUESTER_SECRETS, + Map.of( + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1, + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "2", REQUESTER_SECRET_VALUE_2 + ) + + ) + ); + } + + @Test + void shouldGetPalaemonTokensWithEmptyAppComputeSecretWhenSecretsDoNotExist() { + final String appAddress = createEthereumAddress(); + final String requesterAddress = createEthereumAddress(); + final TaskDescription taskDescription = TaskDescription.builder() + .chainTaskId(TASK_ID) + .appUri(APP_URI) + .appAddress(appAddress) + .appEnclaveConfiguration(enclaveConfig) + .datasetAddress(DATASET_ADDRESS) + .datasetUri(DATASET_URL) + .datasetName(DATASET_NAME) + .datasetChecksum(DATASET_CHECKSUM) + .requester(requesterAddress) + .cmd(ARGS) + .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) + .isResultEncryption(true) + .resultStorageProvider(STORAGE_PROVIDER) + .resultStorageProxy(STORAGE_PROXY) + .botSize(1) + .botFirstIndex(0) + .botIndex(0) + .build(); + PalaemonSessionRequest request = createSessionRequest(taskDescription); + TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); + when(enclaveConfig.getValidator()).thenReturn(validator); + when(validator.isValid()).thenReturn(true); + when(teeTaskComputeSecretService.getSecret( + OnChainObjectType.APPLICATION, + appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + APP_DEVELOPER_SECRET_INDEX)) + .thenReturn(Optional.empty()); + + Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); + verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(appAddress), eq(SecretOwnerRole.APPLICATION_DEVELOPER), eq(""), any()); + verify(teeTaskComputeSecretService, never()).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + PalaemonSessionService.APP_MRENCLAVE, APP_FINGERPRINT, + PalaemonSessionService.APP_ARGS, APP_ENTRYPOINT + " " + ARGS, + PalaemonSessionService.INPUT_FILE_NAMES, + Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", "file1", + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", "file2" + ), + IEXEC_APP_DEVELOPER_SECRET_1, "", + REQUESTER_SECRETS, Collections.emptyMap() + ) + ); } @Test - public void shouldFailToGetAppPalaemonTokensInvalidEnclaveConfig(){ - PalaemonSessionRequest request = createSessionRequest(); + void shouldFailToGetAppPalaemonTokensSinceNoTaskDescription() { + PalaemonSessionRequest request = PalaemonSessionRequest.builder() + .build(); + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, + () -> palaemonSessionService.getAppPalaemonTokens(request)); + Assertions.assertEquals(TeeSessionGenerationError.NO_TASK_DESCRIPTION, exception.getError()); + Assertions.assertEquals("Task description must no be null", exception.getMessage()); + } + + @Test + void shouldFailToGetAppPalaemonTokensSinceNoEnclaveConfig() { + PalaemonSessionRequest request = PalaemonSessionRequest.builder() + .sessionId(SESSION_ID) + .workerAddress(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .taskDescription(TaskDescription.builder().build()) + .build(); + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, + () -> palaemonSessionService.getAppPalaemonTokens(request)); + Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_NO_ENCLAVE_CONFIG, exception.getError()); + Assertions.assertEquals("Enclave configuration must no be null", exception.getMessage()); + } + + @Test + void shouldFailToGetAppPalaemonTokensInvalidEnclaveConfig() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); when(enclaveConfig.getValidator()).thenReturn(validator); String validationError = "validation error"; when(validator.validate()).thenReturn(Collections.singletonList(validationError)); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, () -> palaemonSessionService.getAppPalaemonTokens(request)); - Assertions.assertTrue(exception.getMessage().contains(validationError)); + Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_INVALID_ENCLAVE_CONFIG, exception.getError()); } - // post-compute + @Test + void shouldAddMultipleRequesterSecrets() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); + TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); + when(enclaveConfig.getValidator()).thenReturn(validator); + when(validator.isValid()).thenReturn(true); + addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + addRequesterSecret(REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); + Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); + verify(teeTaskComputeSecretService, times(2)) + .getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_1); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_2); + assertThat(tokens).containsEntry(REQUESTER_SECRETS, + Map.of( + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1, + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "2", REQUESTER_SECRET_VALUE_2 + )); + } @Test - public void shouldGetPostComputePalaemonTokens() throws Exception { - PalaemonSessionRequest request = createSessionRequest(); + void shouldFilterRequesterSecretIndexLowerThanZero() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); + request.getTaskDescription().setSecrets(Map.of("1", REQUESTER_SECRET_KEY_1, "-1", "out-of-bound-requester-secret")); + TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); + when(enclaveConfig.getValidator()).thenReturn(validator); + when(validator.isValid()).thenReturn(true); + addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); + verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); + assertThat(tokens).containsEntry(REQUESTER_SECRETS, + Map.of(IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1)); + } + //endregion + + //region getPostComputePalaemonTokens + @Test + void shouldGetPostComputePalaemonTokens() throws Exception { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); Secret publicKeySecret = new Secret("address", ENCRYPTION_PUBLIC_KEY); when(teeWorkflowConfig.getPostComputeFingerprint()) .thenReturn(POST_COMPUTE_FINGERPRINT); @@ -228,7 +438,7 @@ public void shouldGetPostComputePalaemonTokens() throws Exception { .thenReturn(Optional.of(publicKeySecret)); Secret storageSecret = new Secret("address", STORAGE_TOKEN); when(web2SecretsService.getSecret( - REQUESTER, + requesterAddress, ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, true)) .thenReturn(Optional.of(storageSecret)); @@ -242,55 +452,341 @@ public void shouldGetPostComputePalaemonTokens() throws Exception { Map tokens = palaemonSessionService.getPostComputePalaemonTokens(request); - assertThat(tokens).isNotEmpty(); - assertThat(tokens.get(PalaemonSessionService.POST_COMPUTE_MRENCLAVE)) - .isEqualTo(POST_COMPUTE_FINGERPRINT); - assertThat(tokens.get(PalaemonSessionService.POST_COMPUTE_ENTRYPOINT)) - .isEqualTo(POST_COMPUTE_ENTRYPOINT); + + final Map expectedTokens = new HashMap<>(); + expectedTokens.put(PalaemonSessionService.POST_COMPUTE_MRENCLAVE, POST_COMPUTE_FINGERPRINT); + expectedTokens.put(PalaemonSessionService.POST_COMPUTE_ENTRYPOINT, POST_COMPUTE_ENTRYPOINT); // encryption tokens - assertThat(tokens.get(ResultUtils.RESULT_ENCRYPTION)).isEqualTo("yes") ; - assertThat(tokens.get(ResultUtils.RESULT_ENCRYPTION_PUBLIC_KEY)) - .isEqualTo(ENCRYPTION_PUBLIC_KEY); + expectedTokens.put(ResultUtils.RESULT_ENCRYPTION, "yes"); + expectedTokens.put(ResultUtils.RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY); // storage tokens - assertThat(tokens.get(ResultUtils.RESULT_STORAGE_CALLBACK)).isEqualTo("no"); - assertThat(tokens.get(ResultUtils.RESULT_STORAGE_PROVIDER)) - .isEqualTo(STORAGE_PROVIDER); - assertThat(tokens.get(ResultUtils.RESULT_STORAGE_PROXY)) - .isEqualTo(STORAGE_PROXY); - assertThat(tokens.get(ResultUtils.RESULT_STORAGE_TOKEN)) - .isEqualTo(STORAGE_TOKEN); + expectedTokens.put(ResultUtils.RESULT_STORAGE_CALLBACK, "no"); + expectedTokens.put(ResultUtils.RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER); + expectedTokens.put(ResultUtils.RESULT_STORAGE_PROXY, STORAGE_PROXY); + expectedTokens.put(ResultUtils.RESULT_STORAGE_TOKEN, STORAGE_TOKEN); // sign tokens - assertThat(tokens.get(ResultUtils.RESULT_TASK_ID)).isEqualTo(TASK_ID); - assertThat(tokens.get(ResultUtils.RESULT_SIGN_WORKER_ADDRESS)) - .isEqualTo(WORKER_ADDRESS); - assertThat(tokens.get(ResultUtils.RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY)) - .isEqualTo(challenge.getCredentials().getPrivateKey()); + expectedTokens.put(ResultUtils.RESULT_TASK_ID, TASK_ID); + expectedTokens.put(ResultUtils.RESULT_SIGN_WORKER_ADDRESS, WORKER_ADDRESS); + expectedTokens.put(ResultUtils.RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, challenge.getCredentials().getPrivateKey()); + + assertThat(tokens).containsExactlyEntriesOf(expectedTokens); + } + + @Test + void shouldNotGetPostComputePalaemonTokensSinceTaskDescriptionMissing() { + PalaemonSessionRequest request = PalaemonSessionRequest.builder().build(); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputePalaemonTokens(request) + ); + assertEquals(NO_TASK_DESCRIPTION, exception.getError()); + assertEquals("Task description must not be null", exception.getMessage()); + } + //endregion + + //region getPostComputeEncryptionTokens + @Test + void shouldGetPostComputeStorageTokensWithCallback() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + sessionRequest.getTaskDescription().setCallback("callback"); + + final Map tokens = assertDoesNotThrow( + () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_STORAGE_CALLBACK, "yes", + RESULT_STORAGE_PROVIDER, EMPTY_YML_VALUE, + RESULT_STORAGE_PROXY, EMPTY_YML_VALUE, + RESULT_STORAGE_TOKEN, EMPTY_YML_VALUE + ) + ); + } + + @Test + void shouldGetPostComputeStorageTokensOnIpfs() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + + final String secretValue = "Secret value"; + when(web2SecretsService.getSecret(taskDescription.getRequester(), ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, true)) + .thenReturn(Optional.of(new Secret(null, secretValue))); + + final Map tokens = assertDoesNotThrow( + () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_STORAGE_CALLBACK, "no", + RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER, + RESULT_STORAGE_PROXY, STORAGE_PROXY, + RESULT_STORAGE_TOKEN, secretValue + ) + ); + } + + @Test + void shouldGetPostComputeStorageTokensOnDropbox() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + taskDescription.setResultStorageProvider(DROPBOX_RESULT_STORAGE_PROVIDER); + + final String secretValue = "Secret value"; + when(web2SecretsService.getSecret(taskDescription.getRequester(), IEXEC_RESULT_DROPBOX_TOKEN, true)) + .thenReturn(Optional.of(new Secret(null, secretValue))); + + final Map tokens = assertDoesNotThrow( + () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_STORAGE_CALLBACK, "no", + RESULT_STORAGE_PROVIDER, DROPBOX_RESULT_STORAGE_PROVIDER, + RESULT_STORAGE_PROXY, STORAGE_PROXY, + RESULT_STORAGE_TOKEN, secretValue + ) + ); + } + + @Test + void shouldNotGetPostComputeStorageTokensSinceNoSecret() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + + when(web2SecretsService.getSecret(taskDescription.getRequester(), ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, true)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_STORAGE_TOKENS_FAILED); + assertThat(exception.getMessage()).isEqualTo("Empty requester storage token - taskId: " + taskDescription.getChainTaskId()); + } + + @Test + void shouldGetPostComputeSignTokens() throws GeneralSecurityException { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + final String taskId = taskDescription.getChainTaskId(); + final EthereumCredentials credentials = EthereumCredentials.generate(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.of(TeeChallenge.builder().credentials(credentials).build())); + + final Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeSignTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_TASK_ID, taskId, + RESULT_SIGN_WORKER_ADDRESS, sessionRequest.getWorkerAddress(), + RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, credentials.getPrivateKey() + ) + ); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {""}) + void shouldNotGetPostComputeSignTokensSinceNoWorkerAddress(String emptyWorkerAddress) { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + sessionRequest.setWorkerAddress(emptyWorkerAddress); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) + ); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS); + assertThat(exception.getMessage()).isEqualTo("Empty worker address - taskId: " + taskId); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {""}) + void shouldNotGetPostComputeSignTokensSinceNoEnclaveChallenge(String emptyEnclaveChallenge) { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + sessionRequest.setEnclaveChallenge(emptyEnclaveChallenge); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) + ); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE); + assertThat(exception.getMessage()).isEqualTo("Empty public enclave challenge - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoTeeChallenge() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) + ); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentials() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.of(TeeChallenge.builder().credentials(null).build())); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) + ); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentialsPrivateKey() { + final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.of(TeeChallenge.builder().credentials(new EthereumCredentials("", "", false, "")).build())); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) + ); + + assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); + } + + // endregion + + @Test + void shouldGetPostComputeEncryptionTokensWithEncryption() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); + + Secret publicKeySecret = new Secret("address", ENCRYPTION_PUBLIC_KEY); + when(web2SecretsService.getSecret( + request.getTaskDescription().getBeneficiary(), + IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, + true)) + .thenReturn(Optional.of(publicKeySecret)); + + final Map encryptionTokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeEncryptionTokens(request)); + assertThat(encryptionTokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_ENCRYPTION, "yes", + RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY + ) + ); + } + + @Test + void shouldGetPostComputeEncryptionTokensWithoutEncryption() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); + request.getTaskDescription().setResultEncryption(false); + + final Map encryptionTokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeEncryptionTokens(request)); + assertThat(encryptionTokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + RESULT_ENCRYPTION, "no", + RESULT_ENCRYPTION_PUBLIC_KEY, "" + ) + ); + } + + @Test + void shouldNotGetPostComputeEncryptionTokensSinceEmptyBeneficiaryKey() { + PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); + + when(web2SecretsService.getSecret( + request.getTaskDescription().getBeneficiary(), + IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, + true)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> palaemonSessionService.getPostComputeEncryptionTokens(request) + ); + assertEquals(POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, exception.getError()); + assertEquals("Empty beneficiary encryption key - taskId: taskId", exception.getMessage()); + } + + //endregion + + //region utils + private void addApplicationDeveloperSecret() { + TeeTaskComputeSecret applicationDeveloperSecret = TeeTaskComputeSecret.builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress(appAddress) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .key(APP_DEVELOPER_SECRET_INDEX) + .value(APP_DEVELOPER_SECRET_VALUE) + .build(); + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX)) + .thenReturn(Optional.of(applicationDeveloperSecret)); + } + + private void addRequesterSecret(String secretKey, String secretValue) { + TeeTaskComputeSecret requesterSecret = TeeTaskComputeSecret.builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress("") + .secretOwnerRole(SecretOwnerRole.REQUESTER) + .fixedSecretOwner(requesterAddress) + .key(secretKey) + .value(secretValue) + .build(); + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) + .thenReturn(Optional.of(requesterSecret)); } - private PalaemonSessionRequest createSessionRequest() { + private PalaemonSessionRequest createSessionRequest(TaskDescription taskDescription) { return PalaemonSessionRequest.builder() .sessionId(SESSION_ID) .workerAddress(WORKER_ADDRESS) .enclaveChallenge(ENCLAVE_CHALLENGE) - .taskDescription(createTaskDescription()) + .taskDescription(taskDescription) .build(); } private TaskDescription createTaskDescription() { + appAddress = createEthereumAddress(); + requesterAddress = createEthereumAddress(); return TaskDescription.builder() .chainTaskId(TASK_ID) .appUri(APP_URI) + .appAddress(appAddress) .appEnclaveConfiguration(enclaveConfig) .datasetAddress(DATASET_ADDRESS) .datasetUri(DATASET_URL) .datasetName(DATASET_NAME) .datasetChecksum(DATASET_CHECKSUM) - .requester(REQUESTER) + .requester(requesterAddress) .cmd(ARGS) .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) .isResultEncryption(true) .resultStorageProvider(STORAGE_PROVIDER) .resultStorageProxy(STORAGE_PROXY) + .secrets(Map.of("1", REQUESTER_SECRET_KEY_1, "2", REQUESTER_SECRET_KEY_2)) .botSize(1) .botFirstIndex(0) .botIndex(0) @@ -301,8 +797,8 @@ private Map getPreComputeTokens() { return Map.of( PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, - IS_DATASET_REQUIRED, true, - IEXEC_DATASET_KEY, DATASET_KEY.trim(), + PreComputeUtils.IS_DATASET_REQUIRED, true, + PreComputeUtils.IEXEC_DATASET_KEY, DATASET_KEY.trim(), INPUT_FILE_URLS, Map.of( IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2)); @@ -358,4 +854,5 @@ private void assertRecursively(Object expected, Object actual) { }); } } + //endregion } diff --git a/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java b/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java index 5842f3f9..fd930878 100644 --- a/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java +++ b/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java @@ -8,7 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class TeeWorkflowConfigurationTests { +class TeeWorkflowConfigurationTests { private static final String LAS_IMAGE = "lasImage"; private static final String PRE_COMPUTE_IMAGE = "preComputeImage"; @@ -36,7 +36,7 @@ void beforeEach() { } @Test - public void shouldGetPublicConfiguration() { + void shouldGetPublicConfiguration() { assertThat(teeWorkflowConfiguration.getSharedConfiguration()) .isEqualTo(TeeWorkflowSharedConfiguration.builder() .lasImage(LAS_IMAGE) diff --git a/src/test/java/com/iexec/sms/utils/version/VersionServiceTest.java b/src/test/java/com/iexec/sms/utils/version/VersionServiceTest.java new file mode 100644 index 00000000..2276f95e --- /dev/null +++ b/src/test/java/com/iexec/sms/utils/version/VersionServiceTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 com.iexec.sms.utils.version; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.info.BuildProperties; + +public class VersionServiceTest { + + @Mock + private BuildProperties buildProperties; + + @InjectMocks + private VersionService versionService; + + @BeforeEach + public void preflight() { + MockitoAnnotations.openMocks(this); + } + + @ParameterizedTest + @ValueSource(strings={"x.y.z", "x.y.z-rc"}) + void testNonSnapshotVersion(String version) { + Mockito.when(buildProperties.getVersion()).thenReturn(version); + Assertions.assertEquals(version, versionService.getVersion()); + Assertions.assertFalse(versionService.isSnapshot()); + } + + @Test + void testSnapshotVersion() { + Mockito.when(buildProperties.getVersion()).thenReturn("x.y.z-NEXT-SNAPSHOT"); + Assertions.assertEquals("x.y.z-NEXT-SNAPSHOT", versionService.getVersion()); + Assertions.assertTrue(versionService.isSnapshot()); + } + +}