]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: add s3control apis for PublicAccessBlock
authorCasey Bodley <cbodley@redhat.com>
Tue, 1 Jul 2025 04:49:20 +0000 (00:49 -0400)
committerCasey Bodley <cbodley@redhat.com>
Wed, 20 May 2026 14:20:22 +0000 (10:20 -0400)
Signed-off-by: Casey Bodley <cbodley@redhat.com>
src/rgw/CMakeLists.txt
src/rgw/rgw_appmain.cc
src/rgw/rgw_common.h
src/rgw/rgw_op_type.h
src/rgw/rgw_rest_s3.cc
src/rgw/rgw_rest_s3.h
src/rgw/rgw_rest_s3control.cc [new file with mode: 0644]
src/rgw/rgw_rest_s3control.h [new file with mode: 0644]

index f445184d3189f18092a72bc357f30469dfafcd34..5055d3d5d35529ff32e14f04a296590c657789b1 100644 (file)
@@ -103,6 +103,7 @@ set(librgw_common_srcs
   rgw_rest_iam_group.cc
   rgw_rest_iam_user.cc
   rgw_rest_s3.cc
+  rgw_rest_s3control.cc
   rgw_rest_pubsub.cc
   rgw_rest_zero.cc
   rgw_s3select.cc
index 6641e2b304d882c044b4499175dbc1afe2455c27..16a79f21d528b23cdb16a13ae21dd69c3b056f1b 100644 (file)
@@ -290,6 +290,7 @@ void rgw::AppMain::cond_init_apis()
 
     // S3 website mode is a specialization of S3
     const bool s3website_enabled = apis_set.contains("s3website");
+    const bool s3control_enabled = apis_set.contains("s3control");
     const bool sts_enabled = apis_set.contains("sts");
     const bool iam_enabled = apis_set.contains("iam");
     const bool pubsub_enabled =
@@ -300,7 +301,7 @@ void rgw::AppMain::cond_init_apis()
       if (!swift_at_root) {
         rest.register_default_mgr(set_logging(
             rest_filter(env.driver, RGW_REST_S3,
-                        new RGWRESTMgr_S3(s3website_enabled, sts_enabled,
+                        new RGWRESTMgr_S3(s3control_enabled, s3website_enabled, sts_enabled,
                                           iam_enabled, pubsub_enabled))));
       } else {
         derr << "Cannot have the S3 or S3 Website enabled together with "
index 0c2c6529c8de4aeee734609e7f6924f35ca1bbbf..ae1f3f4f9875a43b06cb023d71003de2b39975d6 100644 (file)
@@ -242,6 +242,7 @@ static inline const char* to_mime_type(const RGWFormat f)
 #define RGW_REST_STS            0x10
 #define RGW_REST_IAM            0x20
 #define RGW_REST_SNS            0x40
+#define RGW_REST_S3CONTROL      0x80
 
 inline constexpr const char* RGW_REST_IAM_XMLNS =
     "https://iam.amazonaws.com/doc/2010-05-08/";
index 0631bf2b393683953f2122e3786cc22dd399d833..96b4fc41b8981809d55122e91af41bcf7a86b4a4 100644 (file)
@@ -165,6 +165,9 @@ enum RGWOpType {
   RGW_OP_PUT_BUCKET_PUBLIC_ACCESS_BLOCK,
   RGW_OP_GET_BUCKET_PUBLIC_ACCESS_BLOCK,
   RGW_OP_DELETE_BUCKET_PUBLIC_ACCESS_BLOCK,
+  RGW_OP_PUT_PUBLIC_ACCESS_BLOCK,
+  RGW_OP_GET_PUBLIC_ACCESS_BLOCK,
+  RGW_OP_DELETE_PUBLIC_ACCESS_BLOCK,
   /*OIDC provider specific*/
   RGW_OP_CREATE_OIDC_PROVIDER,
   RGW_OP_DELETE_OIDC_PROVIDER,
index f17fd09abc9840eb68e29416fab81e959d31760e..c2e78dce9919af0654636226bad63e1c43a51d6c 100644 (file)
@@ -38,6 +38,7 @@
 
 #include "rgw_rest.h"
 #include "rgw_rest_s3.h"
+#include "rgw_rest_s3control.h"
 #include "rgw_rest_s3website.h"
 #include "rgw_rest_pubsub.h"
 #include "rgw_auth_s3.h"
@@ -6007,7 +6008,8 @@ void parse_post_action(const std::string& post_body, req_state* s)
   }
 }
 
-RGWRESTMgr_S3::RGWRESTMgr_S3(bool _enable_s3website,
+RGWRESTMgr_S3::RGWRESTMgr_S3(bool enable_s3control,
+                             bool _enable_s3website,
                              bool _enable_sts,
                              bool _enable_iam,
                              bool _enable_pubsub)
@@ -6015,6 +6017,9 @@ RGWRESTMgr_S3::RGWRESTMgr_S3(bool _enable_s3website,
     enable_iam(_enable_iam),
     enable_pubsub(_enable_pubsub)
 {
+  if (enable_s3control) {
+    s3control = std::make_unique<RGWRESTMgr_S3Control>();
+  }
   if (_enable_s3website) {
     s3website = std::make_unique<RGWRESTMgr_S3Website>();
   }
@@ -6025,6 +6030,22 @@ RGWRESTMgr* RGWRESTMgr_S3::get_resource_mgr_as_default(req_state* s,
                                                        const std::string& uri,
                                                        std::string* out_uri)
 {
+  // s3control apis all expect the request header x-amz-account-id,
+  // and s3 apis don't. use that to disambiguate between s3control
+  // and requests to s3 buckets named v20180820
+  if (s3control && s->info.env->exists("HTTP_X_AMZ_ACCOUNT_ID")) {
+    ldpp_dout(s, 20) << "checking for s3control path v20180820 in "
+        "request_uri=" << uri << dendl;
+    // route matching requests RGWRESTMgr_S3Control
+    constexpr std::string_view s3control_root = "/v20180820";
+    if (auto i = std::ranges::mismatch(s3control_root, uri);
+        i.in1 == s3control_root.end() && // matched full string
+        (i.in2 == uri.end() || *i.in2 == '/')) { // end or /
+      const auto suffix = std::string{i.in2, uri.end()}; // trim prefix
+      return s3control->get_resource_mgr(s, suffix, out_uri);
+    }
+  }
+
   // check the Host header for virtual-host style requests, and
   // rewrite the request_uri with the subdomain as the bucket name.
   // this applies to s3 and s3website requests, but not s3control
@@ -6751,6 +6772,7 @@ AWSGeneralAbstractor::get_auth_data_v4(const req_state* const s,
         case RGW_OP_POST_BUCKET_LOGGING:
         case RGW_OP_GET_BUCKET_LOGGING: 
         case RGW_OP_PUT_BUCKET_OWNERSHIP_CONTROLS:
+        case RGW_OP_PUT_PUBLIC_ACCESS_BLOCK:
           break;
         default:
           ldpp_dout(s, 10) << "ERROR: AWS4 completion for operation: " << s->op_type << ", NOT IMPLEMENTED" << dendl;
index 0a7bbbd8e9960e66cb856d372a7b2de80f9d99df..7bf07aaffc0c2310b40c718d088638dc53bcf0a4 100644 (file)
@@ -821,16 +821,18 @@ public:
   ~RGWHandler_REST_Obj_S3() override = default;
 };
 
+class RGWRESTMgr_S3Control;
 class RGWRESTMgr_S3Website;
 
 class RGWRESTMgr_S3 : public RGWRESTMgr {
 private:
+  std::unique_ptr<RGWRESTMgr_S3Control> s3control;
   std::unique_ptr<RGWRESTMgr_S3Website> s3website;
   const bool enable_sts;
   const bool enable_iam;
   const bool enable_pubsub;
 public:
-  explicit RGWRESTMgr_S3(bool _enable_s3website=false, bool _enable_sts=false, bool _enable_iam=false, bool _enable_pubsub=false);
+  RGWRESTMgr_S3(bool enable_s3control, bool _enable_s3website, bool _enable_sts, bool _enable_iam, bool _enable_pubsub);
   ~RGWRESTMgr_S3() override;
 
   RGWHandler_REST *get_handler(rgw::sal::Driver* driver,
diff --git a/src/rgw/rgw_rest_s3control.cc b/src/rgw/rgw_rest_s3control.cc
new file mode 100644 (file)
index 0000000..89b97a6
--- /dev/null
@@ -0,0 +1,344 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright contributors to the Ceph project
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#include "rgw_rest_s3control.h"
+
+#include <concepts>
+
+#include "rgw_account.h"
+#include "rgw_auth_s3.h"
+#include "rgw_process_env.h"
+#include "common/errno.h"
+
+template <std::invocable<> F>
+int retry_raced_account_write(const DoutPrefixProvider* dpp, optional_yield y,
+                              rgw::sal::Driver* driver, RGWAccountInfo& info,
+                              rgw::sal::Attrs& attrs, RGWObjVersionTracker& objv,
+                              const F& f)
+{
+  int r = f();
+  for (int i = 0; i < 10 && r == -ECANCELED; ++i) {
+    objv.clear();
+    r = driver->load_account_by_id(dpp, y, info.id, info, attrs, objv);
+    if (r >= 0) {
+      r = f();
+    }
+  }
+  return r;
+}
+
+static int get_account_id(req_state* s, rgw_account_id& account_id)
+{
+  auto id = s->info.x_meta_map.find("x-amz-account-id");
+  if (id == s->info.x_meta_map.end()) {
+    s->err.message = "Missing required header x-amz-account-id";
+    return -EINVAL;
+  }
+  account_id = id->second;
+  if (account_id.empty()) {
+    s->err.message = "Missing required value for x-amz-account-id";
+    return -EINVAL;
+  }
+
+  const auto& account = s->auth.identity->get_account();
+  if (!account) {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+  if (account_id != account->id) {
+    s->err.message = "x-amz-account-id must match the requester";
+    return -EINVAL;
+  }
+  return 0;
+}
+
+// GetPublicAccessBlock
+class RGWGetPublicAccessBlock_S3Control : public RGWOp {
+  rgw_account_id account_id;
+  PublicAccessBlockConfiguration public_access_block;
+
+ public:
+  int init_processing(optional_yield y) override;
+  int verify_permission(optional_yield y) override;
+  void execute(optional_yield y) override;
+  void send_response() override;
+
+  const char* name() const override { return "get_public_access_block"; }
+  RGWOpType get_type() override { return RGW_OP_GET_PUBLIC_ACCESS_BLOCK; }
+};
+
+int RGWGetPublicAccessBlock_S3Control::init_processing(optional_yield y)
+{
+  return get_account_id(s, account_id);
+}
+
+int RGWGetPublicAccessBlock_S3Control::verify_permission(optional_yield y)
+{
+  const auto arn = rgw::account::root_arn(account_id);
+  if (verify_user_permission(this, s, arn, rgw::IAM::s3GetAccountPublicAccessBlock, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWGetPublicAccessBlock_S3Control::execute(optional_yield y)
+{
+  RGWAccountInfo account;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+  op_ret = driver->load_account_by_id(this, y, account_id, account, attrs, objv);
+  if (op_ret < 0) {
+    ldpp_dout(this, 4) << "failed to load account id "
+        << account_id << ": " << cpp_strerror(op_ret) << dendl;
+    return;
+  }
+
+  auto i = attrs.find(RGW_ATTR_PUBLIC_ACCESS);
+  if (i == attrs.end()) {
+    op_ret = -ERR_NO_SUCH_PUBLIC_ACCESS_BLOCK_CONFIGURATION;
+    return;
+  }
+
+  try {
+    auto p = i->second.cbegin();
+    decode(public_access_block, p);
+  } catch (const buffer::error&) {
+    op_ret = -EIO;
+    return;
+  }
+}
+
+void RGWGetPublicAccessBlock_S3Control::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    public_access_block.dump_xml(s->formatter);
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+// PutPublicAccessBlock
+class RGWPutPublicAccessBlock_S3Control : public RGWOp {
+  rgw_account_id account_id;
+  bufferlist data;
+  PublicAccessBlockConfiguration public_access_block;
+
+ public:
+  int init_processing(optional_yield y) override;
+  int verify_permission(optional_yield y) override;
+  void execute(optional_yield y) override;
+  void send_response() override;
+
+  const char* name() const override { return "put_public_access_block"; }
+  RGWOpType get_type() override { return RGW_OP_PUT_PUBLIC_ACCESS_BLOCK; }
+};
+
+int RGWPutPublicAccessBlock_S3Control::init_processing(optional_yield y)
+{
+  int ret = get_account_id(s, account_id);
+  if (ret < 0) {
+    return ret;
+  }
+
+  const auto max_size = s->cct->_conf->rgw_max_put_param_size;
+  std::tie(ret, data) = read_all_input(s, max_size, false);
+  if (ret < 0) {
+    return ret;
+  }
+
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    return -EINVAL;
+  }
+  if (!parser.parse(data.c_str(), data.length(), 1)) {
+    return -ERR_MALFORMED_XML;
+  }
+  try {
+    RGWXMLDecoder::decode_xml("PublicAccessBlockConfiguration",
+                              public_access_block, &parser, true);
+  } catch (RGWXMLDecoder::err& err) {
+    ldpp_dout(this, 5) << "unexpected xml:" << err << dendl;
+    return -ERR_MALFORMED_XML;
+  }
+  return 0;
+}
+
+int RGWPutPublicAccessBlock_S3Control::verify_permission(optional_yield y)
+{
+  const rgw::ARN arn{"", "root", account_id, false}; // XXX
+  if (verify_user_permission(this, s, arn, rgw::IAM::s3PutAccountPublicAccessBlock, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWPutPublicAccessBlock_S3Control::execute(optional_yield y)
+{
+  RGWAccountInfo account;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+  op_ret = driver->load_account_by_id(this, y, account_id, account, attrs, objv);
+  if (op_ret < 0) {
+    ldpp_dout(this, 4) << "failed to load account id "
+        << account_id << ": " << cpp_strerror(op_ret) << dendl;
+    return;
+  }
+
+  op_ret = rgw_forward_request_to_master(this, *s->penv.site, s->owner.id,
+                                         &data, nullptr, s->info, s->err, y);
+  if (op_ret < 0) {
+    ldpp_dout(this, 20) << "forward_request_to_master returned ret=" << op_ret << dendl;
+    return;
+  }
+
+  bufferlist conf_bl;
+  encode(public_access_block, conf_bl);
+
+  op_ret = retry_raced_account_write(this, y, driver, account, attrs, objv,
+      [this, y, &conf_bl, &account, &attrs, &objv] {
+        const RGWAccountInfo old_info = account;
+        attrs[RGW_ATTR_PUBLIC_ACCESS] = conf_bl;
+
+        constexpr bool exclusive = false;
+        return driver->store_account(this, y, exclusive, account,
+                                     &old_info, attrs, objv);
+      });
+}
+
+void RGWPutPublicAccessBlock_S3Control::send_response()
+{
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+// DeletePublicAccessBlock
+class RGWDeletePublicAccessBlock_S3Control : public RGWOp {
+  rgw_account_id account_id;
+
+ public:
+  int init_processing(optional_yield y) override;
+  int verify_permission(optional_yield y) override;
+  void execute(optional_yield y) override;
+  void send_response() override;
+
+  const char* name() const override { return "delete_public_access_block"; }
+  RGWOpType get_type() override { return RGW_OP_DELETE_PUBLIC_ACCESS_BLOCK; }
+};
+
+int RGWDeletePublicAccessBlock_S3Control::init_processing(optional_yield y)
+{
+  return get_account_id(s, account_id);
+}
+
+int RGWDeletePublicAccessBlock_S3Control::verify_permission(optional_yield y)
+{
+  const rgw::ARN arn{"", "root", account_id, false}; // XXX
+  if (verify_user_permission(this, s, arn, rgw::IAM::s3PutAccountPublicAccessBlock, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWDeletePublicAccessBlock_S3Control::execute(optional_yield y)
+{
+  RGWAccountInfo account;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+  op_ret = driver->load_account_by_id(this, y, account_id, account, attrs, objv);
+  if (op_ret < 0) {
+    ldpp_dout(this, 4) << "failed to load account id "
+        << account_id << ": " << cpp_strerror(op_ret) << dendl;
+    return;
+  }
+
+  op_ret = rgw_forward_request_to_master(this, *s->penv.site, s->owner.id,
+                                         nullptr, nullptr, s->info, s->err, y);
+  if (op_ret < 0) {
+    ldpp_dout(this, 20) << "forward_request_to_master returned ret=" << op_ret << dendl;
+    return;
+  }
+
+  op_ret = retry_raced_account_write(this, y, driver, account, attrs, objv,
+      [this, y, &account, &attrs, &objv] {
+        const RGWAccountInfo old_info = account;
+
+        auto i = attrs.find(RGW_ATTR_PUBLIC_ACCESS);
+        if (i == attrs.end()) {
+          return 0;
+        }
+        attrs.erase(i);
+
+        constexpr bool exclusive = false;
+        return driver->store_account(this, y, exclusive, account,
+                                     &old_info, attrs, objv);
+      });
+}
+
+void RGWDeletePublicAccessBlock_S3Control::send_response()
+{
+  if (!op_ret) {
+    op_ret = STATUS_NO_CONTENT;
+  }
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+class RGWHandler_PublicAccessBlock : public RGWHandler_REST {
+  const rgw::auth::StrategyRegistry& auth_registry;
+
+  RGWOp* op_delete() override { return new RGWDeletePublicAccessBlock_S3Control; }
+  RGWOp* op_get() override { return new RGWGetPublicAccessBlock_S3Control; }
+  RGWOp* op_put() override { return new RGWPutPublicAccessBlock_S3Control; }
+public:
+  RGWHandler_PublicAccessBlock(const rgw::auth::StrategyRegistry& auth_registry)
+    : auth_registry(auth_registry)
+  {}
+
+  int init(rgw::sal::Driver* driver, req_state *s, rgw::io::BasicClient *cio) {
+    s->dialect = "s3control";
+    s->prot_flags = RGW_REST_S3CONTROL;
+    int r = RGWHandler_REST::allocate_formatter(s, RGWFormat::XML, true);
+    if (r < 0) {
+      return r;
+    }
+    return RGWHandler_REST::init(driver, s, cio);
+  }
+  int authorize(const DoutPrefixProvider* dpp, optional_yield y) override {
+    return RGW_Auth_S3::authorize(dpp, driver, auth_registry, s, y);
+  }
+  int postauth_init(optional_yield y) override { return 0; }
+};
+
+class RGWRESTMgr_S3Control_PublicAccessBlock : public RGWRESTMgr {
+ public:
+  RGWHandler_REST* get_handler(rgw::sal::Driver* driver, req_state* s,
+                               const rgw::auth::StrategyRegistry& auth_registry,
+                               const std::string& prefix) {
+    return new RGWHandler_PublicAccessBlock(auth_registry);
+  }
+};
+
+RGWRESTMgr_S3Control::RGWRESTMgr_S3Control()
+{
+  // register nested resources
+  auto cfg = std::make_unique<RGWRESTMgr>();
+  cfg->register_resource("publicAccessBlock",
+                         new RGWRESTMgr_S3Control_PublicAccessBlock);
+  register_resource("configuration", cfg.release());
+}
diff --git a/src/rgw/rgw_rest_s3control.h b/src/rgw/rgw_rest_s3control.h
new file mode 100644 (file)
index 0000000..1f74458
--- /dev/null
@@ -0,0 +1,25 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright contributors to the Ceph project
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#pragma once
+
+#include "rgw_rest.h"
+
+// Serves the s3control api under the path /v20180820
+class RGWRESTMgr_S3Control : public RGWRESTMgr {
+  friend class RGWRESTMgr_S3; // for protected get_resource_mgr()
+ public:
+  RGWRESTMgr_S3Control();
+};