Skip to content

Commit

Permalink
feat: ZAAS /safIdt endpoint to generate SAF ID token for authenticate…
Browse files Browse the repository at this point in the history
…d user (#3220)

* New ZAAS safIdt endpoint to generate SAF ID tokens for authenticated user.

Signed-off-by: Petr Weinfurt <[email protected]>

* IT tests for safIdt endpoint.

Signed-off-by: Petr Weinfurt <[email protected]>

* Address code review comments.

Signed-off-by: Petr Weinfurt <[email protected]>

* Remove unused class

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix IT tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix unit tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Add Controller Advice to handle exceptions.

Signed-off-by: Petr Weinfurt <[email protected]>

* Handle more exceptions.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add content type and body to negative test.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add content type and body to negative test.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add content type and body to negative test.

Signed-off-by: Petr Weinfurt <[email protected]>

* Add negative tests with valid Okta token and no mapping.

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix Rest assured RequestSpec preparation.

Signed-off-by: Petr Weinfurt <[email protected]>

* Checkout the main branch before Sonar scan to resolve issue 'Could not find ref 'v2.x.x' in refs/heads, refs/remotes/upstream or refs/remotes/origin.'

Signed-off-by: Petr Weinfurt <[email protected]>

* Fetch the main branch before Sonar scan to resolve issue 'Could not find ref 'v2.x.x' in refs/heads, refs/remotes/upstream or refs/remotes/origin.'

Signed-off-by: Petr Weinfurt <[email protected]>

* Replace deprecated sonar.login property

Signed-off-by: Petr Weinfurt <[email protected]>

* Fetch depth 0

Signed-off-by: Petr Weinfurt <[email protected]>

* Handle TokenNotValid and TokenExpired exception with 401 response.

Signed-off-by: Petr Weinfurt <[email protected]>

---------

Signed-off-by: Petr Weinfurt <[email protected]>
Signed-off-by: Pavel Jares <[email protected]>
  • Loading branch information
weinfurt authored and pj892031 committed Dec 18, 2023
1 parent c54f7f7 commit 62f6e67
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 109 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ jobs:
./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 }}
-Doidc.test.pass=${{ secrets.OIDC_TEST_PASS }} -Doidc.test.alt_user=${{ secrets.OKTA_WINNIE_USER }}
-Doidc.test.alt_pass=${{ secrets.OKTA_WINNIE_PASS }}
- name: Dump DC jacoco data
run: >
Expand Down Expand Up @@ -1493,8 +1494,9 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}

