-
Notifications
You must be signed in to change notification settings - Fork 71
feat: API for delegating credentials to generate a z/OS PassTicket #4368
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
base: v3.x.x
Are you sure you want to change the base?
Changes from all commits
2c764b5
ccffa6d
e0c2235
484f486
5343f02
fa1c3ec
9daea3a
50f6ab7
3aa4c35
3331d0a
d0bcee0
113f4cf
2f9e149
51bf96e
c750887
6040e4e
e125f99
2225042
c995a20
716e1df
f3a1284
8dbefcf
6a003ca
fdcca51
364f61a
0f1482a
980fbad
d2df31a
89bd0de
e9e1ba4
f13d33f
1ba8f29
95235b3
58f08e5
7ff39b1
36f0f58
0e1a039
e0f0adf
8b1bfc7
cd2cfb5
60fe2e2
dcc1512
d1dd57b
9ddefa2
811e2f0
8d7bdf5
3d6244c
b28f0bb
ce28617
97a3068
1030490
9f17b46
f7e75ad
16616a6
a6f53fa
a12fb8c
1f4fa6b
b545036
2f5163a
c73ea05
72ae494
c0fcdcd
4d7c3f2
1005a4b
98ad289
4873326
a7f9faf
1d1c0d0
a74dcf9
16e93ae
4ef8221
a0897e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /* | ||
| * 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.apache.commons.lang3.StringUtils; | ||
| import org.apache.logging.log4j.util.Strings; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.zowe.apiml.passticket.PassTicketException; | ||
| import org.zowe.apiml.passticket.PassTicketService; | ||
| import org.zowe.apiml.zaas.security.mapping.NativeMapperWrapper; | ||
| import org.zowe.commons.usermap.MapperResponse; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Hidden; | ||
| import lombok.Builder; | ||
| import lombok.Data; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * 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(SecurityTokenServiceController.CONTROLLER_PATH) | ||
| @Slf4j | ||
| public class SecurityTokenServiceController { | ||
|
|
||
| @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/delegate"; | ||
| public static final String PASSTICKET_PATH = "/passticket"; | ||
|
|
||
| @PostMapping(value = SecurityTokenServiceController.PASSTICKET_PATH, produces = MediaType.APPLICATION_JSON_VALUE) | ||
| @ConditionalOnProperty(value = "apiml.security.delegatePassticket.enabled", havingValue = "true", matchIfMissing = false) | ||
| @PreAuthorize("@safMethodSecurityExpressionRoot.hasSafServiceResourceAccess('DELEGATE.PASSTICKET', 'READ',#root)") | ||
| @Hidden | ||
| 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)) { | ||
| log.debug("getPassTicket: Invalid applId or EmailId"); | ||
| return ResponseEntity.badRequest().build(); | ||
| } | ||
| try { | ||
| MapperResponse response = nativeMapper.getUserIDForDN(emailID, registry); | ||
| if (response.getRc() == 0 && StringUtils.isNotEmpty(response.getUserId())) { | ||
| zosUserId = response.getUserId(); | ||
| } | ||
| log.debug("getPassTicket: Processing request for ZOS userId: {}", zosUserId); | ||
| var ticket = passTicketService.generate(zosUserId, applID); | ||
| log.debug("getPassTicket: Request processed with emailId: {} and ZOS userId: {}", emailID, zosUserId); | ||
| return ResponseEntity.ok(new PassTicketResponse(ticket, zosUserId)); | ||
| } catch (PassTicketException ex) { | ||
| log.error("getPassTicket: Failed to generate passticket", ex); | ||
| return ResponseEntity.internalServerError().build(); | ||
| } | ||
| } | ||
|
|
||
| @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,150 @@ | ||
| /* | ||
| * 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 static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Nested; | ||
| import org.junit.jupiter.api.Test; | ||
| import static org.mockito.ArgumentMatchers.anyString; | ||
| import org.mockito.InjectMocks; | ||
| import org.mockito.Mock; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.verifyNoInteractions; | ||
| import static org.mockito.Mockito.when; | ||
| import org.mockito.MockitoAnnotations; | ||
| 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; | ||
|
|
||
| class SecurityTokenServiceControllerTest { | ||
|
|
||
| @Mock | ||
| private PassTicketService passTicketService; | ||
|
|
||
| @Mock | ||
| private NativeMapperWrapper nativeMapper; | ||
|
|
||
| @InjectMocks | ||
| private SecurityTokenServiceController SecurityTokenServiceController; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| MockitoAnnotations.openMocks(this); | ||
| SecurityTokenServiceController.registry = "testRegistry"; | ||
| } | ||
|
|
||
| @Nested | ||
| class SuccessfulRequests { | ||
|
|
||
| @Test | ||
| void shouldReturnPassTicketWhenRequestIsValid() throws Exception { | ||
| SecurityTokenServiceController.PassTicketRequest request = new SecurityTokenServiceController.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<SecurityTokenServiceController.PassTicketResponse> response = | ||
| SecurityTokenServiceController.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 shouldReturnPassTicketWhenMapperReturnsEmptyUser() throws Exception { | ||
| SecurityTokenServiceController.PassTicketRequest request = new SecurityTokenServiceController.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<SecurityTokenServiceController.PassTicketResponse> response = | ||
| SecurityTokenServiceController.getPassTicket(request); | ||
|
|
||
| assertEquals(200, response.getStatusCode().value()); | ||
| assertEquals("TICKET123", response.getBody().getPassticket()); | ||
| assertEquals("", response.getBody().getTsoUserid()); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| class BadRequests { | ||
|
|
||
| @Test | ||
| void shouldReturnBadRequestWhenEmailIsBlank() throws Exception { | ||
| SecurityTokenServiceController.PassTicketRequest request = new SecurityTokenServiceController.PassTicketRequest(); | ||
| request.setApplId("APPID"); | ||
| request.setEmailId(""); | ||
|
|
||
| ResponseEntity<SecurityTokenServiceController.PassTicketResponse> response = | ||
| SecurityTokenServiceController.getPassTicket(request); | ||
|
|
||
| assertEquals(400, response.getStatusCode().value()); | ||
| verifyNoInteractions(passTicketService, nativeMapper); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldReturnBadRequestWhenApplIdIsBlank() throws Exception { | ||
| SecurityTokenServiceController.PassTicketRequest request = new SecurityTokenServiceController.PassTicketRequest(); | ||
| request.setEmailId("test@company.com"); | ||
| request.setApplId(""); | ||
|
|
||
| ResponseEntity<SecurityTokenServiceController.PassTicketResponse> response = | ||
| SecurityTokenServiceController.getPassTicket(request); | ||
|
|
||
| assertEquals(400, response.getStatusCode().value()); | ||
| verifyNoInteractions(passTicketService, nativeMapper); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| class FailureScenarios { | ||
|
|
||
| @Test | ||
| void shouldPropagateExceptionWhenNativeMapperFails() throws Exception { | ||
| SecurityTokenServiceController.PassTicketRequest request = new SecurityTokenServiceController.PassTicketRequest(); | ||
| request.setApplId("APPID"); | ||
| request.setEmailId("test@company.com"); | ||
|
|
||
| when(nativeMapper.getUserIDForDN(anyString(), anyString())) | ||
| .thenThrow(new RuntimeException("Mapper failed")); | ||
|
|
||
| RuntimeException exception = assertThrows( | ||
| RuntimeException.class, | ||
| () -> SecurityTokenServiceController.getPassTicket(request) | ||
| ); | ||
|
|
||
| assertEquals("Mapper failed", exception.getMessage()); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,8 @@ apiml: | |
| urls: | ||
| authenticate: https://localhost:10013/zss/saf/authenticate | ||
| verify: https://localhost:10013/zss/saf/verify | ||
| delegatePassticket: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we should document this new property and endpoint in zowe docs
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've created issue zowe/docs-site#4920 to do the doc updates.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks Joe
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR zowe/docs-site#4925 covering the endpoint. |
||
| enabled: true | ||
| health: | ||
| protected: false | ||
| spring: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a bit of inconsistency with the naming convention used in the
/ticketendpoint, where we useapplicationNamerather thanapplId, andticketrather thanpassticket. I probably prefer this one, but worthy to mention that