Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ffa865b
API for delegating credentials to generate a z/OS PassTicket
Goutham024 Oct 29, 2025
8390393
API for delegating credentials to generate a z/OS PassTicket
Goutham024 Oct 29, 2025
0901f8f
Merge branch 'passticket-delegation-api' of https://github.com/Joe-Wi…
Goutham024 Nov 6, 2025
ea28f17
feat: Support for keyrings with ICSF keys (#4354)
arxioly Oct 29, 2025
2c824bc
fix: environment variable in start.sh for ICSF (#4369)
pablocarle Oct 30, 2025
a444184
refactor (enabler-nodejs): Initialize client only if config files exi…
pketki Oct 31, 2025
de1a6e7
[Gradle Release plugin] [skip ci] Before tag commit 'enablers-v3.3.19'.
zowe-robot Oct 31, 2025
3849a08
[Gradle Release plugin] Create new version: 'enablers-v3.3.20-SNAPSHOT'.
zowe-robot Oct 31, 2025
7c59860
[skip ci] Update version
zowe-robot Oct 31, 2025
12b77b4
chore: Starlette dependency removal (#4371)
pavel-jares-bcm Oct 31, 2025
2ee95e9
feat: Cut SMF record about mapping OIDC token to mainframe credential…
pavel-jares-bcm Nov 4, 2025
4863275
chore: Fix log level of Apache Tomcat in Gateway (v3) (#4372)
pavel-jares-bcm Nov 4, 2025
7b45e7a
chore: Remove default configuration provided by manifest files in Gat…
pavel-jares-bcm Nov 4, 2025
859efcc
feat: Update z/OSMF template to use SAF provider as the default one (…
pavel-jares-bcm Nov 4, 2025
6f8d8dd
fix: Certificate chain parsing with Java JCA Hybrid provider (#4376)
pablocarle Nov 5, 2025
bbeee64
chore: Fix manifest file validity (#4379)
pavel-jares-bcm Nov 5, 2025
ec01302
chore: Update all non-major dependencies (v3.x.x) (#4309)
zowe-robot Nov 5, 2025
f0be94c
fix: add service ID validation (#4375)
taban03 Nov 5, 2025
135eb7b
feat: Enable opentelemetry for modulith (#4380)
richard-salac Nov 5, 2025
ba4c0a7
API for delegating credentials to generate a z/OS PassTicket
Goutham024 Oct 29, 2025
9fa48f3
Merge branch 'passticket-delegation-api' of https://github.com/Joe-Wi…
Goutham024 Nov 6, 2025
322b756
Merge branch 'v3.x.x' into passticket-delegation-api
Goutham024 Nov 12, 2025
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 @@ -145,7 +145,8 @@ RouterFunction<ServerResponse> 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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -380,6 +381,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi
CONFORMANCE_LONG_URL,
VALIDATE_SHORT_URL,
VALIDATE_LONG_URL,
STS_PASSTICKET_URL,
"/application/**"
))
.authorizeExchange(authorizeExchangeSpec -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PassTicketResponse> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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("[email protected]");

MapperResponse mapperResponse = new MapperResponse("ZOSUSER", 0, 0, 0, 0);

when(nativeMapper.getUserIDForDN("[email protected]", "testRegistry")).thenReturn(mapperResponse);
when(passTicketService.generate("ZOSUSER", "TESTAPP")).thenReturn("TICKET123");

ResponseEntity<StsController.PassTicketResponse> 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("[email protected]", "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<StsController.PassTicketResponse> 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("[email protected]");
request.setApplId("");

ResponseEntity<StsController.PassTicketResponse> 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("[email protected]");

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("[email protected]");

MapperResponse mapperResponse = new MapperResponse("", 0, 0, 0, 0);

when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenReturn(mapperResponse);
when(passTicketService.generate("", "APPID")).thenReturn("TICKET123");

ResponseEntity<StsController.PassTicketResponse> response = stsController.getPassTicket(request);

assertEquals(200, response.getStatusCode().value());
assertEquals("TICKET123", response.getBody().getPassticket());
assertEquals("", response.getBody().getTsoUserid());
}
}
Loading