866688bfbb8e5bca1b61e1991852475fed7dab43
[ccsdk/oran.git] /
1 /*-
2  * ========================LICENSE_START=================================
3  * ONAP : ccsdk oran
4  * ======================================================================
5  * Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
6  * ======================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 package org.onap.ccsdk.oran.a1policymanagementservice.util.v3;
22
23 import com.google.gson.JsonObject;
24 import com.google.gson.JsonParser;
25 import java.lang.invoke.MethodHandles;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Base64;
30 import java.util.List;
31
32 import org.reactivestreams.Publisher;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.slf4j.MDC;
36 import org.springframework.core.io.buffer.DataBuffer;
37 import org.springframework.http.server.reactive.ServerHttpRequest;
38 import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
39 import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
40 import org.springframework.util.MultiValueMap;
41 import org.springframework.web.server.ServerWebExchange;
42 import org.springframework.web.server.WebFilter;
43 import org.springframework.web.server.WebFilterChain;
44 import org.springframework.web.util.pattern.PathPattern;
45 import reactor.core.publisher.Flux;
46 import reactor.core.publisher.Mono;
47 import reactor.util.context.Context;
48 import reactor.util.context.ContextView;
49
50 public class ReactiveEntryExitFilter implements WebFilter {
51
52     private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
53     private static final String BEARER_PREFIX = "Bearer ";
54     private static final String FACILITY_KEY = "facility";
55     private static final String SUBJECT_KEY = "subject";
56
57     private final List<PathPattern> excludedPatterns = new ArrayList<>();
58
59     public ReactiveEntryExitFilter excludePathPatterns(PathPattern pattern) {
60         excludedPatterns.add(pattern);
61         return this;
62     }
63
64     @Override
65     public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
66
67         // Skip processing for excluded patterns
68         for (PathPattern pattern : excludedPatterns) {
69             if (pattern.matches(exchange.getRequest().getPath().pathWithinApplication())) {
70                 return chain.filter(exchange);
71             }
72         }
73
74         // sets FACILITY_KEY and SUBJECT_KEY in MDC
75         auditLog(exchange.getRequest());
76
77         String subject = MDC.get(SUBJECT_KEY);
78         String facility = MDC.get(FACILITY_KEY);
79
80         ServerHttpRequest httpRequest = exchange.getRequest();
81         MultiValueMap<String, String> queryParams = httpRequest.getQueryParams();
82         logger.info("Request received with path: {}, and the Request Id: {}, with HTTP Method: {}", httpRequest.getPath(),
83                 exchange.getRequest().getId(), exchange.getRequest().getMethod());
84         if (!queryParams.isEmpty())
85             logger.trace("For the request Id: {}, the Query parameters are: {}", exchange.getRequest().getId(), queryParams);
86
87         ServerHttpRequestDecorator loggingServerHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
88             String requestBody;
89
90             @Override
91             public Flux<DataBuffer> getBody() {
92                 return Flux.deferContextual(contextView ->
93                     // Now, return the original body flux with a doFinally to clear MDC after processing
94                     super.getBody().doOnNext(dataBuffer -> {
95                         requestBody = dataBuffer.toString(StandardCharsets.UTF_8); // Read the bytes from the DataBuffer
96                         logger.trace("For the request ID: {} the received request body: {}", exchange.getRequest().getId(), requestBody);
97                     }).doOnEach(signal -> MDC.clear())
98                             .doFinally(signal -> MDC.clear())); // Clear MDC after the request body is processed
99             }
100         };
101
102         StringBuilder responseBodyBuilder = new StringBuilder();
103
104         ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
105             @Override
106             public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
107                 return Mono.deferContextual(contextView ->
108                     super.writeWith(Flux.from(body).doOnNext(dataBuffer -> {
109                         String responseBody = dataBuffer.toString(StandardCharsets.UTF_8);
110                         responseBodyBuilder.append(responseBody);
111                     })).doFinally(signalType -> {
112                         restoreFromContextToMdc(contextView);
113                         logger.info("For the request ID: {} the Status code of the response: {}",
114                                 exchange.getRequest().getId(), exchange.getResponse().getStatusCode());
115                         logger.trace("For the request ID: {} the response is: {} ",
116                                 exchange.getRequest().getId(), responseBodyBuilder);
117                         MDC.clear();
118                     })); // Clear MDC to prevent leakage
119             }
120         };
121         return chain.filter(exchange.mutate()
122                         .request(loggingServerHttpRequestDecorator)
123                         .response(loggingServerHttpResponseDecorator)
124                         .build())
125                 .contextWrite(Context.of(FACILITY_KEY, facility, SUBJECT_KEY, subject))
126                 .doFinally(signalType -> MDC.clear());
127     }
128
129     private void auditLog(ServerHttpRequest request) {
130         String authHeader = request.getHeaders().getFirst("Authorization");
131         String subject = "n/av";
132         if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
133             String token = authHeader.substring(BEARER_PREFIX.length());
134             subject = this.getSubjectFromToken(token);
135         }
136
137         String facility = "log audit";
138
139         MDC.put(SUBJECT_KEY, subject);
140         MDC.put(FACILITY_KEY, facility);
141     }
142
143     private String getSubjectFromToken(String token) {
144         try {
145             String[] chunks = token.split("\\.");
146             if (chunks.length < 2) {
147                 logger.warn("Invalid JWT: Missing payload");
148                 return "n/av";
149             }
150
151             Base64.Decoder decoder = Base64.getUrlDecoder();
152             String payload = new String(decoder.decode(chunks[1]));
153             JsonObject jsonObject = JsonParser.parseString(payload).getAsJsonObject();
154
155             if (jsonObject.has("upn")) {
156                 return sanitize(jsonObject.get("upn").getAsString());
157             } else if (jsonObject.has("preferred_username")) {
158                 return sanitize(jsonObject.get("preferred_username").getAsString());
159             } else if (jsonObject.has("sub")) {
160                 return sanitize(jsonObject.get("sub").getAsString());
161             }
162         } catch (Exception e) {
163             logger.warn("Failed to extract subject from token: {}", e.getMessage());
164         }
165         return "n/av";
166     }
167
168     private String sanitize(String input) {
169         if (input == null) {
170             return "n/av";
171         }
172         // Replace dangerous characters
173         return input.replaceAll("[\r\n]", "")  // Remove newlines
174                 .replaceAll("[{}()<>]", "") // Remove brackets
175                 .replaceAll("[^\\p{Alnum}\\p{Punct}\\s]", ""); // Remove non-printable characters
176     }
177
178     private void restoreFromContextToMdc(ContextView context) {
179         context.getOrEmpty(FACILITY_KEY).ifPresent(value -> MDC.put(FACILITY_KEY, value.toString()));
180         context.getOrEmpty(SUBJECT_KEY).ifPresent(value -> MDC.put(SUBJECT_KEY, value.toString()));
181     }
182 }