]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
RGW: add support global CORS rule 66500/head
authorAli Masarwa <amasarwa@redhat.com>
Wed, 3 Dec 2025 17:19:09 +0000 (19:19 +0200)
committerAli Masarwa <amasarwa@redhat.com>
Wed, 25 Mar 2026 14:43:23 +0000 (16:43 +0200)
Signed-off-by: Ali Masarwa <amasarwa@redhat.com>
13 files changed:
doc/radosgw/config-ref.rst
qa/workunits/rgw/test_rgw_global_cors.py [new file with mode: 0644]
src/common/options/rgw.yaml.in
src/rgw/radosgw-admin/radosgw-admin.cc
src/rgw/rgw_cors.cc
src/rgw/rgw_cors.h
src/rgw/rgw_cors_swift.h
src/rgw/rgw_op.cc
src/rgw/rgw_op.h
src/rgw/rgw_process.cc
src/rgw/rgw_rest_s3.cc
src/rgw/rgw_rest_s3.h
src/rgw/rgw_rest_swift.cc

index 246bd43b604f0a07e28142126b57f0427378876a..37cb646f20c9705fc34efef2f1ff583d3a26db21 100644 (file)
@@ -365,3 +365,16 @@ Cloud Restore feature currently enables the restoration of objects transitioned
 .. confval:: rgw_restore_processor_period
 
 These values can be tuned based upon your specific workload to further increase the aggressiveness of restore processing. 
+
+Global CORS
+===========
+
+Configuration options that allows administrators to define a global CORS policy applied at the gateway level, affecting all buckets served by RGW.
+Previously, CORS (Cross-Origin Resource Sharing) support in RGW is limited to per-bucket configuration using the Get/PutBucketCors API, following the standard AWS S3 behavior.
+While this suffices for most use cases, it becomes a blocker when using browser-based tools that require interaction with gateway-wide resources, such as listing all buckets accessible to a user.
+These configuration options are necessary to enable CORS-based access for tools like S3 Browser, which need to list and create buckets across the gateway from a browser client.
+
+.. confval:: rgw_gcors_allow_origins
+.. confval:: rgw_gcors_allow_headers
+.. confval:: rgw_gcors_allow_methods
+.. confval:: rgw_gcors_expose_headers
diff --git a/qa/workunits/rgw/test_rgw_global_cors.py b/qa/workunits/rgw/test_rgw_global_cors.py
new file mode 100644 (file)
index 0000000..5ae9800
--- /dev/null
@@ -0,0 +1,80 @@
+import requests
+from requests_aws4auth import AWS4Auth
+import json
+try:
+    import xmltodict
+except ModuleNotFoundError:
+    print("Module 'xmltodict' is not installed.")
+
+
+ACCESS_KEY = '0555b35654ad1656d804'
+SECRET_KEY = 'h7GhxuBLTrlhVUyxSPUKUV8r/2EI4ngqJxD7iBdBYLhwluN30JaT3Q=='
+REGION = 'us-east-1'  # Replace with your bucket's region
+
+# 1. Initialize AWS4Auth for Signature Version 4
+# This generates the signature and handles the Authorization header automatically
+auth = AWS4Auth(ACCESS_KEY, SECRET_KEY, REGION, 's3')
+
+url = 'http://localhost:8000/'
+origin = "https://www.your-website.com"
+custom_headers = 'Content-Type,Authorization'
+methods = 'GET,PUT, DELETE'
+
+# 1. Step: Send the Preflight OPTIONS Request
+preflight_headers = {
+    "Host": "8.8.8.8",
+    'Origin': origin,
+    'Access-Control-Request-Method': methods,
+    'Access-Control-Request-Headers': custom_headers
+}
+
+# 2. Step: Send the Actual GET Request
+actual_headers = {
+    "Host": "8.8.8.8",
+    'Origin': origin,
+}
+
+def list_buckets():
+    print("Listing all buckets...")
+    url = 'http://localhost:8000/'
+    response = requests.get(url, headers=actual_headers, auth=auth)
+    print(f"Actual Response Status: {response.status_code}")
+    data_dict = xmltodict.parse(response.text, process_namespaces=False)
+    json_data = json.dumps(data_dict["ListAllMyBucketsResult"]["Buckets"], indent=4)
+    print(f"Buckets: {json_data}")
+
+def create_bucket(bucket_name):
+    print(f"Creating a bucket named {bucket_name}...")
+    url = f'http://localhost:8000/{bucket_name}'
+    response = requests.put(url, headers=actual_headers, auth=auth)
+    print(f"Actual Response Status: {response.status_code}")
+
+def delete_bucket(bucket_name):
+    print(f"Deleting a bucket named {bucket_name}...")
+    url = f'http://localhost:8000/{bucket_name}'
+    response = requests.delete(url, headers=actual_headers, auth=auth)
+    print(f"Actual Response Status: {response.status_code}")
+
+preflight_resp = requests.options(url, headers=preflight_headers)
+
+print(f"Preflight Status: {preflight_resp.status_code}")
+
+# Check if the server allows the custom headers
+allowed_headers = preflight_resp.headers.get('Access-Control-Allow-Headers', '')
+
+if preflight_resp.status_code in [200, 204] and custom_headers.lower() in allowed_headers.lower():
+    print(f"Preflight successful! Proceeding with {methods} request...")
+
+    list_buckets()
+
+    create_bucket('bkt')
+    list_buckets()
+
+    create_bucket('bkt2')
+    list_buckets()
+
+    delete_bucket('bkt')
+    list_buckets()
+
+    delete_bucket('bkt2')
+    list_buckets()
index 6a85b076ca5f826bca6ab4782fd00d790dfc20e7..162b618878576efcb9aa33c14f3dfb96c5edc86a 100644 (file)
@@ -4642,3 +4642,27 @@ options:
   see_also:
   - rgw_enable_usage_log
   with_legacy: true
