a23e3aeaebd954e857dcad8a880f53db6fd6b18d
[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... patterns) {
60         excludedPatterns.addAll(Arrays.asList(patterns));
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         ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
102             @Override
103             public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
104                 return Mono.deferContextual(contextView ->
105                     super.writeWith(Flux.from(body).doOnNext(dataBuffer -> {
106                         String responseBody = dataBuffer.toString(StandardCharsets.UTF_8);
107                         restoreFromContextToMdc(contextView);
108                         logger.info("For the request ID: {} the Status code of the response: {}",
109                                 exchange.getRequest().getId(), getStatusCode());
110                         logger.trace("For the request ID: {} the response is: {} ",
111                                 exchange.getRequest().getId(), responseBody);
112                     })).doFinally(signalType -> MDC.clear())); // Clear MDC to prevent leakage
113             }
114         };
115         return chain.filter(exchange.mutate()
116                         .request(loggingServerHttpRequestDecorator)
117                         .response(loggingServerHttpResponseDecorator)
118                         .build())
119                 .contextWrite(Context.of(FACILITY_KEY, facility, SUBJECT_KEY, subject))
120                 .doFinally(signalType -> MDC.clear());
121     }
122
123     private void auditLog(ServerHttpRequest request) {
124         String authHeader = request.getHeaders().getFirst("Authorization");
125         String subject = "n/av";
126         if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
127             String token = authHeader.substring(BEARER_PREFIX.length());
128             subject = this.getSubjectFromToken(token);
129         }
130
131         String facility = "log audit";
132
133         MDC.put(SUBJECT_KEY, subject);
134         MDC.put(FACILITY_KEY, facility);
135     }
136
137     private String getSubjectFromToken(String token) {
138         try {
139             String[] chunks = token.split("\\.");
140             if (chunks.length < 2) {
141                 logger.warn("Invalid JWT: Missing payload");
142                 return "n/av";
143             }
144
145             Base64.Decoder decoder = Base64.getUrlDecoder();
146             String payload = new String(decoder.decode(chunks[1]));
147             JsonObject jsonObject = JsonParser.parseString(payload).getAsJsonObject();
148
149             if (jsonObject.has("upn")) {
150                 return sanitize(jsonObject.get("upn").getAsString());
151             } else if (jsonObject.has("preferred_username")) {
152                 return sanitize(jsonObject.get("preferred_username").getAsString());
153             } else if (jsonObject.has("sub")) {
154                 return sanitize(jsonObject.get("sub").getAsString());
155             }
156         } catch (Exception e) {
157             logger.warn("Failed to extract subject from token: {}", e.getMessage());
158         }
159         return "n/av";
160     }
161
162     private String sanitize(String input) {
163         if (input == null) {
164             return "n/av";
165         }
166         // Replace dangerous characters
167         return input.replaceAll("[\r\n]", "")  // Remove newlines
168                 .replaceAll("[{}()<>]", "") // Remove brackets
169                 .replaceAll("[^\\p{Alnum}\\p{Punct}\\s]", ""); // Remove non-printable characters
170     }
171
172     private void restoreFromContextToMdc(ContextView context) {
173         context.getOrEmpty(FACILITY_KEY).ifPresent(value -> MDC.put(FACILITY_KEY, value.toString()));
174         context.getOrEmpty(SUBJECT_KEY).ifPresent(value -> MDC.put(SUBJECT_KEY, value.toString()));
175     }
176 }