Skip to content

Commit f286ef6

Browse files
Validate-user-can-register-a-component-when-calling-the-endpoint-in-component-catalog (#26)
* Add group-based access restrictions for provisioning actions * Introduce `ProvisionerActionsApiFacade` to streamline group-based validation in provisioning actions and reduce controller complexity. * Remove unused imports and minor formatting adjustments in `ProvisionerActionsApiFacade` and related test * Remove unused imports and `setUp` method from `ProvisionerActionsApiFacadeTest` * Replace direct authentication token retrieval with `AuthenticationFacade` to centralize authentication logic and simplify security handling across facades and tests. * Introduce `AuthenticationFacade` to centralize authentication logic and add corresponding unit tests * Add `AuthenticationFacade` usage in `CatalogItemsApiControllerTest` for handling authentication logic
1 parent fb25222 commit f286ef6

16 files changed

Lines changed: 499 additions & 90 deletions

openapi/openapi-component_catalog-v1.0.0.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,12 @@ components:
11511151
example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog"
11521152
nullable: true
11531153

1154+
accessToken:
1155+
type: string
1156+
description: the access token to be used to get azure groups
1157+
example: "some-access-token"
1158+
nullable: false
1159+
11541160
parameters:
11551161
type: array
11561162
description: List of name/value string parameters.

src/main/java/org/opendevstack/component_catalog/config/ControllerExceptionHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.opendevstack.component_catalog.server.controllers.exceptions.BadConfigurationException;
44
import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException;
5+
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
56
import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException;
67
import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException;
78
import org.opendevstack.component_catalog.server.model.RestErrorMessage;
@@ -47,6 +48,13 @@ public ResponseEntity<RestErrorMessage> handleBadRequestException(BadRequestExce
4748
return defaultErrResponse(ex, HttpStatus.BAD_REQUEST);
4849
}
4950

51+
@ExceptionHandler(ForbiddenException.class)
52+
public ResponseEntity<RestErrorMessage> handleForbiddenException(ForbiddenException ex) {
53+
log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex);
54+
55+
return defaultErrResponse(ex, HttpStatus.FORBIDDEN);
56+
}
57+
5058
@ExceptionHandler(RestEntityNotFoundException.class)
5159
public ResponseEntity<RestErrorMessage> handleEntityNotFoundException(RestEntityNotFoundException ex) {
5260
log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex);

src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public SecurityFilterChain aadForEverythingElse(HttpSecurity http) throws Except
8080
PathPatternRequestMatcher.withDefaults().matcher("/swagger-ui/**"),
8181
PathPatternRequestMatcher.withDefaults().matcher("/v3/api-docs/**"),
8282
PathPatternRequestMatcher.withDefaults().matcher("/v1/user-actions/**"),
83-
PathPatternRequestMatcher.withDefaults().matcher("/v1/provision/*/*"),
8483
PathPatternRequestMatcher.withDefaults().matcher("/actuator/health")
8584
);
8685

src/main/java/org/opendevstack/component_catalog/server/controllers/CatalogItemsApiController.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package org.opendevstack.component_catalog.server.controllers;
22