+- name: rgw_gcors_allow_origins
+  type: str
+  level: advanced
+  desc: When not empty, this value is returned as a response header Access-Control-Allow-Origins.
+  services:
+  - rgw
+- name: rgw_gcors_allow_headers
+  type: str
+  level: advanced
+  desc: When not empty, this value is returned as a response header Access-Control-Allow-Headers to preflight requests.
+  services:
+  - rgw
+- name: rgw_gcors_allow_methods
+  type: str
+  level: advanced
+  desc: When not empty, this value is returned as a response header Access-Control-Allow-Methods.
+  services:
+  - rgw
+- name: rgw_gcors_expose_headers
+  type: str
+  level: advanced
+  desc: When not empty, this value is returned as a response header Access-Control-Expose-Headers.
+  services:
+  - rgw
index a8d22d99e029ac42373700a846b522791a49fedc..ba431cb1d4fcdbe7f4d14d27c9a4f15260df3a80 100644 (file)
@@ -14,6 +14,8 @@
 #include <boost/asio/co_spawn.hpp>
 #include <boost/asio/use_awaitable.hpp>
 
+#include "rgw_cors_s3.h"
+
 extern "C" {
 #include <liboath/oath.h>
 }
@@ -937,6 +939,7 @@ enum class OPT {
   ACCOUNT_LIST,
   RESTORE_STATUS,
   RESTORE_LIST,
+  GLOBAL_CORS_GET,
 };
 
 }
@@ -1205,6 +1208,7 @@ static SimpleCmd::Commands all_cmds = {
   { "account list", OPT::ACCOUNT_LIST },
   { "restore status", OPT::RESTORE_STATUS },
   { "restore list", OPT::RESTORE_LIST },
+  { "global-cors get", OPT::GLOBAL_CORS_GET},
 };
 
 static SimpleCmd::Aliases cmd_aliases = {
@@ -3867,6 +3871,12 @@ int main(int argc, const char **argv)
   std::optional<std::string> restore_status_filter;
   int show_restore_stats = false;
 
+  // global CORS settings
+  std::optional<std::string> gcors_allow_origins;
+  std::optional<std::string> gcors_allow_methods;
+  std::optional<std::string> gcors_allow_headers;
+  std::optional<std::string> gcors_expose_headers;
+
   init_realm_param(cct.get(), realm_id, opt_realm_id, "rgw_realm_id");
   init_realm_param(cct.get(), zonegroup_id, opt_zonegroup_id, "rgw_zonegroup_id");
   init_realm_param(cct.get(), zone_id, opt_zone_id, "rgw_zone_id");
@@ -4467,6 +4477,14 @@ int main(int argc, const char **argv)
       restore_status_filter = val;
     } else if (ceph_argparse_binary_flag(args, i, &show_restore_stats, NULL, "--show-restore-stats", (char*)NULL)){
       // do nothing
+    } else if (ceph_argparse_witharg(args, i, &val, "--allow-origin", (char*)NULL)) {
+      gcors_allow_origins = val;
+    } else if (ceph_argparse_witharg(args, i, &val, "--allow-methods", (char*)NULL)) {
+      gcors_allow_methods = val;
+    } else if (ceph_argparse_witharg(args, i, &val, "--allow-headers", (char*)NULL)) {
+      gcors_allow_headers = val;
+    } else if (ceph_argparse_witharg(args, i, &val, "--expose-headers", (char*)NULL)) {
+      gcors_expose_headers = val;
     } else if (strncmp(*i, "-", 1) == 0) {
       cerr << "ERROR: invalid flag " << *i << std::endl;
       return EINVAL;
@@ -12410,5 +12428,35 @@ next:
                                             err_msg, stream_flusher, null_yield);
     }
   }
