syntax = "proto3";

package envoy.extensions.filters.http.rate_limit_quota.v3;

import "envoy/config/core/v3/base.proto";
import "envoy/config/core/v3/extension.proto";
import "envoy/config/core/v3/grpc_service.proto";
import "envoy/type/v3/http_status.proto";
import "envoy/type/v3/ratelimit_strategy.proto";

import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
import "google/rpc/status.proto";

import "xds/annotations/v3/status.proto";
import "xds/type/matcher/v3/matcher.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.http.rate_limit_quota.v3";
option java_outer_classname = "RateLimitQuotaProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rate_limit_quota/v3;rate_limit_quotav3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;
option (xds.annotations.v3.file_status).work_in_progress = true;

// [#protodoc-title: Rate Limit Quota]
// Rate Limit Quota :ref:`configuration overview <config_http_filters_rate_limit_quota>`.
// [#extension: envoy.filters.http.rate_limit_quota]

// Configures the Rate Limit Quota filter.
//
// Can be overridden in the per-route and per-host configurations.
// The more specific definition completely overrides the less specific definition.
// [#next-free-field: 7]
message RateLimitQuotaFilterConfig {
  // Configures the gRPC Rate Limit Quota Service (RLQS) RateLimitQuotaService.
  config.core.v3.GrpcService rlqs_server = 1 [(validate.rules).message = {required: true}];

  // The application domain to use when calling the service. This enables sharing the quota
  // server between different applications without fear of overlap.
  // E.g., "envoy".
  string domain = 2 [(validate.rules).string = {min_len: 1}];

  // The match tree to use for grouping incoming requests into buckets.
  //
  // Example:
  //
  // .. validated-code-block:: yaml
  //   :type-name: xds.type.matcher.v3.Matcher
  //
  //   matcher_list:
  //     matchers:
  //     # Assign requests with header['env'] set to 'staging' to the bucket { name: 'staging' }
  //     - predicate:
  //         single_predicate:
  //           input:
  //             typed_config:
  //               '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput
  //               header_name: env
  //           value_match:
  //             exact: staging
  //       on_match:
  //         action:
  //           typed_config:
  //             '@type': type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings
  //             bucket_id_builder:
  //               bucket_id_builder:
  //                 name:
  //                   string_value: staging
  //
  //     # Assign requests with header['user_group'] set to 'admin' to the bucket { acl: 'admin_users' }
  //     - predicate:
  //         single_predicate:
  //           input:
  //             typed_config:
  //               '@type': type.googleapis.com/xds.type.matcher.v3.HttpAttributesCelMatchInput
  //           custom_match:
  //             typed_config:
  //               '@type': type.googleapis.com/xds.type.matcher.v3.CelMatcher
  //               expr_match:
  //                 # Shortened for illustration purposes. Here should be parsed CEL expression:
  //                 # request.headers['user_group'] == 'admin'
  //                 parsed_expr: {}
  //       on_match:
  //         action:
  //           typed_config:
  //             '@type': type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings
  //             bucket_id_builder:
  //               bucket_id_builder:
  //                 acl:
  //                   string_value: admin_users
  //
  //   # Catch-all clause for the requests not matched by any of the matchers.
  //   # In this example, deny all requests.
  //   on_no_match:
  //     action:
  //       typed_config:
  //         '@type': type.googleapis.com/envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings
  //         no_assignment_behavior:
  //           fallback_rate_limit:
  //             blanket_rule: DENY_ALL
  //
  // .. attention::
  //  The first matched group wins. Once the request is matched into a bucket, matcher
  //  evaluation ends.
  //
  // Use ``on_no_match`` field to assign the catch-all bucket. If a request is not matched
  // into any bucket, and there's no  ``on_no_match`` field configured, the request will be
  // ALLOWED by default. It will NOT be reported to the RLQS server.
  //
  // Refer to :ref:`Unified Matcher API <envoy_v3_api_msg_.xds.type.matcher.v3.Matcher>`
  // documentation for more information on the matcher trees.
  xds.type.matcher.v3.Matcher bucket_matchers = 3 [(validate.rules).message = {required: true}];

  // If set, this will enable -- but not necessarily enforce -- the rate limit for the given
  // fraction of requests.
  //
  // Defaults to 100% of requests.
  config.core.v3.RuntimeFractionalPercent filter_enabled = 4;

  // If set, this will enforce the rate limit decisions for the given fraction of requests.
  // For requests that are not enforced the filter will still obtain the quota and include it
  // in the load computation, however the request will always be allowed regardless of the outcome
  // of quota application. This allows validation or testing of the rate limiting service
  // infrastructure without disrupting existing traffic.
  //
  // Note: this only applies to the fraction of enabled requests.
  //
  // Defaults to 100% of requests.
  config.core.v3.RuntimeFractionalPercent filter_enforced = 5;

  // Specifies a list of HTTP headers that should be added to each request that
  // has been rate limited and is also forwarded upstream. This can only occur when the
  // filter is enabled but not enforced.
  repeated config.core.v3.HeaderValueOption request_headers_to_add_when_not_enforced = 6
      [(validate.rules).repeated = {max_items: 10}];
}

