Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ZAAS /safIdt endpoint to generate SAF ID token for authenticated user #3220

Merged
merged 24 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ee4c94a
New ZAAS safIdt endpoint to generate SAF ID tokens for authenticated …
weinfurt Nov 28, 2023
31e9b61
IT tests for safIdt endpoint.
weinfurt Nov 28, 2023
2ff2793
Merge branch 'v2.x.x' into reboot/zaas/safIdt-endpoint
weinfurt Nov 30, 2023
e21c6de
Address code review comments.
weinfurt Nov 30, 2023
84e1a54
Remove unused class
weinfurt Nov 30, 2023
61d2316
Fix tests
weinfurt Nov 30, 2023
51ff2c4
Fix IT tests
weinfurt Nov 30, 2023
1b54833
Fix unit tests
weinfurt Nov 30, 2023
b569759
Add Controller Advice to handle exceptions.
weinfurt Nov 30, 2023
5c2d37c
Merge branch 'v2.x.x' into reboot/zaas/safIdt-endpoint
weinfurt Dec 1, 2023
1897532
Handle more exceptions.
weinfurt Dec 1, 2023
2a0d54b
Merge remote-tracking branch 'origin/reboot/zaas/safIdt-endpoint' int…
weinfurt Dec 1, 2023
61a66ad
Merge branch 'v2.x.x' into reboot/zaas/safIdt-endpoint
weinfurt Dec 4, 2023
e89e6dd
Add content type and body to negative test.
weinfurt Dec 4, 2023
7e413c8
Add content type and body to negative test.
weinfurt Dec 4, 2023
d6393ff
Add content type and body to negative test.
weinfurt Dec 4, 2023
4d20aff
Add negative tests with valid Okta token and no mapping.
weinfurt Dec 4, 2023
aad69b8
Fix Rest assured RequestSpec preparation.
weinfurt Dec 4, 2023
e280d5e
Checkout the main branch before Sonar scan to resolve issue 'Could no…
weinfurt Dec 5, 2023
67b217d
Fetch the main branch before Sonar scan to resolve issue 'Could not f…
weinfurt Dec 6, 2023
e3ba8ba
Replace deprecated sonar.login property
weinfurt Dec 6, 2023
c5ae34f
Fetch depth 0
weinfurt Dec 6, 2023
b10699b
Merge branch 'v2.x.x' into reboot/zaas/safIdt-endpoint
weinfurt Dec 8, 2023
0f71a15
Handle TokenNotValid and TokenExpired exception with 401 response.
weinfurt Dec 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -18,6 +18,7 @@
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;
Expand All @@ -44,6 +45,7 @@ public class ZaasController {
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.")
Expand Down Expand Up @@ -116,4 +118,43 @@ public ResponseEntity<Object> getZoweJwt(@RequestAttribute(AUTH_SOURCE_ATTR) Aut
}
}

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

JirkaAichler marked this conversation as resolved.
Show resolved Hide resolved
final String userId = authSourceParsed.getUserId();
if (StringUtils.isEmpty(userId)) {
weinfurt marked this conversation as resolved.
Show resolved Hide resolved
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.build();
}

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);
}

try {
String safIdToken = tokenCreationService.createSafIdTokenWithoutCredentials(userId, applicationName);
return ResponseEntity
.status(HttpStatus.OK)
.body(new ZaasTokenResponse(null, safIdToken));
weinfurt marked this conversation as resolved.
Show resolved Hide resolved

} catch (IRRPassTicketGenerationException e) {
weinfurt marked this conversation as resolved.
Show resolved Hide resolved
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed",
e.getErrorCode().getMessage()).mapToView();
return ResponseEntity
.status(e.getHttpStatus())
.body(messageView);
} catch (Exception e) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.idt.failed", e.getMessage()).mapToView();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.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(e.getMessage(), "Error on generation of PassTicket: An internal error was encountered.");
}