+  if (opt_cmd == OPT::GLOBAL_CORS_GET) {
+    string allow_origins, allow_headers, allow_methods, expose_headers;
+    ret = g_conf().get_val("rgw_gcors_allow_origins", &allow_origins);
+    if (ret < 0 || allow_origins.empty()) {
+      cerr << "ERROR in OPT::GLOBAL_CORS_GET, no rgw_gcors_allow_origins config found or empty, ret=" << ret << std::endl;
+      return -EINVAL;
+    }
+    ret = g_conf().get_val("rgw_gcors_allow_headers", &allow_headers);
+    if (ret < 0 || allow_headers.empty()) {
+      cerr << "ERROR in OPT::GLOBAL_CORS_GET, no rgw_gcors_allow_headers config found or empty, ret=" << ret << std::endl;
+      return -EINVAL;
+    }
+    ret = g_conf().get_val("rgw_gcors_allow_methods", &allow_methods);
+    if (ret < 0 || allow_methods.empty()) {
+      cerr << "ERROR in OPT::GLOBAL_CORS_GET, no rgw_gcors_allow_methods config found or empty, ret=" << ret << std::endl;
+      return -EINVAL;
+    }
+    ret = g_conf().get_val("rgw_gcors_expose_headers", &expose_headers);
+    std::optional<RGWCORSRule> optional_global_cors;
+    if (RGWCORSRule::create_rule(allow_origins.c_str(), allow_headers.c_str(),
+          expose_headers.c_str(), allow_methods.c_str(), optional_global_cors) < 0) {
+      cerr << "ERROR: couldn't create RGWCORSRule from rgw_gcors_allow_origins=" << allow_origins <<
+                 ", rgw_gcors_allow_headers=" << allow_headers << ", rgw_gcors_allow_methods=" << allow_methods <<
+                 ", rgw_gcors_expose_headers=" << expose_headers << std::endl;
+      return -EINVAL;
+    }
+
+    optional_global_cors->dump(formatter.get());
+    formatter->flush(cout);
+  }
   return 0;
 }
index 049e0ab0eacec07791ffea73a34183d41e8af59c..6ca6fad42c7564814b4ec192e10e146b8b105546 100644 (file)
@@ -50,6 +50,7 @@ void RGWCORSRule::dump(Formatter *f) const
   encode_json("AllowedOrigin", allowed_origins, f);
   encode_json("AllowedHeader", allowed_hdrs, f);
   encode_json("ExposeHeader", exposable_hdrs, f);
+  f->close_section();//CORSRule
 }
 
 void RGWCORSRule::erase_origin_if_present(string& origin, bool *rule_empty) {
@@ -81,6 +82,59 @@ list<RGWCORSRule> RGWCORSRule::generate_test_instances()
   return o;
 }
 
