Skip to content

Commit 5e950fa

Browse files
committed
Enhance PolicyAuthorizationManager to extract request body for JSON write requests and update SecurityConfig to include CachedBodyRequestFilter
1 parent bb398b1 commit 5e950fa

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

core-security/src/main/java/org/opendevstack/apiservice/core/security/authorization/PolicyAuthorizationManager.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.opendevstack.apiservice.core.security.authorization;
22

33

4+
import com.fasterxml.jackson.core.type.TypeReference;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import jakarta.servlet.http.HttpServletRequest;
67
import org.opendevstack.apiservice.core.contracts.auth.AuthorizationDecision;
@@ -13,7 +14,9 @@
1314
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
1415
import org.springframework.stereotype.Component;
1516

17+
import java.io.IOException;
1618
import java.util.List;
19+
import java.util.Map;
1720
import java.util.Optional;
1821
import java.util.function.Supplier;
1922

@@ -65,6 +68,7 @@ public org.springframework.security.authorization.AuthorizationDecision check(
6568
}
6669

6770
PolicyContext policyContext = contextFactory.create(apiDef.get(), request);
71+
policyContext = policyContext.withRequestBody(extractRequestBody(request));
6872

6973
List<PolicyRule> rules = policyService.findPolicies(apiDef.get().getId(), policyContext.getClientId());
7074

@@ -74,4 +78,33 @@ public org.springframework.security.authorization.AuthorizationDecision check(
7478
decision != AuthorizationDecision.DENY
7579
);
7680
}
81+
82+
private Map<String, Object> extractRequestBody(HttpServletRequest request) {
83+
if (!isJsonWriteRequest(request)) {
84+
return Map.of();
85+
}
86+
87+
try {
88+
byte[] bytes = request.getInputStream().readAllBytes();
89+
if (bytes.length == 0) {
90+
return Map.of();
91+
}
92+
return objectMapper.readValue(bytes, new TypeReference<>() {
93+
});
94+
} catch (IOException ex) {
95+
return Map.of();
96+
}
97+
}
98+
99+
private boolean isJsonWriteRequest(HttpServletRequest request) {
100+
String method = request.getMethod();
101+
if (!"POST".equalsIgnoreCase(method)
102+
&& !"PUT".equalsIgnoreCase(method)
103+
&& !"PATCH".equalsIgnoreCase(method)) {
104+
return false;
105+
}
106+
107+
String contentType = request.getContentType();
108+
return contentType != null && contentType.toLowerCase().startsWith("application/json");
109+
}
77110
}

core-security/src/main/java/org/opendevstack/apiservice/core/security/config/SecurityConfig.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package org.opendevstack.apiservice.core.security.config;
22

33
import org.opendevstack.apiservice.core.security.authorization.PolicyAuthorizationManager;
4+
import org.opendevstack.apiservice.core.security.filter.CachedBodyRequestFilter;
45
import org.opendevstack.apiservice.core.security.jwt.AzureJwtAuthenticationConverter;
56
import org.springframework.context.annotation.Bean;
67
import org.springframework.context.annotation.Configuration;
78
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
89
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
910
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
11+
import org.springframework.security.web.access.intercept.AuthorizationFilter;
1012
import org.springframework.security.web.SecurityFilterChain;
1113
import lombok.extern.slf4j.Slf4j;
1214
import java.util.Arrays;
@@ -20,12 +22,17 @@ public class SecurityConfig {
2022
private final SecurityProperties securityProperties;
2123
private final PolicyAuthorizationManager policyAuthorizationManager;
2224
private final AzureJwtAuthenticationConverter azureJwtAuthenticationConverter;
25+
private final CachedBodyRequestFilter cachedBodyRequestFilter;
2326

2427

25-
public SecurityConfig(SecurityProperties securityProperties, PolicyAuthorizationManager policyAuthorizationManager, AzureJwtAuthenticationConverter azureJwtAuthenticationConverter) {
28+
public SecurityConfig(SecurityProperties securityProperties,
29+
PolicyAuthorizationManager policyAuthorizationManager,
30+
AzureJwtAuthenticationConverter azureJwtAuthenticationConverter,
31+
CachedBodyRequestFilter cachedBodyRequestFilter) {
2632
this.securityProperties = securityProperties;
2733
this.policyAuthorizationManager = policyAuthorizationManager;
2834
this.azureJwtAuthenticationConverter = azureJwtAuthenticationConverter;
35+
this.cachedBodyRequestFilter = cachedBodyRequestFilter;
2936
}
3037

3138
@Bean
@@ -48,6 +55,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
4855
.headers(headers -> headers.frameOptions(frame -> frame.disable()))
4956
.csrf(csrf -> csrf.disable());
5057

58+
http.addFilterBefore(cachedBodyRequestFilter, AuthorizationFilter.class);
59+
5160
return http.build();
5261
}
5362
}

core-security/src/test/java/org/opendevstack/apiservice/core/security/authorization/PolicyAuthorizationManagerTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.mock.web.MockHttpServletRequest;
1313
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
1414

