Skip to content
Merged
6 changes: 6 additions & 0 deletions openapi/openapi-component_catalog-v1.0.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,12 @@ components:
example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog"
nullable: true

accessToken:
type: string
description: the access token to be used to get azure groups
example: "some-access-token"
nullable: false

parameters:
type: array
description: List of name/value string parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.opendevstack.component_catalog.server.controllers.exceptions.BadConfigurationException;
import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException;
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException;
import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException;
import org.opendevstack.component_catalog.server.model.RestErrorMessage;
Expand Down Expand Up @@ -47,6 +48,13 @@ public ResponseEntity<RestErrorMessage> handleBadRequestException(BadRequestExce
return defaultErrResponse(ex, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<RestErrorMessage> handleForbiddenException(ForbiddenException ex) {
log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex);

return defaultErrResponse(ex, HttpStatus.FORBIDDEN);
}

@ExceptionHandler(RestEntityNotFoundException.class)
public ResponseEntity<RestErrorMessage> handleEntityNotFoundException(RestEntityNotFoundException ex) {
log.trace(SENDING_PREDEFINED_HTTP_STATUS, ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public SecurityFilterChain aadForEverythingElse(HttpSecurity http) throws Except
PathPatternRequestMatcher.withDefaults().matcher("/swagger-ui/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v3/api-docs/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v1/user-actions/**"),
PathPatternRequestMatcher.withDefaults().matcher("/v1/provision/*/*"),
PathPatternRequestMatcher.withDefaults().matcher("/actuator/health")
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package org.opendevstack.component_catalog.server.controllers;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.component_catalog.server.api.CatalogItemsApi;
import org.opendevstack.component_catalog.server.controllers.exceptions.BadRequestException;
import org.opendevstack.component_catalog.server.controllers.exceptions.InvalidRestEntityException;
import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException;
import org.opendevstack.component_catalog.server.facade.AuthenticationFacade;
import org.opendevstack.component_catalog.server.facade.CatalogItemsApiFacade;
import org.opendevstack.component_catalog.server.model.CatalogItem;
import org.opendevstack.component_catalog.server.model.SortOrder;
import org.opendevstack.component_catalog.server.security.AuthorizationInfo;
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException;
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -27,6 +28,7 @@ public class CatalogItemsApiController implements CatalogItemsApi {

private final AuthorizationInfo authInfo;
private final CatalogItemsApiFacade catalogItemsApiFacade;
private final AuthenticationFacade authenticationFacade;

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

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

var catalogRequestParams = CatalogRequestParams.builder()
.catalogItemId(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
package org.opendevstack.component_catalog.server.controllers;

import jakarta.validation.constraints.NotNull;
import org.apache.commons.lang3.tuple.Pair;
import org.jspecify.annotations.NonNull;
import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi;
import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest;
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
import org.opendevstack.component_catalog.server.services.provisioner.Status;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.opendevstack.component_catalog.server.api.ProvisionerActionsApi;
import org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade;
import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest;
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
import org.opendevstack.component_catalog.server.services.provisioner.Status;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;
import static org.opendevstack.component_catalog.server.facade.ProvisionerActionsApiFacade.map;

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

private final ProvisionerActionsApiFacade provisionerActionsApiFacade;
private final ProvisionerActionsService provisionerActionsService;


@SneakyThrows
@Override
public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey,
Expand All @@ -36,12 +36,13 @@ public ResponseEntity<Void> notifyProvisioningStatusUpdate(String projectKey,
projectKey, provisioningStatusUpdateRequest.toString());

var normalizedProjectKey = projectKey.toUpperCase();
provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest);
var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY);
var parameters = map(provisioningStatusUpdateRequest);

provisionerActionsService.updateComponentProvisioningStatus(normalizedProjectKey, Status.valueOf(status),
provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(),
normalizedComponentUrl, parameters);
provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(),
normalizedComponentUrl, parameters);

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

var normalizedProjectKey = projectKey.toUpperCase();
provisionerActionsApiFacade.validateGroupRestrictions(normalizedProjectKey, provisioningStatusUpdateRequest);
var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY);
var parameters = map(provisioningStatusUpdateRequest);

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

return ResponseEntity.ok().build();
}