+int RGWCORSRule::create_rule(const char *allow_origins, const char *allow_headers,
+                  const char *expose_headers, const char* allowed_methods, std::optional<RGWCORSRule>& rule, const char *max_age)
+{
+  std::set<std::string> o, h;
+  std::list<std::string> e;
+  unsigned long a = CORS_MAX_AGE_INVALID;
+  const uint8_t flags = ("*"s == allowed_methods)? RGW_CORS_ALL:get_multi_cors_method_flags(allowed_methods);
+
+  int nr_invalid_names = 0;
+  auto add_host = [&nr_invalid_names, &o] (auto host) {
+    if (validate_name_string(host) == 0) {
+      o.emplace(std::string{host});
+    } else {
+      nr_invalid_names++;
+    }
+  };
+  for_each_substr(allow_origins, ";,= \t", add_host);
+  if (o.empty() || nr_invalid_names > 0) {
+    return -EINVAL;
+  }
+
+  if (allow_headers) {
+    int nr_invalid_headers = 0;
+    auto add_header = [&nr_invalid_headers, &h] (auto allow_header) {
+      if (validate_name_string(allow_header) == 0) {
+        h.emplace(std::string{allow_header});
+      } else {
+        nr_invalid_headers++;
+      }
+    };
+    for_each_substr(allow_headers, ";,= \t", add_header);
+    if (h.empty() || nr_invalid_headers > 0) {
+      return -EINVAL;
+    }
+  }
+
+  if (expose_headers) {
+    for_each_substr(expose_headers, ";,= \t",
+        [&e] (auto expose_header) {
+          e.emplace_back(std::string(expose_header));
+        });
+  }
+  if (max_age) {
+    char *end = NULL;
+    a = strtoul(max_age, &end, 10);
+    if (a == ULONG_MAX)
+      a = CORS_MAX_AGE_INVALID;
+  }
+
+  rule = RGWCORSRule(o, h, e, flags, a);
+  return 0;
+}
+
 /*
  * make attrs look-like-this
  * does not convert underscores or dashes
index 64f85275b5af92cc77a37eb40af52a7534b803a7..fe4f5ecdbd96f73b3f6e3b58d07f473156b7ad26 100644 (file)
@@ -59,6 +59,7 @@ public:
   std::string& get_id() { return id; }
   uint32_t get_max_age() { return max_age; }
   uint8_t get_allowed_methods() { return allowed_methods; }
+  bool has_origin(const std::string& s) const { return allowed_origins.contains(s); }
 
   void encode(bufferlist& bl) const {
     ENCODE_START(1, 1, bl);
@@ -81,11 +82,13 @@ public:
     DECODE_FINISH(bl);
   }
   static std::list<RGWCORSRule> generate_test_instances();
+  static int create_rule(const char *allow_origins, const char *allow_headers,
+                  const char *expose_headers, const char* allowed_methods, std::optional<RGWCORSRule>& rule, const char *max_age="");
   bool has_wildcard_origin();
   bool is_origin_present(const char *o);
   void format_exp_headers(std::string& s);
   void erase_origin_if_present(std::string& origin, bool *rule_empty);
-  void dump_origins(); 
+  void dump_origins();
   void dump(Formatter *f) const;
   bool is_header_allowed(const char *hdr, size_t len);
 };
@@ -142,6 +145,23 @@ static inline uint8_t get_cors_method_flags(const char *req_meth) {
   else if (strcmp(req_meth, "PUT") == 0) flags = RGW_CORS_PUT;
   else if (strcmp(req_meth, "DELETE") == 0) flags = RGW_CORS_DELETE;
   else if (strcmp(req_meth, "HEAD") == 0) flags = RGW_CORS_HEAD;
+  else if (strcmp(req_meth, "COPY") == 0) flags = RGW_CORS_COPY;
+
+  return flags;
+}
+
+static inline uint8_t get_multi_cors_method_flags(const char *req_meth) {
+  uint8_t flags = 0;
+  const std::string allowed_methods(req_meth);
+  auto apply_flag = [&flags] (std::string_view method) {
+    if (method == "GET") flags |= RGW_CORS_GET;
+    else if (method == "POST") flags |= RGW_CORS_POST;
+    else if (method == "PUT") flags |= RGW_CORS_PUT;
+    else if (method == "DELETE") flags |= RGW_CORS_DELETE;
+    else if (method == "HEAD") flags |= RGW_CORS_HEAD;
+    else if (method == "COPY") flags |= RGW_CORS_COPY;
+  };
+  for_each_substr(allowed_methods, ";,= \t", apply_flag);
 
   return flags;
 }
index 8a998ede900b8f843bd41169f6fed3c71d87d34d..50c21b5138cd463c717c625ea8d090350c466c1b 100644 (file)
@@ -30,54 +30,11 @@ class RGWCORSConfiguration_SWIFT : public RGWCORSConfiguration
     ~RGWCORSConfiguration_SWIFT() {}
     int create_update(const char *allow_origins, const char *allow_headers, 
                   const char *expose_headers, const char *max_age) {
-      std::set<std::string> o, h;
-      std::list<std::string> e;
-      unsigned long a = CORS_MAX_AGE_INVALID;
-      uint8_t flags = RGW_CORS_ALL;
-
-      int nr_invalid_names = 0;
-      auto add_host = [&nr_invalid_names, &o] (auto host) {
-        if (validate_name_string(host) == 0) {
-          o.emplace(std::string{host});
-        } else {
-          nr_invalid_names++;
-        }
-      };
-      for_each_substr(allow_origins, ";,= \t", add_host);
-      if (o.empty() || nr_invalid_names > 0) {
+      std::optional<RGWCORSRule> optional_rule;
+      if (RGWCORSRule::create_rule(allow_origins, allow_headers, expose_headers, "*", optional_rule, max_age) < 0) {
         return -EINVAL;
       }
-
-      if (allow_headers) {
-        int nr_invalid_headers = 0;
-        auto add_header = [&nr_invalid_headers, &h] (auto allow_header) {
-          if (validate_name_string(allow_header) == 0) {
-            h.emplace(std::string{allow_header});
-          } else {
-            nr_invalid_headers++;
-          }
-        };
-        for_each_substr(allow_headers, ";,= \t", add_header);
-        if (h.empty() || nr_invalid_headers > 0) {
-          return -EINVAL;
-        }
-      }
-
-      if (expose_headers) {
-        for_each_substr(expose_headers, ";,= \t",
-            [&e] (auto expose_header) {
-              e.emplace_back(std::string(expose_header));
-            });
-      }
-      if (max_age) {
-        char *end = NULL;
-        a = strtoul(max_age, &end, 10);
-        if (a == ULONG_MAX)
-          a = CORS_MAX_AGE_INVALID;
-      }
-
-      RGWCORSRule rule(o, h, e, flags, a);
-      stack_rule(rule);
+      stack_rule(optional_rule.value());
       return 0;
     }
 };
index bd3cfe2a4e2d60b8f198d4d033b2761140cc8125..acc0e734d05820686907eb6dc14fd7e28688c740 100644 (file)
@@ -575,6 +575,10 @@ int rgw_build_bucket_policies(const DoutPrefixProvider *dpp, rgw::sal::Driver* d
        return ret;
       }
       s->bucket_exists = false;
+      if (s->op_type == RGW_OP_OPTIONS_CORS) {
+        ldpp_dout(dpp, 0) << "NOTICE: RGW_OP_OPTIONS_CORS shouldn't return -ERR_NO_SUCH_BUCKET in case we have a global CORS!" << dendl;
+        return 0;
+      }
       return -ERR_NO_SUCH_BUCKET;
     }
     if (!rgw::sal::Object::empty(s->object.get())) {
@@ -1818,7 +1822,7 @@ static bool validate_cors_rule_method(const DoutPrefixProvider *dpp, RGWCORSRule
     return false;
   }
 
-  uint8_t flags = get_cors_method_flags(req_meth);
+  uint8_t flags = get_multi_cors_method_flags(req_meth);
 
   if (rule->get_allowed_methods() & flags) {
     ldpp_dout(dpp, 10) << "Method " << req_meth << " is supported" << dendl;
@@ -1851,7 +1855,6 @@ int RGWOp::read_bucket_cors()
   map<string, bufferlist>::iterator aiter = s->bucket_attrs.find(RGW_ATTR_CORS);
   if (aiter == s->bucket_attrs.end()) {
     ldpp_dout(this, 20) << "no CORS configuration attr found" << dendl;
-    cors_exist = false;
     return 0; /* no CORS configuration found */
   }
 