- uses: ./.github/actions/setup
Expand Down Expand Up @@ -1541,8 +1543,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,containercitestszaas/results"
-Psonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN
-Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD
-Psonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD
env:
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +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.saf.SafIdtProvider;
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.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
Expand All @@ -37,6 +39,7 @@ public class TokenCreationService {
private final ZosmfService zosmfService;
private final PassTicketService passTicketService;
private final AuthenticationService authenticationService;
private final SafIdtProvider safIdtProvider;

@Value("${apiml.security.zosmf.applid:IZUDFLT}")
protected String zosmfApplId;
Expand Down Expand Up @@ -74,6 +77,17 @@ public Map<ZosmfService.TokenType, String> createZosmfTokensWithoutCredentials(S
return zosmfService.authenticate(new UsernamePasswordAuthenticationToken(user, passTicket)).getTokens();
}

public String createSafIdTokenWithoutCredentials(String user, String applId) throws IRRPassTicketGenerationException {

char[] passTicket = "".toCharArray();
try {
passTicket = passTicketService.generate(user, applId).toCharArray();
return safIdtProvider.generate(user, passTicket, applId);
} finally {
Arrays.fill(passTicket, (char) 0);
}
}

private boolean isZosmfAvailable() {
try {
return providers.isZosfmUsed() && providers.isZosmfAvailable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,108 +12,94 @@

import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
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.AuthSourceService;
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.gateway.security.ticket.ApplicationNameNotFoundException;
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.ZaasTokenResponse;

import javax.management.ServiceNotFoundException;

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;
import static org.zowe.apiml.security.SecurityUtils.COOKIE_AUTH_NAME;

@RequiredArgsConstructor
@RestController
@RequestMapping(ZaasController.CONTROLLER_PATH)
@Slf4j
public class ZaasController {
public static final String CONTROLLER_PATH = "gateway/zaas";

private final AuthSourceService authSourceService;
private final MessageService messageService;
private final PassTicketService passTicketService;
private final ZosmfService zosmfService;
private final TokenCreationService tokenCreationService;

@PostMapping(path = "ticket", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Provides PassTicket for authenticated user.")
public ResponseEntity<Object> getPassTicket(@RequestBody TicketRequest ticketRequest, @RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed) {

if (StringUtils.isEmpty(authSourceParsed.getUserId())) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.build();
}
public ResponseEntity<TicketResponse> getPassTicket(@RequestBody TicketRequest ticketRequest, @RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed)
throws IRRPassTicketGenerationException, ApplicationNameNotFoundException {

final String applicationName = ticketRequest.getApplicationName();
if (StringUtils.isBlank(applicationName)) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.invalidApplicationName").mapToView();
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(messageView);
throw new ApplicationNameNotFoundException("ApplicationName not provided.");
}

String ticket = null;
try {
ticket = passTicketService.generate(authSourceParsed.getUserId(), applicationName);
} catch (IRRPassTicketGenerationException e) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed",
e.getErrorCode().getMessage()).mapToView();
return ResponseEntity
.status(e.getHttpStatus())
.body(messageView);
}
String ticket = passTicketService.generate(authSourceParsed.getUserId(), applicationName);

return ResponseEntity
.status(HttpStatus.OK)
.body(new TicketResponse(null, authSourceParsed.getUserId(), applicationName, ticket));
.body(new TicketResponse("", authSourceParsed.getUserId(), applicationName, ticket));
}

@PostMapping(path = "zosmf", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Provides z/OSMF JWT or LTPA token for authenticated user.")
public ResponseEntity<Object> getZosmfToken(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource,
@RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed) {
try {
ZaasTokenResponse zaasTokenResponse = zosmfService.exchangeAuthenticationForZosmfToken(authSource.getRawSource().toString(), authSourceParsed);

return ResponseEntity
.status(HttpStatus.OK)
.body(zaasTokenResponse);

} catch (Exception e) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.zosmf.noZosmfTokenReceived", e.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(messageView);
}
public ResponseEntity<ZaasTokenResponse> getZosmfToken(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource,
@RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed) throws ServiceNotFoundException {

ZaasTokenResponse zaasTokenResponse = zosmfService.exchangeAuthenticationForZosmfToken(authSource.getRawSource().toString(), authSourceParsed);

return ResponseEntity
.status(HttpStatus.OK)
.body(zaasTokenResponse);
}


@PostMapping(path = "zoweJwt", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Provides zoweJwt for authenticated user.")
public ResponseEntity<Object> getZoweJwt(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource) {
try {
String token = authSourceService.getJWT(authSource);

return ResponseEntity
.status(HttpStatus.OK)
.body(new ZaasTokenResponse(COOKIE_AUTH_NAME, token));

} catch (Exception e) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.zoweJwt.noToken", e.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(messageView);
public ResponseEntity<ZaasTokenResponse> getZoweJwt(@RequestAttribute(AUTH_SOURCE_ATTR) AuthSource authSource) {

String token = authSourceService.getJWT(authSource);

return ResponseEntity
.status(HttpStatus.OK)
.body(new ZaasTokenResponse(COOKIE_AUTH_NAME, token));
}

@PostMapping(path = "safIdt", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Provides SAF Identity Token for authenticated user.")
public ResponseEntity<ZaasTokenResponse> getSafIdToken(@RequestBody TicketRequest ticketRequest, @RequestAttribute(AUTH_SOURCE_PARSED_ATTR) AuthSource.Parsed authSourceParsed)
throws IRRPassTicketGenerationException, ApplicationNameNotFoundException {

final String applicationName = ticketRequest.getApplicationName();
if (StringUtils.isBlank(applicationName)) {
throw new ApplicationNameNotFoundException("ApplicationName not provided.");
}

String safIdToken = tokenCreationService.createSafIdTokenWithoutCredentials(authSourceParsed.getUserId(), applicationName);
return ResponseEntity
.status(HttpStatus.OK)
.body(new ZaasTokenResponse("", safIdToken));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.zowe.apiml.gateway.security.service.saf.SafIdtAuthException;
import org.zowe.apiml.gateway.security.service.saf.SafIdtException;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException;
import org.zowe.apiml.gateway.security.ticket.ApplicationNameNotFoundException;
import org.zowe.apiml.message.api.ApiMessageView;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.passticket.IRRPassTicketGenerationException;
import org.zowe.apiml.security.common.token.TokenExpireException;
import org.zowe.apiml.security.common.token.TokenNotValidException;

import javax.management.ServiceNotFoundException;

@ControllerAdvice
@RequiredArgsConstructor
public class ZaasExceptionHandler {
private final MessageService messageService;

@ExceptionHandler(value = {IRRPassTicketGenerationException.class})
public ResponseEntity<ApiMessageView> handlePassTicketException(IRRPassTicketGenerationException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed",
ex.getErrorCode().getMessage()).mapToView();
return ResponseEntity
.status(ex.getHttpStatus())
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {SafIdtException.class, SafIdtAuthException.class})
public ResponseEntity<ApiMessageView> handleSafIdtExceptions(RuntimeException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.idt.failed", ex.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {ApplicationNameNotFoundException.class})
public ResponseEntity<ApiMessageView> handleApplIdNotFoundException(ApplicationNameNotFoundException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.invalidApplicationName").mapToView();
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {ServiceNotFoundException.class})
public ResponseEntity<ApiMessageView> handleServiceNotFoundException(ServiceNotFoundException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.zosmf.noZosmfTokenReceived", ex.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {IllegalStateException.class})
public ResponseEntity<ApiMessageView> handleZoweJwtCreationErrors(IllegalStateException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.zaas.zoweJwt.noToken", ex.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {TokenNotValidException.class, AuthSchemeException.class})
public ResponseEntity<ApiMessageView> handleTokenNotValidException(RuntimeException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.gateway.security.invalidToken").mapToView();
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}

@ExceptionHandler(value = {TokenExpireException.class})
public ResponseEntity<ApiMessageView> handleTokenExpiredException(TokenExpireException ex) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.gateway.security.expiredToken").mapToView();
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON)
.body(messageView);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
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.saf.SafIdtException;
import org.zowe.apiml.gateway.security.service.saf.SafIdtProvider;
import org.zowe.apiml.gateway.security.service.zosmf.ZosmfService;
import org.zowe.apiml.passticket.IRRPassTicketGenerationException;
import org.zowe.apiml.passticket.PassTicketService;
Expand Down Expand Up @@ -58,15 +60,19 @@ class TokenCreationServiceTest {
@Mock
private ZosmfService zosmfService;

@Mock
private SafIdtProvider safIdtProvider;

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";
private final String VALID_SAFIDT = "validSAFIdentityToken";

@BeforeEach
void setUp() {
underTest = new TokenCreationService(providers, Optional.of(zosmfAuthenticationProvider), zosmfService, passTicketService, authenticationService);
underTest = new TokenCreationService(providers, Optional.of(zosmfAuthenticationProvider), zosmfService, passTicketService, authenticationService, safIdtProvider);
underTest.zosmfApplId = "IZUDFLT";
}

Expand Down Expand Up @@ -140,4 +146,36 @@ void givenZosmfAvailable_whenCreatingZosmfToken_thenReturnEmptyResult() throws I
assertEquals(expectedTokens, tokens);
}

@Test
void givenPassTicketGenerated_whenCreatingSafIdToken_thenTokenReturned() throws IRRPassTicketGenerationException {
when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenReturn(PASSTICKET);
when(safIdtProvider.generate(VALID_USER_ID, PASSTICKET.toCharArray(), VALID_ZOSMF_APPLID)).thenReturn(VALID_SAFIDT);

String safIdt = underTest.createSafIdTokenWithoutCredentials(VALID_USER_ID, VALID_ZOSMF_APPLID);

assertEquals(VALID_SAFIDT, safIdt);
}

@Test
void givenPassTicketException_whenCreatingSafIdToken_thenExceptionThrown() throws IRRPassTicketGenerationException {
when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenThrow(new IRRPassTicketGenerationException(8, 8, 8));

Exception e = assertThrows(IRRPassTicketGenerationException.class, () -> {
underTest.createSafIdTokenWithoutCredentials(VALID_USER_ID, VALID_ZOSMF_APPLID);
});

assertEquals("Error on generation of PassTicket: An internal error was encountered.", e.getMessage());
}

@Test
void givenSafIdtException_whenCreatingSafIdToken_thenExceptionThrown() throws IRRPassTicketGenerationException {
when(passTicketService.generate(VALID_USER_ID, VALID_ZOSMF_APPLID)).thenReturn(PASSTICKET);
when(safIdtProvider.generate(VALID_USER_ID, PASSTICKET.toCharArray(), VALID_ZOSMF_APPLID)).thenThrow(new SafIdtException("Test exception"));

Exception e = assertThrows(SafIdtException.class, () -> {
underTest.createSafIdTokenWithoutCredentials(VALID_USER_ID, VALID_ZOSMF_APPLID);
});

assertEquals("Test exception", e.getMessage());
}
}
Loading

0 comments on commit 62f6e67

Please sign in to comment.