diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 2507e5c520..7989691eb3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -360,6 +360,72 @@ jobs: - uses: ./.github/actions/teardown + CITestsZaas: + needs: PublishJibContainers + runs-on: ubuntu-latest + container: ubuntu:latest + timeout-minutes: 15 + + services: + caching-service: + image: ghcr.io/balhar-jakub/caching-service:${{ github.run_id }}-${{ github.run_number }} + env: + ZWE_CACHING_SERVICE_PERSISTENT: 'infinispan' + CACHING_STORAGE_MODE: "infinispan" + JGROUPS_BIND_ADDRESS: "caching-service" + JGROUPS_BIND_PORT: "7099" + cloud-gateway-service: + image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }} + discovery-service: + image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }} + gateway-service: + image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SECURITY_PERSONALACCESSTOKEN_ENABLED: true + APIML_SECURITY_X509_ENABLED: true + APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true + APIML_SECURITY_X509_CERTIFICATESURL: https://cloud-gateway-service:10023/gateway/certificates + APIML_SECURITY_OIDC_CLIENTID: ${{ secrets.OKTA_CLIENT_ID }} + APIML_SECURITY_OIDC_CLIENTSECRET: ${{ secrets.OKTA_CLIENT_PASSWORD }} + APIML_SECURITY_OIDC_ENABLED: true + APIML_SECURITY_OIDC_REGISTRY: zowe.okta.com + APIML_SECURITY_OIDC_JWKS_URI: ${{ secrets.OKTA_JWK_URI }} + APIML_SECURITY_OIDC_IDENTITYMAPPERUSER: APIMTST + APIML_SECURITY_OIDC_IDENTITYMAPPERURL: https://gateway-service:10010/zss/api/v1/certificate/dn + mock-services: + image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }} + env: + ZOSMF_APPLIEDAPARS: PH34201 + + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - uses: ./.github/actions/setup + + - name: Build with Gradle + run: > + ./gradlew :integration-tests:runZaasTest --info -Denvironment.config=-docker -Denvironment.offPlatform=true + -Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }} + -Dokta.client.id=${{ secrets.OKTA_CLIENT_ID }} -Doidc.test.user=${{ secrets.OIDC_TEST_USER }} + -Doidc.test.pass=${{ secrets.OIDC_TEST_PASS }} + + - name: Dump DC jacoco data + run: > + java -jar ./scripts/jacococli.jar dump --address gateway-service --port 6300 --destfile ./results/gateway-service.exec + + - name: Store results + uses: actions/upload-artifact@v3 + if: always() + with: + name: ContainerCITestsZaas-${{ env.JOB_ID }} + path: | + integration-tests/build/reports/** + results/** + + - uses: ./.github/actions/teardown + CITestsZosmfRsu2012: needs: PublishJibContainers runs-on: ubuntu-latest @@ -1454,7 +1520,7 @@ jobs: - name: Code coverage and publish results run: > - ./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results,cloudgatewayproxy/results,citestswebsocketchaoticha/results,cloudgatewayservicerouting/results" + ./gradlew --info coverage sonar -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results,cloudgatewayproxy/results,citestswebsocketchaoticha/results,cloudgatewayservicerouting/results,containercitestszaas/results" -Psonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD env: diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthenticationTokenException.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthenticationTokenException.java index aac57ad48e..eeecf2e2be 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthenticationTokenException.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/error/AuthenticationTokenException.java @@ -13,7 +13,13 @@ import org.springframework.security.core.AuthenticationException; public class AuthenticationTokenException extends AuthenticationException { + public AuthenticationTokenException(String msg) { super(msg); } + + public AuthenticationTokenException(String msg, Throwable cause) { + super(msg, cause); + } + } diff --git a/build.gradle b/build.gradle index 4ee260d158..db3b77ea74 100644 --- a/build.gradle +++ b/build.gradle @@ -243,6 +243,10 @@ task runZosmfAuthTest(dependsOn: ":integration-tests:runZosmfAuthTest") { description "Run zOSMF dependant authentication tests only" group "Integration tests" } +task runZaasTest(dependsOn: ":integration-tests:runZaasTest") { + description "Run Zaas dependant authentication tests only" + group "Integration tests" +} task runX509AuthTest(dependsOn: ":integration-tests:runX509AuthTest") { description "Run x509 dependant authentication tests only" group "Integration tests" diff --git a/common-service-core/src/main/java/org/zowe/apiml/zaas/zosmf/ZosmfResponse.java b/common-service-core/src/main/java/org/zowe/apiml/zaas/zosmf/ZosmfResponse.java new file mode 100644 index 0000000000..b3d6777f35 --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/zaas/zosmf/ZosmfResponse.java @@ -0,0 +1,25 @@ +/* + * 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.zaas.zosmf; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ZosmfResponse { + + String cookieName; + String token; + +} diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index 24ff073238..08effa38ec 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -12,14 +12,14 @@ apiml: webfinger: fileLocation: config/local/webfinger.yml personalAccessToken: - enabled: false + enabled: true oidc: enabled: false clientId: clientSecret: - registry: - identityMapperUrl: - identityMapperUser: + registry: zowe.okta.com + identityMapperUrl: https://localhost:10010/zss/api/v1/certificate/dn + identityMapperUser: APIMTST jwks: uri: auth: @@ -35,7 +35,7 @@ apiml: verifySslCertificatesOfServices: true x509: enabled: true - acceptForwardedCert: false + acceptForwardedCert: true certificatesUrl: https://localhost:10023/gateway/certificates banner: console diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilter.java index bfee04817c..a74a0ac5ce 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilter.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilter.java @@ -28,7 +28,8 @@ @RequiredArgsConstructor @Slf4j public class ExtractAuthSourceFilter extends OncePerRequestFilter { - static final String AUTH_SOURCE_ATTR = "zaas.auth.source"; + public static final String AUTH_SOURCE_ATTR = "zaas.auth.source"; + public static final String AUTH_SOURCE_PARSED_ATTR = "zaas.auth.source.parsed"; private final AuthSourceService authSourceService; private final AuthExceptionHandler authExceptionHandler; @@ -39,7 +40,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse Optional authSource = authSourceService.getAuthSourceFromRequest(request); if (authSource.isPresent()) { AuthSource.Parsed parsed = authSourceService.parse(authSource.get()); - request.setAttribute(AUTH_SOURCE_ATTR, parsed); + request.setAttribute(AUTH_SOURCE_ATTR, authSource.get()); + request.setAttribute(AUTH_SOURCE_PARSED_ATTR, parsed); filterChain.doFilter(request, response); } else { throw new InsufficientAuthenticationException("No authentication source found in the request."); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/NewSecurityConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/NewSecurityConfiguration.java index 49e03deaae..44293ed90d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/NewSecurityConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/NewSecurityConfiguration.java @@ -54,6 +54,7 @@ import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; import org.zowe.apiml.gateway.security.ticket.SuccessfulTicketHandler; import org.zowe.apiml.gateway.services.ServicesInfoController; +import org.zowe.apiml.gateway.zaas.ZaasAuthenticationFilter; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.config.CertificateAuthenticationProvider; import org.zowe.apiml.security.common.config.HandlerInitializer; @@ -309,24 +310,25 @@ private X509AuthenticationFilter x509AuthenticationFilter() { @Configuration @RequiredArgsConstructor @Order(9) - class ZaasTicketEndpoint { + class ZaasEndpoints { private final CompoundAuthProvider compoundAuthProvider; @Bean - public SecurityFilterChain authZaasTicketEndpointFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain authZaasEndpointsFilterChain(HttpSecurity http) throws Exception { baseConfigure(http.requestMatchers().antMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense authConfigurationProperties.getRevokeMultipleAccessTokens() + "/**", authConfigurationProperties.getEvictAccessTokensAndRules(), - "/gateway/zaas/ticket" + "/gateway/zaas/**" ).and()) .authorizeRequests() .anyRequest().authenticated() .and() .x509().userDetailsService(x509UserDetailsService()) .and() - .addFilterBefore(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) - .addFilterBefore(new ExtractAuthSourceFilter(authSourceService, authExceptionHandler), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class); + .addFilterAfter(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) + .addFilterAfter(new ExtractAuthSourceFilter(authSourceService, authExceptionHandler), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class) + .addFilterAfter(new ZaasAuthenticationFilter(authSourceService, authExceptionHandler), CategorizeCertsFilter.class); return http.build(); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java index 9328931429..47ca72ba34 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java @@ -66,7 +66,7 @@ @EnableAspectJAutoProxy(proxyTargetClass = true) public class AuthenticationService { - private static final String LTPA_CLAIM_NAME = "ltpa"; + public static final String LTPA_CLAIM_NAME = "ltpa"; private static final String DOMAIN_CLAIM_NAME = "dom"; private static final String AUTH_PROV_CLAIM = "auth.prov"; private static final String SCOPES = "scopes"; @@ -130,9 +130,18 @@ private String createJWT(String username, String issuer, Map cla .signWith(jwtSecurityInitializer.getJwtSecret(), jwtSecurityInitializer.getSignatureAlgorithm()).compact(); } + @SuppressWarnings("java:S5659") // It is checking the signature securely - https://github.com/zowe/api-layer/issues/3191 public QueryResponse parseJwtWithSignature(String jwt) throws SignatureException { - Jwt parsedJwt = Jwts.parserBuilder().setSigningKey(jwtSecurityInitializer.getJwtSecret()).build().parse(jwt); - return parseQueryResponse(parsedJwt.getBody()); + try { + Jwt parsedJwt = Jwts.parserBuilder() + .setSigningKey(jwtSecurityInitializer.getJwtSecret()) + .build() + .parse(jwt); + + return parseQueryResponse(parsedJwt.getBody()); + } catch (RuntimeException exception) { + throw handleJwtParserException(exception); + } } /** diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/TokenCreationService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/TokenCreationService.java index 2bc873c9c0..3bad98eae0 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/TokenCreationService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/TokenCreationService.java @@ -18,11 +18,14 @@ import org.springframework.stereotype.Service; import org.zowe.apiml.gateway.security.login.Providers; import org.zowe.apiml.gateway.security.login.zosmf.ZosmfAuthenticationProvider; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.security.common.error.AuthenticationTokenException; import org.zowe.apiml.security.common.token.TokenAuthentication; +import java.util.Collections; +import java.util.Map; import java.util.Optional; @RequiredArgsConstructor @@ -31,6 +34,7 @@ public class TokenCreationService { private final Providers providers; private final Optional zosmfAuthenticationProvider; + private final ZosmfService zosmfService; private final PassTicketService passTicketService; private final AuthenticationService authenticationService; @@ -45,26 +49,13 @@ public class TokenCreationService { * @return Valid JWT token or null */ public String createJwtTokenWithoutCredentials(String user) { - boolean isZosmfUsedAndAvailable = false; - try { - isZosmfUsedAndAvailable = providers.isZosfmUsed() && providers.isZosmfAvailable(); - } catch (AuthenticationServiceException ex) { - // Intentionally do nothing. The issue is logged deeper. - } - - if (isZosmfUsedAndAvailable) { - try { - log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket"); - log.debug("Generating PassTicket for user: {} and ZOSMF applid: {}", user, zosmfApplId); - String passTicket = passTicketService.generate(user, zosmfApplId); - log.debug("Generated passticket: {}", passTicket); - return ((TokenAuthentication) zosmfAuthenticationProvider - .orElseThrow(() -> new IllegalStateException("The z/OSMF is not configured. The config value `apiml.security.auth.provider` should be set to `zosmf`.")) - .authenticate(new UsernamePasswordAuthenticationToken(user, passTicket))) - .getCredentials(); - } catch (IRRPassTicketGenerationException e) { - throw new AuthenticationTokenException("Problem with generating PassTicket"); - } + if (isZosmfAvailable()) { + log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket"); + final String passTicket = generatePassTicket(user); + return ((TokenAuthentication) zosmfAuthenticationProvider + .orElseThrow(() -> new IllegalStateException("The z/OSMF is not configured. The config value `apiml.security.auth.provider` should be set to `zosmf`.")) + .authenticate(new UsernamePasswordAuthenticationToken(user, passTicket))) + .getCredentials(); } else { final String domain = "security-domain"; log.debug("ZOSMF is not available or used. Generating APIML's JWT Token."); @@ -73,4 +64,36 @@ public String createJwtTokenWithoutCredentials(String user) { return authenticationService.createTokenAuthentication(user, jwtTokenString).getCredentials(); } } + + public Map createZosmfTokensWithoutCredentials(String user) { + if (!isZosmfAvailable()) return Collections.emptyMap(); + + log.debug("ZOSMF is available and used. Attempt to authenticate with PassTicket"); + final String passTicket = generatePassTicket(user); + + return zosmfService.authenticate(new UsernamePasswordAuthenticationToken(user, passTicket)).getTokens(); + } + + private boolean isZosmfAvailable() { + try { + return providers.isZosfmUsed() && providers.isZosmfAvailable(); + } catch (AuthenticationServiceException ex) { + // Intentionally do nothing. The issue is logged deeper. + } + + return false; + } + + private String generatePassTicket(String user) { + try { + log.debug("Generating PassTicket for user: {} and ZOSMF applid: {}", user, zosmfApplId); + String passTicket = passTicketService.generate(user, zosmfApplId); + log.debug("Generated PassTicket: {}", passTicket); + + return passTicket; + } catch (IRRPassTicketGenerationException e) { + throw new AuthenticationTokenException("Generation of PassTicket failed", e); + } + } + } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSource.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSource.java index b51e050e60..0f0d297197 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSource.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSource.java @@ -13,16 +13,21 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; @RequiredArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class PATAuthSource implements AuthSource { + public static final AuthSource.AuthSourceType type = AuthSource.AuthSourceType.PAT; @EqualsAndHashCode.Include private final String source; + @Setter + private String defaultServiceId; + @Override public Object getRawSource() { return source; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceService.java index ed9e564533..ffa03189c0 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceService.java @@ -33,6 +33,8 @@ @Service public class PATAuthSourceService extends TokenAuthSourceService { + public static final String SERVICE_ID_HEADER = "X-Service-Id"; + @InjectApimlLogger protected final ApimlLogger logger = ApimlLogger.empty(); @@ -50,6 +52,19 @@ public Function getMapper() { return PATAuthSource::new; } + @Override + public Optional getAuthSourceFromRequest(HttpServletRequest request) { + Optional authSource = super.getAuthSourceFromRequest(request); + + if (authSource.isPresent()) { + PATAuthSource patAuthSource = (PATAuthSource) authSource.get(); + String defaultServiceId = request.getHeader(SERVICE_ID_HEADER); + patAuthSource.setDefaultServiceId(defaultServiceId); + } + + return authSource; + } + @Override public Optional getToken(HttpServletRequest request) { Optional tokenOptional = authenticationService.getJwtTokenFromRequest(request); @@ -72,6 +87,9 @@ public boolean isValid(AuthSource authSource) { String token = (String) authSource.getRawSource(); RequestContext context = RequestContext.getCurrentContext(); String serviceId = (String) context.get(SERVICE_ID_KEY); + if (serviceId == null) { + serviceId = ((PATAuthSource) authSource).getDefaultServiceId(); + } boolean validForScopes = tokenProvider.isValidForScopes(token, serviceId); logger.log(MessageType.DEBUG, "PAT is %s for scope: %s ", validForScopes ? "valid" : "not valid", serviceId); boolean invalidate = tokenProvider.isInvalidated(token); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java index d0890d35c3..551bd19c0e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfService.java @@ -28,12 +28,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.security.authentication.AuthenticationServiceException; @@ -44,14 +39,18 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.TokenCreationService; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.login.ChangePasswordRequest; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.zosmf.ZosmfResponse; import javax.annotation.PostConstruct; - +import javax.management.ServiceNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -127,6 +126,8 @@ public ZosmfService( final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, final ObjectMapper securityObjectMapper, final ApplicationContext applicationContext, + final AuthenticationService authenticationService, + final TokenCreationService tokenCreationService, List tokenValidationStrategy ) { super( @@ -137,9 +138,13 @@ public ZosmfService( ); this.applicationContext = applicationContext; this.tokenValidationStrategy = tokenValidationStrategy; + this.authenticationService = authenticationService; + this.tokenCreationService = tokenCreationService; } private ZosmfService meAsProxy; + private final AuthenticationService authenticationService; + private final TokenCreationService tokenCreationService; @PostConstruct public void afterPropertiesSet() { @@ -220,9 +225,33 @@ public String getZosmfRealm(String infoURIEndpoint) { } } + @SuppressWarnings("java:S128") // Break in ZOWE case is left intentionally + public ZosmfResponse exchangeAuthenticationForZosmfToken(String token, AuthSource.Parsed authSource) throws ServiceNotFoundException { + switch (authSource.getOrigin()) { + case ZOSMF: + return new ZosmfResponse(ZosmfService.TokenType.JWT.getCookieName(), token); + case ZOWE: + String ltpaToken = authenticationService.getLtpaToken(token); + if (ltpaToken != null) { + return new ZosmfResponse(ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); + } + default: + Map zosmfTokens = tokenCreationService.createZosmfTokensWithoutCredentials(authSource.getUserId()); + + if (zosmfTokens.containsKey(JWT)) { + return new ZosmfResponse(ZosmfService.TokenType.JWT.getCookieName(), zosmfTokens.get(JWT)); + } else if (zosmfTokens.containsKey(LTPA)) { + return new ZosmfResponse(ZosmfService.TokenType.LTPA.getCookieName(), zosmfTokens.get(LTPA)); + } + } + + throw new ServiceNotFoundException("Unable to obtain a token from z/OSMF service."); + } + + /** * Verify whether the service is actually accessible. - * + *