@@ -1875,6 +1878,40 @@ int RGWOp::read_bucket_cors()
   return 0;
 }
 
+int RGWOp::read_global_cors()
+{
+  string allow_origins, allow_headers, allow_methods, expose_headers;
+  int ret = g_conf().get_val("rgw_gcors_allow_origins", &allow_origins);
+  if (ret < 0 || allow_origins.empty()) {
+    return -EINVAL;
+  }
+  ret = g_conf().get_val("rgw_gcors_allow_headers", &allow_headers);
+  if (ret < 0 || allow_headers.empty()) {
+    return -EINVAL;
+  }
+  ret = g_conf().get_val("rgw_gcors_allow_methods", &allow_methods);
+  if (ret < 0 || allow_methods.empty()) {
+    return -EINVAL;
+  }
+  g_conf().get_val("rgw_gcors_expose_headers", &expose_headers);
+  if (RGWCORSRule::create_rule(allow_origins.c_str(), allow_headers.c_str(), expose_headers.c_str(), allow_methods.c_str(),
+                               optional_global_cors) < 0) {
+    return -EINVAL;
+  }
+
+  cors_exist = true;
+
+  if (s->cct->_conf->subsys.should_gather<ceph_subsys_rgw, 15>()) {
+    XMLFormatter f;
+    RGWCORSRule_S3 *s3cors = static_cast<RGWCORSRule_S3 *>(&(*optional_global_cors));
+    ldpp_dout(this, 15) << "Read global RGWCORSRule";
+    s3cors->to_xml(f);
+    f.flush(*_dout);
+    *_dout << dendl;
+  }
+  return 0;
+}
+
 /** CORS 6.2.6.
  * If any of the header field-names is not a ASCII case-insensitive match for
  * any of the values in list of headers do not set any additional headers and
@@ -1888,6 +1925,7 @@ static void get_cors_response_headers(const DoutPrefixProvider *dpp, RGWCORSRule
       if (!rule->is_header_allowed((*it).c_str(), (*it).length())) {
         ldpp_dout(dpp, 5) << "Header " << (*it) << " is not registered in this rule" << dendl;
       } else {
+        ldpp_dout(dpp, 20) << "Header " << (*it) << " is registered in this rule" << dendl;
         if (hdrs.length() > 0) hdrs.append(",");
         hdrs.append((*it));
       }
@@ -1911,56 +1949,67 @@ bool RGWOp::generate_cors_headers(string& origin, string& method, string& header
   }
 
   /* Custom: */