15+
import java.nio.charset.StandardCharsets;
1516
import java.util.List;
1617
import java.util.Map;
1718
import java.util.Optional;
@@ -21,6 +22,7 @@
2122
import static org.junit.jupiter.api.Assertions.assertFalse;
2223
import static org.junit.jupiter.api.Assertions.assertTrue;
2324
import static org.mockito.Mockito.any;
25+
import static org.mockito.Mockito.anyMap;
2426
import static org.mockito.Mockito.anyString;
2527
import static org.mockito.Mockito.mock;
2628
import static org.mockito.Mockito.verifyNoInteractions;
@@ -75,6 +77,7 @@ void check_securedApi_policyPermits() {
7577

7678
PolicyContext ctx = mock(PolicyContext.class);
7779
when(ctx.getClientId()).thenReturn("client-a");
80+
when(ctx.withRequestBody(anyMap())).thenReturn(ctx);
7881
when(contextFactory.create(apiDef, request)).thenReturn(ctx);
7982

8083
List<PolicyRule> rules = List.of(new PolicyRule(UUID.randomUUID(), "api-1", "client-a", "ALLOWED_CLIENTS", Map.of()));
@@ -95,6 +98,7 @@ void check_securedApi_policyDenies() {
9598

9699
PolicyContext ctx = mock(PolicyContext.class);
97100
when(ctx.getClientId()).thenReturn("client-b");
101+
when(ctx.withRequestBody(anyMap())).thenReturn(ctx);
98102
when(contextFactory.create(apiDef, request)).thenReturn(ctx);
99103

100104
List<PolicyRule> rules = List.of(new PolicyRule(UUID.randomUUID(), "api-1", "client-b", "ALLOWED_CLIENTS", Map.of()));
@@ -115,6 +119,7 @@ void check_securedApi_abstainPermits() {
115119

116120
PolicyContext ctx = mock(PolicyContext.class);
117121
when(ctx.getClientId()).thenReturn("client-a");
122+
when(ctx.withRequestBody(anyMap())).thenReturn(ctx);
118123
when(contextFactory.create(apiDef, request)).thenReturn(ctx);
119124

120125
when(policyService.findPolicies(anyString(), anyString())).thenReturn(List.of());
@@ -124,4 +129,32 @@ void check_securedApi_abstainPermits() {
124129

125130
assertTrue(decision.isGranted());
126131
}
132+
133+
@Test
134+
void check_securedPostRequest_populatesRequestBodyInContext() {
135+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v0/projects");
136+
request.setContentType("application/json");
137+
String requestBody = "{" +
138+
"\"projectFlavor\":\"DLSS\"," +
139+
"\"location\":\"UNKNOWN_REGION\"" +
140+
"}";
141+
request.setContent(requestBody.getBytes(StandardCharsets.UTF_8));
142+
143+
ApiDefinition apiDef = new ApiDefinition("api-1", "Projects", "/projects", "v0",
144+
Set.of(AuthType.CLIENT_CREDENTIALS), false, null, true);
145+
when(resolver.resolve(request)).thenReturn(Optional.of(apiDef));
146+
147+
PolicyContext ctx = mock(PolicyContext.class);
148+
when(ctx.getClientId()).thenReturn("client-a");
149+
when(ctx.withRequestBody(anyMap())).thenReturn(ctx);
150+
when(contextFactory.create(apiDef, request)).thenReturn(ctx);
151+
152+
List<PolicyRule> rules = List.of(new PolicyRule(UUID.randomUUID(), "api-1", "client-a", "FLAVOR_RESTRICTION", Map.of()));
153+
when(policyService.findPolicies("api-1", "client-a")).thenReturn(rules);
154+
when(policyEngine.evaluate(ctx, rules)).thenReturn(AuthorizationDecision.PERMIT);
155+
156+
var decision = manager.check(() -> null, new RequestAuthorizationContext(request));
157+
158+
assertTrue(decision.isGranted());
159+
}
127160
}

core-security/src/test/java/org/opendevstack/apiservice/core/security/config/SecurityConfigTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.jupiter.api.BeforeEach;
44
import org.junit.jupiter.api.Test;
55
import org.opendevstack.apiservice.core.security.authorization.PolicyAuthorizationManager;
6+
import org.opendevstack.apiservice.core.security.filter.CachedBodyRequestFilter;
67
import org.opendevstack.apiservice.core.security.jwt.AzureJwtAuthenticationConverter;
78
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
89
import org.springframework.security.web.SecurityFilterChain;
@@ -17,14 +18,21 @@ class SecurityConfigTest {
1718
private SecurityProperties securityProperties;
1819
private PolicyAuthorizationManager policyAuthorizationManager;
1920
private AzureJwtAuthenticationConverter azureJwtAuthenticationConverter;
21+
private CachedBodyRequestFilter cachedBodyRequestFilter;
2022
private SecurityConfig securityConfig;
2123

2224
@BeforeEach
2325
void setUp() {
2426
securityProperties = mock(SecurityProperties.class);
2527
policyAuthorizationManager = mock(PolicyAuthorizationManager.class);
2628
azureJwtAuthenticationConverter = new AzureJwtAuthenticationConverter();
27-
securityConfig = new SecurityConfig(securityProperties, policyAuthorizationManager, azureJwtAuthenticationConverter);
29+
cachedBodyRequestFilter = mock(CachedBodyRequestFilter.class);
30+
securityConfig = new SecurityConfig(
31+
securityProperties,
32+
policyAuthorizationManager,
33+
azureJwtAuthenticationConverter,
34+
cachedBodyRequestFilter
35+
);
2836
}
2937

3038
@Test

0 commit comments

Comments
 (0)