// Per-route and per-host configuration overrides. The more specific definition completely
// overrides the less specific definition.
message RateLimitQuotaOverride {
  // The application domain to use when calling the service. This enables sharing the quota
  // server between different applications without fear of overlap.
  // E.g., "envoy".
  //
  // If empty, inherits the value from the less specific definition.
  string domain = 1;

  // The match tree to use for grouping incoming requests into buckets.
  //
  // If set, fully overrides the bucket matchers provided on the less specific definition.
  // If not set, inherits the value from the less specific definition.
  //
  // See usage example: :ref:`RateLimitQuotaFilterConfig.bucket_matchers
  // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig.bucket_matchers>`.
  xds.type.matcher.v3.Matcher bucket_matchers = 2;
}

// Rate Limit Quota Bucket Settings to apply on the successful ``bucket_matchers`` match.
//
// Specify this message in the :ref:`Matcher.OnMatch.action
// <envoy_v3_api_field_.xds.type.matcher.v3.Matcher.OnMatch.action>` field of the
// ``bucket_matchers`` matcher tree to assign the matched requests to the Quota Bucket.
// Usage example: :ref:`RateLimitQuotaFilterConfig.bucket_matchers
// <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaFilterConfig.bucket_matchers>`.
// [#next-free-field: 6]
message RateLimitQuotaBucketSettings {
  // Configures the behavior after the first request has been matched to the bucket, and before the
  // the RLQS server returns the first quota assignment.
  message NoAssignmentBehavior {
    oneof no_assignment_behavior {
      option (validate.required) = true;

      // Apply pre-configured rate limiting strategy until the server sends the first assignment.
      type.v3.RateLimitStrategy fallback_rate_limit = 1;
    }
  }

  // Specifies the behavior when the bucket's assignment has expired, and cannot be refreshed for
  // any reason.
  message ExpiredAssignmentBehavior {
    // Reuse the last known quota assignment, effectively extending it for the duration
    // specified in the :ref:`expired_assignment_behavior_timeout
    // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior.expired_assignment_behavior_timeout>`
    // field.
    message ReuseLastAssignment {
    }

    // Limit the time :ref:`ExpiredAssignmentBehavior
    // <envoy_v3_api_msg_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior>`
    // is applied. If the server doesn't respond within this duration:
    //
    // 1. Selected ``ExpiredAssignmentBehavior`` is no longer applied.
    // 2. The bucket is abandoned. The process of abandoning the bucket is described in the
    //    :ref:`AbandonAction <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
    //    message.
    // 3. If a new request is matched into the bucket that has become abandoned,
    //    the data plane restarts the subscription to the bucket. The process of restarting the
    //    subscription is described in the :ref:`AbandonAction
    //    <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
    //    message.
    //
    // If not set, defaults to zero, and the bucket is abandoned immediately.
    google.protobuf.Duration expired_assignment_behavior_timeout = 1
        [(validate.rules).duration = {gt {}}];

    oneof expired_assignment_behavior {
      option (validate.required) = true;

      // Apply the rate limiting strategy to all requests matched into the bucket until the RLQS
      // server sends a new assignment, or the :ref:`expired_assignment_behavior_timeout
      // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior.expired_assignment_behavior_timeout>`
      // runs out.
      type.v3.RateLimitStrategy fallback_rate_limit = 2;

      // Reuse the last ``active`` assignment until the RLQS server sends a new assignment, or the
      // :ref:`expired_assignment_behavior_timeout
      // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior.expired_assignment_behavior_timeout>`
      // runs out.
      ReuseLastAssignment reuse_last_assignment = 3;
    }
  }

  // Customize the deny response to the requests over the rate limit.
  message DenyResponseSettings {
    // HTTP response code to deny for HTTP requests (gRPC excluded).
    // Defaults to 429 (:ref:`StatusCode.TooManyRequests<envoy_v3_api_enum_value_type.v3.StatusCode.TooManyRequests>`).
    type.v3.HttpStatus http_status = 1;

    // HTTP response body used to deny for HTTP requests (gRPC excluded).
    // If not set, an empty body is returned.
    google.protobuf.BytesValue http_body = 2;

    // Configure the deny response for gRPC requests over the rate limit.
    // Allows to specify the `RPC status code
    // <https://cloud.google.com/natural-language/docs/reference/rpc/google.rpc#google.rpc.Code>`_,
    // and the error message.
    // Defaults to the Status with the RPC Code ``UNAVAILABLE`` and empty message.
    //
    // To identify gRPC requests, Envoy checks that the ``Content-Type`` header is
    // ``application/grpc``, or one of the various ``application/grpc+`` values.
    //
    // .. note::
    //   The HTTP code for a gRPC response is always 200.
    google.rpc.Status grpc_status = 3;

    // Specifies a list of HTTP headers that should be added to each response for requests that
    // have been rate limited. Applies both to plain HTTP, and gRPC requests.
    // The headers are added even when the rate limit quota was not enforced.
    repeated config.core.v3.HeaderValueOption response_headers_to_add = 4
        [(validate.rules).repeated = {max_items: 10}];
  }

  // ``BucketIdBuilder`` makes it possible to build :ref:`BucketId
  // <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>` with values substituted
  // from the dynamic properties associated with each individual request. See usage examples in
  // the docs to :ref:`bucket_id_builder
  // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.bucket_id_builder>`
  // field.
  message BucketIdBuilder {
    // Produces the value of the :ref:`BucketId
    // <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>` map.
    message ValueBuilder {
      oneof value_specifier {
        option (validate.required) = true;

        // Static string value — becomes the value in the :ref:`BucketId
        // <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>` map as is.
        string string_value = 1;

        // Dynamic value — evaluated for each request. Must produce a string output, which becomes
        // the value in the :ref:`BucketId <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>`
        // map. For example, extensions with the ``envoy.matching.http.input`` category can be used.
        config.core.v3.TypedExtensionConfig custom_value = 2;
      }
    }

    // The map translated into the ``BucketId`` map.
    //
    // The ``string key`` of this map and becomes the key of ``BucketId`` map as is.
    //
    // The ``ValueBuilder value`` for the key can be:
    //
    // * static ``StringValue string_value`` — becomes the value in the ``BucketId`` map as is.
    // * dynamic ``TypedExtensionConfig custom_value`` — evaluated for each request. Must produce
    //   a string output, which becomes the value in the the ``BucketId`` map.
    //
    // See usage examples in the docs to :ref:`bucket_id_builder
    // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.bucket_id_builder>`
    // field.
    map<string, ValueBuilder> bucket_id_builder = 1 [(validate.rules).map = {min_pairs: 1}];
  }

  // ``BucketId`` builder.
  //
  // :ref:`BucketId <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>` is a map from
  // the string key to the string value which serves as bucket identifier common for on
  // the control plane and the data plane.
  //
  // While ``BucketId`` is always static, ``BucketIdBuilder`` allows to populate map values
  // with the dynamic properties associated with the each individual request.
  //
  // Example 1: static fields only
  //
  // ``BucketIdBuilder``:
  //
  // .. validated-code-block:: yaml
  //   :type-name: envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.BucketIdBuilder
  //
  //   bucket_id_builder:
  //     name:
  //       string_value: my_bucket
  //     hello:
  //       string_value: world
  //
  // Produces the following ``BucketId`` for all requests:
  //
  // .. validated-code-block:: yaml
  //   :type-name: envoy.service.rate_limit_quota.v3.BucketId
  //
  //   bucket:
  //     name: my_bucket
  //     hello: world
  //
  // Example 2: static and dynamic fields
  //
  // .. validated-code-block:: yaml
  //   :type-name: envoy.extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.BucketIdBuilder
  //
  //   bucket_id_builder:
  //     name:
  //       string_value: my_bucket
  //     env:
  //       custom_value:
  //         typed_config:
  //           '@type': type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput
  //           header_name: environment
  //
  // In this example, the value of ``BucketId`` key ``env`` is substituted from the ``environment``
  // request header.
  //
  // This is equivalent to the following ``pseudo-code``:
  //
  // .. code-block:: yaml
  //
  //    name: 'my_bucket'
  //    env: $header['environment']
  //
  // For example, the request with the HTTP header ``env`` set to ``staging`` will produce
  // the following ``BucketId``:
  //
  // .. validated-code-block:: yaml
  //   :type-name: envoy.service.rate_limit_quota.v3.BucketId
  //
  //   bucket:
  //     name: my_bucket
  //     env: staging
  //
  // For the request with the HTTP header ``environment`` set to ``prod``, will produce:
  //
  // .. validated-code-block:: yaml
  //   :type-name: envoy.service.rate_limit_quota.v3.BucketId
  //
  //   bucket:
  //     name: my_bucket
  //     env: prod
  //
  // .. note::
  //   The order of ``BucketId`` keys do not matter. Buckets ``{ a: 'A', b: 'B' }`` and
  //   ``{ b: 'B', a: 'A' }`` are identical.
  //
  // If not set, requests will NOT be reported to the server, and will always limited
  // according to :ref:`no_assignment_behavior
  // <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.no_assignment_behavior>`
  // configuration.
  BucketIdBuilder bucket_id_builder = 1;

  // The interval at which the data plane (RLQS client) is to report quota usage for this bucket.
  //
  // When the first request is matched to a bucket with no assignment, the data plane is to report
  // the request immediately in the :ref:`RateLimitQuotaUsageReports
  // <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaUsageReports>` message.
  // For the RLQS server, this signals that the data plane is now subscribed to
  // the quota assignments in this bucket, and will start sending the assignment as described in
  // the :ref:`RLQS documentation <envoy_v3_api_file_envoy/service/rate_limit_quota/v3/rlqs.proto>`.
  //
  // After sending the initial report, the data plane is to continue reporting the bucket usage with
  // the internal specified in this field.
  //
  // If for any reason RLQS client doesn't receive the initial assignment for the reported bucket,
  // the data plane will eventually consider the bucket abandoned and stop sending the usage
  // reports. This is explained in more details at :ref:`Rate Limit Quota Service (RLQS)
  // <envoy_v3_api_file_envoy/service/rate_limit_quota/v3/rlqs.proto>`.
  //
  // [#comment: 100000000 nanoseconds = 0.1 seconds]
  google.protobuf.Duration reporting_interval = 2 [(validate.rules).duration = {
    required: true
    gt {nanos: 100000000}
  }];

  // Customize the deny response to the requests over the rate limit.
  // If not set, the filter will be configured as if an empty message is set,
  // and will behave according to the defaults specified in :ref:`DenyResponseSettings
  // <envoy_v3_api_msg_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.DenyResponseSettings>`.
  DenyResponseSettings deny_response_settings = 3;

  // Configures the behavior in the "no assignment" state: after the first request has been
  // matched to the bucket, and before the the RLQS server returns the first quota assignment.
  //
  // If not set, the default behavior is to allow all requests.
  NoAssignmentBehavior no_assignment_behavior = 4;

  // Configures the behavior in the "expired assignment" state: the bucket's assignment has expired,
  // and cannot be refreshed.
  //
  // If not set, the bucket is abandoned when its ``active`` assignment expires.
  // The process of abandoning the bucket, and restarting the subscription is described in the
  // :ref:`AbandonAction <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
  // message.
  ExpiredAssignmentBehavior expired_assignment_behavior = 5;
}
