From 60fbb8cc2de73df49367fd540ee156df074fb9c9 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 7 May 2026 15:40:35 +0200 Subject: [PATCH 1/3] [auth-component-registration] - Skip validation if app token with valid oids. --- .../facade/ProvisionerActionsApiFacade.java | 34 ++- .../resources/application-local.env.template | 3 + src/main/resources/application.yml | 7 +- .../ProvisionerActionsApiFacadeTest.java | 212 +++++++++++++----- src/test/resources/application-testing.env | 4 +- 5 files changed, 198 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java index ba6e84a..5487a12 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -1,7 +1,6 @@ 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; @@ -14,12 +13,13 @@ 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.opendevstack.component_catalog.util.JwtUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.List; @Component -@AllArgsConstructor @Slf4j public class ProvisionerActionsApiFacade { private final ProjectsInfoService projectsInfoService; @@ -27,6 +27,21 @@ public class ProvisionerActionsApiFacade { private final ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; private final AuthenticationFacade authenticationFacade; + private final List permittedOids; + + public ProvisionerActionsApiFacade(ProjectsInfoService projectsInfoService, + GroupsRestrictionsEvaluator groupsRestrictionsEvaluator, + ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps, + AuthenticationFacade authenticationFacade, + @Value("${devstack.marketplace-api.permitted-oids}") List permittedOids) { + this.projectsInfoService = projectsInfoService; + this.groupsRestrictionsEvaluator = groupsRestrictionsEvaluator; + this.groupsRestrictionProps = groupsRestrictionProps; + this.authenticationFacade = authenticationFacade; + this.permittedOids = permittedOids; + } + + public static @NonNull List>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { return provisioningStatusUpdateRequest.getParameters().stream() .map(parameter -> Pair.of(parameter.getName(), parameter.getValues())) @@ -34,6 +49,20 @@ public class ProvisionerActionsApiFacade { } public void validateGroupRestrictions(String projectKey) { + var accessToken = authenticationFacade.getAccessToken(); + + var oid = JwtUtils.extractClaim(accessToken, "oid"); + + boolean isAValidApplicationToken = oid.map(permittedOids::contains).orElse(false); + + if (isAValidApplicationToken) { + log.debug("Token with oid '{}' is allowed to bypass group restrictions for project {}", oid.orElse("unknown"), projectKey); + } else { + validateGroupRestrictions(projectKey, accessToken); + } + } + + private void validateGroupRestrictions(String projectKey, String accessToken) { var groupRestriction = CatalogItemUserActionGroupsRestriction.builder() .prefix(groupsRestrictionProps.getPrefix()) .suffix(groupsRestrictionProps.getSuffix()) @@ -44,7 +73,6 @@ public void validateGroupRestrictions(String projectKey) { .build(); var evaluationRestrictions = new EvaluationRestrictions(projectKey, userActionEntityRestrictions); - var accessToken = authenticationFacade.getAccessToken(); var userGroups = projectsInfoService.getProjectGroups(accessToken); var params = RestrictionsParams.builder() diff --git a/src/main/resources/application-local.env.template b/src/main/resources/application-local.env.template index bf4e638..c3ee1e7 100644 --- a/src/main/resources/application-local.env.template +++ b/src/main/resources/application-local.env.template @@ -27,3 +27,6 @@ PROVISIONER_BITBUCKET_REPOSITORY_SLUG=project-components PROVISIONER_BITBUCKET_SUB_PATH=projects/${project-key}.json PROVISIONER_BITBUCKET_SUB_PATH_TOKEN=${project-key} PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master + +DEVSTACK_MARKETPLACE-API_SCOPE: api://6d81fd84-9b38-4aed-9403-ed95f6395cd7/.default +DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="104e7cab-3fbd-470b-ac4f-194118288e4a" \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e94652e..0dc0a8b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -69,4 +69,9 @@ provisioner: repository-slug: ${PROVISIONER_BITBUCKET_REPOSITORY_SLUG} sub-path: ${PROVISIONER_BITBUCKET_SUB_PATH} sub-path-token: ${PROVISIONER_BITBUCKET_SUB_PATH_TOKEN} - branch-name: ${PROVISIONER_BITBUCKET_BRANCH_NAME} \ No newline at end of file + branch-name: ${PROVISIONER_BITBUCKET_BRANCH_NAME} + + +devstack: + marketplace-api: + permitted-oids: ${DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS} \ No newline at end of file diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java index 1e79c7e..b28467e 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -1,9 +1,9 @@ package org.opendevstack.component_catalog.server.facade; import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.config.ApplicationPropertiesConfiguration; @@ -14,12 +14,17 @@ 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.opendevstack.component_catalog.util.JwtUtils; import java.util.List; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -33,9 +38,17 @@ class ProvisionerActionsApiFacadeTest { private ApplicationPropertiesConfiguration.CatalogItemUserActionGroupsRestrictionProps groupsRestrictionProps; @Mock private AuthenticationFacade authenticationFacade; - @InjectMocks + private ProvisionerActionsApiFacade provisionerActionsApiFacade; + @BeforeEach + void setUp() { + var permittedOids = List.of("oid1", "oid2", "oid3"); + + provisionerActionsApiFacade = new ProvisionerActionsApiFacade(projectsInfoService, + groupsRestrictionsEvaluator, groupsRestrictionProps, authenticationFacade, permittedOids); + } + @Test void map_convertsParametersToPairs() { // given @@ -72,59 +85,144 @@ void map_withEmptyParameters_returnsEmptyList() { assertThat(result).isEmpty(); } - @Test - void validateGroupRestrictions_whenUserHasPermissions_doesNotThrow() { - // given - var projectKey = "PROJECT"; - var accessToken = "accessToken"; - var userGroups = List.of("group1"); - - when(authenticationFacade.getAccessToken()).thenReturn(accessToken); - when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); - when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); - when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(true, "Allowed")); - - // when / then - provisionerActionsApiFacade.validateGroupRestrictions(projectKey); - } - - @Test - void validateGroupRestrictions_whenUserHasNoPermissions_throwsForbiddenException() { - // given - var projectKey = "PROJECT"; - var accessToken = "accessToken"; - var userGroups = List.of("group1"); - - when(authenticationFacade.getAccessToken()).thenReturn(accessToken); - when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); - when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); - when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(false, "Forbidden")); - - // when / then - assertThatThrownBy(() -> provisionerActionsApiFacade.validateGroupRestrictions(projectKey)) - .isInstanceOf(ForbiddenException.class) - .hasMessage("User not allowed to perform this action"); - } - - @Test - void validateGroupRestrictions_whenEvaluatorReturnsNull_doesNotThrow() { - // given - var projectKey = "PROJECT"; - var accessToken = "accessToken"; - var userGroups = List.of("group1"); - - when(authenticationFacade.getAccessToken()).thenReturn(accessToken); - when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); - when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); - when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); - when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) - .thenReturn(Pair.of(null, "Unknown")); - - // when / then - provisionerActionsApiFacade.validateGroupRestrictions(projectKey); - } + @Test + void validateGroupRestrictions_whenUserHasPermissions_doesNotThrow() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var userGroups = List.of("group1"); + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "Allowed")); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.empty()); + + // when / then + provisionerActionsApiFacade.validateGroupRestrictions(projectKey); + } + } + + @Test + void validateGroupRestrictions_whenUserHasNoPermissions_throwsForbiddenException() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var userGroups = List.of("group1"); + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(false, "Forbidden")); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.empty()); + + // when / then + assertThatThrownBy(() -> provisionerActionsApiFacade.validateGroupRestrictions(projectKey)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("User not allowed to perform this action"); + } + } + + @Test + void validateGroupRestrictions_whenEvaluatorReturnsNull_doesNotThrow() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var userGroups = List.of("group1"); + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(null, "Unknown")); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.empty()); + + // when / then + provisionerActionsApiFacade.validateGroupRestrictions(projectKey); + } + } + + @Test + void validateGroupRestrictions_whenPermittedOidsContainsExtractedOid_bypassesGroupRestrictions() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var permittedOid = "oid1"; + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.of(permittedOid)); + + // when + provisionerActionsApiFacade.validateGroupRestrictions(projectKey); + + // then - Verify that the group restrictions evaluator is never called when oid is in permittedOids + verify(groupsRestrictionsEvaluator, never()) + .evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class)); + verify(projectsInfoService, never()).getProjectGroups(accessToken); + } + } + + @Test + void validateGroupRestrictions_whenPermittedOidsContainsExtractedOid_doesNotThrow() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var permittedOid = "oid2"; + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.of(permittedOid)); + + // when / then - should not throw any exception + provisionerActionsApiFacade.validateGroupRestrictions(projectKey); + } + } + + @Test + void validateGroupRestrictions_whenTokenHasNoOid_callsGroupRestrictionsEvaluation() { + // given + var projectKey = "PROJECT"; + var accessToken = "accessToken"; + var userGroups = List.of("group1"); + + when(authenticationFacade.getAccessToken()).thenReturn(accessToken); + when(groupsRestrictionProps.getPrefix()).thenReturn(List.of("prefix-")); + when(groupsRestrictionProps.getSuffix()).thenReturn(List.of("-suffix")); + when(projectsInfoService.getProjectGroups(accessToken)).thenReturn(userGroups); + when(groupsRestrictionsEvaluator.evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class))) + .thenReturn(Pair.of(true, "Allowed")); + + try (var jwtUtilsMocked = mockStatic(JwtUtils.class)) { + jwtUtilsMocked.when(() -> JwtUtils.extractClaim(accessToken, "oid")) + .thenReturn(Optional.empty()); + + // when + provisionerActionsApiFacade.validateGroupRestrictions(projectKey); + + // then - Verify that the group restrictions evaluator is called when oid is not in permittedOids + verify(groupsRestrictionsEvaluator) + .evaluate(any(EvaluationRestrictions.class), any(RestrictionsParams.class)); + verify(projectsInfoService).getProjectGroups(accessToken); + } + } } + diff --git a/src/test/resources/application-testing.env b/src/test/resources/application-testing.env index 8e5b4f6..5524ae5 100644 --- a/src/test/resources/application-testing.env +++ b/src/test/resources/application-testing.env @@ -19,4 +19,6 @@ PROVISIONER_BITBUCKET_PROJECT_KEY=CATALOGS PROVISIONER_BITBUCKET_REPOSITORY_SLUG=project-components PROVISIONER_BITBUCKET_SUB_PATH=projects/.json PROVISIONER_BITBUCKET_SUB_PATH_TOKEN= -PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master \ No newline at end of file +PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master + +DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="104e7cab-3fbd-470b-ac4f-194118288e4a" \ No newline at end of file From 7c14a8763c1af780702930715892ea57c7dd6a76 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 7 May 2026 16:08:48 +0200 Subject: [PATCH 2/3] [auth-component-registration] - clean configuration. --- src/main/resources/application-local.env.template | 1 - src/test/resources/application-testing.env | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/resources/application-local.env.template b/src/main/resources/application-local.env.template index c3ee1e7..f7b8658 100644 --- a/src/main/resources/application-local.env.template +++ b/src/main/resources/application-local.env.template @@ -28,5 +28,4 @@ PROVISIONER_BITBUCKET_SUB_PATH=projects/${project-key}.json PROVISIONER_BITBUCKET_SUB_PATH_TOKEN=${project-key} PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master -DEVSTACK_MARKETPLACE-API_SCOPE: api://6d81fd84-9b38-4aed-9403-ed95f6395cd7/.default DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="104e7cab-3fbd-470b-ac4f-194118288e4a" \ No newline at end of file diff --git a/src/test/resources/application-testing.env b/src/test/resources/application-testing.env index 5524ae5..72fe993 100644 --- a/src/test/resources/application-testing.env +++ b/src/test/resources/application-testing.env @@ -21,4 +21,4 @@ PROVISIONER_BITBUCKET_SUB_PATH=projects/.json PROVISIONER_BITBUCKET_SUB_PATH_TOKEN= PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master -DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="104e7cab-3fbd-470b-ac4f-194118288e4a" \ No newline at end of file +DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="any-valid-odis-for-token" \ No newline at end of file From 780e52571f842ae89a154cf29664e57e39944ff4 Mon Sep 17 00:00:00 2001 From: soriaoli Date: Thu, 7 May 2026 16:10:48 +0200 Subject: [PATCH 3/3] [auth-component-registration] - clean configuration. --- src/main/resources/application-local.env.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-local.env.template b/src/main/resources/application-local.env.template index f7b8658..6636624 100644 --- a/src/main/resources/application-local.env.template +++ b/src/main/resources/application-local.env.template @@ -28,4 +28,4 @@ PROVISIONER_BITBUCKET_SUB_PATH=projects/${project-key}.json PROVISIONER_BITBUCKET_SUB_PATH_TOKEN=${project-key} PROVISIONER_BITBUCKET_BRANCH_NAME=refs/heads/master -DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="104e7cab-3fbd-470b-ac4f-194118288e4a" \ No newline at end of file +DEVSTACK_MARKETPLACE-API_PERMITTED_OIDS="any-valid-odis-for-token" \ No newline at end of file