Skip to content

Commit f47e37c

Browse files
authored
Add recursive permission check (#137)
Signed-off-by: Hugo Marcellin <[email protected]>
1 parent 06cd284 commit f47e37c

File tree

7 files changed

+102
-42
lines changed

7 files changed

+102
-42
lines changed

src/main/java/org/gridsuite/explore/server/ExploreController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public ResponseEntity<Void> replaceFilterWithScript(@PathVariable("id") UUID id,
236236
@ApiResponse(responseCode = "404", description = "Directory/element was not found"),
237237
@ApiResponse(responseCode = "403", description = "Access forbidden for the directory/element")
238238
})
239-
@PreAuthorize("@authorizationService.isAuthorized(#userId, #elementUuid, null, T(org.gridsuite.explore.server.dto.PermissionType).WRITE)")
239+
@PreAuthorize("@authorizationService.isRecursivelyAuthorized(#userId, #elementUuid, null)")
240240
public ResponseEntity<Void> deleteElement(@PathVariable("elementUuid") UUID elementUuid,
241241
@RequestHeader(QUERY_PARAM_USER_ID) String userId) {
242242
exploreService.deleteElement(elementUuid, userId);
@@ -537,8 +537,7 @@ public ResponseEntity<Void> updateElement(
537537
@ApiResponse(responseCode = "404", description = "The elements or the targeted directory was not found"),
538538
@ApiResponse(responseCode = "403", description = "Not authorized execute this update")
539539
})
540-
@PreAuthorize(
541-
"@authorizationService.isAuthorized(#userId, #elementsUuids, #targetDirectoryUuid, T(org.gridsuite.explore.server.dto.PermissionType).WRITE)")
540+
@PreAuthorize("@authorizationService.isRecursivelyAuthorized(#userId, #elementsUuids, #targetDirectoryUuid)")
542541
public ResponseEntity<Void> moveElementsDirectory(
543542
@RequestParam UUID targetDirectoryUuid,
544543
@RequestBody List<UUID> elementsUuids,
@@ -654,13 +653,14 @@ public ResponseEntity<String> searchElements(
654653
@ApiResponse(responseCode = "200", description = "The user has the right on the directory"),
655654
@ApiResponse(responseCode = "204", description = "The user has not the right on the directory"),
656655
})
657-
public ResponseEntity<Void> hasRight(@PathVariable("directoryUuid") UUID directoryUuid,
656+
public ResponseEntity<String> hasRight(@PathVariable("directoryUuid") UUID directoryUuid,
658657
@RequestParam(name = "permission") PermissionType permission,
659658
@RequestHeader(QUERY_PARAM_USER_ID) String userId) {
660-
if (directoryService.hasPermission(List.of(directoryUuid), null, userId, permission)) {
659+
PermissionResponse permissionResponse = directoryService.checkPermission(List.of(directoryUuid), null, userId, permission);
660+
if (permissionResponse.hasPermission()) {
661661
return ResponseEntity.ok().build();
662662
} else {
663-
return ResponseEntity.noContent().build();
663+
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(permissionResponse.permissionCheckResult());
664664
}
665665
}
666666

src/main/java/org/gridsuite/explore/server/RestResponseEntityExceptionHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected ResponseEntity<Object> handleExploreException(ExploreException excepti
3535
case NOT_FOUND:
3636
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
3737
case NOT_ALLOWED:
38-
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_ALLOWED);
38+
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exception.getMessage());
3939
case REMOTE_ERROR:
4040
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMessage());
4141
case INCORRECT_CASE_FILE:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
package org.gridsuite.explore.server.dto;
8+
9+
/**
10+
* @author Hugo Marcellin <hugo.marcelin at rte-france.com>
11+
*/
12+
public record PermissionResponse(boolean hasPermission, String permissionCheckResult) { }

src/main/java/org/gridsuite/explore/server/services/AuthorizationService.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.gridsuite.explore.server.services;
88

