Skip to content

Commit

Permalink
feat: Passticket endpoint for ZAAS component (#3125)
Browse files Browse the repository at this point in the history
* Add empty ZAAS controller

Signed-off-by: Petr Weinfurt <[email protected]>

* The /zaas/ticket endpoint

Signed-off-by: Petr Weinfurt <[email protected]>

* Testing security configuration for x509 and basic auth.

Signed-off-by: Petr Weinfurt <[email protected]>

* OIDC authentication filter

Signed-off-by: Petr Weinfurt <[email protected]>

* OIDC authentication filter tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Move the security filter into a new configuration class

Signed-off-by: at670475 <[email protected]>

* fix bean

Signed-off-by: at670475 <[email protected]>

* Support JWT as source (only with client cert) + test

Signed-off-by: sj895092 <[email protected]>

* Fix JWT authentication filter. Add integration tests.

Signed-off-by: Petr Weinfurt <[email protected]>

* ZaasController unit tests.

Signed-off-by: Petr Weinfurt <[email protected]>

* TEST - disable some integration tests.

Signed-off-by: Petr Weinfurt <[email protected]>

* Remove condition for OIDC Auth source service.

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix checkstyle

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix broken test

Signed-off-by: Petr Weinfurt <[email protected]>

* Remove dependency on Zuul RequestContext. Add simple filter to extract authentication source from request.

Signed-off-by: Petr Weinfurt <[email protected]>

* add extract auth source filter

Signed-off-by: achmelo <[email protected]>

* Working zaas controller with request attribute

Signed-off-by: Petr Weinfurt <[email protected]>

* add filters directly

Signed-off-by: achmelo <[email protected]>

* Fix gateway unit tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Exception handling

Signed-off-by: Petr Weinfurt <[email protected]>

* Fix integration tests

Signed-off-by: Petr Weinfurt <[email protected]>

* Cleanup. Add some more unit tests.

Signed-off-by: Petr Weinfurt <[email protected]>

* Test ZaasController with mockMvc.

Signed-off-by: Petr Weinfurt <[email protected]>

---------

Signed-off-by: Petr Weinfurt <[email protected]>
Signed-off-by: at670475 <[email protected]>
Signed-off-by: sj895092 <[email protected]>
Signed-off-by: achmelo <[email protected]>
Co-authored-by: Andrea Tabone <[email protected]>
Co-authored-by: sj895092 <[email protected]>
Co-authored-by: achmelo <[email protected]>
  • Loading branch information
4 people authored Oct 12, 2023
1 parent aef60d4 commit b9e9958
Show file tree
Hide file tree
Showing 30 changed files with 767 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.gateway.controllers;

import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;
import org.zowe.apiml.message.api.ApiMessageView;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.passticket.IRRPassTicketGenerationException;
import org.zowe.apiml.passticket.PassTicketService;
import org.zowe.apiml.ticket.TicketRequest;
import org.zowe.apiml.ticket.TicketResponse;

@RequiredArgsConstructor
@RestController
@RequestMapping(ZaasController.CONTROLLER_PATH)
@Slf4j
public class ZaasController {
public static final String CONTROLLER_PATH = "gateway/zaas";

private final PassTicketService passTicketService;
private final MessageService messageService;

@PostMapping(path = "ticket", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Provides PassTicket for authenticated user.")
@ResponseBody
public ResponseEntity<Object> getPassTicket(@RequestBody TicketRequest ticketRequest, @RequestAttribute("zaas.auth.source") AuthSource.Parsed authSource) {

if (StringUtils.isEmpty(authSource.getUserId())) {
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);
}

String ticket = null;
try {
ticket = passTicketService.generate(authSource.getUserId(), applicationName);
} catch (IRRPassTicketGenerationException e) {
ApiMessageView messageView = messageService.createMessage("org.zowe.apiml.security.ticket.generateFailed",
e.getErrorCode().getMessage()).mapToView();
return ResponseEntity
.status(e.getHttpStatus())
.body(messageView);
}
return ResponseEntity
.status(HttpStatus.OK)
.body(new TicketResponse(null, authSource.getUserId(), applicationName, ticket));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.gateway.filters.pre;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.web.filter.OncePerRequestFilter;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

@RequiredArgsConstructor
@Slf4j
public class ExtractAuthSourceFilter extends OncePerRequestFilter {
static final String AUTH_SOURCE_ATTR = "zaas.auth.source";

private final AuthSourceService authSourceService;
private final AuthExceptionHandler authExceptionHandler;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
Optional<AuthSource> authSource = authSourceService.getAuthSourceFromRequest(request);
if (authSource.isPresent()) {
AuthSource.Parsed parsed = authSourceService.parse(authSource.get());
request.setAttribute(AUTH_SOURCE_ATTR, parsed);
filterChain.doFilter(request, response);
} else {
throw new InsufficientAuthenticationException("No authentication source found in the request.");
}
}
catch (RuntimeException ex) {
authExceptionHandler.handleException(request, response, ex);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.zowe.apiml.gateway.controllers.CacheServiceController;
import org.zowe.apiml.gateway.controllers.SafResourceAccessController;
import org.zowe.apiml.gateway.error.controllers.InternalServerErrorController;
import org.zowe.apiml.gateway.filters.pre.ExtractAuthSourceFilter;
import org.zowe.apiml.gateway.security.login.FailedAccessTokenHandler;
import org.zowe.apiml.gateway.security.login.SuccessfulAccessTokenHandler;
import org.zowe.apiml.gateway.security.login.x509.X509AuthenticationProvider;
Expand All @@ -51,6 +52,7 @@
import org.zowe.apiml.gateway.security.query.TokenAuthenticationProvider;
import org.zowe.apiml.gateway.security.refresh.SuccessfulRefreshHandler;
import org.zowe.apiml.gateway.security.service.AuthenticationService;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.gateway.security.ticket.SuccessfulTicketHandler;
import org.zowe.apiml.gateway.services.ServicesInfoController;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
Expand All @@ -60,6 +62,7 @@
import org.zowe.apiml.security.common.content.BasicContentFilter;
import org.zowe.apiml.security.common.content.BearerContentFilter;
import org.zowe.apiml.security.common.content.CookieContentFilter;
import org.zowe.apiml.security.common.error.AuthExceptionHandler;
import org.zowe.apiml.security.common.filter.CategorizeCertsFilter;
import org.zowe.apiml.security.common.filter.StoreAccessTokenInfoFilter;
import org.zowe.apiml.security.common.handler.FailedAuthenticationHandler;
Expand Down Expand Up @@ -103,6 +106,9 @@ public class NewSecurityConfiguration {
private final Set<String> publicKeyCertificatesBase64;
private final CertificateValidator certificateValidator;
private final X509AuthenticationProvider x509AuthenticationProvider;
private final AuthSourceService authSourceService;
private final AuthExceptionHandler authExceptionHandler;

@Value("${server.attls.enabled:false}")
private boolean isAttlsEnabled;

Expand Down Expand Up @@ -298,6 +304,33 @@ private X509AuthenticationFilter x509AuthenticationFilter() {

}

@Configuration
@RequiredArgsConstructor
@Order(9)
class ZaasTicketEndpoint {

private final CompoundAuthProvider compoundAuthProvider;

@Bean
public SecurityFilterChain authZaasTicketEndpointFilterChain(HttpSecurity http) throws Exception {
baseConfigure(http.requestMatchers().antMatchers( // no http method to catch all attempts to login and handle them here. Otherwise it falls to default filterchain and tries to route the calls, which doesnt make sense
authConfigurationProperties.getRevokeMultipleAccessTokens() + "/**",
authConfigurationProperties.getEvictAccessTokensAndRules(),
"/gateway/zaas/ticket"
).and())
.authorizeRequests()
.anyRequest().authenticated()
.and()
.x509().userDetailsService(x509UserDetailsService())
.and()
.addFilterBefore(new CategorizeCertsFilter(publicKeyCertificatesBase64, certificateValidator), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class)
.addFilterBefore(new ExtractAuthSourceFilter(authSourceService, authExceptionHandler), org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter.class);

return http.build();
}

}


/**
* Query and Ticket and Refresh endpoints share single filter that handles auth with and without certificate. This logic is encapsulated in the queryFilter or ticketFilter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public AuthenticationCommand createCommand(Authentication authentication, AuthSo

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return authSourceService.getAuthSourceFromRequest(RequestContext.getCurrentContext().getRequest());
}

@Value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public AuthenticationCommand createCommand(Authentication authentication, AuthSo

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return authSourceService.getAuthSourceFromRequest(RequestContext.getCurrentContext().getRequest());
}

private String getApplId(Authentication authentication) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import com.netflix.appinfo.InstanceInfo;
import com.netflix.zuul.context.RequestContext;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -21,14 +20,15 @@
import org.zowe.apiml.auth.AuthenticationScheme;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException;
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.X509AuthSource;
import org.zowe.apiml.message.core.MessageType;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;

import java.util.Optional;

/**
* This schema adds requested information about client certificate. This information is added
* to HTTP header. If no header is specified in Authentication object, empty command is returned.
Expand Down Expand Up @@ -80,7 +80,7 @@ public AuthenticationCommand createCommand(Authentication authentication, AuthSo

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return authSourceService.getAuthSourceFromRequest(RequestContext.getCurrentContext().getRequest());
}

public class X509Command extends AuthenticationCommand {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public AuthenticationCommand createCommand(Authentication authentication, AuthSo

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return authSourceService.getAuthSourceFromRequest(RequestContext.getCurrentContext().getRequest());
}

@lombok.Value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public AuthenticationScheme getScheme() {

@Override
public Optional<AuthSource> getAuthSource() {
return authSourceService.getAuthSourceFromRequest();
return authSourceService.getAuthSourceFromRequest(RequestContext.getCurrentContext().getRequest());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.gateway.security.service.schema.source;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

/**
Expand All @@ -21,7 +22,7 @@ public interface AuthSourceService {
* in case if more than one source is present.
* @return AuthSource object which hold original source of authentication (JWT token, client certificate etc.)
*/
Optional<AuthSource> getAuthSourceFromRequest();
Optional<AuthSource> getAuthSourceFromRequest(HttpServletRequest request);

/**
* Implements validation logic for specific source of authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource.AuthSourceType;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource.Parsed;

import javax.servlet.http.HttpServletRequest;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -90,20 +91,20 @@ public DefaultAuthSourceService(@Autowired JwtAuthSourceService jwtAuthSourceSer
* or Optional.empty() when no authentication source found.
*/
@Override
public Optional<AuthSource> getAuthSourceFromRequest() {
public Optional<AuthSource> getAuthSourceFromRequest(HttpServletRequest request) {
AuthSourceService service = getService(AuthSourceType.JWT);
Optional<AuthSource> authSource = service.getAuthSourceFromRequest();
Optional<AuthSource> authSource = service.getAuthSourceFromRequest(request);
if (!authSource.isPresent() && isPATEnabled) {
service = getService(AuthSourceType.PAT);
authSource = service.getAuthSourceFromRequest();
authSource = service.getAuthSourceFromRequest(request);
}
if (!authSource.isPresent() && isOIDCEnabled) {
service = getService(AuthSourceType.OIDC);
authSource = service.getAuthSourceFromRequest();
authSource = service.getAuthSourceFromRequest(request);
}
if (!authSource.isPresent() && isX509Enabled) {
service = getService(AuthSourceType.CLIENT_CERT);
authSource = service.getAuthSourceFromRequest();
authSource = service.getAuthSourceFromRequest(request);
}
return authSource;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

package org.zowe.apiml.gateway.security.service.schema.source;

import com.netflix.zuul.context.RequestContext;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Scope;
Expand All @@ -23,6 +22,7 @@
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.security.common.token.QueryResponse;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.function.Function;

Expand Down Expand Up @@ -50,8 +50,8 @@ public Function<String, AuthSource> getMapper() {
}

@Override
public Optional<String> getToken(RequestContext context) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(context.getRequest());
public Optional<String> getToken(HttpServletRequest request) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(request);
if (tokenOptional.isPresent()) {
AuthSource.Origin origin = authenticationService.getTokenOrigin(tokenOptional.get());
if (Origin.ZOSMF == origin || Origin.ZOWE == origin) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

package org.zowe.apiml.gateway.security.service.schema.source;

import com.netflix.zuul.context.RequestContext;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -27,6 +26,7 @@
import org.zowe.apiml.security.common.token.QueryResponse;
import org.zowe.apiml.security.common.token.TokenNotValidException;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.function.Function;

Expand Down Expand Up @@ -54,8 +54,8 @@ public Function<String, AuthSource> getMapper() {
}

@Override
public Optional<String> getToken(RequestContext context) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(context.getRequest());
public Optional<String> getToken(HttpServletRequest request) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(request);
if (tokenOptional.isPresent()) {
AuthSource.Origin origin = authenticationService.getTokenOrigin(tokenOptional.get());
if (AuthSource.Origin.OIDC == origin) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.zowe.apiml.security.common.token.AccessTokenProvider;
import org.zowe.apiml.security.common.token.QueryResponse;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.function.Function;

Expand Down Expand Up @@ -50,11 +51,11 @@ public Function<String, AuthSource> getMapper() {
}

@Override
public Optional<String> getToken(RequestContext context) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(context.getRequest());
public Optional<String> getToken(HttpServletRequest request) {
Optional<String> tokenOptional = authenticationService.getJwtTokenFromRequest(request);
if (!tokenOptional.isPresent()) {
// try to get token also from PAT specific cookie or header
tokenOptional = authenticationService.getPATFromRequest(context.getRequest());
tokenOptional = authenticationService.getPATFromRequest(request);
}
if (tokenOptional.isPresent()) {
AuthSource.Origin origin = authenticationService.getTokenOrigin(tokenOptional.get());
Expand Down
Loading

0 comments on commit b9e9958

Please sign in to comment.