Skip to content

Commit 9e4ea9e

Browse files
authored
Merge split URL configuration properties to a single URL field (#155)
1 parent 19c97a3 commit 9e4ea9e

File tree

9 files changed

+266
-66
lines changed

9 files changed

+266
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## [[NEXT]](https://github.com/iExecBlockchainComputing/iexec-result-proxy/releases/tag/vNEXT) 2025
66

7-
### Quality
8-
9-
- Harmonize YML internal variables to proper case. (#154)
10-
117
### Breaking API changes
128

139
- Remove legacy authorization mechanism, allows to remove challenge services as well. (#150)
10+
- Harmonize YML internal variables to proper case. (#154)
11+
- Merge split URL configuration properties (protocol, host, port) to a single URL field to offer URL validation at startup. (#155)
1412

1513
### Dependency Upgrades
1614

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ You can configure the iExec Result Proxy with the following properties:
2929
| `IEXEC_BLOCK_TIME` | Duration between consecutive blocks on the blockchain network. | String | `PT5S` |
3030
| `IEXEC_GAS_PRICE_MULTIPLIER` | Transactions will be sent with `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER`. | Float | `1.0` |
3131
| `IEXEC_GAS_PRICE_CAP` | In Wei, will be used for transactions if `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER > gasPriceCap`. | Integer | `22000000000` |
32-
| `IEXEC_IPFS_HOST` | Host to connect to the IPFS node. | String | `127.0.0.1` |
33-
| `IEXEC_IPFS_PORT` | Server port of the IPFS node. | Positive integer | `5001` |
32+
| `IEXEC_IPFS_URL` | URL to connect to the IPFS node. | String | `http://127.0.0.1:5001` |
3433

3534
### Spring web application properties
3635

src/main/java/com/iexec/resultproxy/authorization/AuthorizationService.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2024-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@ public AuthorizationService(AuthorizationRepository authorizationRepository, Iex
6464
* @param workerpoolAuthorization The authorization to check
6565
* @return the reason if unauthorized, an empty {@code Optional} otherwise
6666
*/
67-
public Optional<AuthorizationError> isAuthorizedOnExecutionWithDetailedIssue(WorkerpoolAuthorization workerpoolAuthorization) {
67+
public Optional<AuthorizationError> isAuthorizedOnExecutionWithDetailedIssue(final WorkerpoolAuthorization workerpoolAuthorization) {
6868
if (workerpoolAuthorization == null || StringUtils.isEmpty(workerpoolAuthorization.getChainTaskId())) {
6969
log.error("Not authorized with empty params");
7070
return Optional.of(EMPTY_PARAMS_UNAUTHORIZED);
@@ -112,19 +112,19 @@ public Optional<AuthorizationError> isAuthorizedOnExecutionWithDetailedIssue(Wor
112112
return Optional.empty();
113113
}
114114

115-
public boolean isSignedByHimself(String message, String signature, String address) {
115+
public boolean isSignedByHimself(final String message, final String signature, final String address) {
116116
return SignatureUtils.isSignatureValid(BytesUtils.stringToBytes(message), new Signature(signature), address);
117117
}
118118

119-
public String getChallengeForWorker(WorkerpoolAuthorization workerpoolAuthorization) {
119+
public String getChallengeForWorker(final WorkerpoolAuthorization workerpoolAuthorization) {
120120
return HashUtils.concatenateAndHash(
121121
workerpoolAuthorization.getWorkerWallet(),
122122
workerpoolAuthorization.getChainTaskId(),
123123
workerpoolAuthorization.getEnclaveChallenge());
124124
}
125125

126126
// region workerpool authorization cache
127-
public boolean checkEnclaveSignature(ResultModel model, String walletAddress) {
127+
public boolean checkEnclaveSignature(final ResultModel model, final String walletAddress) {
128128
if (ResultModel.EMPTY_WEB3_SIG.equals(model.getEnclaveSignature())) {
129129
log.warn("Empty enclave signature {}", walletAddress);
130130
return false;
@@ -154,7 +154,7 @@ public boolean checkEnclaveSignature(ResultModel model, String walletAddress) {
154154
return isSignedByEnclave;
155155
}
156156

157-
public void putIfAbsent(WorkerpoolAuthorization workerpoolAuthorization) {
157+
public void putIfAbsent(final WorkerpoolAuthorization workerpoolAuthorization) {
158158
try {
159159
authorizationRepository.save(new Authorization(workerpoolAuthorization));
160160
log.debug("Workerpool authorization entry added [chainTaskId:{}, workerWallet:{}]",
Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1+
/*
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.iexec.resultproxy.ipfs;
218

3-
import lombok.Data;
19+
import jakarta.validation.constraints.NotEmpty;
20+
import lombok.Value;
21+
import org.hibernate.validator.constraints.URL;
422
import org.springframework.boot.context.properties.ConfigurationProperties;
23+
import org.springframework.validation.annotation.Validated;
524

6-
@Data
25+
@Value
26+
@Validated
727
@ConfigurationProperties(prefix = "ipfs")
828
public class IpfsConfig {
9-
private final String host;
10-
private final String port;
29+
30+
@URL(message = "IPFS URL must be a valid URL")
31+
@NotEmpty(message = "IPFS URL must not be empty")
32+
String url;
1133
}

src/main/java/com/iexec/resultproxy/ipfs/IpfsService.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
/*
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.iexec.resultproxy.ipfs;
218

3-
import com.iexec.common.utils.NetworkUtils;
419
import io.ipfs.api.IPFS;
520
import io.ipfs.api.MerkleNode;
621
import io.ipfs.api.NamedStreamable;
@@ -12,6 +27,8 @@
1227
import org.springframework.stereotype.Service;
1328

1429
import java.io.IOException;
30+
import java.net.InetAddress;
31+
import java.net.URL;
1532
import java.util.Optional;
1633

1734
@Service
@@ -21,17 +38,24 @@ public class IpfsService implements SmartLifecycle {
2138
private IPFS ipfs;
2239
private final String multiAddress;
2340

24-
public IpfsService(IpfsConfig ipfsConfig) {
25-
String ipfsHost = ipfsConfig.getHost();
26-
String ipfsNodeIp = NetworkUtils.isIPAddress(ipfsHost) ? ipfsHost : NetworkUtils.convertHostToIp(ipfsHost);
27-
this.multiAddress = "/ip4/" + ipfsNodeIp + "/tcp/" + ipfsConfig.getPort();
41+
public IpfsService(final IpfsConfig ipfsConfig) {
42+
try {
43+
final URL ipfsUrl = new URL(ipfsConfig.getUrl());
44+
final String ipfsHost = ipfsUrl.getHost();
45+
final int port = ipfsUrl.getPort() != -1 ? ipfsUrl.getPort() : ipfsUrl.getDefaultPort();
46+
final String ipfsNodeIp = InetAddress.getByName(ipfsHost).getHostAddress();
47+
this.multiAddress = "/ip4/" + ipfsNodeIp + "/tcp/" + port;
48+
} catch (Exception e) {
49+
log.error("Failed to convert IPFS URL to MultiAddress: {}", ipfsConfig.getUrl(), e);
50+
throw new IllegalArgumentException("Invalid IPFS URL: " + ipfsConfig.getUrl(), e);
51+
}
2852
}
2953

30-
public Optional<byte[]> get(String ipfsHash) {
54+
public Optional<byte[]> get(final String ipfsHash) {
3155
if (!isIpfsHash(ipfsHash)){
3256
return Optional.empty();
3357
}
34-
Multihash filePointer = Multihash.fromBase58(ipfsHash);
58+
final Multihash filePointer = Multihash.fromBase58(ipfsHash);
3559
try {
3660
return Optional.of(ipfs.cat(filePointer));
3761
} catch (IOException e) {
@@ -40,18 +64,18 @@ public Optional<byte[]> get(String ipfsHash) {
4064
return Optional.empty();
4165
}
4266

43-
public String add(String fileName, byte[] fileContent) {
44-
NamedStreamable.ByteArrayWrapper file = new NamedStreamable.ByteArrayWrapper(fileName, fileContent);
67+
public String add(final String fileName, final byte[] fileContent) {
68+
final NamedStreamable.ByteArrayWrapper file = new NamedStreamable.ByteArrayWrapper(fileName, fileContent);
4569
try {
46-
MerkleNode pushedContent = ipfs.add(file, false).get(0);
70+
final MerkleNode pushedContent = ipfs.add(file, false).get(0);
4771
return pushedContent.hash.toString();
4872
} catch (IOException e) {
4973
log.error("Error when trying to push ipfs object [fileName:{}]", fileName);
5074
}
5175
return "";
5276
}
5377

54-
public static boolean isIpfsHash(String hash) {
78+
public static boolean isIpfsHash(final String hash) {
5579
try {
5680
return Multihash.fromBase58(hash).toBase58() != null;
5781
} catch (Exception e) {
@@ -66,7 +90,7 @@ public void start() {
6690
}
6791

6892
@Recover
69-
public void start(RuntimeException exception) {
93+
public void start(final RuntimeException exception) {
7094
log.error("Exception when initializing IPFS connection", exception);
7195
log.warn("Shutting down service since IPFS is necessary");
7296
throw exception;

src/main/resources/application.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ chain:
1919
gas-price-cap: ${IEXEC_GAS_PRICE_CAP:22000000000} #in Wei, will be used for txs if networkGasPrice*gasPriceMultiplier > gasPriceCap
2020

2121
ipfs:
22-
host: ${IEXEC_IPFS_HOST:127.0.0.1}
23-
port: ${IEXEC_IPFS_PORT:5001}
22+
url: ${IEXEC_IPFS_URL:http://127.0.0.1:5001}
2423

2524
jwt:
2625
key-path: /data/jwt-sign.key

src/test/java/com/iexec/resultproxy/authorization/AuthorizationServiceTests.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2024-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -101,19 +101,20 @@ void shouldBeAuthorizedOnExecutionOfTeeTaskWithDetails() {
101101
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask));
102102
when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.of(chainDeal));
103103

104-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
104+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
105105
assertThat(isAuth).isEmpty();
106106
}
107107

108108
@Test
109109
void shouldNotBeAuthorizedOnExecutionOfTeeTaskWithNullAuthorizationWithDetails() {
110-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(null);
110+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(null);
111111
assertThat(isAuth).isEqualTo(Optional.of(EMPTY_PARAMS_UNAUTHORIZED));
112112
}
113113

114114
@Test
115115
void shouldNotBeAuthorizedOnExecutionOfTeeTaskWithEmptyAuthorizationWithDetails() {
116-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(WorkerpoolAuthorization.builder().build());
116+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(WorkerpoolAuthorization.builder()
117+
.chainTaskId(null).build());
117118
assertThat(isAuth).isEqualTo(Optional.of(EMPTY_PARAMS_UNAUTHORIZED));
118119
}
119120

@@ -129,7 +130,7 @@ void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskTypeNotMatchedOnchainWithD
129130
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask));
130131
when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.of(chainDeal));
131132

132-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
133+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
133134
assertThat(isAuth).isEqualTo(Optional.of(NO_MATCH_ONCHAIN_TYPE));
134135
}
135136

@@ -138,7 +139,7 @@ void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenGetTaskFailedWithDetails() {
138139
final WorkerpoolAuthorization auth = getWorkerpoolAuthorization(true);
139140
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.empty());
140141

141-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
142+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
142143
assertThat(isAuth).isEqualTo(Optional.of(GET_CHAIN_TASK_FAILED));
143144
}
144145

@@ -151,34 +152,34 @@ void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenFinalDeadlineReached() {
151152
.build();
152153
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask));
153154

154-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
155+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
155156
assertThat(isAuth).isEqualTo(Optional.of(TASK_FINAL_DEADLINE_REACHED));
156157
}
157158

158159
@Test
159160
void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenGetDealFailedWithDetails() {
160161
final ChainTask chainTask = getChainTask(ACTIVE);
161-
final WorkerpoolAuthorization auth = getWorkerpoolAuthorization(true);
162-
auth.setSignature(new Signature(POOL_WRONG_SIGNATURE));
162+
final Signature wrongSignature = new Signature(POOL_WRONG_SIGNATURE);
163+
final WorkerpoolAuthorization auth = getWorkerpoolAuthorizationWithWrongSignature(wrongSignature);
163164

164165
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask));
165166
when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.empty());
166167

167-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
168+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
168169
assertThat(isAuth).isEqualTo(Optional.of(GET_CHAIN_DEAL_FAILED));
169170
}
170171

171172
@Test
172173
void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenPoolSignatureIsNotValidWithDetails() {
173174
final ChainDeal chainDeal = getChainDeal();
174175
final ChainTask chainTask = getChainTask(ACTIVE);
175-
final WorkerpoolAuthorization auth = getWorkerpoolAuthorization(true);
176-
auth.setSignature(new Signature(POOL_WRONG_SIGNATURE));
176+
final Signature wrongSignature = new Signature(POOL_WRONG_SIGNATURE);
177+
final WorkerpoolAuthorization auth = getWorkerpoolAuthorizationWithWrongSignature(wrongSignature);
177178

178179
when(iexecHubService.getChainTask(auth.getChainTaskId())).thenReturn(Optional.of(chainTask));
179180
when(iexecHubService.getChainDeal(chainTask.getDealid())).thenReturn(Optional.of(chainDeal));
180181

181-
Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
182+
final Optional<AuthorizationError> isAuth = authorizationService.isAuthorizedOnExecutionWithDetailedIssue(auth);
182183
assertThat(isAuth).isEqualTo(Optional.of(INVALID_SIGNATURE));
183184
}
184185
// endregion
@@ -276,15 +277,15 @@ void shouldNotAddAuthorizationTwiceInCollection() {
276277
// endregion
277278

278279
// region utils
279-
String getEnclaveSignature(ECKeyPair ecKeyPair) {
280+
String getEnclaveSignature(final ECKeyPair ecKeyPair) {
280281
final String resultHash = HashUtils.concatenateAndHash(CHAIN_TASK_ID, RESULT_DIGEST);
281282
final String resultSeal = HashUtils.concatenateAndHash(workerCreds.getAddress(), CHAIN_TASK_ID, RESULT_DIGEST);
282283
final String messageHash = HashUtils.concatenateAndHash(resultHash, resultSeal);
283284
return SignatureUtils.signMessageHashAndGetSignature(messageHash,
284285
Numeric.toHexStringWithPrefix(ecKeyPair.getPrivateKey())).getValue();
285286
}
286287

287-
WorkerpoolAuthorization getWorkerpoolAuthorization(boolean isTeeTask) {
288+
private WorkerpoolAuthorization getWorkerpoolAuthorization(final boolean isTeeTask) {
288289
final String enclaveChallenge = isTeeTask ? enclaveCreds.getAddress() : BytesUtils.EMPTY_ADDRESS;
289290
final String hash = HashUtils.concatenateAndHash(workerCreds.getAddress(), CHAIN_TASK_ID, enclaveChallenge);
290291
final Signature signature = SignatureUtils.signMessageHashAndGetSignature(hash, POOL_PRIVATE);
@@ -295,5 +296,15 @@ WorkerpoolAuthorization getWorkerpoolAuthorization(boolean isTeeTask) {
295296
.signature(signature)
296297
.build();
297298
}
299+
300+
private WorkerpoolAuthorization getWorkerpoolAuthorizationWithWrongSignature(final Signature signature) {
301+
final String enclaveChallenge = enclaveCreds.getAddress();
302+
return WorkerpoolAuthorization.builder()
303+
.chainTaskId(CHAIN_TASK_ID)
304+
.enclaveChallenge(enclaveChallenge)
305+
.workerWallet(workerCreds.getAddress())
306+
.signature(signature)
307+
.build();
308+
}
298309
// endregion
299310
}

0 commit comments

Comments
 (0)