+  cors_exist = false;
   origin = orig;
-  int temp_op_ret = read_bucket_cors();
-  if (temp_op_ret < 0) {
-    op_ret = temp_op_ret;
-    return false;
-  }
 
+  const int read_global_cors_ret = read_global_cors();
+  const int temp_op_ret = read_bucket_cors();
   if (!cors_exist) {
-    ldpp_dout(this, 2) << "No CORS configuration set yet for this bucket" << dendl;
+    ldpp_dout(this, 2) << "No global CORS or bucket CORS configuration set yet" << dendl;
+    op_ret = std::min(temp_op_ret, read_global_cors_ret);
     return false;
   }
 
   /* CORS 6.2.2. */
   RGWCORSRule *rule = bucket_cors.host_name_rule(orig);
-  if (!rule)
-    return false;
+  auto is_allowed_to_generate_rule_cors_header = [this, &origin, &method, &headers, &exp_headers, &max_age] (RGWCORSRule *rule) {
+    if (!rule)
+      return false;
 
-  /*
-   * Set the Allowed-Origin header to a asterisk if this is allowed in the rule
-   * and no Authorization was send by the client
-   *
-   * The origin parameter specifies a URI that may access the resource.  The browser must enforce this.
-   * For requests without credentials, the server may specify "*" as a wildcard,
-   * thereby allowing any origin to access the resource.
-   */
-  const char *authorization = s->info.env->get("HTTP_AUTHORIZATION");
-  if (!authorization && rule->has_wildcard_origin())
-    origin = "*";
-
-  /* CORS 6.2.3. */
-  const char *req_meth = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_METHOD");
-  if (!req_meth) {
-    req_meth = s->info.method;
-  }
+    /*
+     * Set the Allowed-Origin header to a asterisk if this is allowed in the rule
+     * and no Authorization was send by the client
+     *
+     * The origin parameter specifies a URI that may access the resource.  The browser must enforce this.
+     * For requests without credentials, the server may specify "*" as a wildcard,
+     * thereby allowing any origin to access the resource.
+     */
+    const char *authorization = s->info.env->get("HTTP_AUTHORIZATION");
+    if (!authorization && rule->has_wildcard_origin())
+      origin = "*";
 
-  if (req_meth) {
-    method = req_meth;
-    /* CORS 6.2.5. */
-    if (!validate_cors_rule_method(this, rule, req_meth)) {
-     return false;
+    /* CORS 6.2.3. */
+    const char *req_meth = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_METHOD");
+    if (!req_meth) {
+      req_meth = s->info.method;
+    }
+
+    if (req_meth) {
+      method = req_meth;
+      /* CORS 6.2.5. */
+      if (!validate_cors_rule_method(this, rule, req_meth)) {
+        return false;
+      }
     }
-  }
 
-  /* CORS 6.2.4. */
-  const char *req_hdrs = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_HEADERS");
+    /* CORS 6.2.4. */
+    const char *req_hdrs = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_HEADERS");
 
-  /* CORS 6.2.6. */
-  get_cors_response_headers(this, rule, req_hdrs, headers, exp_headers, max_age);
+    /* CORS 6.2.6. */
+    get_cors_response_headers(this, rule, req_hdrs, headers, exp_headers, max_age);
 
-  return true;
+    return true;
+  };
+
+  if (is_allowed_to_generate_rule_cors_header(rule)) {
+    return true;
+  }
+
+  if (optional_global_cors.has_value() && is_allowed_to_generate_rule_cors_header(&(*optional_global_cors))) {
+    return true;
+  }
+
+  return false;
 }
 
 int rgw_policy_from_attrset(const DoutPrefixProvider *dpp, CephContext *cct, map<string, bufferlist>& attrset, RGWAccessControlPolicy *policy)