3+
import lombok.AllArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
35
import org.opendevstack.component_catalog.server.api.CatalogItemsApi;
46
import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException;
57
import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException;
68
import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException;
9+
import org.opendevstack.component_catalog.server.facade.AuthenticationFacade;
710
import org.opendevstack.component_catalog.server.facade.CatalogItemsApiFacade;
811
import org.opendevstack.component_catalog.server.model.CatalogItem;
912
import org.opendevstack.component_catalog.server.model.SortOrder;
1013
import org.opendevstack.component_catalog.server.security.AuthorizationInfo;
1114
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException;
1215
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
1316
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
14-
import lombok.AllArgsConstructor;
15-
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.http.ResponseEntity;
1718
import org.springframework.stereotype.Controller;
1819
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,6 +28,7 @@ public class CatalogItemsApiController implements CatalogItemsApi {
2728

2829
private final AuthorizationInfo authInfo;
2930
private final CatalogItemsApiFacade catalogItemsApiFacade;
31+
private final AuthenticationFacade authenticationFacade;
3032

3133
@Override
3234
public ResponseEntity<List<CatalogItem>> getCatalogItems(String catalogId, SortOrder sortByTitle) {
@@ -51,7 +53,7 @@ public ResponseEntity<List<CatalogItem>> getCatalogItemsForProjectKey(String cat
5153
log.debug("User '{}' requested catalog items for catalog id and projectKey: '{}', '{}'",
5254
authInfo.getCurrentPrincipalName(), catalogId, projectKey);
5355
try {
54-
var idToken = catalogItemsApiFacade.getIdToken();
56+
var idToken = authenticationFacade.getIdToken();
5557

5658
var catalogItemRequestParams = CatalogRequestParams.builder()
5759
.catalogId(catalogId)
@@ -93,7 +95,7 @@ public ResponseEntity<CatalogItem> getCatalogItemByIdForProjectKey(String id, St
9395
log.debug("User '{}' requested catalog item with id and projectKey: '{}', '{}'",
9496
authInfo.getCurrentPrincipalName(), id, projectKey);
9597
try {
96-
var idToken = catalogItemsApiFacade.getIdToken();
98+
var idToken = authenticationFacade.getIdToken();
9799

98100
var catalogRequestParams = CatalogRequestParams.builder()
99101
.catalogItemId(id)

src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
package org.opendevstack.component_catalog.server.controllers;
22

3-
import jakarta.validation.constraints.NotNull;
4-
import org.apache.commons.lang3.tuple.Pair;
5-
import org.jspecify.annotations.NonNull;
6-
import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi;
7-
import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest;
8-
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
9-
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
10-
import org.opendevstack.component_catalog.server.services.provisioner.Status;
113
import com.fasterxml.jackson.core.JsonProcessingException;
124
import lombok.AllArgsConstructor;
135
import lombok.SneakyThrows;
146
import lombok.extern.slf4j.Slf4j;
157
import org.apache.logging.log4j.util.Strings;
8+
import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi;
9+
import org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade;
10+
import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest;
11+
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
12+
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
13+
import org.opendevstack.component_catalog.server.services.provisioner.Status;
1614
import org.springframework.http.ResponseEntity;
1715
import org.springframework.stereotype.Controller;
1816
import org.springframework.web.bind.annotation.RequestMapping;
1917

20-
import java.util.List;
18+
import static org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade.map;
2119

2220
@Controller
2321
@RequestMapping("${openapi.componentCatalogREST.base-path:/v1}")
2422
@AllArgsConstructor
2523
@Slf4j
2624
public class ProvisionerActionsApiController implements ProvisionerActionsApi {
2725

26+
private final ProvisionerActionsApiFacade provisionerActionsApiFacade;
2827
private final ProvisionerActionsService provisionerActionsService;
2928

29+
3030
@SneakyThrows
3131
@Override
3232
public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey,
@@ -36,12 +36,13 @@ public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey,
3636
projectKey, provisioningStatusUpdateRequest.toString());
3737

3838
var normalizedProjectKey = projectKey.toUpperCase();
39+
provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest);
3940
var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY);
4041
var parameters = map(provisioningStatusUpdateRequest);
4142

4243
provisionerActionsService.updateComponentProvisioningStatus(normalizedProjectKey, Status.valueOf(status),
43-
provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(),
44-
normalizedComponentUrl, parameters);
44+
provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(),
45+
normalizedComponentUrl, parameters);
4546

4647
return ResponseEntity.ok().build();
4748
}
@@ -53,6 +54,7 @@ public ResponseEntity<Void> notifyProvisioningStatusUpdatePartially(String proje
5354
projectKey, provisioningStatusUpdateRequest.toString());
5455

5556
var normalizedProjectKey = projectKey.toUpperCase();
57+
provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest);
5658
var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY);
5759
var parameters = map(provisioningStatusUpdateRequest);
5860

