Skip to content

Commit aae08aa

Browse files
committed
API for delegating credentials to generate a z/OS PassTicket
Signed-off-by: Gowtham Selvaraj <[email protected]>
1 parent 0bd682e commit aae08aa

File tree

4 files changed

+216
-2
lines changed

4 files changed

+216
-2
lines changed

gateway-service/src/main/java/org/zowe/apiml/gateway/config/AuthEndpointConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ RouterFunction<ServerResponse> routes() {
145145
.andRoute(path("/gateway/api/v1/auth/keys/public/current"), resendTo("/api/v1/auth/keys/public/current"))
146146
.andRoute(path("/gateway/api/v1/auth/oidc-token/validate"), resendTo("/api/v1/auth/oidc-token/validate"))
147147
.andRoute(path("/gateway/api/v1/auth/oidc/webfinger"), resendTo("/api/v1/auth/oidc/webfinger"))
148-
.andRoute(path("/gateway/auth/check"), resendTo("/auth/check"));
148+
.andRoute(path("/gateway/auth/check"), resendTo("/auth/check"))
149+
.andRoute(path("/gateway/api/v1/auth/delegations/passticket"), resendTo("/api/v1/auth/delegations/passticket"));
149150
}
150151

151152
}

gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public class WebSecurity {
130130
public static final String OAUTH_2_AUTHORIZATION_URI = CONTEXT_PATH + "/oauth2/authorization/{registrationId}";
131131
public static final String OAUTH_2_REDIRECT_URI = CONTEXT_PATH + "/login/oauth2/code/**";
132132
public static final String OAUTH_2_REDIRECT_LOGIN_URI = CONTEXT_PATH + "/login/oauth2/code/{registrationId}";
133+
public static final String STS_PASSTICKET_URL = "/gateway/api/v1/auth/delegations/passticket";
133134

134135
@Value("${apiml.security.oidc.cookie.sameSite:Lax}")
135136
public String sameSite;
@@ -367,7 +368,7 @@ SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity http) {
367368
@Bean
368369
@Order(1)
369370
@ConditionalOnMissingBean(name = "modulithConfig")
370-
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) {
371+
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) {
371372
return defaultSecurityConfig(http)
372373
.securityMatcher(ServerWebExchangeMatchers.pathMatchers(
373374
REGISTRY_PATH,
@@ -380,6 +381,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi
380381
CONFORMANCE_LONG_URL,
381382
VALIDATE_SHORT_URL,
382383
VALIDATE_LONG_URL,
384+
STS_PASSTICKET_URL,
383385
"/application/**"
384386
))
385387
.authorizeExchange(authorizeExchangeSpec -> {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
11+
package org.zowe.apiml.zaas.controllers;
12+
13+
import io.swagger.v3.oas.annotations.Operation;
14+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
15+
import lombok.Builder;
16+
import lombok.Data;
17+
import lombok.RequiredArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.apache.logging.log4j.util.Strings;
22+
23+
import org.springframework.beans.factory.annotation.Value;
24+
import org.springframework.http.MediaType;
25+
import org.springframework.http.ResponseEntity;
26+
import org.springframework.web.bind.annotation.*;
27+
import org.zowe.commons.usermap.MapperResponse;
28+
import org.zowe.apiml.passticket.PassTicketService;
29+
import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper;
30+
31+
32+
/**
33+
* Controller offer method to control security. It can contain method for user
34+
* and also method for calling services
35+
* by gateway to distribute state of authentication between nodes.
36+
*/
37+
@RequiredArgsConstructor
38+
@RestController
39+
@RequestMapping(StsController.CONTROLLER_PATH)
40+
@Slf4j
41+
public class StsController {
42+
43+
@Value("${apiml.security.oidc.registry:}")
44+
protected String registry;
45+
46+
private final PassTicketService passTicketService;
47+
private final NativeMapperWrapper nativeMapper;
48+
49+
public static final String CONTROLLER_PATH = "/zaas/api/v1/auth/delegations";
50+
public static final String PASSTICKET_PATH = "/passticket";
51+
52+
@PostMapping(value = StsController.PASSTICKET_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
53+
@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 = {
54+
"Security" }, security = {
55+
@SecurityRequirement(name = "Bearer"),
56+
@SecurityRequirement(name = "LoginBasicAuth"),
57+
@SecurityRequirement(name = "ClientCert")
58+
})
59+
public ResponseEntity<PassTicketResponse> getPassTicket(@RequestBody PassTicketRequest passticketRequest)
60+
throws Exception {
61+
String applID = passticketRequest.getApplId();
62+
String emailID = passticketRequest.getEmailId();
63+
String zosUserId = "";
64+
65+
if (Strings.isBlank(emailID) || Strings.isBlank(applID)) {
66+
return ResponseEntity.badRequest().build();
67+
}
68+
try {
69+
MapperResponse response = nativeMapper.getUserIDForDN(emailID, registry);
70+
if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) {
71+
zosUserId = response.getUserId();
72+
}
73+
log.info("Getting ZOS_User_id: {} ", zosUserId);
74+
var ticket = passTicketService.generate(zosUserId, applID);
75+
log.info("Getting request email id: {} and ZOS_Userid: {}", emailID, zosUserId);
76+
return ResponseEntity.ok(new PassTicketResponse(ticket, zosUserId));
77+
} catch (Exception ex) {
78+
log.error("Error calling delegations passticket api", ex);
79+
throw ex;
80+
}
81+
}
82+
83+
@Data
84+
public static class PassTicketRequest {
85+
private String emailId;
86+
private String applId;
87+
}
88+
89+
@Data
90+
@Builder
91+
public static class PassTicketResponse {
92+
private String passticket;
93+
private String tsoUserid;
94+
}
95+
96+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
11+
package org.zowe.apiml.zaas.controllers;
12+
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
import org.mockito.*;
16+
import org.springframework.http.ResponseEntity;
17+
import org.zowe.apiml.passticket.PassTicketService;
18+
import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper;
19+
import org.zowe.commons.usermap.MapperResponse;
20+
21+
import static org.junit.jupiter.api.Assertions.*;
22+
import static org.mockito.Mockito.*;
23+
24+
class StsControllerTest {
25+
26+
@Mock
27+
private PassTicketService passTicketService;
28+
29+
@Mock
30+
private NativeMapperWrapper nativeMapper;
31+
32+
@InjectMocks
33+
private StsController stsController;
34+
35+
@BeforeEach
36+
void setUp() {
37+
MockitoAnnotations.openMocks(this);
38+
stsController.registry = "testRegistry";
39+
}
40+
41+
@Test
42+
void testGetPassTicket_Success() throws Exception {
43+
StsController.PassTicketRequest request = new StsController.PassTicketRequest();
44+
request.setApplId("TESTAPP");
45+
request.setEmailId("[email protected]");
46+
47+
MapperResponse mapperResponse = new MapperResponse("ZOSUSER", 0, 0, 0, 0);
48+
49+
when(nativeMapper.getUserIDForDN("[email protected]", "testRegistry")).thenReturn(mapperResponse);
50+
when(passTicketService.generate("ZOSUSER", "TESTAPP")).thenReturn("TICKET123");
51+
52+
ResponseEntity<StsController.PassTicketResponse> response = stsController.getPassTicket(request);
53+
assertEquals(200, response.getStatusCode().value());
54+
assertNotNull(response.getBody());
55+
assertEquals("TICKET123", response.getBody().getPassticket());
56+
assertEquals("ZOSUSER", response.getBody().getTsoUserid());
57+
58+
verify(nativeMapper).getUserIDForDN("[email protected]", "testRegistry");
59+
verify(passTicketService).generate("ZOSUSER", "TESTAPP");
60+
}
61+
62+
@Test
63+
void testGetPassTicket_BadRequest_BlankEmail() throws Exception {
64+
StsController.PassTicketRequest request = new StsController.PassTicketRequest();
65+
request.setApplId("APPID");
66+
request.setEmailId("");
67+
68+
ResponseEntity<StsController.PassTicketResponse> response = stsController.getPassTicket(request);
69+
70+
assertEquals(400, response.getStatusCode().value());
71+
verifyNoInteractions(passTicketService, nativeMapper);
72+
}
73+
74+
@Test
75+
void testGetPassTicket_BadRequest_BlankApplId() throws Exception {
76+
StsController.PassTicketRequest request = new StsController.PassTicketRequest();
77+
request.setEmailId("[email protected]");
78+
request.setApplId("");
79+
80+
ResponseEntity<StsController.PassTicketResponse> response = stsController.getPassTicket(request);
81+
82+
assertEquals(400, response.getStatusCode().value());
83+
verifyNoInteractions(passTicketService, nativeMapper);
84+
}
85+
86+
@Test
87+
void testGetPassTicket_NativeMapperFailure() throws Exception {
88+
StsController.PassTicketRequest request = new StsController.PassTicketRequest();
89+
request.setApplId("APPID");
90+
request.setEmailId("[email protected]");
91+
92+
when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenThrow(new RuntimeException("Mapper failed"));
93+
94+
Exception exception = assertThrows(RuntimeException.class, () -> stsController.getPassTicket(request));
95+
assertEquals("Mapper failed", exception.getMessage());
96+
}
97+
98+
@Test
99+
void testGetPassTicket_MapperReturnsNoUser() throws Exception {
100+
StsController.PassTicketRequest request = new StsController.PassTicketRequest();
101+
request.setApplId("APPID");
102+
request.setEmailId("[email protected]");
103+
104+
MapperResponse mapperResponse = new MapperResponse("", 0, 0, 0, 0);
105+
106+
when(nativeMapper.getUserIDForDN(anyString(), anyString())).thenReturn(mapperResponse);
107+
when(passTicketService.generate("", "APPID")).thenReturn("TICKET123");
108+
109+
ResponseEntity<StsController.PassTicketResponse> response = stsController.getPassTicket(request);
110+
111+
assertEquals(200, response.getStatusCode().value());
112+
assertEquals("TICKET123", response.getBody().getPassticket());
113+
assertEquals("", response.getBody().getTsoUserid());
114+
}
115+
}

0 commit comments

Comments
 (0)