@@ -7030,11 +7079,38 @@ int RGWOptionsCORS::validate_cors_request(RGWCORSConfiguration *cc) {
   return 0;
 }
 
+int RGWOptionsCORS::validate_global_cors_request(RGWCORSRule *global_cors_rule) {
+  ldpp_dout(this, 20) << "Validating request with global CORS" << dendl;
+  rule = global_cors_rule;
+  if (!rule) {
+    ldpp_dout(this, 10) << "There is no global cors rule present" << dendl;
+    return -ENOENT;
+  }
+
+  if (!rule->is_origin_present(origin)) {
+    ldpp_dout(this, 10) << "There is no cors rule present for " << origin << dendl;
+    return -ENOENT;
+  }
+
+  if (!validate_cors_rule_method(this, rule, req_meth)) {
+    return -ENOENT;
+  }
+
+  if (!validate_cors_rule_header(this, rule, req_hdrs)) {
+    return -ENOENT;
+  }
+
+  return 0;
+}
+
 void RGWOptionsCORS::execute(optional_yield y)
 {
   op_ret = read_bucket_cors();
-  if (op_ret < 0)
-    return;
+  int ret = read_global_cors();
+  if (ret < 0 && op_ret < 0) {
+      ldpp_dout(this, 2) << "No CORS configuration set yet for this bucket nor globally" << dendl;
+      return;
+  }
 
   origin = s->info.env->get("HTTP_ORIGIN");
   if (!origin) {
@@ -7055,11 +7131,13 @@ void RGWOptionsCORS::execute(optional_yield y)
   }
   req_hdrs = s->info.env->get("HTTP_ACCESS_CONTROL_REQUEST_HEADERS");
   op_ret = validate_cors_request(&bucket_cors);
-  if (!rule) {
+  if ((op_ret < 0 || !rule) && optional_global_cors.has_value()) {
+    op_ret = validate_global_cors_request(&(*optional_global_cors)) ;
+  }
+  if (op_ret < 0 || !rule) {
     origin = req_meth = NULL;
     return;
   }
-  return;
 }
 
 int RGWGetRequestPayment::verify_permission(optional_yield y)
@@ -9138,7 +9216,7 @@ void RGWPutBucketPolicy::send_response()
     set_req_state_err(s, op_ret);
   }
   dump_errno(s);
-  end_header(s);
+  end_header(s, this);
 }
 
 int RGWPutBucketPolicy::verify_permission(optional_yield y)
index 4408bbb2e868148728e56e0e74d1e62f690f9ab1..843432b644c06ac620323b40df9059758d75fe3a 100644 (file)
@@ -214,6 +214,7 @@ protected:
   req_state *s;
   RGWHandler *dialect_handler;
   rgw::sal::Driver* driver;
+  std::optional<RGWCORSRule> optional_global_cors;
   RGWCORSConfiguration bucket_cors;
   bool cors_exist;
   RGWQuota quota;
@@ -278,6 +279,7 @@ public:
     this->dialect_handler = dialect_handler;
   }
   int read_bucket_cors();
+  int read_global_cors();
   bool generate_cors_headers(std::string& origin, std::string& method, std::string& headers, std::string& exp_headers, unsigned *max_age);
 
   virtual int verify_params() { return 0; }
@@ -1929,6 +1931,7 @@ public:
 
   int verify_permission(optional_yield y) override {return 0;}
   int validate_cors_request(RGWCORSConfiguration *cc);
+  int validate_global_cors_request(RGWCORSRule *cc);
   void execute(optional_yield y) override;
   void get_response_params(std::string& allowed_hdrs, std::string& exp_hdrs, unsigned *max_age);
   void send_response() override = 0;
