Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
89aae2b
wip actuator rules
Jul 14, 2026
b384c73
wip test
Jul 15, 2026
ed0e205
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 16, 2026
26cec21
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 16, 2026
54fd023
add rules, update tests
Jul 17, 2026
32f50ba
Merge branch 'v3.x.x' into reboot/fix/actuator-debug
pablocarle Jul 17, 2026
804f279
update test
Jul 20, 2026
7c87ba5
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 20, 2026
20a7eed
wip tests
Jul 21, 2026
915f3dd
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 21, 2026
00cae0d
wip use production properties in tests
Jul 21, 2026
be4cee5
wip test
Jul 21, 2026
70d3e1a
wip tests
Jul 21, 2026
303d24b
fix gateway-service tests
Jul 21, 2026
40104a6
wip actuator tests in apiml modulith
Jul 21, 2026
736d1f2
fix startup issue
Jul 21, 2026
3c9f3f7
fix tests in apiml modulith
Jul 21, 2026
1a2a4c4
unit test
Jul 21, 2026
ffd8658
try fix GAs wip
Jul 21, 2026
9bde7a3
try update saf
Jul 21, 2026
bc9cf50
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 22, 2026
b08a00f
try authorization endpoint
Jul 22, 2026
8ece2a7
rollback authorization endpoint change
Jul 22, 2026
873a923
ai review
Jul 22, 2026
7055735
dummy saf logs
Jul 22, 2026
c7f09ed
add lower case user
Jul 22, 2026
dda9d1d
logs
Jul 22, 2026
192711b
Merge branch 'v3.x.x' into reboot/fix/actuator-debug
pablocarle Jul 22, 2026
a9512af
sonar and log
Jul 22, 2026
2bb4326
save reports
Jul 22, 2026
bfae899
Merge remote-tracking branch 'origin/v3.x.x' into reboot/fix/actuator…
Jul 22, 2026
b107ca5
try rest config
Jul 22, 2026
f50d7bc
add retries to tests (for sonar stage)
Jul 22, 2026
f1f25f8
restore config
Jul 22, 2026
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
@@ -0,0 +1,45 @@
/*
* 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.security.common.auth.saf;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.util.List;

@RequiredArgsConstructor
public class SafAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {

private final SafResourceAccessVerifying safResourceAccessVerifying;
private final String safResourceClass;
private final String safResourceName;
private final String safResourceAccess;

@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
// @formatter:off
return authentication.filter(Authentication::isAuthenticated)
.flatMap(auth -> Mono.fromCallable(() ->
safResourceAccessVerifying.hasSafResourceAccess(auth, safResourceClass, safResourceName, safResourceAccess))
.subscribeOn(Schedulers.boundedElastic()))
.filter(value -> value != null && value)
.map(auth -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(true, List.of(new SimpleGrantedAuthority(String.format("%s.%s:%s", safResourceClass, safResourceName, safResourceAccess))))))
.defaultIfEmpty(new AuthorityAuthorizationDecision(false, List.of()));
// @formatter:on
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@Component
@RequiredArgsConstructor
public class SafMethodSecurityExpressionRoot {

private final SafSecurityConfigurationProperties safSecurityConfigurationProperties;
private final SafResourceAccessVerifying safResourceAccessVerifying;

Expand Down
4 changes: 4 additions & 0 deletions apiml-security-common/src/main/resources/mock-saf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
# This file is stored in `src/main/resources/mock-saf.yml` which means that it will be used by the service and its unit tests.
# If you can create a different file in `src/test/resources/mock-saf.yml` then unit tests will use different definitions.
safAccess:
ZOWE:
APIML.DEBUG:
CONTROL:
- USER
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.security.common.auth.saf;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authorization.AuthorityAuthorizationDecision;
import org.springframework.security.core.Authentication;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class SafAuthorizationManagerTest {

private static final String RESOURCE_CLASS = "ZOWE";
private static final String RESOURCE_NAME = "APIML.SERVICES";
private static final String RESOURCE_ACCESS = "READ";

@Mock
private SafResourceAccessVerifying safResourceAccessVerifying;

@Mock
private Authentication authentication;

private SafAuthorizationManager<Object> safAuthorizationManager;

@BeforeEach
void setUp() {
safAuthorizationManager = new SafAuthorizationManager<>(safResourceAccessVerifying, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS);
}

@Nested
class GivenSafAuthorization {

@Test
void whenAuthenticatedAndHasAccess_thenGrantWithAuthority() {
when(authentication.isAuthenticated()).thenReturn(true);
when(safResourceAccessVerifying.hasSafResourceAccess(authentication, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS)).thenReturn(true);

StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object()))
.assertNext(decision -> {
assertTrue(decision.isGranted());
var authorities = ((AuthorityAuthorizationDecision) decision).getAuthorities();
assertEquals(1, authorities.size());
assertEquals(String.format("%s.%s:%s", RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS), authorities.iterator().next().getAuthority());
})
.verifyComplete();
}

@Test
void whenAuthenticatedButNoAccess_thenDenyWithoutAuthorities() {
when(authentication.isAuthenticated()).thenReturn(true);
when(safResourceAccessVerifying.hasSafResourceAccess(authentication, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS)).thenReturn(false);

StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object()))
.assertNext(decision -> {
assertFalse(decision.isGranted());
assertTrue(((AuthorityAuthorizationDecision) decision).getAuthorities().isEmpty());
})
.verifyComplete();
}

@Test
void whenNotAuthenticated_thenDenyWithoutCallingVerifier() {
when(authentication.isAuthenticated()).thenReturn(false);

StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object()))
.assertNext(decision -> assertFalse(decision.isGranted()))
.verifyComplete();

verify(safResourceAccessVerifying, never()).hasSafResourceAccess(any(), any(), any(), any());
}

}

}
3 changes: 3 additions & 0 deletions apiml-security-common/src/test/resources/mock-saf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ safAccess:
APIML.RESOURCE:
READ:
- user
APIML.DEBUG:
CONTROL:
- USER
CLASS:
RESOURCE:
READ:
Expand Down
28 changes: 20 additions & 8 deletions apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
Expand All @@ -42,7 +45,6 @@
import org.zowe.apiml.constants.ApimlConstants;
import org.zowe.apiml.filter.BasicLoginFilter;
import org.zowe.apiml.filter.CachedBodyFilter;
import org.zowe.apiml.security.common.filter.CategorizeCertsWebFilter;
import org.zowe.apiml.filter.LogoutHandler;
import org.zowe.apiml.filter.OIDCAuthFilter;
import org.zowe.apiml.filter.QueryWebFilter;
Expand All @@ -52,7 +54,10 @@
import org.zowe.apiml.handler.FailedAuthenticationWebHandler;
import org.zowe.apiml.handler.LocalTokenProvider;
import org.zowe.apiml.product.constants.CoreService;
import org.zowe.apiml.security.common.auth.saf.SafAuthorizationManager;
import org.zowe.apiml.security.common.auth.saf.SafResourceAccessVerifying;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.filter.CategorizeCertsWebFilter;
import org.zowe.apiml.security.common.token.OIDCProvider;
import org.zowe.apiml.security.common.util.X509Util;
import org.zowe.apiml.security.common.verify.CertificateValidator;
Expand All @@ -61,16 +66,12 @@
import org.zowe.apiml.zaas.security.login.x509.X509AuthenticationProvider;
import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper;
import org.zowe.apiml.zaas.security.query.TokenAuthenticationProvider;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import reactor.core.publisher.Mono;

import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
Expand Down Expand Up @@ -354,15 +355,26 @@ SecurityWebFilterChain healthEndpointFilterChain(ServerHttpSecurity http,
@Bean
SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http,
AuthConfigurationProperties authConfigurationProperties,
AuthExceptionHandlerReactive authExceptionHandlerReactive) {
AuthExceptionHandlerReactive authExceptionHandlerReactive,
SafResourceAccessVerifying safResourceAccessVerifying) {

return http
.securityMatcher(new AndServerWebExchangeMatcher(
pathMatchers("/application/**"),
new NegatedServerWebExchangeMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, "/application/version"))
))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchange -> exchange.anyExchange().authenticated())
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.authorizeExchange(exchange -> exchange.matchers(
new OrServerWebExchangeMatcher(
pathMatchers(HttpMethod.GET, "/application/**"),
pathMatchers(HttpMethod.HEAD, "/application/**")
))
.authenticated()
)
.authorizeExchange(exchange -> exchange.matchers(pathMatchers("/application/**"))
.access(new SafAuthorizationManager<>(safResourceAccessVerifying, "ZOWE", "APIML.DEBUG", "CONTROL"))
)
.addFilterAfter(new TokenAuthFilter(localTokenProvider, authConfigurationProperties, authExceptionHandlerReactive), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(new BasicLoginFilter(compoundAuthProvider, failedAuthenticationWebHandler), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
Expand Down
21 changes: 18 additions & 3 deletions apiml/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# for back-compatibility
spring.profiles.group.attls: attlsServer,attlsClient

eureka:
dashboard:
path: /eureka
Expand All @@ -18,6 +16,10 @@ eureka:
useReadOnlyResponseCache: false
peer-node-read-timeout-ms: 15000
spring:
profiles:
group:
attls: attlsServer,attlsClient
debug-control: debug
cloud:
gateway:
server:
Expand Down Expand Up @@ -380,11 +382,24 @@ logging:
management:
endpoint:
gateway:
access: unrestricted
access: read-only
loggers:
access: read-only
endpoints:
web:
exposure:
include: health,info,gateway,loggers

---
spring.config.activate.on-profile: debug-control

management:
endpoint:
gateway:
access: unrestricted
loggers:
access: unrestricted

---
spring.config.activate.on-profile: wiretap

Expand Down
14 changes: 14 additions & 0 deletions apiml/src/main/resources/mock-saf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This file is used by the SafResourceAccessDummy class for testing and when the service is running outside of z/OS
# It defines what are the access levels for SAF resources and users
#
# There are to special values of access level:
# - `FAILURE` - the check request will fail with an internal error
# - `NONE` - there is no access to the resource but the resource is defined
#
# This file is stored in `src/main/resources/mock-saf.yml` which means that it will be used by the service and its unit tests.
# If you can create a different file in `src/test/resources/mock-saf.yml` then unit tests will use different definitions.
safAccess:
ZOWE:
APIML.DEBUG:
CONTROL:
- USER
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
}
)
@Execution(ExecutionMode.SAME_THREAD)
@ActiveProfiles("ApimlModulithAcceptanceTest")
@ActiveProfiles({"ApimlModulithAcceptanceTest", "test" })
@AutoConfigureWebTestClient
@DirtiesContext
public @interface AcceptanceTest {
Expand Down
Loading
Loading