/proc/self/cwd/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include "source/extensions/filters/common/ext_authz/ext_authz_http_impl.h" |
2 | | |
3 | | #include "envoy/config/core/v3/base.pb.h" |
4 | | #include "envoy/extensions/filters/http/ext_authz/v3/ext_authz.pb.h" |
5 | | #include "envoy/service/auth/v3/external_auth.pb.h" |
6 | | #include "envoy/type/matcher/v3/string.pb.h" |
7 | | |
8 | | #include "source/common/common/enum_to_int.h" |
9 | | #include "source/common/common/fmt.h" |
10 | | #include "source/common/common/matchers.h" |
11 | | #include "source/common/http/async_client_impl.h" |
12 | | #include "source/common/http/codes.h" |
13 | | #include "source/common/runtime/runtime_features.h" |
14 | | |
15 | | #include "absl/strings/str_cat.h" |
16 | | #include "absl/types/optional.h" |
17 | | #include "check_request_utils.h" |
18 | | |
19 | | namespace Envoy { |
20 | | namespace Extensions { |
21 | | namespace Filters { |
22 | | namespace Common { |
23 | | namespace ExtAuthz { |
24 | | |
25 | | namespace { |
26 | | |
27 | | // Static header map used for creating authorization requests. |
28 | 0 | const Http::HeaderMap& lengthZeroHeader() { |
29 | 0 | static const auto headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>( |
30 | 0 | {{Http::Headers::get().ContentLength, std::to_string(0)}}); |
31 | 0 | return *headers; |
32 | 0 | } |
33 | | |
34 | | // Static response used for creating authorization ERROR responses. |
35 | 0 | const Response& errorResponse() { |
36 | 0 | CONSTRUCT_ON_FIRST_USE(Response, Response{CheckStatus::Error, |
37 | 0 | Http::HeaderVector{}, |
38 | 0 | Http::HeaderVector{}, |
39 | 0 | Http::HeaderVector{}, |
40 | 0 | Http::HeaderVector{}, |
41 | 0 | Http::HeaderVector{}, |
42 | 0 | {{}}, |
43 | 0 | Http::Utility::QueryParamsVector{}, |
44 | 0 | {}, |
45 | 0 | EMPTY_STRING, |
46 | 0 | Http::Code::Forbidden, |
47 | 0 | ProtobufWkt::Struct{}}); |
48 | 0 | } |
49 | | |
50 | | // SuccessResponse used for creating either DENIED or OK authorization responses. |
51 | | struct SuccessResponse { |
52 | | SuccessResponse(const Http::HeaderMap& headers, const MatcherSharedPtr& matchers, |
53 | | const MatcherSharedPtr& append_matchers, |
54 | | const MatcherSharedPtr& response_matchers, |
55 | | const MatcherSharedPtr& dynamic_metadata_matchers, Response&& response) |
56 | | : headers_(headers), matchers_(matchers), append_matchers_(append_matchers), |
57 | | response_matchers_(response_matchers), |
58 | | to_dynamic_metadata_matchers_(dynamic_metadata_matchers), |
59 | 0 | response_(std::make_unique<Response>(response)) { |
60 | 0 | headers_.iterate([this](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { |
61 | | // UpstreamHeaderMatcher |
62 | 0 | if (matchers_->matches(header.key().getStringView())) { |
63 | 0 | response_->headers_to_set.emplace_back( |
64 | 0 | Http::LowerCaseString{std::string(header.key().getStringView())}, |
65 | 0 | std::string(header.value().getStringView())); |
66 | 0 | } |
67 | 0 | if (append_matchers_->matches(header.key().getStringView())) { |
68 | | // If there is an existing matching key in the current headers, the new entry will be |
69 | | // appended with the same key. For example, given {"key": "value1"} headers, if there is |
70 | | // a matching "key" from the authorization response headers {"key": "value2"}, the |
71 | | // request to upstream server will have two entries for "key": {"key": "value1", "key": |
72 | | // "value2"}. |
73 | 0 | response_->headers_to_add.emplace_back( |
74 | 0 | Http::LowerCaseString{std::string(header.key().getStringView())}, |
75 | 0 | std::string(header.value().getStringView())); |
76 | 0 | } |
77 | 0 | if (response_matchers_->matches(header.key().getStringView())) { |
78 | | // For HTTP implementation, the response headers from the auth server will, by default, be |
79 | | // appended (using addCopy) to the encoded response headers. |
80 | 0 | response_->response_headers_to_add.emplace_back( |
81 | 0 | Http::LowerCaseString{std::string(header.key().getStringView())}, |
82 | 0 | std::string(header.value().getStringView())); |
83 | 0 | } |
84 | 0 | if (to_dynamic_metadata_matchers_->matches(header.key().getStringView())) { |
85 | 0 | const std::string key{header.key().getStringView()}; |
86 | 0 | const std::string value{header.value().getStringView()}; |
87 | 0 | (*response_->dynamic_metadata.mutable_fields())[key] = ValueUtil::stringValue(value); |
88 | 0 | } |
89 | 0 | return Http::HeaderMap::Iterate::Continue; |
90 | 0 | }); |
91 | 0 | } |
92 | | |
93 | | const Http::HeaderMap& headers_; |
94 | | // All matchers below are used on headers_. |
95 | | const MatcherSharedPtr& matchers_; |
96 | | const MatcherSharedPtr& append_matchers_; |
97 | | const MatcherSharedPtr& response_matchers_; |
98 | | const MatcherSharedPtr& to_dynamic_metadata_matchers_; |
99 | | ResponsePtr response_; |
100 | | }; |
101 | | |
102 | | } // namespace |
103 | | |
104 | | // Config |
105 | | ClientConfig::ClientConfig(const envoy::extensions::filters::http::ext_authz::v3::ExtAuthz& config, |
106 | | uint32_t timeout, absl::string_view path_prefix) |
107 | | : client_header_matchers_(toClientMatchers( |
108 | | config.http_service().authorization_response().allowed_client_headers())), |
109 | | client_header_on_success_matchers_(toClientMatchersOnSuccess( |
110 | | config.http_service().authorization_response().allowed_client_headers_on_success())), |
111 | | to_dynamic_metadata_matchers_(toDynamicMetadataMatchers( |
112 | | config.http_service().authorization_response().dynamic_metadata_from_headers())), |
113 | | upstream_header_matchers_(toUpstreamMatchers( |
114 | | config.http_service().authorization_response().allowed_upstream_headers())), |
115 | | upstream_header_to_append_matchers_(toUpstreamMatchers( |
116 | | config.http_service().authorization_response().allowed_upstream_headers_to_append())), |
117 | | cluster_name_(config.http_service().server_uri().cluster()), timeout_(timeout), |
118 | | path_prefix_(path_prefix), |
119 | | tracing_name_(fmt::format("async {} egress", config.http_service().server_uri().cluster())), |
120 | | request_headers_parser_(Router::HeaderParser::configure( |
121 | | config.http_service().authorization_request().headers_to_add(), |
122 | 0 | envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD)) {} |
123 | | |
124 | | MatcherSharedPtr |
125 | 0 | ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list) { |
126 | 0 | std::vector<Matchers::StringMatcherPtr> matchers(CheckRequestUtils::createStringMatchers(list)); |
127 | 0 | return std::make_shared<HeaderKeyMatcher>(std::move(matchers)); |
128 | 0 | } |
129 | | |
130 | | MatcherSharedPtr |
131 | 0 | ClientConfig::toDynamicMetadataMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { |
132 | 0 | std::vector<Matchers::StringMatcherPtr> matchers(CheckRequestUtils::createStringMatchers(list)); |
133 | 0 | return std::make_shared<HeaderKeyMatcher>(std::move(matchers)); |
134 | 0 | } |
135 | | |
136 | | MatcherSharedPtr |
137 | 0 | ClientConfig::toClientMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { |
138 | 0 | std::vector<Matchers::StringMatcherPtr> matchers(CheckRequestUtils::createStringMatchers(list)); |
139 | | |
140 | | // If list is empty, all authorization response headers, except Host, should be added to |
141 | | // the client response. |
142 | 0 | if (matchers.empty()) { |
143 | 0 | envoy::type::matcher::v3::StringMatcher matcher; |
144 | 0 | matcher.set_exact(Http::Headers::get().Host.get()); |
145 | 0 | matchers.push_back( |
146 | 0 | std::make_unique<Matchers::StringMatcherImpl<envoy::type::matcher::v3::StringMatcher>>( |
147 | 0 | matcher)); |
148 | |
|
149 | 0 | return std::make_shared<NotHeaderKeyMatcher>(std::move(matchers)); |
150 | 0 | } |
151 | | |
152 | | // If not empty, all user defined matchers and default matcher's list will |
153 | | // be used instead. |
154 | 0 | std::vector<Http::LowerCaseString> keys{ |
155 | 0 | {Http::Headers::get().Status, Http::Headers::get().ContentLength, |
156 | 0 | Http::Headers::get().WWWAuthenticate, Http::Headers::get().Location}}; |
157 | |
|
158 | 0 | for (const auto& key : keys) { |
159 | 0 | envoy::type::matcher::v3::StringMatcher matcher; |
160 | 0 | matcher.set_exact(key.get()); |
161 | 0 | matchers.push_back( |
162 | 0 | std::make_unique<Matchers::StringMatcherImpl<envoy::type::matcher::v3::StringMatcher>>( |
163 | 0 | matcher)); |
164 | 0 | } |
165 | |
|
166 | 0 | return std::make_shared<HeaderKeyMatcher>(std::move(matchers)); |
167 | 0 | } |
168 | | |
169 | | MatcherSharedPtr |
170 | 0 | ClientConfig::toUpstreamMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { |
171 | 0 | return std::make_unique<HeaderKeyMatcher>(CheckRequestUtils::createStringMatchers(list)); |
172 | 0 | } |
173 | | |
174 | | RawHttpClientImpl::RawHttpClientImpl(Upstream::ClusterManager& cm, ClientConfigSharedPtr config) |
175 | 0 | : cm_(cm), config_(config) {} |
176 | | |
177 | 0 | RawHttpClientImpl::~RawHttpClientImpl() { ASSERT(callbacks_ == nullptr); } |
178 | | |
179 | 0 | void RawHttpClientImpl::cancel() { |
180 | 0 | ASSERT(callbacks_ != nullptr); |
181 | 0 | request_->cancel(); |
182 | 0 | callbacks_ = nullptr; |
183 | 0 | } |
184 | | |
185 | | // Client |
186 | | void RawHttpClientImpl::check(RequestCallbacks& callbacks, |
187 | | const envoy::service::auth::v3::CheckRequest& request, |
188 | | Tracing::Span& parent_span, |
189 | 0 | const StreamInfo::StreamInfo& stream_info) { |
190 | 0 | ASSERT(callbacks_ == nullptr); |
191 | 0 | callbacks_ = &callbacks; |
192 | |
|
193 | 0 | Http::RequestHeaderMapPtr headers; |
194 | |
|
195 | 0 | const auto& http_request = request.attributes().request().http(); |
196 | 0 | const auto& http_request_body = |
197 | 0 | http_request.body().empty() ? http_request.raw_body() : http_request.body(); |
198 | |
|
199 | 0 | uint64_t request_length = http_request_body.size(); |
200 | |
|
201 | 0 | if (request_length > 0) { |
202 | 0 | headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>( |
203 | 0 | {{Http::Headers::get().ContentLength, std::to_string(request_length)}}); |
204 | 0 | } else { |
205 | 0 | headers = Http::createHeaderMap<Http::RequestHeaderMapImpl>(lengthZeroHeader()); |
206 | 0 | } |
207 | |
|
208 | 0 | for (const auto& header : http_request.headers()) { |
209 | 0 | const Http::LowerCaseString key{header.first}; |
210 | | |
211 | | // Skip setting content-length header since it is already configured at initialization. |
212 | 0 | if (key == Http::Headers::get().ContentLength) { |
213 | 0 | continue; |
214 | 0 | } |
215 | | |
216 | 0 | if (key == Http::Headers::get().Path && !config_->pathPrefix().empty()) { |
217 | 0 | headers->addCopy(key, absl::StrCat(config_->pathPrefix(), header.second)); |
218 | 0 | } else { |
219 | 0 | headers->addCopy(key, header.second); |
220 | 0 | } |
221 | 0 | } |
222 | |
|
223 | 0 | config_->requestHeaderParser().evaluateHeaders(*headers, stream_info); |
224 | |
|
225 | 0 | Http::RequestMessagePtr message = |
226 | 0 | std::make_unique<Envoy::Http::RequestMessageImpl>(std::move(headers)); |
227 | 0 | if (request_length > 0) { |
228 | 0 | message->body().add(http_request_body); |
229 | 0 | } |
230 | |
|
231 | 0 | const std::string& cluster = config_->cluster(); |
232 | | |
233 | | // It's possible that the cluster specified in the filter configuration no longer exists due to a |
234 | | // CDS removal. |
235 | 0 | const auto thread_local_cluster = cm_.getThreadLocalCluster(cluster); |
236 | 0 | if (thread_local_cluster == nullptr) { |
237 | | // TODO(dio): Add stats related to this. |
238 | 0 | ENVOY_LOG(debug, "ext_authz cluster '{}' does not exist", cluster); |
239 | 0 | callbacks_->onComplete(std::make_unique<Response>(errorResponse())); |
240 | 0 | callbacks_ = nullptr; |
241 | 0 | } else { |
242 | | // Do not enforce a sampling decision on this span; instead keep the parent's sampling status. |
243 | 0 | auto options = Http::AsyncClient::RequestOptions() |
244 | 0 | .setTimeout(config_->timeout()) |
245 | 0 | .setParentSpan(parent_span) |
246 | 0 | .setChildSpanName(config_->tracingName()) |
247 | 0 | .setSampled(absl::nullopt); |
248 | |
|
249 | 0 | if (Runtime::runtimeFeatureEnabled( |
250 | 0 | "envoy.reloadable_features.ext_authz_http_send_original_xff")) { |
251 | 0 | options.setSendXff(false); |
252 | 0 | } |
253 | |
|
254 | 0 | request_ = thread_local_cluster->httpAsyncClient().send(std::move(message), *this, options); |
255 | 0 | } |
256 | 0 | } |
257 | | |
258 | | void RawHttpClientImpl::onSuccess(const Http::AsyncClient::Request&, |
259 | 0 | Http::ResponseMessagePtr&& message) { |
260 | 0 | callbacks_->onComplete(toResponse(std::move(message))); |
261 | 0 | callbacks_ = nullptr; |
262 | 0 | } |
263 | | |
264 | | void RawHttpClientImpl::onFailure(const Http::AsyncClient::Request&, |
265 | 0 | Http::AsyncClient::FailureReason reason) { |
266 | 0 | ASSERT(reason == Http::AsyncClient::FailureReason::Reset); |
267 | 0 | callbacks_->onComplete(std::make_unique<Response>(errorResponse())); |
268 | 0 | callbacks_ = nullptr; |
269 | 0 | } |
270 | | |
271 | | void RawHttpClientImpl::onBeforeFinalizeUpstreamSpan( |
272 | 0 | Tracing::Span& span, const Http::ResponseHeaderMap* response_headers) { |
273 | 0 | if (response_headers != nullptr) { |
274 | 0 | const uint64_t status_code = Http::Utility::getResponseStatus(*response_headers); |
275 | 0 | span.setTag(TracingConstants::get().HttpStatus, |
276 | 0 | Http::CodeUtility::toString(static_cast<Http::Code>(status_code))); |
277 | 0 | span.setTag(TracingConstants::get().TraceStatus, status_code == enumToInt(Http::Code::OK) |
278 | 0 | ? TracingConstants::get().TraceOk |
279 | 0 | : TracingConstants::get().TraceUnauthz); |
280 | 0 | } |
281 | 0 | } |
282 | | |
283 | 0 | ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) { |
284 | 0 | const uint64_t status_code = Http::Utility::getResponseStatus(message->headers()); |
285 | | |
286 | | // Set an error status if the call to the authorization server returns any of the 5xx HTTP error |
287 | | // codes. A Forbidden response is sent to the client if the filter has not been configured with |
288 | | // failure_mode_allow. |
289 | 0 | if (Http::CodeUtility::is5xx(status_code)) { |
290 | 0 | return std::make_unique<Response>(errorResponse()); |
291 | 0 | } |
292 | | |
293 | | // Extract headers-to-remove from the storage header coming from the |
294 | | // authorization server. |
295 | 0 | const auto& storage_header_name = Headers::get().EnvoyAuthHeadersToRemove; |
296 | | // If we are going to construct an Ok response we need to save the |
297 | | // headers_to_remove in a variable first. |
298 | 0 | std::vector<Http::LowerCaseString> headers_to_remove; |
299 | 0 | if (status_code == enumToInt(Http::Code::OK)) { |
300 | 0 | const auto& get_result = message->headers().get(storage_header_name); |
301 | 0 | for (size_t i = 0; i < get_result.size(); ++i) { |
302 | 0 | const Http::HeaderEntry* entry = get_result[i]; |
303 | 0 | if (entry != nullptr) { |
304 | 0 | absl::string_view storage_header_value = entry->value().getStringView(); |
305 | 0 | std::vector<absl::string_view> header_names = StringUtil::splitToken( |
306 | 0 | storage_header_value, ",", /*keep_empty_string=*/false, /*trim_whitespace=*/true); |
307 | 0 | headers_to_remove.reserve(headers_to_remove.size() + header_names.size()); |
308 | 0 | for (const auto& header_name : header_names) { |
309 | 0 | headers_to_remove.push_back(Http::LowerCaseString(std::string(header_name))); |
310 | 0 | } |
311 | 0 | } |
312 | 0 | } |
313 | 0 | } |
314 | | // Now remove the storage header from the authz server response headers before |
315 | | // we reuse them to construct an Ok/Denied authorization response below. |
316 | 0 | message->headers().remove(storage_header_name); |
317 | | |
318 | | // Create an Ok authorization response. |
319 | 0 | if (status_code == enumToInt(Http::Code::OK)) { |
320 | 0 | SuccessResponse ok{message->headers(), |
321 | 0 | config_->upstreamHeaderMatchers(), |
322 | 0 | config_->upstreamHeaderToAppendMatchers(), |
323 | 0 | config_->clientHeaderOnSuccessMatchers(), |
324 | 0 | config_->dynamicMetadataMatchers(), |
325 | 0 | Response{CheckStatus::OK, |
326 | 0 | Http::HeaderVector{}, |
327 | 0 | Http::HeaderVector{}, |
328 | 0 | Http::HeaderVector{}, |
329 | 0 | Http::HeaderVector{}, |
330 | 0 | Http::HeaderVector{}, |
331 | 0 | std::move(headers_to_remove), |
332 | 0 | Http::Utility::QueryParamsVector{}, |
333 | 0 | {}, |
334 | 0 | EMPTY_STRING, |
335 | 0 | Http::Code::OK, |
336 | 0 | ProtobufWkt::Struct{}}}; |
337 | 0 | return std::move(ok.response_); |
338 | 0 | } |
339 | | |
340 | | // Create a Denied authorization response. |
341 | 0 | SuccessResponse denied{message->headers(), |
342 | 0 | config_->clientHeaderMatchers(), |
343 | 0 | config_->upstreamHeaderToAppendMatchers(), |
344 | 0 | config_->clientHeaderOnSuccessMatchers(), |
345 | 0 | config_->dynamicMetadataMatchers(), |
346 | 0 | Response{CheckStatus::Denied, |
347 | 0 | Http::HeaderVector{}, |
348 | 0 | Http::HeaderVector{}, |
349 | 0 | Http::HeaderVector{}, |
350 | 0 | Http::HeaderVector{}, |
351 | 0 | Http::HeaderVector{}, |
352 | 0 | {{}}, |
353 | 0 | Http::Utility::QueryParamsVector{}, |
354 | 0 | {}, |
355 | 0 | message->bodyAsString(), |
356 | 0 | static_cast<Http::Code>(status_code), |
357 | 0 | ProtobufWkt::Struct{}}}; |
358 | 0 | return std::move(denied.response_); |
359 | 0 | } |
360 | | |
361 | | } // namespace ExtAuthz |
362 | | } // namespace Common |
363 | | } // namespace Filters |
364 | | } // namespace Extensions |
365 | | } // namespace Envoy |