@@ -75,10 +77,4 @@ public ResponseEntity<Void> deleteProvisioningStatus(String projectKey, Provisio
7577

7678
return ResponseEntity.ok().build();
7779
}
78-
79-
private static @NonNull List<Pair<@NotNull String, @NotNull List<String>>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
80-
return provisioningStatusUpdateRequest.getParameters().stream()
81-
.map(parameter -> Pair.of(parameter.getName(), parameter.getValues()))
82-
.toList();
83-
}
8480
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.opendevstack.component_catalog.server.controllers.exceptions;
2+
3+
public class ForbiddenException extends RuntimeException {
4+
5+
public ForbiddenException(String message) {
6+
super(message);
7+
}
8+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.opendevstack.component_catalog.server.facade;
2+
3+
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.core.context.SecurityContextHolder;
8+
import org.springframework.stereotype.Component;
9+
10+
@Component
11+
@Slf4j
12+
public class AuthenticationFacade {
13+
14+
public String getIdToken() {
15+
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
16+
17+
if (auth == null || !(auth.getPrincipal() instanceof UserPrincipal principal)) {
18+
throw new ForbiddenException("User not authenticated");
19+
}
20+
21+
log.debug("Authenticated user '{}'", auth.getName());
22+
23+
return principal.getAadIssuedBearerToken();
24+
}
25+
}

src/main/java/org/opendevstack/component_catalog/server/facade/CatalogItemsApiFacade.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.opendevstack.component_catalog.server.facade;
22

3-
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
3+
import lombok.AllArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
45
import org.opendevstack.component_catalog.client.projects_info_service.v1_0_0.model.ProjectInfo;
56
import org.opendevstack.component_catalog.server.controllers.CatalogApiAdapter;
67
import org.opendevstack.component_catalog.server.controllers.CatalogRequestParams;
@@ -16,10 +17,6 @@
1617
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException;
1718
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
1819
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
19-
import lombok.AllArgsConstructor;
20-
import lombok.extern.slf4j.Slf4j;
21-
import org.springframework.security.core.Authentication;
22-
import org.springframework.security.core.context.SecurityContextHolder;
2320
import org.springframework.stereotype.Component;
2421

2522
import java.util.Collections;
@@ -54,16 +51,6 @@ public List<CatalogItemFilter> catalogItemFiltersFrom(CatalogRequestParams catal
5451
return catalogApiAdapter.catalogItemFiltersFrom(catalogRequestParams, clusters, userGroups);
5552
}
5653

57-
public String getIdToken() {
58-
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
59-
60-
log.debug("Authenticated user '{}'", auth.getName());
61-
62-
var principal = (UserPrincipal) auth.getPrincipal();
63-
64-
return principal.getAadIssuedBearerToken();
65-
}
66-
6754
private List<String> getProjectGroups(CatalogRequestParams catalogRequestParams) {
6855
if (catalogRequestParams.getAccessToken() == null) {
6956
return Collections.emptyList();

src/main/java/org/opendevstack/component_catalog/server/facade/ProjectComponentsFacade.java

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package org.opendevstack.component_catalog.server.facade;
22

3-
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
3+
import lombok.AllArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.apache.commons.lang3.StringUtils;
46
import org.opendevstack.component_catalog.server.mappers.ProjectComponentsInfoMapper;
57
import org.opendevstack.component_catalog.server.model.ProjectComponentInfo;
68
import org.opendevstack.component_catalog.server.services.ProjectsInfoService;
79
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
810
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
911
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
1012
import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents;
11-
import lombok.AllArgsConstructor;
12-
import lombok.extern.slf4j.Slf4j;
13-
import org.apache.commons.lang3.StringUtils;
14-
import org.springframework.security.core.Authentication;
15-
import org.springframework.security.core.context.SecurityContextHolder;
1613
import org.springframework.stereotype.Component;
1714

1815
import java.util.Collections;
@@ -27,16 +24,7 @@ public class ProjectComponentsFacade {
2724
private final ProvisionerActionsService provisionerActionsService;
2825
private final ProjectComponentsInfoMapper projectComponentsInfoMapper;
2926
private final ProjectsInfoService projectsInfoService;
30-
31-
public String getIdToken() {
32-
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
33-
34-
log.debug("Authenticated user '{}'", auth.getName());
35-
36-
var principal = (UserPrincipal) auth.getPrincipal();
37-
38-
return principal.getAadIssuedBearerToken();
39-
}
27+
private final AuthenticationFacade authenticationFacade;
4028

4129
public List<ProjectComponentInfo> getProjectComponentsInfo(String projectKey, String accessToken) {
4230
var projectComponents = provisionerActionsService.getProjectComponents(projectKey);
@@ -45,8 +33,8 @@ public List<ProjectComponentInfo> getProjectComponentsInfo(String projectKey, St
4533
return Collections.emptyList();
4634
}
4735

48-
String idToken = getIdToken();
49-
List<String> userGroups = projectsInfoService.getProjectGroups(idToken,accessToken);
36+
String idToken = authenticationFacade.getIdToken();
37+
List<String> userGroups = projectsInfoService.getProjectGroups(idToken, accessToken);
5038

5139
return projectComponents.getComponents()
5240
.values()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.opendevstack.component_catalog.server.facade;
2+
3+
import jakarta.validation.constraints.NotNull;
4+
import lombok.AllArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.apache.commons.lang3.tuple.Pair;
7+
import org.jspecify.annotations.NonNull;
8+
import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration;
9+
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
10+
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
11+
import org.opendevstack.component_catalog.server.services.ProjectsInfoService;
12+
import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction;
13+
import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions;
14+
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions;
15+
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator;
16+
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams;
17+
import org.springframework.stereotype.Component;
18+
19+
import java.util.List;
20+
21+
@Component
22+
@AllArgsConstructor
23+
@Slf4j
24+
public class ProvisionerActionsApiFacade {
25+
private final ProjectsInfoService projectsInfoService;
26+
private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator;
27+
private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps;
28+
private final AuthenticationFacade authenticationFacade;
29+
30+
public static @NonNull List<Pair<@NotNull String, @NotNull List<String>>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
31+
return provisioningStatusUpdateRequest.getParameters().stream()
32+
.map(parameter -> Pair.of(parameter.getName(), parameter.getValues()))
33+
.toList();
34+
}
35+
36+
public void validateGroupRestrictions(String projectKey, ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
37+
var groupRestriction = CatalogItemUserActionGroupsRestriction.builder()
38+
.prefix(groupsRestrictionProps.getPrefix())
39+
.suffix(groupsRestrictionProps.getSuffix())
40+
.build();
41+
42+
var userActionEntityRestrictions = UserActionEntityRestrictions.builder()
43+
.groups(groupRestriction)
44+
.build();
45+
46+
var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions);
47+
var userGroups = projectsInfoService.getProjectGroups(authenticationFacade.getIdToken(), provisioningStatusUpdateRequest.getAccessToken());
48+
49+
var params = RestrictionsParams.builder()
50+
.userGroups(userGroups)
51+
.projectKey(projectKey)
52+
.build();
53+
54+
if (Boolean.FALSE.equals(groupsRestrictionsEvaluator.evaluate(evaluationRestrictions, params).getLeft())) {
55+
log.error("The user has no permissions to perform this action based on group restrictions for project {}", projectKey);
56+
throw new ForbiddenException("User not allowed to perform this action");
57+
}
58+
}
59+
60+
}

0 commit comments

Comments
 (0)