@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(e.getMessage(), "Test exception");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
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.TokenCreationService;
import org.zowe.apiml.gateway.security.service.saf.SafIdtException;
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;
Expand Down Expand Up @@ -62,6 +64,9 @@ class ZaasControllerTest {
@Mock
private ZosmfService zosmfService;

@Mock
private TokenCreationService tokenCreationService;

private MockMvc mockMvc;
private JSONObject ticketBody;
private AuthSource authSource;
Expand All @@ -70,18 +75,20 @@ class ZaasControllerTest {
private static final String PASSTICKET_URL = "/gateway/zaas/ticket";
private static final String ZOSMF_TOKEN_URL = "/gateway/zaas/zosmf";
private static final String ZOWE_TOKEN_URL = "/gateway/zaas/zoweJwt";
private static final String SAFIDT_URL = "/gateway/zaas/safIdt";

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";
private static final String SAFIDT = "saf_id_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(authSourceService, messageService, passTicketService, zosmfService);
ZaasController zaasController = new ZaasController(authSourceService, messageService, passTicketService, zosmfService, tokenCreationService);
mockMvc = MockMvcBuilders.standaloneSetup(zaasController).build();
ticketBody = new JSONObject()
.put("applicationName", APPLID);
Expand Down Expand Up @@ -146,16 +153,40 @@ void whenRequestPassticketAndNoApplNameProvided_thenBadRequest() throws Exceptio
.andExpect(jsonPath("$.messages[0].messageContent", is("The 'applicationName' parameter name is missing.")));
}

@Test
void whenRequestSafIdtAndApplNameProvided_thenResponseOk() throws Exception {
when(tokenCreationService.createSafIdTokenWithoutCredentials(USER, APPLID)).thenReturn(SAFIDT);
mockMvc.perform(post(SAFIDT_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(ticketBody.toString())
.requestAttr(AUTH_SOURCE_PARSED_ATTR, authParsedSource))
.andExpect(status().is(SC_OK))
.andExpect(jsonPath("$.safIdToken", is(SAFIDT)));
}

@Test
void whenRequestSafIdtAndNoApplNameProvided_thenBadRequest() throws Exception {
when(tokenCreationService.createSafIdTokenWithoutCredentials(USER, APPLID)).thenReturn(SAFIDT);
ticketBody.put("applicationName", "");

mockMvc.perform(post(SAFIDT_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 {
when(passTicketService.generate(USER, APPLID)).thenThrow(new IRRPassTicketGenerationException(8, 8, 8));

mockMvc.perform(post(PASSTICKET_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(ticketBody.toString())
Expand Down Expand Up @@ -196,6 +227,35 @@ void whenRequestingZoweTokens_thenInternalServerError() throws Exception {
.andExpect(jsonPath("$.messages[0].messageContent", containsString(expectedMessage)));
}

@Test
void whenRequestingSafIdtAndPassticketException_thenInternalServerError() throws Exception {
when(passTicketService.generate(USER, APPLID)).thenThrow(new IRRPassTicketGenerationException(8, 8, 8));

mockMvc.perform(post(SAFIDT_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 whenRequestingSafIdtAndSafIdtException_thenInternalServerError() throws Exception {
when(tokenCreationService.createSafIdTokenWithoutCredentials(USER, APPLID)).thenThrow(new SafIdtException("Test exception message."));

mockMvc.perform(post(SAFIDT_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("ZWEAG150E"))
.andExpect(jsonPath("$.messages[0].messageContent", is("SAF IDT generation failed. Reason: Test exception message.")));
}
}
}

Expand All @@ -208,7 +268,7 @@ void setUp() {
}

@ParameterizedTest
@ValueSource(strings = {PASSTICKET_URL})
@ValueSource(strings = {PASSTICKET_URL, SAFIDT_URL})
void thenRespondUnauthorized(String url) throws Exception {
mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
Expand Down
Loading
Loading