* Note: This method uses getURI, it's also verifying eureka registration * * @return true when it's possible to access the Info endpoint via GET. @@ -251,7 +280,7 @@ public boolean isAccessible() { if (info.getStatusCode() != HttpStatus.OK) { log.error("Unexpected status code {} from z/OSMF accessing URI {}\n" - + "Response from z/OSMF was \"{}\"", info.getStatusCodeValue(), infoURIEndpoint, String.valueOf(info.getBody())); + + "Response from z/OSMF was \"{}\"", info.getStatusCodeValue(), infoURIEndpoint, info.getBody()); } return info.getStatusCode() == HttpStatus.OK; @@ -275,7 +304,7 @@ private String getURI(String serviceId, String path) { /** * POST to provided url and return authentication response * - * @param authentication + * @param authentication with credentials * @param url String containing auth endpoint to be used * @return AuthenticationResponse containing auth token, either LTPA or JWT */ @@ -298,7 +327,7 @@ protected AuthenticationResponse issueAuthenticationRequest(Authentication authe /** * PUT to provided url and return authentication response * - * @param authentication + * @param authentication with credentials * @param url String containing change password endpoint to be used * @return ResponseEntity */ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilter.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilter.java new file mode 100644 index 0000000000..6e72bf86f3 --- /dev/null +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilter.java @@ -0,0 +1,49 @@ +/* + * 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.gateway.zaas; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.web.filter.OncePerRequestFilter; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; +import org.zowe.apiml.security.common.error.AuthExceptionHandler; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_ATTR; + +@RequiredArgsConstructor +public class ZaasAuthenticationFilter extends OncePerRequestFilter { + + private final AuthSourceService authSourceService; + private final AuthExceptionHandler authExceptionHandler; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + Optional authSource = Optional.ofNullable((AuthSource) request.getAttribute(AUTH_SOURCE_ATTR)); + if (!authSource.isPresent() || !authSourceService.isValid(authSource.get())) { + throw new InsufficientAuthenticationException("Authentication failed."); + } + filterChain.doFilter(request, response); + } catch (RuntimeException e) { + authExceptionHandler.handleException(request, response, e); + } + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/ZaasController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java similarity index 59% rename from gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/ZaasController.java rename to gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java index e0560d629e..92315ff0af 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/ZaasController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/zaas/ZaasController.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.gateway.controllers; +package org.zowe.apiml.gateway.zaas; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -19,12 +19,17 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.message.api.ApiMessageView; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.ticket.TicketRequest; import org.zowe.apiml.ticket.TicketResponse; +import org.zowe.apiml.zaas.zosmf.ZosmfResponse; + +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_ATTR; +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_PARSED_ATTR; @RequiredArgsConstructor @RestController @@ -33,15 +38,16 @@ public class ZaasController { public static final String CONTROLLER_PATH = "gateway/zaas"; - private final PassTicketService passTicketService; private final MessageService messageService; + private final PassTicketService passTicketService; + private final ZosmfService zosmfService; @PostMapping(path = "ticket", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Provides PassTicket for authenticated user.") @ResponseBody - public ResponseEntity getPassTicket(@RequestBody TicketRequest ticketRequest, @RequestAttribute("zaas.auth.source") AuthSource.Parsed authSource) { + public ResponseEntity getPassTicket(@RequestBody TicketRequest ticketRequest, @RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed) { - if (StringUtils.isEmpty(authSource.getUserId())) { + if (StringUtils.isEmpty(authSourceParsed.getUserId())) { return ResponseEntity .status(HttpStatus.UNAUTHORIZED) .build(); @@ -57,7 +63,7 @@ public ResponseEntity getPassTicket(@RequestBody TicketRequest ticketReq String ticket = null; try { - ticket = passTicketService.generate(authSource.getUserId(), applicationName); + ticket = passTicketService.generate(authSourceParsed.getUserId(), applicationName); } catch (IRRPassTicketGenerationException e) { ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed", e.getErrorCode().getMessage()).mapToView(); @@ -67,6 +73,27 @@ public ResponseEntity getPassTicket(@RequestBody TicketRequest ticketReq } return ResponseEntity .status(HttpStatus.OK) - .body(new TicketResponse(null, authSource.getUserId(), applicationName, ticket)); + .body(new TicketResponse(null, authSourceParsed.getUserId(), applicationName, ticket)); + } + + @PostMapping(path = "zosmf", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Provides z/OSMF JWT or LTPA token for authenticated user.") + @ResponseBody + public ResponseEntity getZosmfToken(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource, + @RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed) { + try { + ZosmfResponse zosmfResponse = zosmfService.exchangeAuthenticationForZosmfToken(authSource.getRawSource().toString(), authSourceParsed); + + return ResponseEntity + .status(HttpStatus.OK) + .body(zosmfResponse); + + } catch (Exception e) { + ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.zosmf.noZosmfTokenReceived", e.getMessage()).mapToView(); + return ResponseEntity + .status(HttpStatus.SERVICE_UNAVAILABLE) + .body(messageView); + } } + } diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index 64df5bad50..2a3af9e578 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -449,3 +449,12 @@ messages: text: z/OSMF JWT builder endpoint call (%s) failed with %s reason: z/OSMF returned an error code when calling JWT endpoint. action: Review z/OSMF status. Contact your system administrator. + + # ZAAS error messages (#600) TODO: Messaging requires clean up + + - key: org.zowe.apiml.zaas.zosmf.noZosmfTokenReceived + number: ZWEAZ600 + type: WARNING + text: "z/OSMF is not available or z/OSMF response does not contain any token. Reason: %s" + reason: z/OSMF does not return JWT or LTPA tokens. + action: Make sure z/OSMF is available to API ML or review your z/OSMF configuration. diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/ZaasControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/ZaasControllerTest.java deleted file mode 100644 index c180651b8b..0000000000 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/ZaasControllerTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.gateway.controllers; - -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; -import org.zowe.apiml.gateway.security.service.schema.source.ParsedTokenAuthSource; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.message.yaml.YamlMessageService; -import org.zowe.apiml.passticket.IRRPassTicketGenerationException; -import org.zowe.apiml.passticket.PassTicketService; - -import java.util.Date; - -import static org.apache.http.HttpStatus.*; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.hasSize; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@ExtendWith(SpringExtension.class) -class ZaasControllerTest { - - @Mock - private PassTicketService passTicketService; - - private MockMvc mockMvc; - - private JSONObject body; - - private AuthSource.Parsed authSource; - - private static final String URL = "/gateway/zaas/ticket"; - - private static final String PASSTICKET = "test_passticket"; - private static final String APPLID = "test_applid"; - - @BeforeEach - void setUp() throws IRRPassTicketGenerationException, JSONException { - MessageService messageService = new YamlMessageService("/gateway-messages.yml"); - - when(passTicketService.generate(anyString(), anyString())).thenReturn(PASSTICKET); - ZaasController zaasController = new ZaasController(passTicketService, messageService); - mockMvc = MockMvcBuilders.standaloneSetup(zaasController).build(); - body = new JSONObject() - .put("applicationName", APPLID); - } - - @Nested - class GivenAuthenticated { - - private static final String USER = "test_user"; - - @BeforeEach - void setUp() { - authSource = new ParsedTokenAuthSource(USER, new Date(111), new Date(222), AuthSource.Origin.ZOSMF); - } - - @Test - void whenApplNameProvided_thenPassTicketInResponse() throws Exception { - mockMvc.perform(post(URL) - .contentType(MediaType.APPLICATION_JSON) - .content(body.toString()) - .requestAttr("zaas.auth.source", authSource)) - .andExpect(status().is(SC_OK)) - .andExpect(jsonPath("$.ticket", is(PASSTICKET))) - .andExpect(jsonPath("$.userId", is(USER))) - .andExpect(jsonPath("$.applicationName", is(APPLID))); - } - - @Test - void whenNoApplNameProvided_thenBadRequest() throws Exception { - body.put("applicationName", ""); - - mockMvc.perform(post(URL) - .contentType(MediaType.APPLICATION_JSON) - .content(body.toString()) - .requestAttr("zaas.auth.source", authSource)) - .andExpect(status().is(SC_BAD_REQUEST)) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAG140E")) - .andExpect(jsonPath("$.messages[0].messageContent", is("The 'applicationName' parameter name is missing."))); - } - - @Test - void whenErrorGeneratingPassticket_thenInternalServerError() throws Exception { - when(passTicketService.generate(anyString(), anyString())).thenThrow(new IRRPassTicketGenerationException(8, 8, 8)); - - mockMvc.perform(post(URL) - .contentType(MediaType.APPLICATION_JSON) - .content(body.toString()) - .requestAttr("zaas.auth.source", authSource)) - .andExpect(status().is(SC_INTERNAL_SERVER_ERROR)) - .andExpect(jsonPath("$.messages", hasSize(1))) - .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) - .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAG141E")) - .andExpect(jsonPath("$.messages[0].messageContent", is("The generation of the PassTicket failed. Reason: An internal error was encountered."))); - } - } - - @Nested - class GivenNotAuthenticated { - - @BeforeEach - void setUp() { - authSource = new ParsedTokenAuthSource(null, null, null, null); - } - - @Test - void thenRespondUnauthorized() throws Exception { - mockMvc.perform(post(URL) - .contentType(MediaType.APPLICATION_JSON) - .content(body.toString()) - .requestAttr("zaas.auth.source", authSource)) - .andExpect(status().is(SC_UNAUTHORIZED)); - } - } -} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilterTest.java index 41e5b13339..aa2e56bc29 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilterTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/pre/ExtractAuthSourceFilterTest.java @@ -74,7 +74,7 @@ void setUp() { void thenParsedAuthSourceInRequestAttribute() throws ServletException, IOException { ExtractAuthSourceFilter filter = new ExtractAuthSourceFilter(authSourceService, authExceptionHandler); filter.doFilterInternal(request, response, filterChain); - verify(request, times(1)).setAttribute(ExtractAuthSourceFilter.AUTH_SOURCE_ATTR, parseAuthSource); + verify(request, times(1)).setAttribute(ExtractAuthSourceFilter.AUTH_SOURCE_PARSED_ATTR, parseAuthSource); } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 3ba567a903..0ed2f5bde2 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -17,7 +17,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationContext; import org.springframework.http.*; @@ -28,10 +31,12 @@ import org.springframework.security.authentication.jaas.JaasAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; @@ -48,52 +53,49 @@ import static org.mockito.Mockito.*; import static org.zowe.apiml.security.common.config.AuthConfigurationProperties.JWT_AUTOCONFIGURATION_MODE.*; +@ExtendWith(MockitoExtension.class) class ZosmfAuthenticationProviderTest { private static final String USERNAME = "user"; private static final String PASSWORD = "password"; - private static final String SERVICE_ID = "service"; private static final String HOST = "localhost"; private static final int PORT = 0; private static final String ZOSMF = "zosmf"; private static final String COOKIE1 = "Cookie1=testCookie1"; private static final String COOKIE2 = "LtpaToken2=test"; private static final String DOMAIN = "realm"; - private static final String RESPONSE = "{\"zosmf_saf_realm\": \"" + DOMAIN + "\"}"; private static final String INVALID_RESPONSE = "{\"saf_realm\": \"" + DOMAIN + "\"}"; - private UsernamePasswordAuthenticationToken usernamePasswordAuthentication; - private AuthConfigurationProperties authConfigurationProperties; + + @Mock private DiscoveryClient discovery; + + @Mock + private AuthenticationService authenticationService; + + @Mock private RestTemplate restTemplate; + + @Mock + private TokenCreationService tokenCreationService; + + + private UsernamePasswordAuthenticationToken usernamePasswordAuthentication; + private AuthConfigurationProperties authConfigurationProperties; private InstanceInfo zosmfInstance; - private AuthenticationService authenticationService; private final ObjectMapper securityObjectMapper = new ObjectMapper(); - protected static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; - private ZosmfService.AuthenticationResponse getResponse(boolean valid) { - if (valid) return new ZosmfService.AuthenticationResponse(RESPONSE, null); - return new ZosmfService.AuthenticationResponse(INVALID_RESPONSE, null); - } - @BeforeEach - void setUp() { - usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(USERNAME, PASSWORD); - authConfigurationProperties = new AuthConfigurationProperties(); - authConfigurationProperties.getZosmf().setJwtAutoconfiguration(AUTO); - discovery = mock(DiscoveryClient.class); - authenticationService = mock(AuthenticationService.class); - restTemplate = mock(RestTemplate.class); - zosmfInstance = createInstanceInfo(SERVICE_ID, HOST, PORT); + private ZosmfService.ZosmfInfo getZosmfResponse() { + ZosmfService.ZosmfInfo info = new ZosmfService.ZosmfInfo(); + info.setSafRealm("realm"); - doAnswer((Answer) invocation -> TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1))).when(authenticationService).createTokenAuthentication(anyString(), anyString()); - when(authenticationService.createJwtToken(anyString(), anyString(), anyString())).thenReturn("someJwtToken"); + return info; } - private InstanceInfo createInstanceInfo(String serviceId, String host, int port) { + private InstanceInfo createInstanceInfo(String host, int port) { InstanceInfo out = mock(InstanceInfo.class); - when(out.getAppName()).thenReturn(serviceId); - when(out.getHostName()).thenReturn(host); - when(out.getPort()).thenReturn(port); + lenient().when(out.getHostName()).thenReturn(host); + lenient().when(out.getPort()).thenReturn(port); return out; } @@ -110,11 +112,31 @@ private ZosmfService createZosmfService() { restTemplate, securityObjectMapper, applicationContext, + authenticationService, + tokenCreationService, new ArrayList<>()); ReflectionTestUtils.setField(zosmfService, "meAsProxy", zosmfService); - ZosmfService output = spy(zosmfService); - when(applicationContext.getBean(ZosmfService.class)).thenReturn(output); - return output; + + return spy(zosmfService); + } + + private void mockZosmfAuthenticationRestCallResponse() { + when(restTemplate.exchange(eq("http://localhost:0/zosmf/services/authenticate"), + Mockito.eq(HttpMethod.POST), + any(), + Mockito.>any())) + .thenThrow(new HttpClientErrorException(HttpStatus.NOT_FOUND)); + } + + @BeforeEach + void setUp() { + usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(USERNAME, PASSWORD); + authConfigurationProperties = new AuthConfigurationProperties(); + authConfigurationProperties.getZosmf().setJwtAutoconfiguration(AUTO); + zosmfInstance = createInstanceInfo(HOST, PORT); + + lenient().doAnswer((Answer) invocation -> TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1))).when(authenticationService).createTokenAuthentication(anyString(), anyString()); + lenient().when(authenticationService.createJwtToken(anyString(), anyString(), anyString())).thenReturn("someJwtToken"); } @Test @@ -124,21 +146,20 @@ void loginWithExistingUser() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); + mockZosmfAuthenticationRestCallResponse(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getZosmfResponse(), headers, HttpStatus.OK)); ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); - mockZosmfRealmRestCallResponse(); - mockZosmfRealmRestCallResponse(); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); @@ -153,18 +174,17 @@ void loginWithBadUser() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); - HttpHeaders headers = new HttpHeaders(); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + mockZosmfAuthenticationRestCallResponse(); + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getZosmfResponse(), new HttpHeaders(), HttpStatus.OK)); ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); - mockZosmfRealmRestCallResponse(); Exception exception = assertThrows(BadCredentialsException.class, () -> zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication), "Expected exception is not BadCredentialsException"); @@ -207,12 +227,13 @@ void notValidZosmfResponse() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); + mockZosmfAuthenticationRestCallResponse(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(new ZosmfService.ZosmfInfo(), headers, HttpStatus.OK)); @@ -236,12 +257,13 @@ void noDomainInResponse() throws IOException { ZosmfService.ZosmfInfo zosmfInfoNoDomain = securityObjectMapper.reader().forType(ZosmfService.ZosmfInfo.class).readValue(INVALID_RESPONSE); + mockZosmfAuthenticationRestCallResponse(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(zosmfInfoNoDomain, headers, HttpStatus.OK)); @@ -264,18 +286,16 @@ void invalidCookieInResponse() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); + mockZosmfAuthenticationRestCallResponse(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, invalidCookie); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); - mockZosmfRealmRestCallResponse(); + .thenReturn(new ResponseEntity<>(getZosmfResponse(), headers, HttpStatus.OK)); ZosmfService zosmfService = createZosmfService(); - doReturn(false).when(zosmfService).loginEndpointExists(); - mockZosmfRealmRestCallResponse(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); Exception exception = assertThrows(BadCredentialsException.class, @@ -284,18 +304,6 @@ void invalidCookieInResponse() { assertEquals("Invalid Credentials", exception.getMessage()); } - private void mockZosmfRealmRestCallResponse() { - final HttpHeaders zosmfheaders = new HttpHeaders(); - zosmfheaders.add(ZOSMF_CSRF_HEADER, ""); - ZosmfService.ZosmfInfo info = new ZosmfService.ZosmfInfo(); - info.setSafRealm("realm"); - when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), - Mockito.eq(HttpMethod.GET), - eq(new HttpEntity<>(null, zosmfheaders)), - Mockito.>any())) - .thenReturn(new ResponseEntity<>(info, zosmfheaders, HttpStatus.OK)); - } - @Test void cookieWithSemicolon() { String cookie = "LtpaToken2=test;"; @@ -305,19 +313,19 @@ void cookieWithSemicolon() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); + mockZosmfAuthenticationRestCallResponse(); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, cookie); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getZosmfResponse(), headers, HttpStatus.OK)); ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); - mockZosmfRealmRestCallResponse(); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); assertTrue(tokenAuthentication.isAuthenticated()); @@ -330,9 +338,11 @@ void shouldThrowNewExceptionIfRestClientException() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + + mockZosmfAuthenticationRestCallResponse(); + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) .thenThrow(RestClientException.class); ZosmfService zosmfService = createZosmfService(); @@ -351,9 +361,11 @@ void shouldThrowNewExceptionIfResourceAccessException() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), + + mockZosmfAuthenticationRestCallResponse(); + when(restTemplate.exchange(eq("http://localhost:0/zosmf/info"), + eq(HttpMethod.GET), + any(), Mockito.>any())) .thenThrow(ResourceAccessException.class); ZosmfService zosmfService = createZosmfService(); @@ -370,8 +382,6 @@ void shouldThrowNewExceptionIfResourceAccessException() { void shouldReturnTrueWhenSupportMethodIsCalledWithCorrectClass() { authConfigurationProperties.getZosmf().setServiceId(ZOSMF); - final Application application = createApplication(zosmfInstance); - when(discovery.getApplication(ZOSMF)).thenReturn(application); ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); @@ -427,21 +437,20 @@ void testJwt_givenZosmfJwt_whenItIsIgnoring_thenCreateZoweJwt() { @Nested class givenOverride { - private ZosmfAuthenticationProvider underTest; + @Mock private AuthenticationService authenticationService; + + private ZosmfAuthenticationProvider underTest; private EnumMap tokens; private final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken("userId", "password"); @BeforeEach void setUp() { - authenticationService = mock(AuthenticationService.class); - when(authenticationService.createJwtToken(any(), any(), any())).thenReturn("ltpaToken"); ZosmfService zosmfService = mock(ZosmfService.class); underTest = new ZosmfAuthenticationProvider(authenticationService, zosmfService, authConfigurationProperties); tokens = new EnumMap<>(ZosmfService.TokenType.class); - when(zosmfService.authenticate(any())).thenReturn(new ZosmfService.AuthenticationResponse("domain", tokens)); } @@ -456,6 +465,7 @@ void willChooseJwtWhenPresent() { @Test void willChooseLtpaWhenOnlyLtpa() { + when(authenticationService.createJwtToken(any(), any(), any())).thenReturn("ltpaToken"); tokens.put(ZosmfService.TokenType.LTPA, "ltpaToken"); underTest.authenticate(usernamePasswordAuthenticationToken); @@ -464,6 +474,8 @@ void willChooseLtpaWhenOnlyLtpa() { @Test void willChooseLtpaWhenOverride() { + when(authenticationService.createJwtToken(any(), any(), any())).thenReturn("ltpaToken"); + authConfigurationProperties.getZosmf().setJwtAutoconfiguration(LTPA); tokens.put(ZosmfService.TokenType.LTPA, "ltpaToken"); tokens.put(ZosmfService.TokenType.JWT, "jwtToken"); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java index 23524ab5dd..4cb64a7e0f 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -27,7 +27,7 @@ import org.springframework.web.client.RestTemplate; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.JwtSecurity; -import org.zowe.apiml.gateway.security.service.zosmf.TokenValidationStrategy; +import org.zowe.apiml.gateway.security.service.TokenCreationService; import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.security.SecurityUtils; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; @@ -67,6 +67,12 @@ class SuccessfulQueryHandlerTest { @Mock private CacheManager cacheManager; + @Mock + private AuthenticationService authenticationService; + + @Mock + private TokenCreationService tokenCreationService; + @BeforeEach void setup() { httpServletRequest = new MockHttpServletRequest(); @@ -84,7 +90,9 @@ void setup() { restTemplate, new ObjectMapper(), applicationContext, - new ArrayList()); + authenticationService, + tokenCreationService, + new ArrayList<>()); AuthenticationService authenticationService = new AuthenticationService( applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, discoveryClient, restTemplate, cacheManager, new CacheUtils() diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java index 578f406bef..255dc27b4d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java @@ -215,6 +215,15 @@ void givenExpiredToken_thenThrowsTokenExpireException() { () -> authService.validateJwtToken(token) ); } + + @Test + void whenParseJWT_thenThrowTokenNotValidException() { + String invalidToken = "invalidToken"; + + assertThrows(TokenNotValidException.class, + () -> authService.parseJwtWithSignature(invalidToken)); + } + } @Nested diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/TokenCreationServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/TokenCreationServiceTest.java index 841a473d76..9638aabe32 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/TokenCreationServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/TokenCreationServiceTest.java @@ -12,51 +12,67 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.zowe.apiml.gateway.security.login.Providers; import org.zowe.apiml.gateway.security.login.zosmf.ZosmfAuthenticationProvider; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; import org.zowe.apiml.passticket.IRRPassTicketGenerationException; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.security.common.error.AuthenticationTokenException; import org.zowe.apiml.security.common.token.TokenAuthentication; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.JWT; +import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.LTPA; +@ExtendWith(MockitoExtension.class) class TokenCreationServiceTest { + private TokenCreationService underTest; + @Mock private PassTicketService passTicketService; + + @Mock private ZosmfAuthenticationProvider zosmfAuthenticationProvider; + + @Mock private Providers providers; + + @Mock private AuthenticationService authenticationService; + @Mock + private ZosmfService zosmfService; + private final String VALID_USER_ID = "validUserId"; private final String VALID_ZOSMF_TOKEN = "validZosmfToken"; private final String VALID_APIML_TOKEN = "validApimlToken"; + private final String PASSTICKET = "passTicket"; private final String VALID_ZOSMF_APPLID = "IZUDFLT"; @BeforeEach void setUp() { - passTicketService = mock(PassTicketService.class); - zosmfAuthenticationProvider = mock(ZosmfAuthenticationProvider.class); - providers = mock(Providers.class); - authenticationService = mock(AuthenticationService.class); - - underTest = new TokenCreationService(providers, Optional.of(zosmfAuthenticationProvider), passTicketService, authenticationService); + underTest = new TokenCreationService(providers, Optional.of(zosmfAuthenticationProvider), zosmfService, passTicketService, authenticationService); underTest.zosmfApplId = "IZUDFLT"; } @Test void givenZosmfIsUnavailable_whenTokenIsRequested_thenTokenCreatedByApiMlIsReturned() { - when(providers.isZosmfAvailable()).thenReturn(false); + when(providers.isZosfmUsed()).thenReturn(false); when(authenticationService.createJwtToken(eq(VALID_USER_ID), any(), any())).thenReturn(VALID_APIML_TOKEN); when(authenticationService.createTokenAuthentication(VALID_USER_ID, VALID_APIML_TOKEN)).thenReturn(new TokenAuthentication(VALID_USER_ID, VALID_APIML_TOKEN)); @@ -66,7 +82,7 @@ void givenZosmfIsUnavailable_whenTokenIsRequested_thenTokenCreatedByApiMlIsRetur @Test void givenZosmfIsntPresentBecauseOfError_whenTokenIsRequested_shouldReturnTokenCreatedByApiMl() { - when(providers.isZosmfAvailable()).thenThrow(new AuthenticationServiceException("zOSMF id invalid")); + when(providers.isZosfmUsed()).thenThrow(new AuthenticationServiceException("zOSMF id invalid")); when(authenticationService.createJwtToken(eq(VALID_USER_ID), any(), any())).thenReturn(VALID_APIML_TOKEN); when(authenticationService.createTokenAuthentication(VALID_USER_ID, VALID_APIML_TOKEN)).thenReturn(new TokenAuthentication(VALID_USER_ID, VALID_APIML_TOKEN)); @@ -78,7 +94,7 @@ void givenZosmfIsntPresentBecauseOfError_whenTokenIsRequested_shouldReturnTokenC void givenZosmfIsAvailable_whenTokenIsRequested_thenTokenCreatedByZosmfIsReturned() throws IRRPassTicketGenerationException { when(providers.isZosmfAvailable()).thenReturn(true); when(providers.isZosfmUsed()).thenReturn(true); - when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenReturn("validPassticket"); + when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenReturn(PASSTICKET); when(zosmfAuthenticationProvider.authenticate(any())).thenReturn(new TokenAuthentication(VALID_USER_ID, VALID_ZOSMF_TOKEN)); String jwtToken = underTest.createJwtTokenWithoutCredentials(VALID_USER_ID); @@ -95,4 +111,33 @@ void givenZosmfIsAvailableButPassticketGenerationFails_whenTokenIsRequested_then () -> underTest.createJwtTokenWithoutCredentials(VALID_USER_ID) ); } + + @Test + void givenNoZosmf_whenCreatingZosmfToken_thenReturnEmptyResult() { + when(providers.isZosfmUsed()).thenReturn(false); + + Map tokens = underTest.createZosmfTokensWithoutCredentials("user"); + + assertTrue(tokens.isEmpty()); + } + + @Test + void givenZosmfAvailable_whenCreatingZosmfToken_thenReturnEmptyResult() throws IRRPassTicketGenerationException { + when(providers.isZosfmUsed()).thenReturn(true); + when(providers.isZosmfAvailable()).thenReturn(true); + when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenReturn(PASSTICKET); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(VALID_USER_ID, PASSTICKET); + Map expectedTokens = new HashMap() {{ + put(LTPA, "ltpaToken"); + put(JWT, "jwtToken"); + }}; + ZosmfService.AuthenticationResponse authenticationResponse = new ZosmfService.AuthenticationResponse("domain", expectedTokens); + when(zosmfService.authenticate(authToken)).thenReturn(authenticationResponse); + + Map tokens = underTest.createZosmfTokensWithoutCredentials(VALID_USER_ID); + + assertEquals(expectedTokens, tokens); + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceServiceTest.java index b7ae341447..9d0d3d71e3 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/source/PATAuthSourceServiceTest.java @@ -15,6 +15,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.TokenCreationService; @@ -29,25 +32,31 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.mockito.Mockito.times; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; +import static org.zowe.apiml.gateway.security.service.schema.source.PATAuthSourceService.SERVICE_ID_HEADER; +@ExtendWith(MockitoExtension.class) class PATAuthSourceServiceTest { - AuthenticationService authenticationService; - AccessTokenProvider tokenProvider; - TokenCreationService tokenCreationService; - PATAuthSourceService patAuthSourceService; - RequestContext context; - String token = "token"; + @Mock + private AuthenticationService authenticationService; + + @Mock + private AccessTokenProvider tokenProvider; + + @Mock + private RequestContext context; + + @Mock + private TokenCreationService tokenCreationService; + + private PATAuthSourceService patAuthSourceService; + + private static final String TOKEN = "token"; @BeforeEach void setUp() { - tokenProvider = mock(AccessTokenProvider.class); - tokenCreationService = mock(TokenCreationService.class); - authenticationService = mock(AuthenticationService.class); patAuthSourceService = new PATAuthSourceService(authenticationService, tokenProvider, tokenCreationService); - context = mock(RequestContext.class); RequestContext.testSetCurrentContext(context); } @@ -63,21 +72,23 @@ void returnPATSourceMapper() { @Nested class GivenValidTokenTest { + @Test void givenPatTokenInRequestContext_thenReturnTheToken() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getPATFromRequest(request)).thenReturn(Optional.of(token)); - when(authenticationService.getTokenOrigin(token)).thenReturn(AuthSource.Origin.ZOWE_PAT); + when(authenticationService.getPATFromRequest(request)).thenReturn(Optional.of(TOKEN)); + when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.ZOWE_PAT); Optional tokenResult = patAuthSourceService.getToken(request); assertTrue(tokenResult.isPresent()); - assertEquals(token, tokenResult.get()); + assertEquals(TOKEN, tokenResult.get()); } @Test void givenZoweTokenInRequestContext_thenReturnEmpty() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(token)); - when(authenticationService.getTokenOrigin(token)).thenReturn(AuthSource.Origin.ZOWE); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); + when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.ZOWE); + assertFalse(patAuthSourceService.getToken(request).isPresent()); } @@ -85,13 +96,41 @@ void givenZoweTokenInRequestContext_thenReturnEmpty() { void givenTokenInAuthSource_thenReturnValid() { String serviceId = "gateway"; when(context.get(SERVICE_ID_KEY)).thenReturn(serviceId); - when(tokenProvider.isValidForScopes(token, serviceId)).thenReturn(true); - when(tokenProvider.isInvalidated(token)).thenReturn(false); - PATAuthSource authSource = new PATAuthSource(token); - PATAuthSourceService patAuthSourceService = new PATAuthSourceService(authenticationService, tokenProvider, tokenCreationService); + when(tokenProvider.isValidForScopes(TOKEN, serviceId)).thenReturn(true); + when(tokenProvider.isInvalidated(TOKEN)).thenReturn(false); + PATAuthSource authSource = new PATAuthSource(TOKEN); + assertTrue(patAuthSourceService.isValid(authSource)); } + @Test + void givenScopeFromHeader_whenIsValid_thenReturnTheToken() { + String serviceId = "gateway"; + when(tokenProvider.isValidForScopes(TOKEN, serviceId)).thenReturn(true); + PATAuthSource authSource = new PATAuthSource(TOKEN); + authSource.setDefaultServiceId(serviceId); + + assertTrue(patAuthSourceService.isValid(authSource)); + } + + @Test + void whenGetAuthSourceFromRequest_thenReturnAuthSource() { + String serviceId = "gateway"; + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(SERVICE_ID_HEADER, serviceId); + + when(authenticationService.getPATFromRequest(request)).thenReturn(Optional.of(TOKEN)); + when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.ZOWE_PAT); + + Optional authSource = patAuthSourceService.getAuthSourceFromRequest(request); + + assertTrue(authSource.isPresent()); + PATAuthSource patAuthSource = (PATAuthSource) authSource.get(); + assertEquals(serviceId, patAuthSource.getDefaultServiceId()); + assertEquals(TOKEN, patAuthSource.getSource()); + } + } @Nested @@ -100,32 +139,37 @@ class GivenInvalidTokenTest { void whenExceptionIsThrown_thenReturnTokenInvalid() { String serviceId = "gateway"; when(context.get(SERVICE_ID_KEY)).thenReturn(serviceId); - when(tokenProvider.isValidForScopes(token, serviceId)).thenThrow(new RuntimeException()); - PATAuthSource authSource = new PATAuthSource(token); - PATAuthSourceService patAuthSourceService = new PATAuthSourceService(authenticationService, tokenProvider, tokenCreationService); + when(tokenProvider.isValidForScopes(TOKEN, serviceId)).thenThrow(new RuntimeException()); + PATAuthSource authSource = new PATAuthSource(TOKEN); + + assertFalse(patAuthSourceService.isValid(authSource)); + } + + @Test + void whenNoScope_thenReturnTokenInvalid() { + PATAuthSource authSource = new PATAuthSource(TOKEN); + assertFalse(patAuthSourceService.isValid(authSource)); } @Test void whenTokenIsExpired_thenThrow() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(token)); - when(authenticationService.getTokenOrigin(token)).thenThrow(new TokenExpireException("token expired")); - PATAuthSourceService patAuthSourceService = new PATAuthSourceService(authenticationService, tokenProvider, tokenCreationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); + when(authenticationService.getTokenOrigin(TOKEN)).thenThrow(new TokenExpireException("token expired")); assertThrows(TokenExpireException.class, () -> patAuthSourceService.getToken(request)); - verify(authenticationService, times(1)).getTokenOrigin(token); + verify(authenticationService, times(1)).getTokenOrigin(TOKEN); } @Test void whenTokenIsNotValid_thenThrow() { HttpServletRequest request = new MockHttpServletRequest(); - when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(token)); - when(authenticationService.getTokenOrigin(token)).thenThrow(new TokenNotValidException("token not valid")); - PATAuthSourceService patAuthSourceService = new PATAuthSourceService(authenticationService, tokenProvider, tokenCreationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of(TOKEN)); + when(authenticationService.getTokenOrigin(TOKEN)).thenThrow(new TokenNotValidException("token not valid")); assertThrows(TokenNotValidException.class, () -> patAuthSourceService.getToken(request)); - verify(authenticationService, times(1)).getTokenOrigin(token); + verify(authenticationService, times(1)).getTokenOrigin(TOKEN); } } @@ -135,16 +179,16 @@ void whenTokenIsNotValid_thenThrow() { class GivenDifferentAuthSourcesTest { @Test void givenPATAuthSource_thenReturnCorrectUserInfo() { - PATAuthSource authSource = new PATAuthSource(token); + PATAuthSource authSource = new PATAuthSource(TOKEN); QueryResponse response = new QueryResponse(null, "user", new Date(), new Date(), "issuer", null, QueryResponse.Source.ZOWE_PAT); - when(authenticationService.parseJwtWithSignature(token)).thenReturn(response); + when(authenticationService.parseJwtWithSignature(TOKEN)).thenReturn(response); AuthSource.Parsed parsedSource = patAuthSourceService.parse(authSource); assertEquals(response.getUserId(), parsedSource.getUserId()); } @Test void givenJWTAuthSource_thenReturnNull() { - JwtAuthSource authSource = new JwtAuthSource(token); + JwtAuthSource authSource = new JwtAuthSource(TOKEN); AuthSource.Parsed parsedSource = patAuthSourceService.parse(authSource); assertNull(parsedSource); } @@ -152,26 +196,25 @@ void givenJWTAuthSource_thenReturnNull() { @Test void givenValidAuthSource_thenReturnLTPAToken() { String ltpa = "ltpa"; - PATAuthSource authSource = new PATAuthSource(token); + PATAuthSource authSource = new PATAuthSource(TOKEN); QueryResponse response = new QueryResponse(null, "user", new Date(), new Date(), "issuer", null, QueryResponse.Source.ZOWE); - when(authenticationService.parseJwtWithSignature(token)).thenReturn(response); - when(tokenCreationService.createJwtTokenWithoutCredentials(response.getUserId())).thenReturn(token); - when(authenticationService.getTokenOrigin(token)).thenReturn(AuthSource.Origin.ZOWE); - when(authenticationService.getLtpaToken(token)).thenReturn(ltpa); + when(authenticationService.parseJwtWithSignature(TOKEN)).thenReturn(response); + when(tokenCreationService.createJwtTokenWithoutCredentials(response.getUserId())).thenReturn(TOKEN); + when(authenticationService.getTokenOrigin(TOKEN)).thenReturn(AuthSource.Origin.ZOWE); + when(authenticationService.getLtpaToken(TOKEN)).thenReturn(ltpa); String ltpaResult = patAuthSourceService.getLtpaToken(authSource); assertEquals(ltpa, ltpaResult); } @Test void givenValidAuthSource_thenReturnJWT() { - PATAuthSource authSource = new PATAuthSource(token); + PATAuthSource authSource = new PATAuthSource(TOKEN); QueryResponse response = new QueryResponse(null, "user", new Date(), new Date(), "issuer", null, QueryResponse.Source.ZOWE_PAT); - when(authenticationService.parseJwtWithSignature(token)).thenReturn(response); - when(tokenCreationService.createJwtTokenWithoutCredentials(response.getUserId())).thenReturn(token); + when(authenticationService.parseJwtWithSignature(TOKEN)).thenReturn(response); + when(tokenCreationService.createJwtTokenWithoutCredentials(response.getUserId())).thenReturn(TOKEN); String jwt = patAuthSourceService.getJWT(authSource); - assertEquals(token, jwt); + assertEquals(TOKEN, jwt); } } - } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java index 5f1ccfa59c..6bb404703d 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceTest.java @@ -32,83 +32,71 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.*; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.TokenCreationService; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.ParsedTokenAuthSource; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.login.ChangePasswordRequest; import org.zowe.apiml.security.common.login.LoginRequest; import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.zaas.zosmf.ZosmfResponse; +import javax.management.ServiceNotFoundException; import javax.net.ssl.SSLHandshakeException; - import java.net.ConnectException; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.JWT; import static org.zowe.apiml.gateway.security.service.zosmf.ZosmfService.TokenType.LTPA; @ExtendWith(MockitoExtension.class) class ZosmfServiceTest { - @Captor - private ArgumentCaptor loggingCaptor; - private static final String ZOSMF_ID = "zosmf"; private final AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); - private final DiscoveryClient discovery = mock(DiscoveryClient.class); - private final RestTemplate restTemplate = mock(RestTemplate.class); - ApplicationContext applicationContext = mock(ApplicationContext.class); - private final TokenValidationStrategy tokenValidationStrategy1 = mock(TokenValidationStrategy.class); - private final TokenValidationStrategy tokenValidationStrategy2 = mock(TokenValidationStrategy.class); + + @Mock + private DiscoveryClient discovery; + + @Mock + private RestTemplate restTemplate; + + @Mock + private ApplicationContext applicationContext; + + @Mock + private TokenValidationStrategy tokenValidationStrategy1; + + @Mock + private TokenValidationStrategy tokenValidationStrategy2; + + @Mock + private AuthenticationService authenticationService; + + @Mock + private TokenCreationService tokenCreationService; + private final List validationStrategyList = new ArrayList<>(); { when(authConfigurationProperties.getZosmf()).thenReturn(mock(AuthConfigurationProperties.Zosmf.class)); - validationStrategyList.add(tokenValidationStrategy1); - validationStrategyList.add(tokenValidationStrategy2); } private final ObjectMapper securityObjectMapper = spy(ObjectMapper.class); @@ -119,6 +107,8 @@ private ZosmfService getZosmfServiceSpy() { restTemplate, securityObjectMapper, applicationContext, + authenticationService, + tokenCreationService, null); ZosmfService zosmfService = spy(zosmfServiceObj); doReturn(ZOSMF_ID).when(zosmfService).getZosmfServiceId(); @@ -133,6 +123,8 @@ private ZosmfService getZosmfServiceWithValidationStrategy(List responseEntity = new ResponseEntity<>("{}", responseHeaders, HttpStatus.OK); doReturn(new ZosmfService.AuthenticationResponse( "domain1", - Collections.singletonMap(ZosmfService.TokenType.JWT, "jwtToken1"))).when(zosmfService).getAuthenticationResponse(any()); - doReturn(responseEntity).when(restTemplate).exchange( - "http://zosmf:1433/zosmf/services/authenticate", - HttpMethod.POST, - new HttpEntity<>(null, requestHeaders), - String.class - ); + Collections.singletonMap(JWT, "jwtToken1"))).when(zosmfService).getAuthenticationResponse(any()); + when(loginRequest.getPassword()).thenReturn("password".toCharArray()); when(authentication.getPrincipal()).thenReturn("principal"); @@ -272,18 +254,19 @@ void thenAuthenticateWithSuccess() { assertNotNull(response); assertNotNull(response.getTokens()); assertEquals(1, response.getTokens().size()); - assertEquals("jwtToken1", response.getTokens().get(ZosmfService.TokenType.JWT)); + assertEquals("jwtToken1", response.getTokens().get(JWT)); } } @Nested class WhenChangingPassword { - private LoginRequest loginRequest; - private Authentication authentication; + private final LoginRequest loginRequest; + private final Authentication authentication; private final HttpHeaders requiredHeaders; private ZosmfService zosmfService; + { requiredHeaders = new HttpHeaders(); requiredHeaders.add("X-CSRF-ZOSMF-HEADER", ""); @@ -300,17 +283,10 @@ void setUp() { @Test void thenChangePasswordWithSuccess() { - LoginRequest loginRequest = new LoginRequest("username", "password".toCharArray(), "newPassword".toCharArray()); Authentication authentication = mock(UsernamePasswordAuthenticationToken.class); ResponseEntity responseEntity = new ResponseEntity<>("{}", null, HttpStatus.OK); doReturn(responseEntity).when(zosmfService).issueChangePasswordRequest(any(), any(), any()); - doReturn(responseEntity).when(restTemplate).exchange( - "http://zosmf:1433/zosmf/services/authenticate", - HttpMethod.PUT, - new HttpEntity<>(loginRequest, null), - String.class - ); ResponseEntity response = zosmfService.changePassword(authentication); assertTrue(response.getStatusCode().is2xxSuccessful()); @@ -327,7 +303,7 @@ void thenChangePasswordWithClientError() { HttpMethod.PUT, new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), String.class)) - .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST)); + .thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST)); assertThrows(BadCredentialsException.class, () -> zosmfService.changePassword(authentication)); } @@ -340,7 +316,7 @@ void thenChangePasswordWithUnsupportedZosmf() { HttpMethod.PUT, new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), String.class)) - .thenThrow(HttpClientErrorException.create(HttpStatus.METHOD_NOT_ALLOWED, "Method not allowed", null, null, null)); + .thenThrow(HttpClientErrorException.create(HttpStatus.METHOD_NOT_ALLOWED, "Method not allowed", null, null, null)); assertThrows(ServiceNotAccessibleException.class, () -> zosmfService.changePassword(authentication)); } @@ -356,7 +332,7 @@ void thenChangePasswordWithServerError() { HttpMethod.PUT, new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), String.class)) - .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); assertThrows(AuthenticationServiceException.class, () -> zosmfService.changePassword(authentication)); } @@ -369,7 +345,7 @@ void thenChangePasswordWithZosmfInternalError() { HttpMethod.PUT, new HttpEntity<>(new ChangePasswordRequest(loginRequest), requiredHeaders), String.class)) - .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 4}".getBytes(), Charset.defaultCharset())); + .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error", "{\"returnCode\": 4}".getBytes(), Charset.defaultCharset())); assertThrows(AuthenticationServiceException.class, () -> zosmfService.changePassword(authentication)); } @@ -441,7 +417,7 @@ void thenProvidedLoginRequest() { String.class ); - Authentication authentication = new UsernamePasswordAuthenticationToken("user", new LoginRequest("user","pass".toCharArray())); + Authentication authentication = new UsernamePasswordAuthenticationToken("user", new LoginRequest("user", "pass".toCharArray())); ZosmfService.AuthenticationResponse response = zosmfService.authenticate(authentication); assertNotNull(response); @@ -522,6 +498,12 @@ void prepareZosmfEndpoint(Exception exception) { @Nested class WhenValidateToken { + @BeforeEach + void setUp() { + validationStrategyList.add(tokenValidationStrategy1); + validationStrategyList.add(tokenValidationStrategy2); + } + @Test void givenException_thenHandleExceptions() { ZosmfService zosmfService = getZosmfServiceWithValidationStrategy(Collections.singletonList(tokenValidationStrategy1)); @@ -584,7 +566,7 @@ void doesNotRethrowExceptionsFromValidationStrategies() { void suppliesValidationRequestWithVerifiedEndpointsList() { ZosmfService zosmfService = getZosmfServiceWithValidationStrategy(validationStrategyList); zosmfService.validate("TOKN"); - verify(tokenValidationStrategy1).validate(argThat(request -> request.getEndpointExistenceMap().size() > 0)); + verify(tokenValidationStrategy1).validate(argThat(request -> !request.getEndpointExistenceMap().isEmpty())); } private void doValidate(TokenValidationStrategy tokenValidationStrategy1, TokenValidationRequest.STATUS status) { @@ -625,7 +607,7 @@ void thenInvalidateWithSuccess() { String.class ); - assertDoesNotThrow(() -> zosmfService.invalidate(ZosmfService.TokenType.JWT, "jwt")); + assertDoesNotThrow(() -> zosmfService.invalidate(JWT, "jwt")); } @Test @@ -664,7 +646,7 @@ void thenTestInvalidateUnexpectedHttpStatusCode() { ); try { - zosmfService.invalidate(ZosmfService.TokenType.JWT, "jwt"); + zosmfService.invalidate(JWT, "jwt"); } catch (ServiceNotAccessibleException e) { assertEquals("Could not get an access to z/OSMF service.", e.getMessage()); } @@ -687,7 +669,7 @@ void thenTestInvalidateRuntimeException() { ); try { - zosmfService.invalidate(ZosmfService.TokenType.JWT, "jwt"); + zosmfService.invalidate(JWT, "jwt"); } catch (RuntimeException e) { assertEquals("Runtime Exception", e.getMessage()); } @@ -725,7 +707,15 @@ void thenSuccess() throws JSONException { @Test void thenReturnNull() { - assertNull(new ZosmfService(null, null, null, null, null, null).readTokenFromCookie(null, null)); + assertNull(new ZosmfService(null, + null, + null, + null, + null, + null, + null, + null) + .readTokenFromCookie(null, null)); } } @@ -764,6 +754,8 @@ void setUp() { restTemplate, securityObjectMapper, applicationContext, + authenticationService, + tokenCreationService, null ); @@ -816,7 +808,7 @@ private String loggedValues() { List values = loggingCaptor.getAllValues(); assertNotNull(values); assertFalse(values.isEmpty()); - return values.stream().map(element -> element.getFormattedMessage()).collect(Collectors.joining("\n")); + return values.stream().map(LoggingEvent::getFormattedMessage).collect(Collectors.joining("\n")); } @Test @@ -838,7 +830,7 @@ void givenZosmfIsUnavailable_thenFalseIsReturned() { assertThat(underTest.isAccessible(), is(false)); verify(mockedAppender, atLeast(1)).doAppend(loggingCaptor.capture()); String values = loggedValues(); - assertTrue(values.length() > 0); + assertFalse(values.isEmpty()); assertTrue(values.contains("z/OSMF isn't accessible"), values); } @@ -854,7 +846,7 @@ void givenSSLError_thenFalseAndException() { assertThat(underTest.isAccessible(), is(false)); verify(mockedAppender, atLeast(1)).doAppend(loggingCaptor.capture()); String values = loggedValues(); - assertTrue(values.length() > 0); + assertFalse(values.isEmpty()); assertTrue(values.contains("ResourceAccessException accessing"), values); verify(apimlLogger, times(1)).log("org.zowe.apiml.security.auth.zosmf.sslError", "resource access exception; nested exception is javax.net.ssl.SSLHandshakeException: handshake exception"); @@ -903,6 +895,8 @@ void setUp() { restTemplate, securityObjectMapper, applicationContext, + authenticationService, + tokenCreationService, null ); @@ -920,4 +914,109 @@ void givenGetURIFails_thenFalseReturned() { } } } + + @Nested + class WhenExchangingAuthenticationForZosmfToken { + + private static final String USER = "user"; + private static final String ZOWE_JWT_TOKEN = "zoweToken"; + private static final String ZOSMF_JWT_TOKEN = "zosmfToken"; + private static final String LTPA_TOKEN = "ltpaToken"; + + private ZosmfService underTest; + + @BeforeEach + void setup() { + underTest = new ZosmfService( + authConfigurationProperties, + discovery, + restTemplate, + securityObjectMapper, + applicationContext, + authenticationService, + tokenCreationService, + null + ); + } + + @Test + void givenZosmfAuthSource_thenSameTokenIsReturned() throws ServiceNotFoundException { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.ZOSMF); + + ZosmfResponse zosmfResponse = underTest.exchangeAuthenticationForZosmfToken(ZOSMF_JWT_TOKEN, authParsedSource); + + assertEquals(ZOSMF_JWT_TOKEN, zosmfResponse.getToken()); + assertEquals(JWT.getCookieName(), zosmfResponse.getCookieName()); + } + + @Test + void givenZoweAuthSourceWithLtpa_thenSameLtpaTokenIsReturned() throws ServiceNotFoundException { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.ZOWE); + + when(authenticationService.getLtpaToken(ZOWE_JWT_TOKEN)).thenReturn(LTPA_TOKEN); + + ZosmfResponse zosmfResponse = underTest.exchangeAuthenticationForZosmfToken(ZOWE_JWT_TOKEN, authParsedSource); + + assertEquals(LTPA_TOKEN, zosmfResponse.getToken()); + assertEquals(LTPA.getCookieName(), zosmfResponse.getCookieName()); + } + + @Test + void givenZoweAuthSourceWithoutLtpa_thenNewJwtTokenIsReturned() throws ServiceNotFoundException { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.ZOWE); + + Map tokens = new HashMap() {{ + put(JWT, ZOSMF_JWT_TOKEN); + }}; + when(authenticationService.getLtpaToken(ZOWE_JWT_TOKEN)).thenReturn(null); + when(tokenCreationService.createZosmfTokensWithoutCredentials(USER)).thenReturn(tokens); + + ZosmfResponse zosmfResponse = underTest.exchangeAuthenticationForZosmfToken(ZOWE_JWT_TOKEN, authParsedSource); + + assertEquals(ZOSMF_JWT_TOKEN, zosmfResponse.getToken()); + assertEquals(JWT.getCookieName(), zosmfResponse.getCookieName()); + } + + @Test + void givenOtherAuthSourceAndZosmfProducesJwt_thenNewJwtTokenIsReturned() throws ServiceNotFoundException { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.OIDC); + + Map tokens = new HashMap() {{ + put(LTPA, LTPA_TOKEN); + put(JWT, ZOSMF_JWT_TOKEN); + }}; + when(tokenCreationService.createZosmfTokensWithoutCredentials(USER)).thenReturn(tokens); + + ZosmfResponse zosmfResponse = underTest.exchangeAuthenticationForZosmfToken("OidcToken", authParsedSource); + + assertEquals(ZOSMF_JWT_TOKEN, zosmfResponse.getToken()); + assertEquals(JWT.getCookieName(), zosmfResponse.getCookieName()); + } + + @Test + void givenOtherAuthSourceAndZosmfProducesOnlyLtpa_thenNewLtpaTokenIsReturned() throws ServiceNotFoundException { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.OIDC); + + Map tokens = new HashMap() {{ + put(LTPA, LTPA_TOKEN); + }}; + when(tokenCreationService.createZosmfTokensWithoutCredentials(USER)).thenReturn(tokens); + + ZosmfResponse zosmfResponse = underTest.exchangeAuthenticationForZosmfToken("OidcToken", authParsedSource); + + assertEquals(LTPA_TOKEN, zosmfResponse.getToken()); + assertEquals(LTPA.getCookieName(), zosmfResponse.getCookieName()); + } + + @Test + void givenOtherAuthSourceAndZosmaIsNotAvailable_thenExceptionIsThrown() { + AuthSource.Parsed authParsedSource = new ParsedTokenAuthSource(USER, new Date(), new Date(), AuthSource.Origin.OIDC); + + when(tokenCreationService.createZosmfTokensWithoutCredentials(USER)).thenReturn(Collections.emptyMap()); + + assertThrows(ServiceNotFoundException.class, () -> underTest.exchangeAuthenticationForZosmfToken("OidcToken", authParsedSource)); + } + + } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilterTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilterTest.java new file mode 100644 index 0000000000..3ca7597823 --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasAuthenticationFilterTest.java @@ -0,0 +1,117 @@ +/* + * 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.gateway.zaas; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService; +import org.zowe.apiml.gateway.security.service.schema.source.JwtAuthSource; +import org.zowe.apiml.security.common.error.AuthExceptionHandler; +import org.zowe.apiml.security.common.token.TokenExpireException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_ATTR; + +@ExtendWith(MockitoExtension.class) +class ZaasAuthenticationFilterTest { + + @Mock + private AuthSourceService authSourceService; + + @Mock + private AuthExceptionHandler authExceptionHandler; + + @Mock + private FilterChain filterChain; + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + private final MockHttpServletResponse response = new MockHttpServletResponse(); + + private ZaasAuthenticationFilter underTest; + + @BeforeEach + void setup() { + underTest = new ZaasAuthenticationFilter(authSourceService, authExceptionHandler); + } + + @Nested + class ThenContinue { + + @Test + void givenValidAuth_whenFilter() throws ServletException, IOException { + mockAuthSource(true); + + underTest.doFilterInternal(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + } + + @Nested + class ThenHandleError { + + @Test + void givenNoAuth_whenFilter() throws ServletException, IOException { + underTest.doFilterInternal(request, response, filterChain); + + assertException(InsufficientAuthenticationException.class); + } + + @Test + void givenInvalidAuth_whenFilter() throws ServletException, IOException { + mockAuthSource(false); + + underTest.doFilterInternal(request, response, filterChain); + + assertException(InsufficientAuthenticationException.class); + } + + @Test + void givenAuthException_whenFilter() throws ServletException, IOException { + AuthSource authSource = new JwtAuthSource("token"); + request.setAttribute(AUTH_SOURCE_ATTR, authSource); + when(authSourceService.isValid(authSource)).thenThrow(new TokenExpireException("Expired")); + + underTest.doFilterInternal(request, response, filterChain); + + assertException(TokenExpireException.class); + } + + private void assertException(Class exceptionClass) throws ServletException { + ArgumentCaptor exceptionCaptor = ArgumentCaptor.forClass(RuntimeException.class); + verify(authExceptionHandler, times(1)).handleException(eq(request), eq(response), exceptionCaptor.capture()); + assertEquals(exceptionClass, exceptionCaptor.getValue().getClass()); + } + + } + + private void mockAuthSource(boolean isValid) { + AuthSource authSource = new JwtAuthSource("token"); + request.setAttribute(AUTH_SOURCE_ATTR, authSource); + when(authSourceService.isValid(authSource)).thenReturn(isValid); + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java new file mode 100644 index 0000000000..31b8d2ef1d --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/zaas/ZaasControllerTest.java @@ -0,0 +1,190 @@ +/* + * 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.gateway.zaas; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.zowe.apiml.gateway.security.service.schema.source.AuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.JwtAuthSource; +import org.zowe.apiml.gateway.security.service.schema.source.ParsedTokenAuthSource; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.yaml.YamlMessageService; +import org.zowe.apiml.passticket.IRRPassTicketGenerationException; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.zosmf.ZosmfResponse; + +import javax.management.ServiceNotFoundException; +import java.util.Date; + +import static org.apache.http.HttpStatus.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_ATTR; +import static org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter.AUTH_SOURCE_PARSED_ATTR; + +@ExtendWith(SpringExtension.class) +class ZaasControllerTest { + + @Mock + private PassTicketService passTicketService; + + @Mock + private ZosmfService zosmfService; + + private MockMvc mockMvc; + private JSONObject ticketBody; + private AuthSource authSource; + private AuthSource.Parsed authParsedSource; + + private static final String PASSTICKET_URL = "/gateway/zaas/ticket"; + private static final String ZOSMF_TOKEN_URL = "/gateway/zaas/zosmf"; + + private static final String USER = "test_user"; + private static final String PASSTICKET = "test_passticket"; + private static final String APPLID = "test_applid"; + private static final String JWT_TOKEN = "jwt_test_token"; + + @BeforeEach + void setUp() throws IRRPassTicketGenerationException, JSONException { + MessageService messageService = new YamlMessageService("/gateway-messages.yml"); + + when(passTicketService.generate(anyString(), anyString())).thenReturn(PASSTICKET); + ZaasController zaasController = new ZaasController(messageService, passTicketService, zosmfService); + mockMvc = MockMvcBuilders.standaloneSetup(zaasController).build(); + ticketBody = new JSONObject() + .put("applicationName", APPLID); + } + + @Nested + class GivenAuthenticated { + + @BeforeEach + void setUp() { + authSource = new JwtAuthSource(JWT_TOKEN); + authParsedSource = new ParsedTokenAuthSource(USER, new Date(111), new Date(222), AuthSource.Origin.ZOSMF); + } + + @Test + void whenRequestZosmfToken_thenResponseOK() throws Exception { + when(zosmfService.exchangeAuthenticationForZosmfToken(JWT_TOKEN, authParsedSource)) + .thenReturn(new ZosmfResponse(ZosmfService.TokenType.JWT.getCookieName(), JWT_TOKEN)); + + mockMvc.perform(post(ZOSMF_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_OK)) + .andExpect(jsonPath("$.cookieName", is(ZosmfService.TokenType.JWT.getCookieName()))) + .andExpect(jsonPath("$.token", is(JWT_TOKEN))); + } + + @Test + void whenRequestPassticketAndApplNameProvided_thenPassTicketInResponse() throws Exception { + mockMvc.perform(post(PASSTICKET_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_OK)) + .andExpect(jsonPath("$.ticket", is(PASSTICKET))) + .andExpect(jsonPath("$.userId", is(USER))) + .andExpect(jsonPath("$.applicationName", is(APPLID))); + } + + @Test + void whenRequestPassticketAndNoApplNameProvided_thenBadRequest() throws Exception { + ticketBody.put("applicationName", ""); + + mockMvc.perform(post(PASSTICKET_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_BAD_REQUEST)) + .andExpect(jsonPath("$.messages", hasSize(1))) + .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAG140E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("The 'applicationName' parameter name is missing."))); + } + + @Nested + class WhenExceptionOccurs { + + @BeforeEach + void setUp() throws IRRPassTicketGenerationException { + when(passTicketService.generate(anyString(), anyString())).thenThrow(new IRRPassTicketGenerationException(8, 8, 8)); + } + + @Test + void whenRequestingPassticket_thenInternalServerError() throws Exception { + mockMvc.perform(post(PASSTICKET_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_INTERNAL_SERVER_ERROR)) + .andExpect(jsonPath("$.messages", hasSize(1))) + .andExpect(jsonPath("$.messages[0].messageType").value("ERROR")) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAG141E")) + .andExpect(jsonPath("$.messages[0].messageContent", is("The generation of the PassTicket failed. Reason: An internal error was encountered."))); + } + + @Test + void whenRequestingZosmfTokens_thenInternalServerError() throws Exception { + when(zosmfService.exchangeAuthenticationForZosmfToken(JWT_TOKEN, authParsedSource)) + .thenThrow(new ServiceNotFoundException("Unable to obtain a token from z/OSMF service.")); + + mockMvc.perform(post(ZOSMF_TOKEN_URL) + .requestAttr(AUTH_SOURCE_ATTR, authSource) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_SERVICE_UNAVAILABLE)) + .andExpect(jsonPath("$.messages", hasSize(1))) + .andExpect(jsonPath("$.messages[0].messageNumber").value("ZWEAZ600W")) + .andExpect(jsonPath("$.messages[0].messageContent", containsString("Unable to obtain a token from z/OSMF service."))); + } + } + } + + @Nested + class GivenNotAuthenticated { + + @BeforeEach + void setUp() { + authParsedSource = new ParsedTokenAuthSource(null, null, null, null); + } + + @ParameterizedTest + @ValueSource(strings = {PASSTICKET_URL}) + void thenRespondUnauthorized(String url) throws Exception { + mockMvc.perform(post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(ticketBody.toString()) + .requestAttr(AUTH_SOURCE_ATTR, authParsedSource) + .requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource)) + .andExpect(status().is(SC_UNAUTHORIZED)); + } + + } +} diff --git a/gateway-service/src/test/resources/gateway-messages.yml b/gateway-service/src/test/resources/gateway-messages.yml index 04eac407cf..2bf247e6d7 100644 --- a/gateway-service/src/test/resources/gateway-messages.yml +++ b/gateway-service/src/test/resources/gateway-messages.yml @@ -338,3 +338,12 @@ messages: Enable debugging to see further details in stack trace. reason: "The z/OSMF connection is incorrectly configured." action: "Verify z/OSMF connection details. Verify z/OSMF can be accessed with HTTPS. Configure sslDebug to see SSL debugging messages." + + # ZAAS error messages (#600) TODO: Messaging requires clean up + + - key: org.zowe.apiml.zaas.zosmf.noZosmfTokenReceived + number: ZWEAZ600 + type: WARNING + text: "z/OSMF is not available or z/OSMF response does not contain any token. Reason: %s" + reason: z/OSMF does not return JWT or LTPA tokens. + action: Make sure z/OSMF is available to API ML or review your z/OSMF configuration. diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 0316339f66..e9e1391514 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -31,6 +31,7 @@ dependencies { testImplementation libs.json testImplementation libs.jjwt testImplementation libs.reactorTest + testImplementation libs.bcpkix; testImplementation project(':apiml-security-common') testImplementation project(':zaas-client') @@ -311,7 +312,8 @@ task runContainerTests(type: Test) { 'OktaOauth2Test', 'CloudGatewayProxyTest', 'CloudGatewayServiceRouting', - 'CloudGatewayCentralRegistry' + 'CloudGatewayCentralRegistry', + 'ZaasTest' ) } } @@ -463,6 +465,7 @@ task runSafAuthTest(type: Test) { ) } } + task runZosmfAuthTest(type: Test) { group "integration tests" description "Run zOSMF dependant authentication tests only" @@ -480,6 +483,20 @@ task runZosmfAuthTest(type: Test) { } } +task runZaasTest(type: Test) { + group "integration tests" + description "Run Zaas tests only" + + outputs.cacheIf { false } + + systemProperties System.getProperties() + useJUnitPlatform { + includeTags( + 'ZaasTest' + ) + } +} + task runX509AuthTest(type: Test) { group "integration tests" description "Run x509 dependant authentication tests only" diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java new file mode 100644 index 0000000000..b929d0d64b --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZaasNegativeTest.java @@ -0,0 +1,105 @@ +/* + * 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.integration.zaas; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.util.SecurityUtils; +import org.zowe.apiml.util.categories.GeneralAuthenticationTest; +import org.zowe.apiml.util.categories.ZaasTest; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.core.Is.is; +import static org.zowe.apiml.integration.zaas.ZosmfTokensTest.WhenGeneratingZosmfTokens_returnValidZosmfToken.COOKIE; +import static org.zowe.apiml.integration.zaas.ZosmfTokensTest.ZAAS_ZOSMF_URI; +import static org.zowe.apiml.util.SecurityUtils.generateJwtWithRandomSignature; +import static org.zowe.apiml.util.SecurityUtils.getConfiguredSslConfig; + +@ZaasTest +public class ZaasNegativeTest { + + private static Stream provideToken() { + return Stream.of( + Arguments.of(generateJwtWithRandomSignature(QueryResponse.Source.ZOSMF.value)), + Arguments.of(generateJwtWithRandomSignature(QueryResponse.Source.ZOWE.value)), + Arguments.of(generateJwtWithRandomSignature(QueryResponse.Source.ZOWE_PAT.value)), + Arguments.of(generateJwtWithRandomSignature("https://localhost:10010")) + ); + } + + + @Nested + @GeneralAuthenticationTest + class ReturnUnauthorized { + + @BeforeEach + void setUpCertificateAndToken() { + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + } + + @Test + void givenNoToken() { + //@formatter:off + when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_UNAUTHORIZED)); + //@formatter:on + } + + @ParameterizedTest + @MethodSource("org.zowe.apiml.integration.zaas.ZaasNegativeTest#provideToken") + void givenInvalidOAuthToken(String token) { + //@formatter:off + given() + .header("Authorization", "Bearer " + token) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_UNAUTHORIZED)); + //@formatter:on + } + + } + + @Nested + @GeneralAuthenticationTest + class GivenNoCertificate { + + @BeforeEach + void setUpCertificateAndToken() { + RestAssured.useRelaxedHTTPSValidation(); + } + + @Test + void thenReturnUnauthorized() { + //@formatter:off + given() + .cookie(COOKIE, SecurityUtils.gatewayToken()) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_UNAUTHORIZED)); + //@formatter:on + } + + } +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java new file mode 100644 index 0000000000..d5d7a92957 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/integration/zaas/ZosmfTokensTest.java @@ -0,0 +1,152 @@ +/* + * 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.integration.zaas; + +import io.restassured.RestAssured; +import org.bouncycastle.operator.OperatorCreationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.zowe.apiml.util.TestWithStartedInstances; +import org.zowe.apiml.util.categories.ZaasTest; +import org.zowe.apiml.util.http.HttpRequestUtils; + +import java.io.IOException; +import java.net.URI; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.zowe.apiml.util.SecurityUtils.*; +import static org.zowe.apiml.util.requests.Endpoints.ZAAS_ZOSMF_ENDPOINT; + +@ZaasTest +class ZosmfTokensTest implements TestWithStartedInstances { + + static final URI ZAAS_ZOSMF_URI = HttpRequestUtils.getUriFromGateway(ZAAS_ZOSMF_ENDPOINT); + + private static Stream provideClientCertificates() throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, NoSuchProviderException, OperatorCreationException { + return Stream.of( + Arguments.of(getClientCertificate()), + Arguments.of(getDummyClientCertificate()) + ); + } + + @Nested + class WhenGeneratingZosmfTokens_returnValidZosmfToken { + + final static String COOKIE = "apimlAuthenticationToken"; + private final static String LTPA_COOKIE = "LtpaToken2"; + private final static String JWT_COOKIE = "jwtToken"; + + + @BeforeEach + void setUpCertificate() { + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + } + + @Test + void givenValidZosmfToken() { + String zosmfToken = getZosmfJwtToken(); + + //@formatter:off + given() + .cookie(COOKIE, zosmfToken) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_OK)) + .body("cookieName", is(JWT_COOKIE)) + .body("token", is(zosmfToken)); + //@formatter:on + } + + @Test + void givenValidZoweTokenWithLtpa() throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + String ltpaToken = getZosmfToken(LTPA_COOKIE); + String zoweToken = generateZoweJwtWithLtpa(ltpaToken); + + //@formatter:off + given() + .header("Authorization", "Bearer " + zoweToken) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_OK)) + .body("cookieName", is(LTPA_COOKIE)) + .body("token", is(ltpaToken)); + //@formatter:on + } + + @Test + void givenValidAccessToken() { + String serviceId = "gateway"; + String pat = personalAccessToken(Collections.singleton(serviceId)); + + //@formatter:off + given() + .header("Authorization", "Bearer " + pat) + .header("X-Service-Id", serviceId) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_OK)) + .body("cookieName", is(JWT_COOKIE)) + .body("token", not("")); + //@formatter:on + } + + @ParameterizedTest + @MethodSource("org.zowe.apiml.integration.zaas.ZosmfTokensTest#provideClientCertificates") + void givenX509Certificate(String certificate) { + //@formatter:off + given() + .header("Client-Cert", certificate) + .when() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_OK)) + .body("cookieName", is(JWT_COOKIE)) + .body("token", not("")); + //@formatter:on + } + + @Test + void givenValidOAuthToken() { + String oAuthToken = validOktaAccessToken(true); + + //@formatter:off + given() + .cookie(COOKIE, oAuthToken) + .when() + .log().all() + .post(ZAAS_ZOSMF_URI) + .then() + .statusCode(is(SC_OK)) + .body("cookieName", is(JWT_COOKIE)) + .body("token", not("")); + //@formatter:on + } + } + + // Negative tests are in ZaasNegativeTest since they are common for the whole service +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/startup/CheckEnvironmentTest.java b/integration-tests/src/test/java/org/zowe/apiml/startup/CheckEnvironmentTest.java index 6726f577b5..57b63634a8 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/startup/CheckEnvironmentTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/startup/CheckEnvironmentTest.java @@ -19,6 +19,7 @@ import static org.apache.http.HttpStatus.SC_NO_CONTENT; import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.core.Is.is; +import static org.zowe.apiml.util.requests.Endpoints.ZOSMF_AUTH_ENDPOINT; @EnvironmentCheck @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -28,7 +29,6 @@ class CheckEnvironmentTest { private String password; private String zosmfHost; private int zosmfPort; - private String zosmfAuthEndpoint; private String zosmfProtectedEndpoint; private String zosmfScheme; @@ -39,7 +39,6 @@ void setUp() { password = config.getCredentials().getPassword(); zosmfHost = config.getZosmfServiceConfiguration().getHost(); zosmfPort = config.getZosmfServiceConfiguration().getPort(); - zosmfAuthEndpoint = "/zosmf/services/authenticate"; zosmfProtectedEndpoint = "/zosmf/restfiles/ds?dslevel=sys1.p*"; zosmfScheme = config.getZosmfServiceConfiguration().getScheme(); } @@ -53,7 +52,7 @@ void unblockLockedITUser() { .auth().preemptive().basic(username, new String(password)) .header("X-CSRF-ZOSMF-HEADER", "") .when() - .post(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, zosmfAuthEndpoint)) + .post(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, ZOSMF_AUTH_ENDPOINT)) .then().statusCode(is(SC_OK)) .extract().cookie("LtpaToken2"); // Logout LTPA @@ -61,7 +60,7 @@ void unblockLockedITUser() { .header("X-CSRF-ZOSMF-HEADER", "") .cookie("LtpaToken2", ltpa2) .when() - .delete(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, zosmfAuthEndpoint)) + .delete(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, ZOSMF_AUTH_ENDPOINT)) .then() .statusCode(is(SC_NO_CONTENT)); } @@ -75,7 +74,7 @@ void checkZosmfIsUpAndJwtTokenFromLoginCanBeUsed() { given().auth().preemptive().basic(username, new String(password)) .header("X-CSRF-ZOSMF-HEADER", "") .when() - .post(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, zosmfAuthEndpoint)) + .post(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, ZOSMF_AUTH_ENDPOINT)) .then().statusCode(is(SC_OK)) .extract().cookie("jwtToken"); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java index 3bca0d9c4a..06f111a610 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/SecurityUtils.java @@ -15,6 +15,8 @@ import com.nimbusds.jose.util.Base64; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; import io.restassured.RestAssured; import io.restassured.config.RestAssuredConfig; import io.restassured.config.SSLConfig; @@ -25,10 +27,19 @@ import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContexts; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.json.JSONObject; import org.springframework.http.HttpStatus; import org.zowe.apiml.gateway.security.login.SuccessfulAccessTokenHandler; import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.security.common.token.QueryResponse; import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.config.GatewayServiceConfiguration; import org.zowe.apiml.util.config.TlsConfiguration; @@ -37,13 +48,15 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; import java.net.URI; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; +import java.security.*; +import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.*; import static io.restassured.RestAssured.given; @@ -62,11 +75,16 @@ public class SecurityUtils { public final static String GATEWAY_TOKEN_COOKIE_NAME = "apimlAuthenticationToken"; private final static GatewayServiceConfiguration serviceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration(); + private final static TlsConfiguration tlsConfiguration = ConfigReader.environmentConfiguration().getTlsConfiguration(); private final static String gatewayScheme = serviceConfiguration.getScheme(); private final static String gatewayHost = serviceConfiguration.getHost(); private final static int gatewayPort = serviceConfiguration.getPort(); + private final static String zosmfScheme = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getScheme(); + private final static String zosmfHost = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getHost(); + private final static int zosmfPort = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getPort(); + public final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); public final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); @@ -131,6 +149,117 @@ public static String gatewayToken(URI gatewayLoginEndpoint, String username, Str return cookie; } + public static String getZosmfJwtToken() { + return getZosmfToken("jwtToken"); + } + + public static String getZosmfToken(String cookie) { + SSLConfig originalConfig = RestAssured.config().getSSLConfig(); + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + + String zosmfToken = given() + .contentType(JSON) + .auth().preemptive().basic(USERNAME, PASSWORD) + .header("X-CSRF-ZOSMF-HEADER", "") + .when() + .post(String.format("%s://%s:%d%s", zosmfScheme, zosmfHost, zosmfPort, ZOSMF_AUTH_ENDPOINT)) + .then() + .statusCode(is(SC_OK)) + .cookie(cookie, not(isEmptyString())) + .extract().cookie(cookie); + + RestAssured.config = RestAssured.config().sslConfig(originalConfig); + + return zosmfToken; + } + + public static String generateZoweJwtWithLtpa(String ltpaToken) throws UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + long now = System.currentTimeMillis(); + long expiration = now + 100_000L; + + return Jwts.builder() + .setSubject(USERNAME) + .setIssuedAt(new Date(now)) + .setExpiration(new Date(expiration)) + .setIssuer(QueryResponse.Source.ZOWE.value) + .setId(UUID.randomUUID().toString()) + .claim("ltpa", ltpaToken) + .signWith(getKey(), SignatureAlgorithm.RS256) + .compact(); + } + + public static String generateJwtWithRandomSignature(String issuer) { + long now = System.currentTimeMillis(); + long expiration = now + 100_000L; + + return Jwts.builder() + .setSubject(USERNAME) + .setIssuedAt(new Date(now)) + .setExpiration(new Date(expiration)) + .setIssuer(issuer) + .setId(UUID.randomUUID().toString()) + .setHeaderParam("kid", "apiKey") + .signWith(Keys.secretKeyFor(SignatureAlgorithm.HS256)) + .compact(); + } + + private static KeyStore loadKeystore(String keystore) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + File keyStoreFile = new File(keystore); + InputStream inputStream = new FileInputStream(keyStoreFile); + + KeyStore ks = KeyStore.getInstance(SecurityUtils.tlsConfiguration.getKeyStoreType()); + ks.load(inputStream, SecurityUtils.tlsConfiguration.getKeyStorePassword()); + + return ks; + } + + private static Key getKey() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException { + KeyStore ks = loadKeystore(SecurityUtils.tlsConfiguration.getKeyStore()); + + return ks.getKey(SecurityUtils.tlsConfiguration.getKeyAlias(), SecurityUtils.tlsConfiguration.getKeyStorePassword()); + } + + public static String getClientCertificate() throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + KeyStore ks = loadKeystore(SecurityUtils.tlsConfiguration.getClientKeystore()); + Certificate certificate = ks.getCertificate(ks.aliases().nextElement()); + + return Base64.encode(certificate.getEncoded()).toString(); + } + + public static String getDummyClientCertificate()throws CertificateException, NoSuchAlgorithmException, NoSuchProviderException, OperatorCreationException, IOException { + Security.addProvider(new BouncyCastleProvider()); + + long now = System.currentTimeMillis(); + Calendar calendar = Calendar.getInstance(); + + X500Name dnName = new X500Name("CN=USER"); + BigInteger certSerialNumber = new BigInteger(Long.toString(now)); + Date startDate = new Date(now); + calendar.setTime(startDate); + calendar.add(Calendar.YEAR, 1); + Date endDate = calendar.getTime(); + + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + + JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + dnName, + certSerialNumber, + startDate, + endDate, + dnName, + keyPair.getPublic()); + + String signatureAlgorithm = "SHA256WithRSA"; + ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm).build(keyPair.getPrivate()); + + X509CertificateHolder certificateHolder = certBuilder.build(contentSigner); + X509Certificate certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder); + + return Base64.encode(certificate.getEncoded()).toString(); + } + public static String personalAccessToken(Set scopes) { URI gatewayGenerateAccessTokenEndpoint = HttpRequestUtils.getUriFromGateway(GENERATE_ACCESS_TOKEN); SuccessfulAccessTokenHandler.AccessTokenRequest accessTokenRequest = new SuccessfulAccessTokenHandler.AccessTokenRequest(60, scopes); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/categories/ZaasTest.java b/integration-tests/src/test/java/org/zowe/apiml/util/categories/ZaasTest.java new file mode 100644 index 0000000000..2531077e2e --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/util/categories/ZaasTest.java @@ -0,0 +1,31 @@ +/* + * 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.util.categories; + +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +/** + * For testing ZAAS features + * It contains all authentication features enabled and the typical z/OSMF configuration. + * It can be reused also for other testing. Rename this tag if reused for other purposes. + */ +@Tag("ZaasTest") +@Target({ TYPE, METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ZaasTest { +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java index eb32410714..c3c3b20afb 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/Endpoints.java @@ -24,6 +24,8 @@ public class Endpoints { public final static String ROUTED_LOGIN = "/gateway/api/v1/auth/login"; public final static String ROUTED_LOGOUT = "/gateway/api/v1/auth/logout"; + public final static String ZAAS_ZOSMF_ENDPOINT = "/gateway/zaas/zosmf"; + public final static String ROUTED_LOGIN_OLD_FORMAT = "/gateway/api/v1/auth/login"; public final static String ROUTED_LOGOUT_OLD_FORMAT = "/gateway/api/v1/auth/logout"; @@ -72,4 +74,6 @@ public class Endpoints { public final static String API_SERVICE_VERSION_DIFF_ENDPOINT_WRONG_SERVICE = "/apicatalog/api/v1/apidoc/invalidService/v1/v2"; public final static String CLOUD_GATEWAY_CERTIFICATES = "/gateway/certificates"; + + public final static String ZOSMF_AUTH_ENDPOINT = "/zosmf/services/authenticate"; } diff --git a/mock-services/src/main/resources/application.yml b/mock-services/src/main/resources/application.yml index 3be75226a3..bbfdee7b7e 100644 --- a/mock-services/src/main/resources/application.yml +++ b/mock-services/src/main/resources/application.yml @@ -33,7 +33,7 @@ zosmf: username: USER,APIMTST,USER1,USER2 password: validPassword,PASSTICKET baseVersion: 2.4 - appliedApars: PH12143 + appliedApars: PH34201,JwtKeys jwtKeyStorePath: keystore/localhost/localhost.keystore.p12 timeout: 60