Coverage Report

Created: 2023-11-12 09:30

/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