private static @NonNull List<Pair<@NotNull String, @NotNull List<String>>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
return provisioningStatusUpdateRequest.getParameters().stream()
.map(parameter -> Pair.of(parameter.getName(), parameter.getValues()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.opendevstack.component_catalog.server.controllers.exceptions;

public class ForbiddenException extends RuntimeException {

public ForbiddenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.opendevstack.component_catalog.server.facade;

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class AuthenticationFacade {

public String getIdToken() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth == null || !(auth.getPrincipal() instanceof UserPrincipal principal)) {
throw new ForbiddenException("User not authenticated");
}

log.debug("Authenticated user '{}'", auth.getName());

return principal.getAadIssuedBearerToken();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opendevstack.component_catalog.server.facade;

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.component_catalog.client.projects_info_service.v1_0_0.model.ProjectInfo;
import org.opendevstack.component_catalog.server.controllers.CatalogApiAdapter;
import org.opendevstack.component_catalog.server.controllers.CatalogRequestParams;
Expand All @@ -16,10 +17,6 @@
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogEntityException;
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

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

public String getIdToken() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

log.debug("Authenticated user '{}'", auth.getName());

var principal = (UserPrincipal) auth.getPrincipal();

return principal.getAadIssuedBearerToken();
}

private List<String> getProjectGroups(CatalogRequestParams catalogRequestParams) {
if (catalogRequestParams.getAccessToken() == null) {
return Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package org.opendevstack.component_catalog.server.facade;

import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.UserPrincipal;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.opendevstack.component_catalog.server.mappers.ProjectComponentsInfoMapper;
import org.opendevstack.component_catalog.server.model.ProjectComponentInfo;
import org.opendevstack.component_catalog.server.services.ProjectsInfoService;
import org.opendevstack.component_catalog.server.services.ProvisionerActionsService;
import org.opendevstack.component_catalog.server.services.catalog.InvalidCatalogItemEntityException;
import org.opendevstack.component_catalog.server.services.exceptions.InvalidIdException;
import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Collections;
Expand All @@ -27,16 +24,7 @@ public class ProjectComponentsFacade {
private final ProvisionerActionsService provisionerActionsService;
private final ProjectComponentsInfoMapper projectComponentsInfoMapper;
private final ProjectsInfoService projectsInfoService;

public String getIdToken() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

log.debug("Authenticated user '{}'", auth.getName());

var principal = (UserPrincipal) auth.getPrincipal();

return principal.getAadIssuedBearerToken();
}
private final AuthenticationFacade authenticationFacade;

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

String idToken = getIdToken();
List<String> userGroups = projectsInfoService.getProjectGroups(idToken,accessToken);
String idToken = authenticationFacade.getIdToken();
List<String> userGroups = projectsInfoService.getProjectGroups(idToken, accessToken);

return projectComponents.getComponents()
.values()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.opendevstack.component_catalog.server.facade;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.jspecify.annotations.NonNull;
import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration;
import org.opendevstack.component_catalog.server.controllers.exceptions.ForbiddenException;
import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest;
import org.opendevstack.component_catalog.server.services.ProjectsInfoService;
import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction;
import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions;
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions;
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator;
import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@AllArgsConstructor
@Slf4j
public class ProvisionerActionsApiFacade {
private final ProjectsInfoService projectsInfoService;
private final GroupsRestrictionsEvaluator groupsRestrictionsEvaluator;
private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps;
private final AuthenticationFacade authenticationFacade;

public static @NonNull List<Pair<@NotNull String, @NotNull List<String>>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
return provisioningStatusUpdateRequest.getParameters().stream()
.map(parameter -> Pair.of(parameter.getName(), parameter.getValues()))
.toList();
}

public void validateGroupRestrictions(String projectKey, ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) {
var groupRestriction = CatalogItemUserActionGroupsRestriction.builder()
.prefix(groupsRestrictionProps.getPrefix())
.suffix(groupsRestrictionProps.getSuffix())
.build();

var userActionEntityRestrictions = UserActionEntityRestrictions.builder()
.groups(groupRestriction)
.build();

var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions);
var userGroups = projectsInfoService.getProjectGroups(authenticationFacade.getIdToken(), provisioningStatusUpdateRequest.getAccessToken());

var params = RestrictionsParams.builder()
.userGroups(userGroups)
.projectKey(projectKey)
.build();

if (Boolean.FALSE.equals(groupsRestrictionsEvaluator.evaluate(evaluationRestrictions, params).getLeft())) {
log.error("The user has no permissions to perform this action based on group restrictions for project {}", projectKey);
throw new ForbiddenException("User not allowed to perform this action");
}
}

}
Loading
Loading