diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java index 6350b7fb6a..90022b1d5a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java @@ -145,7 +145,8 @@ RouterFunction routes() { .andRoute(path("/gateway/api/v1/auth/keys/public/current"), resendTo("/api/v1/auth/keys/public/current")) .andRoute(path("/gateway/api/v1/auth/oidc-token/validate"), resendTo("/api/v1/auth/oidc-token/validate")) .andRoute(path("/gateway/api/v1/auth/oidc/webfinger"), resendTo("/api/v1/auth/oidc/webfinger")) - .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")); + .andRoute(path("/gateway/auth/check"), resendTo("/auth/check")) + .andRoute(path("/gateway/api/v1/auth/delegations/passticket"), resendTo("/api/v1/auth/delegations/passticket")); } } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index b38c0806e2..4e6beb3dfb 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -130,6 +130,7 @@ public class WebSecurity { public static final String OAUTH_2_AUTHORIZATION_URI = CONTEXT_PATH + "/oauth2/authorization/{registrationId}"; public static final String OAUTH_2_REDIRECT_URI = CONTEXT_PATH + "/login/oauth2/code/**"; public static final String OAUTH_2_REDIRECT_LOGIN_URI = CONTEXT_PATH + "/login/oauth2/code/{registrationId}"; + public static final String STS_PASSTICKET_URL = "/gateway/api/v1/auth/delegations/passticket"; @Value("${apiml.security.oidc.cookie.sameSite:Lax}") public String sameSite; @@ -367,7 +368,7 @@ SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity http) { @Bean @Order(1) @ConditionalOnMissingBean(name = "modulithConfig") - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { + SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { return defaultSecurityConfig(http) .securityMatcher(ServerWebExchangeMatchers.pathMatchers( REGISTRY_PATH, @@ -380,6 +381,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi CONFORMANCE_LONG_URL, VALIDATE_SHORT_URL, VALIDATE_LONG_URL, + STS_PASSTICKET_URL, "/application/**" )) .authorizeExchange(authorizeExchangeSpec -> { diff --git a/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java new file mode 100644 index 0000000000..1493c46185 --- /dev/null +++ b/zaas-service/src/main/java/org/zowe/apiml/zaas/controllers/StsController.java @@ -0,0 +1,96 @@ +/* + * 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.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.zowe.commons.usermap.MapperResponse; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; + + +/** + * Controller offer method to control security. It can contain method for user + * and also method for calling services + * by gateway to distribute state of authentication between nodes. + */ +@RequiredArgsConstructor +@RestController +@RequestMapping(StsController.CONTROLLER_PATH) +@Slf4j +public class StsController { + + @Value("${apiml.security.oidc.registry:}") + protected String registry; + + private final PassTicketService passTicketService; + private final NativeMapperWrapper nativeMapper; + + public static final String CONTROLLER_PATH = "/zaas/api/v1/auth/delegations"; + public static final String PASSTICKET_PATH = "/passticket"; + + @PostMapping(value = StsController.PASSTICKET_PATH, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(description = "The authenticated service uses this endpoint to request a PassTicket for a target user (identified by emailId) for a specific z/OS application (applid). The incoming Bearer token is validated to ensure the requester is authorized to perform delegation before the ticket is generated.", tags = { + "Security" }, security = { + @SecurityRequirement(name = "Bearer"), + @SecurityRequirement(name = "LoginBasicAuth"), + @SecurityRequirement(name = "ClientCert") + }) + public ResponseEntity getPassTicket(@RequestBody PassTicketRequest passticketRequest) + throws Exception { + String applID = passticketRequest.getApplId(); + String emailID = passticketRequest.getEmailId(); + String zosUserId = ""; + + if (Strings.isBlank(emailID) || Strings.isBlank(applID)) { + return ResponseEntity.badRequest().build(); + } + try { + MapperResponse response = nativeMapper.getUserIDForDN(emailID, registry); + if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) { + zosUserId = response.getUserId(); + } + log.info("Getting ZOS_User_id: {} ", zosUserId); + var ticket = passTicketService.generate(zosUserId, applID); + log.info("Getting request email id: {} and ZOS_Userid: {}", emailID, zosUserId); + return ResponseEntity.ok(new PassTicketResponse(ticket, zosUserId)); + } catch (Exception ex) { + log.error("Error calling delegations passticket api", ex); + throw ex; + } + } + + @Data + public static class PassTicketRequest { + private String emailId; + private String applId; + } + + @Data + @Builder + public static class PassTicketResponse { + private String passticket; + private String tsoUserid; + } + +} diff --git a/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java new file mode 100644 index 0000000000..26440df5c5 --- /dev/null +++ b/zaas-service/src/test/java/org/zowe/apiml/zaas/controllers/StsControllerTest.java @@ -0,0 +1,115 @@ +/* + * 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.controllers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.http.ResponseEntity; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; +import org.zowe.commons.usermap.MapperResponse; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class StsControllerTest { + + @Mock + private PassTicketService passTicketService; + + @Mock + private NativeMapperWrapper nativeMapper; + + @InjectMocks + private StsController stsController; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + stsController.registry = "testRegistry"; + } + + @Test + void testGetPassTicket_Success() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("TESTAPP"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("ZOSUSER", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN("test@company.com", "testRegistry")).thenReturn(mapperResponse); + when(passTicketService.generate("ZOSUSER", "TESTAPP")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + assertEquals(200, response.getStatusCode().value()); + assertNotNull(response.getBody()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("ZOSUSER", response.getBody().getTsoUserid()); + + verify(nativeMapper).getUserIDForDN("test@company.com", "testRegistry"); + verify(passTicketService).generate("ZOSUSER", "TESTAPP"); + } + + @Test + void testGetPassTicket_BadRequest_BlankEmail() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_BadRequest_BlankApplId() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setEmailId("test@company.com"); + request.setApplId(""); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(400, response.getStatusCode().value()); + verifyNoInteractions(passTicketService, nativeMapper); + } + + @Test + void testGetPassTicket_NativeMapperFailure() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenThrow(new RuntimeException("Mapper failed")); + + Exception exception = assertThrows(RuntimeException.class, () -> stsController.getPassTicket(request)); + assertEquals("Mapper failed", exception.getMessage()); + } + + @Test + void testGetPassTicket_MapperReturnsNoUser() throws Exception { + StsController.PassTicketRequest request = new StsController.PassTicketRequest(); + request.setApplId("APPID"); + request.setEmailId("test@company.com"); + + MapperResponse mapperResponse = new MapperResponse("", 0, 0, 0, 0); + + when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenReturn(mapperResponse); + when(passTicketService.generate("", "APPID")).thenReturn("TICKET123"); + + ResponseEntity response = stsController.getPassTicket(request); + + assertEquals(200, response.getStatusCode().value()); + assertEquals("TICKET123", response.getBody().getPassticket()); + assertEquals("", response.getBody().getTsoUserid()); + } +}