diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java index 0779c3ea60..96ef478599 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogController.java @@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalThrowable; import org.zowe.apiml.apicatalog.model.APIContainer; +import org.zowe.apiml.apicatalog.security.OidcUtils; import org.zowe.apiml.apicatalog.services.cached.CachedApiDocService; import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; import org.zowe.apiml.message.log.ApimlLogger; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.StreamSupport; /** @@ -51,6 +53,8 @@ public class ApiCatalogController { @InjectApimlLogger private final ApimlLogger apimlLog = ApimlLogger.empty(); + private AtomicReference> oidcProviderCache = new AtomicReference<>(); + /** * Create the controller and autowire in the repository services * @@ -64,6 +68,15 @@ public ApiCatalogController(CachedProductFamilyService cachedProductFamilyServic this.cachedApiDocService = cachedApiDocService; } + @GetMapping(value = "/oidc/provider", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getOidcProvider() { + if (oidcProviderCache.get() == null) { + oidcProviderCache.set(OidcUtils.getOidcProvider()); + } + + return new ResponseEntity<>(oidcProviderCache.get(), oidcProviderCache.get().isEmpty() ? HttpStatus.NO_CONTENT : HttpStatus.OK); + } + /** * Get all containers diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java new file mode 100644 index 0000000000..3c34fbf496 --- /dev/null +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/OidcUtils.java @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.apicatalog.security; + +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +@UtilityClass +public class OidcUtils { + + private String PREFIX = "ZWE_components_gateway_spring_security_oauth2_client_"; + + public List getOidcProvider() { + return System.getenv().keySet().stream() + .filter(key -> StringUtils.startsWith(key, PREFIX)) + .map(key -> key.substring(PREFIX.length())) + .map(key -> key.split("_")) + .filter(parts -> parts.length > 2) + .map(parts -> parts[1]) + .distinct() + .sorted() + .toList(); + } + +} diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java index 581bad4fb9..08b20773f3 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java @@ -46,6 +46,7 @@ import org.zowe.apiml.security.common.content.BasicContentFilter; import org.zowe.apiml.security.common.content.BearerContentFilter; import org.zowe.apiml.security.common.content.CookieContentFilter; +import org.zowe.apiml.security.common.content.OidcContentFilter; import org.zowe.apiml.security.common.filter.CategorizeCertsFilter; import org.zowe.apiml.security.common.login.LoginFilter; import org.zowe.apiml.security.common.login.ShouldBeAlreadyAuthenticatedFilter; @@ -151,7 +152,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/favicon.ico", "/v3/api-docs", "/index.html", - "/application/info" + "/application/info", + "/oidc/provider" }; return web -> web.ignoring().requestMatchers(noSecurityAntMatchers); } @@ -232,7 +234,8 @@ public void configure(HttpSecurity http) throws Exception { .addFilterBefore(loginFilter(authConfigurationProperties.getServiceLoginEndpoint(), authenticationManager), ShouldBeAlreadyAuthenticatedFilter.class) .addFilterBefore(basicFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(cookieFilter(authenticationManager), BasicContentFilter.class) - .addFilterAfter(bearerContentFilter(authenticationManager), CookieContentFilter.class); + .addFilterAfter(bearerContentFilter(authenticationManager), CookieContentFilter.class) + .addFilterAfter(oidcFilter(authenticationManager), BearerContentFilter.class); } private LoginFilter loginFilter(String loginEndpoint, AuthenticationManager authenticationManager) { @@ -278,6 +281,18 @@ private BearerContentFilter bearerContentFilter(AuthenticationManager authentica handlerInitializer.getResourceAccessExceptionHandler() ); } + + /** + * Secures content with a OIDC token + */ + private OidcContentFilter oidcFilter(AuthenticationManager authenticationManager) { + return new OidcContentFilter( + authenticationManager, + handlerInitializer.getAuthenticationFailureHandler(), + handlerInitializer.getResourceAccessExceptionHandler() + ); + } + } @Bean diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index 870fcfbda6..c9674a62e2 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -172,6 +172,7 @@ eureka: authentication: sso: true + scheme: zoweJwt client: healthcheck: enabled: true diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java index d0499d0b70..0784640e21 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/controllers/api/ApiCatalogControllerTests.java @@ -13,10 +13,7 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import io.restassured.module.mockmvc.RestAssuredMockMvc; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.zowe.apiml.apicatalog.exceptions.ContainerStatusRetrievalThrowable; @@ -26,15 +23,15 @@ import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; +import java.lang.reflect.Field; import java.util.*; +import static io.restassured.module.mockmvc.RestAssuredMockMvc.standaloneSetup; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static io.restassured.module.mockmvc.RestAssuredMockMvc.standaloneSetup; class ApiCatalogControllerTests { private final String pathToContainers = "/containers"; @@ -142,9 +139,9 @@ void thenPopulateApiDocForServices() throws ContainerStatusRetrievalThrowable { containers.getBody().forEach(apiContainer -> apiContainer.getServices().forEach(apiService -> { - Assertions.assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - Assertions.assertEquals(apiVersions, apiService.getApiVersions()); - Assertions.assertEquals(defaultApiVersion, apiService.getDefaultApiVersion()); + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiVersions, apiService.getApiVersions()); + assertEquals(defaultApiVersion, apiService.getDefaultApiVersion()); })); } @@ -158,8 +155,8 @@ void thenPopulateApiDocForServicesExceptOneWhichFails() throws ContainerStatusRe containers.getBody().forEach(apiContainer -> apiContainer.getServices().forEach(apiService -> { if (apiService.getServiceId().equals("service1")) { - Assertions.assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - Assertions.assertEquals(apiService.getApiVersions(), apiVersions); + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiService.getApiVersions(), apiVersions); } if (apiService.getServiceId().equals("service2")) { Assertions.assertNull(apiService.getApiDoc()); @@ -177,11 +174,11 @@ void thenPopulateApiVersionsForServicesExceptOneWhichFails() throws ContainerSta containers.getBody().forEach(apiContainer -> apiContainer.getServices().forEach(apiService -> { if (apiService.getServiceId().equals("service1")) { - Assertions.assertEquals(apiService.getServiceId(), apiService.getApiDoc()); - Assertions.assertEquals(apiService.getApiVersions(), apiVersions); + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiService.getApiVersions(), apiVersions); } if (apiService.getServiceId().equals("service2")) { - Assertions.assertEquals(apiService.getServiceId(), apiService.getApiDoc()); + assertEquals(apiService.getServiceId(), apiService.getApiDoc()); Assertions.assertNull(apiService.getApiVersions()); } })); @@ -238,4 +235,52 @@ private InstanceInfo getStandardInstance(String serviceId, InstanceInfo.Instance null, null, null, null, null, null, null, 0, null, "hostname", status, null, null, null, null, null, null, null, null, null); } + + @Nested + class OidcProviders { + + private String[] env = { + "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_authorizationUri", + "ZWE_components_gateway_spring_security_oauth2_client_registration_oidc2_clientId", + "ZWE_components_gateway_spring_security_oauth2_client_provider_oidc1_tokenUri" + }; + + Map getEnvMap() { + try { + Class envVarClass = System.getenv().getClass(); + Field mField = envVarClass.getDeclaredField("m"); + mField.setAccessible(true); + return (Map) mField.get(System.getenv()); + } catch (NoSuchFieldException | IllegalAccessException e) { + fail(e); + return null; + } + } + + @AfterEach + void tearDown() { + Arrays.stream(env).forEach(k -> getEnvMap().remove(k)); + } + + @Test + void givenSystemEnv_whenInvokeOidcProviders_thenReturnTheList() { + Arrays.stream(env).forEach(k -> getEnvMap().put(k, "anyValue")); + List oidcProviders = RestAssuredMockMvc.given() + .when().get("/oidc/provider") + .getBody().jsonPath().getList("."); + assertEquals(2, oidcProviders.size()); + assertTrue(oidcProviders.contains("oidc1")); + assertTrue(oidcProviders.contains("oidc2")); + } + + @Test + void givenNoSystemEnv_whenInvokeOidcProviders_thenReturnAnEmptyList() { + List oidcProviders = RestAssuredMockMvc.given() + .when().get("/oidc/provider") + .getBody().jsonPath().getList("."); + assertEquals(0, oidcProviders.size()); + } + + } + } diff --git a/api-catalog-ui/frontend/src/components/Login/Login.jsx b/api-catalog-ui/frontend/src/components/Login/Login.jsx index 8d1df9d59e..fd545781b0 100644 --- a/api-catalog-ui/frontend/src/components/Login/Login.jsx +++ b/api-catalog-ui/frontend/src/components/Login/Login.jsx @@ -31,6 +31,7 @@ import WarningIcon from '@material-ui/icons/Warning'; import Spinner from '../Spinner/Spinner'; import './Login.css'; import Footer from '../Footer/Footer'; +import getBaseUrl from "../../helpers/urls"; function Login(props) { const [username, setUsername] = useState(''); @@ -42,6 +43,9 @@ function Login(props) { const [warning, setWarning] = useState(false); const [submitted, setSubmitted] = useState(false); + const [oidcProvidersLoadingStarted, setOidcProviderLoadingStarted] = useState(false); + const [oidcProviders, setOidcProviders] = useState([]); + const { returnToLogin, login, authentication, isFetching, validateInput } = props; const enterNewPassMsg = 'Enter a new password for account'; const invalidPassMsg = 'The specified username or password is invalid.'; @@ -122,6 +126,21 @@ function Login(props) { setSubmitted(true); } + function loadOidcProviders() { + if (!oidcProvidersLoadingStarted) { + setOidcProviderLoadingStarted(true); + fetch(`${getBaseUrl()}/oidc/provider`).then(resp => + resp.json().then(json => { + console.log('json') + setOidcProviders(json) + return json + }) + ).catch(() => setOidcProviderLoadingStarted(false)) + } + } + + loadOidcProviders(); + let errorData = { messageText: null, expired: false, invalidNewPassword: true, invalidCredentials: false }; if ( authentication !== undefined && @@ -299,6 +318,17 @@ function Login(props) { )}
+ { + oidcProviders.map(oidcProvider => + ) + } +