99
import org.gridsuite.explore.server.ExploreException;
10+
import org.gridsuite.explore.server.dto.PermissionResponse;
1011
import org.gridsuite.explore.server.dto.PermissionType;
1112
import org.springframework.stereotype.Service;
1213

@@ -28,17 +29,30 @@ public AuthorizationService(DirectoryService directoryService) {
2829

2930
//This method should only be called inside of @PreAuthorize to centralize permission checks
3031
public boolean isAuthorized(String userId, List<UUID> elementUuids, UUID targetDirectoryUuid, PermissionType permissionType) {
31-
if (!directoryService.hasPermission(elementUuids, targetDirectoryUuid, userId, permissionType)) {
32-
throw new ExploreException(ExploreException.Type.NOT_ALLOWED);
32+
PermissionResponse permissionResponse = directoryService.checkPermission(elementUuids, targetDirectoryUuid, userId, permissionType);
33+
if (!permissionResponse.hasPermission()) {
34+
throw new ExploreException(ExploreException.Type.NOT_ALLOWED, permissionResponse.permissionCheckResult());
3335
}
3436
return true;
3537
}
3638

3739
//This method should only be called inside of @PreAuthorize to centralize permission checks
3840
public boolean isAuthorizedForDuplication(String userId, UUID elementToDuplicate, UUID targetDirectoryUuid) {
39-
if (!directoryService.hasPermission(List.of(elementToDuplicate), null, userId, PermissionType.READ) ||
40-
!directoryService.hasPermission(List.of(targetDirectoryUuid != null ? targetDirectoryUuid : elementToDuplicate), null, userId, PermissionType.WRITE)) {
41-
throw new ExploreException(ExploreException.Type.NOT_ALLOWED);
41+
PermissionResponse readCheck = directoryService.checkPermission(List.of(elementToDuplicate), null, userId, PermissionType.READ);
42+
if (!readCheck.hasPermission()) {
43+
throw new ExploreException(ExploreException.Type.NOT_ALLOWED, readCheck.permissionCheckResult());
44+
}
45+
PermissionResponse writeCheck = directoryService.checkPermission(List.of(targetDirectoryUuid != null ? targetDirectoryUuid : elementToDuplicate), null, userId, PermissionType.WRITE);
46+
if (!writeCheck.hasPermission()) {
47+
throw new ExploreException(ExploreException.Type.NOT_ALLOWED, writeCheck.permissionCheckResult());
48+
}
49+
return true;
50+
}
51+
52+
public boolean isRecursivelyAuthorized(String userId, List<UUID> elementUuids, UUID targetDirectoryUuid) {
53+
PermissionResponse permissionResponse = directoryService.checkPermission(elementUuids, targetDirectoryUuid, userId, PermissionType.WRITE, true);
54+
if (!permissionResponse.hasPermission()) {
55+
throw new ExploreException(ExploreException.Type.NOT_ALLOWED, permissionResponse.permissionCheckResult());
4256
}
4357
return true;
4458
}

src/main/java/org/gridsuite/explore/server/services/DirectoryService.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import org.gridsuite.explore.server.ExploreException;
1010
import org.gridsuite.explore.server.dto.ElementAttributes;
11+
import org.gridsuite.explore.server.dto.PermissionResponse;
1112
import org.gridsuite.explore.server.dto.PermissionDTO;
1213
import org.gridsuite.explore.server.dto.PermissionType;
1314
import org.gridsuite.explore.server.utils.ParametersType;
@@ -52,10 +53,13 @@ public class DirectoryService implements IDirectoryElementsService {
5253
private static final String PARAM_ELEMENT_TYPES = "elementTypes";
5354
private static final String PARAM_RECURSIVE = "recursive";
5455
private static final String PARAM_DIRECTORY_NAME = "directoryName";
56+
private static final String PARAM_RECURSIVE_CHECK = "recursiveCheck";
5557
private static final String PARAM_TYPE = "type";
5658
private static final String PARAM_DIRECTORY_UUID = "directoryUuid";
5759
private static final String PARAM_USER_INPUT = "userInput";
5860

61+
private static final String HEADER_PERMISION_ERROR = "X-Permission-Error";
62+
5963
private final Map<String, IDirectoryElementsService> genericServices;
6064
private final RestTemplate restTemplate;
6165
private String directoryServerBaseUri;
@@ -394,8 +398,12 @@ public void moveElementsDirectory(List<UUID> elementsUuids, UUID targetDirectory
394398
restTemplate.exchange(directoryServerBaseUri + path, HttpMethod.PUT, httpEntity, Void.class);
395399
}
396400

401+
public PermissionResponse checkPermission(List<UUID> elementUuids, UUID targetDirectoryUuid, String userId, PermissionType permissionType) {
402+
return checkPermission(elementUuids, targetDirectoryUuid, userId, permissionType, false);
403+
}
404+
397405
//This method should only be called inside of AuthorizationService to centralize permission checks
398-
public boolean hasPermission(List<UUID> elementUuids, UUID targetDirectoryUuid, String userId, PermissionType permissionType) {
406+
public PermissionResponse checkPermission(List<UUID> elementUuids, UUID targetDirectoryUuid, String userId, PermissionType permissionType, boolean recursiveCheck) {
399407
String ids = elementUuids.stream().map(UUID::toString).collect(Collectors.joining(","));
400408
HttpHeaders headers = new HttpHeaders();
401409
headers.add(HEADER_USER_ID, userId);
@@ -404,16 +412,25 @@ public boolean hasPermission(List<UUID> elementUuids, UUID targetDirectoryUuid,
404412
.queryParam(PARAM_ACCESS_TYPE, permissionType)
405413
.queryParam(PARAM_IDS, ids)
406414
.queryParam(PARAM_TARGET_DIRECTORY_UUID, targetDirectoryUuid)
415+
.queryParam(PARAM_RECURSIVE_CHECK, recursiveCheck)
407416
.buildAndExpand()
408417
.toUriString();
409418

410-
ResponseEntity<Void> response = null;
411419
try {
412-
response = restTemplate.exchange(directoryServerBaseUri + path, HttpMethod.HEAD, new HttpEntity<>(headers), Void.class);
420+
restTemplate.exchange(directoryServerBaseUri + path, HttpMethod.HEAD, new HttpEntity<>(headers), Void.class);
413421
} catch (HttpStatusCodeException e) {
414-
handleException(e);
422+
if (!HttpStatus.FORBIDDEN.equals(e.getStatusCode())) {
423+
handleException(e);
424+
}
425+
426+
String permissionCheckResult = null;
427+
HttpHeaders responseHeader = e.getResponseHeaders();
428+
if (responseHeader != null && responseHeader.getFirst(HEADER_PERMISION_ERROR) != null) {
429+
permissionCheckResult = responseHeader.getFirst(HEADER_PERMISION_ERROR);
430+
}
431+
return new PermissionResponse(false, permissionCheckResult);
415432
}
416-
return !HttpStatus.NO_CONTENT.equals(response.getStatusCode());
433+
return new PermissionResponse(true, null);
417434
}
418435

419436
public List<PermissionDTO> getDirectoryPermissions(UUID directoryUuid, String userId) {
@@ -453,9 +470,7 @@ public void setDirectoryPermissions(UUID directoryUuid, List<PermissionDTO> perm
453470
}
454471

455472
private void handleException(HttpStatusCodeException e) {
456-
if (HttpStatus.FORBIDDEN.equals(e.getStatusCode())) {
457-
throw new ExploreException(NOT_ALLOWED);
458-
} else if (HttpStatus.NOT_FOUND.equals(e.getStatusCode())) {
473+
if (HttpStatus.NOT_FOUND.equals(e.getStatusCode())) {
459474
throw new ExploreException(NOT_FOUND);
460475
} else {
461476
throw e;

src/test/java/org/gridsuite/explore/server/ExploreTest.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class ExploreTest {
134134
private static final UUID SCRIPT_ID_BASE_FORM_CONTINGENCY_LIST_UUID = UUID.randomUUID();
135135
private static final UUID ELEMENT_UUID = UUID.randomUUID();
136136
private static final UUID FORBIDDEN_ELEMENT_UUID = UUID.randomUUID();
137+
private static final UUID DIRECTORY_NOT_OWNED_SUBELEMENT_UUID = UUID.randomUUID();
137138

138139
@Autowired
139140
private MockMvc mockMvc;
@@ -433,29 +434,31 @@ public MockResponse dispatch(RecordedRequest request) {
433434
}
434435
return new MockResponse(404);
435436
} else if ("HEAD".equals(request.getMethod())) {
436-
if (path.matches("/v1/elements\\?accessType=.*&ids=" + PARENT_DIRECTORY_UUID + "&targetDirectoryUuid")) {
437+
if (path.matches("/v1/elements\\?accessType=.*&ids=" + PARENT_DIRECTORY_UUID + "&targetDirectoryUuid&recursiveCheck=.*")) {
437438
return new MockResponse(200);
438-
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + NO_CONTENT_DIRECTORY_UUID + "&targetDirectoryUuid")) {
439-
return new MockResponse(204);
440-
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + FORBIDDEN_STUDY_UUID + "&targetDirectoryUuid")) {
439+
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + NO_CONTENT_DIRECTORY_UUID + "&targetDirectoryUuid&recursiveCheck=.*")) {
441440
return new MockResponse(403);
442-
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + PARENT_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid")) {
441+
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + FORBIDDEN_STUDY_UUID + "&targetDirectoryUuid&recursiveCheck=.*")) {
442+
return new MockResponse(403);
443+
} else if (path.matches("/v1/elements\\?accessType=.*&ids=" + PARENT_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid&recursiveCheck=.*")) {
443444
return new MockResponse(403);
444445
} else if (path.matches("/v1/elements\\?forUpdate=true&ids=" + FORBIDDEN_ELEMENT_UUID) && USER_NOT_ALLOWED.equals(request.getHeaders().get("userId"))) {
445446
return new MockResponse(403);
447+
} else if (path.matches("/v1/elements\\?accessType=WRITE&ids=" + DIRECTORY_NOT_OWNED_SUBELEMENT_UUID + "&targetDirectoryUuid.*&recursiveCheck=true")) {
448+
return new MockResponse(409);
446449
} else if (path.matches("/v1/elements\\?forDeletion=true&ids=.*") || path.matches("/v1/elements\\?forUpdate=true&ids=.*")) {
447450
return new MockResponse(200);
448451
} else if (path.matches("/v1/directories/" + PARENT_DIRECTORY_UUID2 + "/elements/elementName/types/type")) {
449452
return new MockResponse(200);
450-
} else if (path.matches("/v1/elements\\?accessType=READ&ids=" + TEST_ACCESS_DIRECTORY_UUID_ALLOWED + "&targetDirectoryUuid")) {
453+
} else if (path.matches("/v1/elements\\?accessType=READ&ids=" + TEST_ACCESS_DIRECTORY_UUID_ALLOWED + "&targetDirectoryUuid&recursiveCheck=.*")) {
451454
return new MockResponse(200);
452-
} else if (path.matches("/v1/elements\\?accessType=READ&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid")) {
453-
return new MockResponse(204);
454-
} else if (path.matches("/v1/elements\\?accessType=WRITE&ids=" + TEST_ACCESS_DIRECTORY_UUID_ALLOWED + "&targetDirectoryUuid")) {
455+
} else if (path.matches("/v1/elements\\?accessType=READ&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid&recursiveCheck=.*")) {
456+
return new MockResponse(403);
457+
} else if (path.matches("/v1/elements\\?accessType=WRITE&ids=" + TEST_ACCESS_DIRECTORY_UUID_ALLOWED + "&targetDirectoryUuid&recursiveCheck=.*")) {
455458
return new MockResponse(200);
456-
} else if (path.matches("/v1/elements\\?accessType=WRITE&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid")) {
457-
return new MockResponse(204);
458-
} else if (path.matches("/v1/elements\\?accessType=.*&ids=.*&targetDirectoryUuid.*")) {
459+
} else if (path.matches("/v1/elements\\?accessType=WRITE&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid&recursiveCheck=.*")) {
460+
return new MockResponse(403);
461+
} else if (path.matches("/v1/elements\\?accessType=.*&ids=.*&targetDirectoryUuid.*&recursiveCheck=.*")) {
459462
return new MockResponse(200);
460463
}
461464
}
@@ -660,6 +663,7 @@ void testDeleteElement() throws Exception {
660663
deleteElement(MODIFICATION_UUID);
661664
deleteElementsNotAllowed(List.of(FORBIDDEN_STUDY_UUID), PARENT_DIRECTORY_UUID_FORBIDDEN, 403);
662665
deleteElementNotAllowed(FORBIDDEN_STUDY_UUID, 403);
666+
deleteElementNotAllowed(DIRECTORY_NOT_OWNED_SUBELEMENT_UUID, 409);
663667
}
664668

665669
@Test
@@ -1242,6 +1246,18 @@ void testUpdateElementNotOk() throws Exception {
12421246
).andExpect(status().isForbidden());
12431247
}
12441248

1249+
@Test
1250+
void testMoveDirectoryContainingNotOwnedSubelements() throws Exception {
1251+
ElementAttributes elementAttributes = new ElementAttributes();
1252+
elementAttributes.setElementName(STUDY1);
1253+
mockMvc.perform(put("/v1/explore/elements?targetDirectoryUuid={parentDirectoryUuid}",
1254+
PARENT_DIRECTORY_UUID)
1255+
.header("userId", USER1)
1256+
.contentType(MediaType.APPLICATION_JSON)
1257+
.content(mapper.writeValueAsString(List.of(DIRECTORY_NOT_OWNED_SUBELEMENT_UUID)))
1258+
).andExpect(status().isConflict());
1259+
}
1260+
12451261
@Test
12461262
void testGetRootDirectories(final MockWebServer server) throws Exception {
12471263
MvcResult result = mockMvc.perform(get("/v1/explore/directories/root-directories")
@@ -1358,15 +1374,15 @@ void testHasRights(final MockWebServer server) throws Exception {
13581374
// test read access forbidden
13591375
mockMvc.perform(head("/v1/explore/directories/" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "?permission=READ")
13601376
.header("userId", NOT_ADMIN_USER)
1361-
).andExpect(status().isNoContent());
1377+
).andExpect(status().isForbidden());
13621378

13631379
requests = TestUtils.getRequestsWithBodyDone(1, server);
13641380
assertTrue(requests.stream().anyMatch(r -> r.getPath().contains("v1/elements?accessType=READ&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid")));
13651381

13661382
// test write access forbidden
13671383
mockMvc.perform(head("/v1/explore/directories/" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "?permission=WRITE")
13681384
.header("userId", NOT_ADMIN_USER)
1369-
).andExpect(status().isNoContent());
1385+
).andExpect(status().isForbidden());
13701386

13711387
requests = TestUtils.getRequestsWithBodyDone(1, server);
13721388
assertTrue(requests.stream().anyMatch(r -> r.getPath().contains("v1/elements?accessType=WRITE&ids=" + TEST_ACCESS_DIRECTORY_UUID_FORBIDDEN + "&targetDirectoryUuid")));

0 commit comments

Comments
 (0)