2bb9dd03352d25abc0e7c19d76ce3bba57388cf3
[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.Base64;
28 import org.reactivestreams.Publisher;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.slf4j.MDC;
32 import org.springframework.core.io.buffer.DataBuffer;
33 import org.springframework.http.server.reactive.ServerHttpRequest;
34 import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
35 import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
36 import org.springframework.util.MultiValueMap;
37 import org.springframework.web.server.ServerWebExchange;
38 import org.springframework.web.server.WebFilter;
39 import org.springframework.web.server.WebFilterChain;
40 import reactor.core.publisher.Flux;
41 import reactor.core.publisher.Mono;
42 import reactor.util.context.Context;
43 import reactor.util.context.ContextView;
44
45 public class ReactiveEntryExitFilter implements WebFilter {
46
47     private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
48     private static final String BEARER_PREFIX = "Bearer ";
49     private static final String FACILITY_KEY = "facility";
50     private static final String SUBJECT_KEY = "subject";
51
52     @Override
53     public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
54
55         // sets FACILITY_KEY and SUBJECT_KEY in MDC
56         auditLog(exchange.getRequest());
57
58         String subject = MDC.get(SUBJECT_KEY);
59         String facility = MDC.get(FACILITY_KEY);
60
61         ServerHttpRequest httpRequest = exchange.getRequest();
62         MultiValueMap<String, String> queryParams = httpRequest.getQueryParams();
63         logger.info("Request received with path: {}, and the Request Id: {}, with HTTP Method: {}", httpRequest.getPath(),
64                 exchange.getRequest().getId(), exchange.getRequest().getMethod());
65         if (!queryParams.isEmpty())
66             logger.trace("For the request Id: {}, the Query parameters are: {}", exchange.getRequest().getId(), queryParams);
67
68         ServerHttpRequestDecorator loggingServerHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
69             String requestBody;
70
71             @Override
72             public Flux<DataBuffer> getBody() {
73                 return Flux.deferContextual(contextView ->
74                     // Now, return the original body flux with a doFinally to clear MDC after processing
75                     super.getBody().doOnNext(dataBuffer -> {
76                         requestBody = dataBuffer.toString(StandardCharsets.UTF_8); // Read the bytes from the DataBuffer
77                         logger.trace("For the request ID: {} the received request body: {}", exchange.getRequest().getId(), requestBody);
78                     }).doOnEach(signal -> MDC.clear())
79                             .doFinally(signal -> MDC.clear())); // Clear MDC after the request body is processed
80             }
81         };
82         ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
83             @Override
84             public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
85                 return Mono.deferContextual(contextView ->
86                     super.writeWith(Flux.from(body).doOnNext(dataBuffer -> {
87                         String responseBody = dataBuffer.toString(StandardCharsets.UTF_8);
88                         restoreFromContextToMdc(contextView);
89                         logger.info("For the request ID: {} the Status code of the response: {}",
90                                 exchange.getRequest().getId(), getStatusCode());
91                         logger.trace("For the request ID: {} the response is: {} ",
92                                 exchange.getRequest().getId(), responseBody);
93                     })).doFinally(signalType -> MDC.clear())); // Clear MDC to prevent leakage
94             }
95         };
96         return chain.filter(exchange.mutate()
97                         .request(loggingServerHttpRequestDecorator)
98                         .response(loggingServerHttpResponseDecorator)
99                         .build())
100                 .contextWrite(Context.of(FACILITY_KEY, facility, SUBJECT_KEY, subject))
101                 .doFinally(signalType -> MDC.clear());
102     }
103
104     private void auditLog(ServerHttpRequest request) {
105         String authHeader = request.getHeaders().getFirst("Authorization");
106         String subject = "n/av";
107         if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
108             String token = authHeader.substring(BEARER_PREFIX.length());
109             subject = this.getSubjectFromToken(token);
110         }
111
112         String facility = "log audit";
113
114         MDC.put(SUBJECT_KEY, subject);
115         MDC.put(FACILITY_KEY, facility);
116     }
117
118     private String getSubjectFromToken(String token) {
119         try {
120             String[] chunks = token.split("\\.");
121             if (chunks.length < 2) {
122                 logger.warn("Invalid JWT: Missing payload");
123                 return "n/av";
124             }
125
126             Base64.Decoder decoder = Base64.getUrlDecoder();
127             String payload = new String(decoder.decode(chunks[1]));
128             JsonObject jsonObject = JsonParser.parseString(payload).getAsJsonObject();
129
130             if (jsonObject.has("upn")) {
131                 return sanitize(jsonObject.get("upn").getAsString());
132             } else if (jsonObject.has("preferred_username")) {
133                 return sanitize(jsonObject.get("preferred_username").getAsString());
134             } else if (jsonObject.has("sub")) {
135                 return sanitize(jsonObject.get("sub").getAsString());
136             }
137         } catch (Exception e) {
138             logger.warn("Failed to extract subject from token: {}", e.getMessage());
139         }
140         return "n/av";
141     }
142
143     private String sanitize(String input) {
144         if (input == null) {
145             return "n/av";
146         }
147         // Replace dangerous characters
148         return input.replaceAll("[\r\n]", "")  // Remove newlines
149                 .replaceAll("[{}()<>]", "") // Remove brackets
150                 .replaceAll("[^\\p{Alnum}\\p{Punct}\\s]", ""); // Remove non-printable characters
151     }
152
153     private void restoreFromContextToMdc(ContextView context) {
154         context.getOrEmpty(FACILITY_KEY).ifPresent(value -> MDC.put(FACILITY_KEY, value.toString()));
155         context.getOrEmpty(SUBJECT_KEY).ifPresent(value -> MDC.put(SUBJECT_KEY, value.toString()));
156     }
157 }