index 507d4e3ff7b1125380e8a03546d9e09f529120d3..9163bc1fdfe06c6d1a52f7e76573dfeadd2ba746 100644 (file)
@@ -210,6 +210,10 @@ int rgw_process_authenticated(RGWHandler_REST * const handler,
 
   ldpp_dout(op, 2) << "init op" << dendl;
   ret = op->init_processing(y);
+  if (op->get_type() == RGW_OP_OPTIONS_CORS && ret == -EINVAL) {
+    ldpp_dout(op, 0) << "NOTICE: RGW_OP_OPTIONS_CORS shouldn't return -EINVAL in case we have a global CORS!" << dendl;
+    ret = 0;
+  }
   if (ret < 0) {
     return ret;
   }
@@ -351,7 +355,6 @@ int process_request(const RGWProcessEnv& penv,
   req->op = op;
   ldpp_dout(op, 10) << "op=" << typeid(*op).name() << " " << dendl;
   s->op_type = op->get_type();
-
   try {
     ldpp_dout(op, 2) << "verifying requester" << dendl;
     ret = op->verify_requester(*penv.auth_registry, yield);
index 909246c221f86004065629a5c6cb7d563f0e7a7a..948d026567c187ae48115dc93941a0dcb87d4e69 100644 (file)
@@ -1661,7 +1661,7 @@ void RGWListBuckets_ObjStore_S3::send_response_begin(bool has_buckets)
   dump_start(s);
   // Explicitly use chunked transfer encoding so that we can stream the result
   // to the user without having to wait for the full length of it.
-  end_header(s, NULL, to_mime_type(s->format), CHUNKED_TRANSFER_ENCODING);
+  end_header(s, this, to_mime_type(s->format), CHUNKED_TRANSFER_ENCODING);
 
   if (! op_ret) {
     list_all_buckets_start(s);
@@ -2764,7 +2764,7 @@ void RGWCreateBucket_ObjStore_S3::send_response()
     set_req_state_err(s, op_ret);
   }
   dump_errno(s);
-  end_header(s);
+  end_header(s, this);
 
   if (op_ret < 0)
     return;
@@ -4388,7 +4388,7 @@ void RGWPutBucketEncryption_ObjStore_S3::send_response()
     set_req_state_err(s, op_ret);
   }
   dump_errno(s);
-  end_header(s);
+  end_header(s, this);
 }
 
 void RGWGetBucketEncryption_ObjStore_S3::send_response()
@@ -4419,7 +4419,7 @@ void RGWDeleteBucketEncryption_ObjStore_S3::send_response()
 
   set_req_state_err(s, op_ret);
   dump_errno(s);
-  end_header(s);
+  end_header(s,this);
 }
 
 int RGWPutBucketOwnershipControls_ObjStore_S3::get_params(optional_yield y)
@@ -5131,7 +5131,7 @@ void RGWPutBucketObjectLock_ObjStore_S3::send_response()
     set_req_state_err(s, op_ret);
   }
   dump_errno(s);
-  end_header(s);
+  end_header(s, this);
 }
 
 void RGWGetBucketObjectLock_ObjStore_S3::send_response()
@@ -5265,6 +5265,11 @@ RGWOp *RGWHandler_REST_Service_S3::op_get()
   }
 }
 
+RGWOp *RGWHandler_REST_Service_S3::op_options()
+{
+  return new RGWOptionsCORS_ObjStore_S3;
+}
+
 RGWOp *RGWHandler_REST_Service_S3::op_head()
 {
   return new RGWListBuckets_ObjStore_S3;
index bd46763fd986b6de00681d35ecec8b80f03313b9..37f113ed67593022afde60e5a460d5cd38b179ff 100644 (file)
@@ -710,6 +710,7 @@ protected:
     return s->info.args.exists("usage");
   }
   RGWOp *op_get() override;
+  RGWOp *op_options() override;
   RGWOp *op_head() override;
 public:
    RGWHandler_REST_Service_S3(const rgw::auth::StrategyRegistry& auth_registry) :
index bc442840fa0fe3c445bdc3882831278fc3d1d065..d95558b7c4928cffef1c2dba34cb65313ec79e23 100644 (file)
@@ -294,7 +294,7 @@ void RGWListBuckets_ObjStore_SWIFT::send_response_begin(bool has_buckets)
             s->user_acl);
     dump_errno(s);
     dump_header(s, "Accept-Ranges", "bytes");
-    end_header(s, NULL, NULL, NO_CONTENT_LENGTH, true);
+    end_header(s, this, NULL, NO_CONTENT_LENGTH, true);
   }
 
   if (! op_ret) {
@@ -393,7 +393,7 @@ void RGWListBuckets_ObjStore_SWIFT::send_response_end()
             s->user->get_max_buckets(),
             s->user_acl);
     dump_errno(s);
-    end_header(s, nullptr, nullptr, s->formatter->get_len(), true);
+    end_header(s, this, nullptr, s->formatter->get_len(), true);
   }
 
   if (sent_data || s->cct->_conf->rgw_swift_enforce_content_length) {