]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
rgw/iam: add Group/GroupPolicy APIs
authorCasey Bodley <cbodley@redhat.com>
Sun, 11 Feb 2024 17:21:45 +0000 (12:21 -0500)
committerCasey Bodley <cbodley@redhat.com>
Fri, 12 Apr 2024 19:34:28 +0000 (15:34 -0400)
Signed-off-by: Casey Bodley <cbodley@redhat.com>
(cherry picked from commit 386276ed00e3619e119bdd18d6fe20b656d2f05d)

src/rgw/CMakeLists.txt
src/rgw/rgw_auth_s3.cc
src/rgw/rgw_iam_policy.cc
src/rgw/rgw_iam_policy.h
src/rgw/rgw_op_type.h
src/rgw/rgw_rest_iam.cc
src/rgw/rgw_rest_iam.h
src/rgw/rgw_rest_iam_group.cc [new file with mode: 0644]
src/rgw/rgw_rest_iam_group.h [new file with mode: 0644]
src/test/rgw/test_rgw_iam_policy.cc

index 5592c29757b8194c48c670b09745653dd8a3cf55..0b2b6b83512e0471939211a43650a8b10e9be8e1 100644 (file)
@@ -111,6 +111,7 @@ set(librgw_common_srcs
   rgw_rest_metadata.cc
   rgw_rest_ratelimit.cc
   rgw_rest_role.cc
+  rgw_rest_iam_group.cc
   rgw_rest_iam_user.cc
   rgw_rest_s3.cc
   rgw_rest_pubsub.cc
index e43fe24c5eb162390100edeeb235ed1f58510134..61c2118b672fdeee5258646917f3e0350baf9571 100644 (file)
@@ -514,6 +514,21 @@ bool is_non_s3_op(RGWOpType op_type)
   case RGW_OP_UPDATE_ACCESS_KEY:
   case RGW_OP_DELETE_ACCESS_KEY:
   case RGW_OP_LIST_ACCESS_KEYS:
+  case RGW_OP_CREATE_GROUP:
+  case RGW_OP_GET_GROUP:
+  case RGW_OP_UPDATE_GROUP:
+  case RGW_OP_DELETE_GROUP:
+  case RGW_OP_LIST_GROUPS:
+  case RGW_OP_ADD_USER_TO_GROUP:
+  case RGW_OP_REMOVE_USER_FROM_GROUP:
+  case RGW_OP_LIST_GROUPS_FOR_USER:
+  case RGW_OP_PUT_GROUP_POLICY:
+  case RGW_OP_GET_GROUP_POLICY:
+  case RGW_OP_LIST_GROUP_POLICIES:
+  case RGW_OP_DELETE_GROUP_POLICY:
+  case RGW_OP_ATTACH_GROUP_POLICY:
+  case RGW_OP_DETACH_GROUP_POLICY:
+  case RGW_OP_LIST_ATTACHED_GROUP_POLICIES:
     return true;
   default:
     return false;
index f00cbfeac3662ff7ad4be2357ddb434c55c597d4..cc8243022c5c966a201f8194402a3a49d41d775f 100644 (file)
@@ -173,6 +173,21 @@ static const actpair actpairs[] =
  { "iam:UpdateAccessKey", iamUpdateAccessKey},
  { "iam:DeleteAccessKey", iamDeleteAccessKey},
  { "iam:ListAccessKeys", iamListAccessKeys},
+ { "iam:CreateGroup", iamCreateGroup},
+ { "iam:GetGroup", iamGetGroup},
+ { "iam:UpdateGroup", iamUpdateGroup},
+ { "iam:DeleteGroup", iamDeleteGroup},
+ { "iam:ListGroups", iamListGroups},
+ { "iam:AddUserToGroup", iamAddUserToGroup},
+ { "iam:RemoveUserFromGroup", iamRemoveUserFromGroup},
+ { "iam:ListGroupsForUser", iamListGroupsForUser},
+ { "iam:PutGroupPolicy", iamPutGroupPolicy },
+ { "iam:GetGroupPolicy", iamGetGroupPolicy },
+ { "iam:ListGroupPolicies", iamListGroupPolicies },
+ { "iam:DeleteGroupPolicy", iamDeleteGroupPolicy },
+ { "iam:AttachGroupPolicy", iamAttachGroupPolicy },
+ { "iam:DetachGroupPolicy", iamDetachGroupPolicy },
+ { "iam:ListAttachedGroupPolicies", iamListAttachedGroupPolicies },
  { "iam:GenerateCredentialReport", iamGenerateCredentialReport},
  { "iam:GenerateServiceLastAccessedDetails", iamGenerateServiceLastAccessedDetails},
  { "iam:SimulateCustomPolicy", iamSimulateCustomPolicy},
@@ -1568,6 +1583,51 @@ const char* action_bit_string(uint64_t action) {
   case iamListAccessKeys:
     return "iam:ListAccessKeys";
 
+  case iamCreateGroup:
+    return "iam:CreateGroup";
+
+  case iamGetGroup:
+    return "iam:GetGroup";
+
+  case iamUpdateGroup:
+    return "iam:UpdateGroup";
+
+  case iamDeleteGroup:
+    return "iam:DeleteGroup";
+
+  case iamListGroups:
+    return "iam:ListGroups";
+
+  case iamAddUserToGroup:
+    return "iam:AddUserToGroup";
+
+  case iamRemoveUserFromGroup:
+    return "iam:RemoveUserFromGroup";
+
+  case iamListGroupsForUser:
+    return "iam:ListGroupsForUser";
+
+  case iamPutGroupPolicy:
+    return "iam:PutGroupPolicy";
+
+  case iamGetGroupPolicy:
+    return "iam:GetGroupPolicy";
+
+  case iamListGroupPolicies:
+    return "iam:ListGroupPolicies";
+
+  case iamDeleteGroupPolicy:
+    return "iam:DeleteGroupPolicy";
+
+  case iamAttachGroupPolicy:
+    return "iam:AttachGroupPolicy";
+
+  case iamDetachGroupPolicy:
+    return "iam:DetachGroupPolicy";
+
+  case iamListAttachedGroupPolicies:
+    return "iam:ListAttachedGroupPolicies";
+
   case iamGenerateCredentialReport:
     return "iam:GenerateCredentialReport";
 
index cf5310bfc6f4b9ce4f018879649d87815d7db124..6fa2437bcb4c2685f382e0fd9b48df7ec9a6ac2e 100644 (file)
@@ -156,6 +156,21 @@ enum {
   iamUpdateAccessKey,
   iamDeleteAccessKey,
   iamListAccessKeys,
+  iamCreateGroup,
+  iamGetGroup,
+  iamUpdateGroup,
+  iamDeleteGroup,
+  iamListGroups,
+  iamAddUserToGroup,
+  iamRemoveUserFromGroup,
+  iamListGroupsForUser,
+  iamPutGroupPolicy,
+  iamGetGroupPolicy,
+  iamListGroupPolicies,
+  iamDeleteGroupPolicy,
+  iamAttachGroupPolicy,
+  iamDetachGroupPolicy,
+  iamListAttachedGroupPolicies,
   iamGenerateCredentialReport,
   iamGenerateServiceLastAccessedDetails,
   iamSimulateCustomPolicy,
index 79edf3f2f506d3ea863f0b5c29aa9043dd78b35b..fc661b51dbe945e43a27b8d1874d79e5a1c8f5f9 100644 (file)
@@ -100,6 +100,21 @@ enum RGWOpType {
   RGW_OP_UPDATE_ACCESS_KEY,
   RGW_OP_DELETE_ACCESS_KEY,
   RGW_OP_LIST_ACCESS_KEYS,
+  RGW_OP_CREATE_GROUP,
+  RGW_OP_GET_GROUP,
+  RGW_OP_UPDATE_GROUP,
+  RGW_OP_DELETE_GROUP,
+  RGW_OP_LIST_GROUPS,
+  RGW_OP_ADD_USER_TO_GROUP,
+  RGW_OP_REMOVE_USER_FROM_GROUP,
+  RGW_OP_LIST_GROUPS_FOR_USER,
+  RGW_OP_PUT_GROUP_POLICY,
+  RGW_OP_GET_GROUP_POLICY,
+  RGW_OP_LIST_GROUP_POLICIES,
+  RGW_OP_DELETE_GROUP_POLICY,
+  RGW_OP_ATTACH_GROUP_POLICY,
+  RGW_OP_DETACH_GROUP_POLICY,
+  RGW_OP_LIST_ATTACHED_GROUP_POLICIES,
   /* rgw specific */
   RGW_OP_ADMIN_SET_METADATA,
   RGW_OP_GET_OBJ_LAYOUT,
index 80b4228b513f53413028aa0afc906892c100fe2c..079a9f46071826d8955f1dbaca05b62258f44b6d 100644 (file)
@@ -10,6 +10,7 @@
 #include "rgw_rest_role.h"
 #include "rgw_rest_user_policy.h"
 #include "rgw_rest_oidc_provider.h"
+#include "rgw_rest_iam_group.h"
 #include "rgw_rest_iam_user.h"
 #include "rgw_rest_conn.h"
 #include "driver/rados/rgw_zone.h"
@@ -57,6 +58,21 @@ static const std::unordered_map<std::string_view, op_generator> op_generators =
   {"UpdateAccessKey", make_iam_update_access_key_op},
   {"DeleteAccessKey", make_iam_delete_access_key_op},
   {"ListAccessKeys", make_iam_list_access_keys_op},
+  {"CreateGroup", make_iam_create_group_op},
+  {"GetGroup", make_iam_get_group_op},
+  {"UpdateGroup", make_iam_update_group_op},
+  {"DeleteGroup", make_iam_delete_group_op},
+  {"ListGroups", make_iam_list_groups_op},
+  {"AddUserToGroup", make_iam_add_user_to_group_op},
+  {"RemoveUserFromGroup", make_iam_remove_user_from_group_op},
+  {"ListGroupsForUser", make_iam_list_groups_for_user_op},
+  {"PutGroupPolicy", make_iam_put_group_policy_op},
+  {"GetGroupPolicy", make_iam_get_group_policy_op},
+  {"ListGroupPolicies", make_iam_list_group_policies_op},
+  {"DeleteGroupPolicy", make_iam_delete_group_policy_op},
+  {"AttachGroupPolicy", make_iam_attach_group_policy_op},
+  {"DetachGroupPolicy", make_iam_detach_group_policy_op},
+  {"ListAttachedGroupPolicies", make_iam_list_attached_group_policies_op},
 };
 
 bool RGWHandler_REST_IAM::action_exists(const req_state* s) 
@@ -189,6 +205,26 @@ bool validate_iam_role_name(const std::string& name, std::string& err)
   return true;
 }
 
+static constexpr size_t MAX_GROUP_NAME_LEN = 128;
+
+bool validate_iam_group_name(const std::string& name, std::string& err)
+{
+  if (name.empty()) {
+    err = "Missing required element GroupName";
+    return false;
+  }
+  if (name.size() > MAX_GROUP_NAME_LEN) {
+    err = "GroupName too long";
+    return false;
+  }
+  const std::regex pattern("[\\w+=,.@-]+");
+  if (!std::regex_match(name, pattern)) {
+    err = "GroupName contains invalid characters";
+    return false;
+  }
+  return true;
+}
+
 static constexpr size_t MAX_PATH_LEN = 512;
 
 bool validate_iam_path(const std::string& path, std::string& err)
@@ -220,6 +256,16 @@ std::string iam_user_arn(const RGWUserInfo& info)
                      acct, path, info.display_name);
 }
 
+std::string iam_group_arn(const RGWGroupInfo& info)
+{
+  std::string_view path = info.path;
+  if (path.empty()) {
+    path = "/";
+  }
+  return fmt::format("arn:aws:iam::{}:group{}{}",
+                     info.account_id, path, info.name);
+}
+
 int forward_iam_request_to_master(const DoutPrefixProvider* dpp,
                                   const rgw::SiteConfig& site,
                                   const RGWUserInfo& user,
index a9bb7127e0304a40fb239fbe72c9c0faf2529cd2..0c2b262bd62b7614a43b09baacfb84692543118f 100644 (file)
@@ -3,6 +3,8 @@
 
 #pragma once
 
+#include <concepts>
+
 #include "rgw_auth.h"
 #include "rgw_auth_filters.h"
 #include "rgw_rest.h"
 class DoutPrefixProvider;
 namespace rgw { class SiteConfig; }
 struct RGWUserInfo;
+struct RGWGroupInfo;
 
 bool validate_iam_policy_name(const std::string& name, std::string& err);
 bool validate_iam_policy_arn(const std::string& arn, std::string& err);
 bool validate_iam_user_name(const std::string& name, std::string& err);
 bool validate_iam_role_name(const std::string& name, std::string& err);
+bool validate_iam_group_name(const std::string& name, std::string& err);
 bool validate_iam_path(const std::string& path, std::string& err);
 
 std::string iam_user_arn(const RGWUserInfo& info);
+std::string iam_group_arn(const RGWGroupInfo& info);
 
 int forward_iam_request_to_master(const DoutPrefixProvider* dpp,
                                   const rgw::SiteConfig& site,
@@ -28,6 +33,26 @@ int forward_iam_request_to_master(const DoutPrefixProvider* dpp,
                                   RGWXMLDecoder::XMLParser& parser,
                                   req_info& req, optional_yield y);
 
+/// Perform an atomic read-modify-write operation on the given group metadata.
+/// Racing writes are detected here as ECANCELED errors, where we reload the
+/// updated group metadata and retry the operation.
+template <std::invocable<> F>
+int retry_raced_group_write(const DoutPrefixProvider* dpp, optional_yield y,
+                            rgw::sal::Driver* driver, RGWGroupInfo& 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_group_by_id(dpp, y, info.id, info, attrs, objv);
+    if (r >= 0) {
+      r = f();
+    }
+  }
+  return r;
+}
+
 class RGWHandler_REST_IAM : public RGWHandler_REST {
   const rgw::auth::StrategyRegistry& auth_registry;
   bufferlist bl_post_body;
diff --git a/src/rgw/rgw_rest_iam_group.cc b/src/rgw/rgw_rest_iam_group.cc
new file mode 100644 (file)
index 0000000..307ba89
--- /dev/null
@@ -0,0 +1,2114 @@
+// -*- 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_iam_group.h"
+
+#include <utility>
+#include "include/buffer.h"
+#include "common/errno.h"
+#include "rgw_arn.h"
+#include "rgw_common.h"
+#include "rgw_iam_managed_policy.h"
+#include "rgw_op.h"
+#include "rgw_process_env.h"
+#include "rgw_rest.h"
+#include "rgw_rest_iam.h"
+
+
+static std::string make_resource_name(const RGWGroupInfo& info)
+{
+  std::string_view path = info.path;
+  if (path.empty()) {
+    path = "/";
+  }
+  return string_cat_reserve(path, info.name);
+}
+
+static void dump_iam_group(const RGWGroupInfo& info, Formatter* f)
+{
+  encode_json("Path", info.path, f);
+  encode_json("GroupName", info.name, f);
+  encode_json("GroupId", info.id, f);
+  encode_json("Arn", iam_group_arn(info), f);
+}
+
+static void dump_iam_user(const RGWUserInfo& info, Formatter* f)
+{
+  encode_json("Path", info.path, f);
+  encode_json("UserName", info.display_name, f);
+  encode_json("UserId", info.user_id, f);
+  encode_json("Arn", iam_user_arn(info), f);
+}
+
+
+// CreateGroup
+class RGWCreateGroup_IAM : public RGWOp {
+  bufferlist post_body;
+  RGWGroupInfo info;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site, std::string& uid);
+ public:
+  explicit RGWCreateGroup_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "create_group"; }
+  RGWOpType get_type() override { return RGW_OP_CREATE_GROUP; }
+};
+
+int RGWCreateGroup_IAM::init_processing(optional_yield y)
+{
+  // use account id from authenticated user/role. with AssumeRole, this may not
+  // match the account of s->user
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  info.path = s->info.args.get("Path");
+  if (info.path.empty()) {
+    info.path = "/";
+  } else if (!validate_iam_path(info.path, s->err.message)) {
+    return -EINVAL;
+  }
+
+  info.name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(info.name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  return 0;
+}
+
+int RGWCreateGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamCreateGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWCreateGroup_IAM::forward_to_master(optional_yield y,
+                                         const rgw::SiteConfig& site,
+                                         std::string& id)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("Path");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+
+  XMLObj* response = parser.find_first("CreateGroupResponse");;
+  if (!response) {
+    ldpp_dout(this, 5) << "ERROR: unexpected xml: CreateGroupResponse" << dendl;
+    return -EINVAL;
+  }
+
+  XMLObj* result = response->find_first("CreateGroupResult");
+  if (!result) {
+    ldpp_dout(this, 5) << "ERROR: unexpected xml: CreateGroupResult" << dendl;
+    return -EINVAL;
+  }
+
+  XMLObj* group = result->find_first("Group");
+  if (!group) {
+    ldpp_dout(this, 5) << "ERROR: unexpected xml: Group" << dendl;
+    return -EINVAL;
+  }
+
+  try {
+    RGWXMLDecoder::decode_xml("GroupId", id, group, true);
+  } catch (RGWXMLDecoder::err& err) {
+    ldpp_dout(this, 5) << "ERROR: unexpected xml: GroupId" << dendl;
+    return -EINVAL;
+  }
+
+  ldpp_dout(this, 4) << "group id decoded from forwarded response is " << id << dendl;
+  return 0;
+}
+
+void RGWCreateGroup_IAM::execute(optional_yield y)
+{
+  {
+    // check the current group count against account limit
+    RGWAccountInfo account;
+    rgw::sal::Attrs attrs; // unused
+    RGWObjVersionTracker objv; // unused
+    op_ret = driver->load_account_by_id(this, y, info.account_id,
+                                        account, attrs, objv);
+    if (op_ret < 0) {
+      ldpp_dout(this, 4) << "failed to load iam account "
+          << info.account_id << ": " << cpp_strerror(op_ret) << dendl;
+    }
+
+    if (account.max_groups >= 0) { // max_groups < 0 means unlimited
+      uint32_t count = 0;
+      op_ret = driver->count_account_groups(this, y, info.account_id, count);
+      if (op_ret < 0) {
+        ldpp_dout(this, 4) << "failed to count groups for iam account "
+            << info.account_id << ": " << cpp_strerror(op_ret) << dendl;
+        return;
+      }
+      if (std::cmp_greater_equal(count, account.max_groups)) {
+        s->err.message = fmt::format("Group limit {} exceeded",
+                                     account.max_groups);
+        op_ret = -ERR_LIMIT_EXCEEDED;
+        return;
+      }
+    }
+  }
+
+  // generate group id. forward_to_master() may overwrite this
+  uuid_d uuid;
+  uuid.generate_random();
+  info.id = uuid.to_string();
+  info.tenant = s->auth.identity->get_tenant();
+
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site, info.id);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+  objv.generate_new_write_ver(get_cct());
+  constexpr bool exclusive = true;
+  op_ret = driver->store_group(this, y, info, attrs, objv, exclusive, nullptr);
+}
+
+void RGWCreateGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "CreateGroupResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "CreateGroupResult"};
+      Formatter::ObjectSection group{*f, "Group"};
+      dump_iam_group(info, f);
+      // /Group
+      // /CreateGroupResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /CreateGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// GetGroup
+class RGWGetGroup_IAM : public RGWOp {
+  rgw_account_id account_id;
+  RGWGroupInfo info;
+  std::string marker;
+  int max_items = 100;
+  rgw::sal::UserList listing;
+ 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_group"; }
+  RGWOpType get_type() override { return RGW_OP_GET_GROUP; }
+};
+
+int RGWGetGroup_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  marker = s->info.args.get("Marker");
+
+  int r = s->info.args.get_int("MaxItems", &max_items, max_items);
+  if (r < 0 || max_items > 1000) {
+    s->err.message = "Invalid value for MaxItems";
+    return -EINVAL;
+  }
+
+  rgw::sal::Attrs attrs_ignored;
+  RGWObjVersionTracker objv_ignored;
+  r = driver->load_group_by_name(this, y, account_id, name, info,
+                                 attrs_ignored, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWGetGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamGetGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWGetGroup_IAM::execute(optional_yield y)
+{
+  const auto& tenant = s->auth.identity->get_tenant();
+  op_ret = driver->list_group_users(this, y, tenant, info.id,
+                                    marker, max_items, listing);
+}
+
+void RGWGetGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "GetGroupResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "GetGroupResult"};
+      {
+        Formatter::ObjectSection Group{*f, "Group"};
+        dump_iam_group(info, f);
+      } // /Group
+      {
+        Formatter::ArraySection users{*f, "Users"};
+        for (const auto& user : listing.users) {
+          Formatter::ObjectSection result{*f, "member"};
+          dump_iam_user(user, f);
+        } // /member
+      } // /Users
+      const bool is_truncated = !listing.next_marker.empty();
+      f->dump_bool("IsTruncated", is_truncated);
+      if (is_truncated) {
+        f->dump_string("Marker", listing.next_marker);
+      }
+      // /GetGroupResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /GetGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// UpdateGroup
+class RGWUpdateGroup_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string new_path;
+  std::string new_name;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWUpdateGroup_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "update_group"; }
+  RGWOpType get_type() override { return RGW_OP_UPDATE_GROUP; }
+};
+
+int RGWUpdateGroup_IAM::init_processing(optional_yield y)
+{
+  rgw_account_id account_id;
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  new_path = s->info.args.get("NewPath");
+  if (!new_path.empty() && !validate_iam_path(new_path, s->err.message)) {
+    return -EINVAL;
+  }
+
+  new_name = s->info.args.get("NewGroupName");
+  if (!new_name.empty() &&
+      !validate_iam_group_name(new_name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (name.empty()) {
+    s->err.message = "Missing required element GroupName";
+    return -EINVAL;
+  }
+
+  int r = driver->load_group_by_name(this, y, account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWUpdateGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamUpdateGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWUpdateGroup_IAM::forward_to_master(optional_yield y, const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("NewPath");
+  s->info.args.remove("NewGroupName");
+  s->info.args.remove("GroupName");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWUpdateGroup_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y] {
+        const RGWGroupInfo old_info = info;
+
+        if (!new_path.empty()) {
+          info.path = new_path;
+        }
+        if (!new_name.empty()) {
+          info.name = new_name;
+        }
+
+        if (info.path == old_info.path &&
+            info.name == old_info.name) {
+          return 0; // nothing to do, return success
+        }
+
+        constexpr bool exclusive = false;
+        return driver->store_group(this, y, info, attrs, objv,
+                                   exclusive, &old_info);
+      });
+}
+
+void RGWUpdateGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "UpdateGroupResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "UpdateGroupResult"};
+      Formatter::ObjectSection group{*f, "Group"};
+      dump_iam_group(info, f);
+      // /Group
+      // /UpdateGroupResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /UpdateGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// DeleteGroup
+class RGWDeleteGroup_IAM : public RGWOp {
+  bufferlist post_body;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+  int check_empty(optional_yield y);
+ public:
+  explicit RGWDeleteGroup_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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_group"; }
+  RGWOpType get_type() override { return RGW_OP_DELETE_GROUP; }
+};
+
+int RGWDeleteGroup_IAM::init_processing(optional_yield y)
+{
+  rgw_account_id account_id;
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (name.empty()) {
+    s->err.message = "Missing required element GroupName";
+    return -EINVAL;
+  }
+
+  int r = driver->load_group_by_name(this, y, account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWDeleteGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamDeleteGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWDeleteGroup_IAM::forward_to_master(optional_yield y, const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+int RGWDeleteGroup_IAM::check_empty(optional_yield y)
+{
+  if (!s->penv.site->is_meta_master()) {
+    // only check on the master zone. if a forwarded DeleteGroup request
+    // succeeds on the master zone, it needs to succeed here too
+    return 0;
+  }
+
+  // verify that all policies are removed first
+  if (auto p = attrs.find(RGW_ATTR_IAM_POLICY); p != attrs.end()) {
+    std::map<std::string, std::string> policies;
+    try {
+      decode(policies, p->second);
+    } catch (const buffer::error&) {
+      ldpp_dout(this, 0) << "ERROR: failed to decode group policies" << dendl;
+      return -EIO;
+    }
+
+    if (!policies.empty()) {
+      s->err.message = "The group cannot be deleted until all group policies are removed";
+      return -ERR_DELETE_CONFLICT;
+    }
+  }
+  if (auto p = attrs.find(RGW_ATTR_MANAGED_POLICY); p != attrs.end()) {
+    rgw::IAM::ManagedPolicies policies;
+    try {
+      decode(policies, p->second);
+    } catch (const buffer::error&) {
+      ldpp_dout(this, 0) << "ERROR: failed to decode managed policies" << dendl;
+      return -EIO;
+    }
+
+    if (!policies.arns.empty()) {
+      s->err.message = "The group cannot be deleted until all managed policies are detached";
+      return -ERR_DELETE_CONFLICT;
+    }
+  }
+
+  // check that group has no users
+  const std::string& tenant = s->auth.identity->get_tenant();
+  rgw::sal::UserList listing;
+  int r = driver->list_group_users(this, y, tenant, info.id, "", 1, listing);
+  if (r < 0) {
+    return r;
+  }
+
+  if (listing.users.size()) {
+    s->err.message = "The group cannot be deleted until all users are removed";
+    return -ERR_DELETE_CONFLICT;
+  }
+
+  return 0;
+}
+
+void RGWDeleteGroup_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y] {
+        if (int r = check_empty(y); r < 0) {
+          return r;
+        }
+        return driver->remove_group(this, y, info, objv);
+      });
+
+  if (op_ret == -ENOENT) {
+    if (!site.is_meta_master()) {
+      // delete succeeded on the master, return that success here too
+      op_ret = 0;
+    } else {
+      s->err.message = "No such GroupName in the account";
+      op_ret = -ERR_NO_SUCH_ENTITY;
+    }
+  }
+}
+
+void RGWDeleteGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "DeleteGroupResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /DeleteGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListGroups
+class RGWListGroups_IAM : public RGWOp {
+  rgw_account_id account_id;
+  std::string marker;
+  std::string path_prefix;
+  int max_items = 100;
+
+  bool started_response = false;
+  void start_response();
+  void end_response(std::string_view next_marker);
+  void send_response_data(std::span<RGWGroupInfo> groups);
+ 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 "list_groups"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_GROUPS; }
+};
+
+int RGWListGroups_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  marker = s->info.args.get("Marker");
+  path_prefix = s->info.args.get("PathPrefix");
+
+  int r = s->info.args.get_int("MaxItems", &max_items, max_items);
+  if (r < 0 || max_items > 1000) {
+    s->err.message = "Invalid value for MaxItems";
+    return -EINVAL;
+  }
+
+  return 0;
+}
+
+int RGWListGroups_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = "";
+  const rgw::ARN arn{resource_name, "group", account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamListGroups, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListGroups_IAM::execute(optional_yield y)
+{
+  rgw::sal::GroupList listing;
+  listing.next_marker = marker;
+
+  op_ret = driver->list_account_groups(this, y, account_id,
+                                       path_prefix, listing.next_marker,
+                                       max_items, listing);
+  if (op_ret == -ENOENT) {
+    op_ret = 0;
+  } else if (op_ret < 0) {
+    return;
+  }
+
+  send_response_data(listing.groups);
+
+  if (!started_response) {
+    started_response = true;
+    start_response();
+  }
+  end_response(listing.next_marker);
+}
+
+void RGWListGroups_IAM::start_response()
+{
+  const int64_t proposed_content_length =
+      op_ret ? NO_CONTENT_LENGTH : CHUNKED_TRANSFER_ENCODING;
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this, to_mime_type(s->format), proposed_content_length);
+
+  if (op_ret) {
+    return;
+  }
+
+  dump_start(s); // <?xml block ?>
+  s->formatter->open_object_section_in_ns("ListGroupsResponse", RGW_REST_IAM_XMLNS);
+  s->formatter->open_object_section("ListGroupsResult");
+  s->formatter->open_array_section("Groups");
+}
+
+void RGWListGroups_IAM::end_response(std::string_view next_marker)
+{
+  s->formatter->close_section(); // Groups
+
+  const bool truncated = !next_marker.empty();
+  s->formatter->dump_bool("IsTruncated", truncated);
+  if (truncated) {
+    s->formatter->dump_string("Marker", next_marker);
+  }
+
+  s->formatter->close_section(); // ListGroupsResult
+  s->formatter->close_section(); // ListGroupsResponse
+  rgw_flush_formatter_and_reset(s, s->formatter);
+}
+
+void RGWListGroups_IAM::send_response_data(std::span<RGWGroupInfo> groups)
+{
+  if (!started_response) {
+    started_response = true;
+    start_response();
+  }
+
+  for (const auto& info : groups) {
+    s->formatter->open_object_section("member");
+    dump_iam_group(info, s->formatter);
+    s->formatter->close_section(); // member
+  }
+
+  // flush after each chunk
+  rgw_flush_formatter(s, s->formatter);
+}
+
+void RGWListGroups_IAM::send_response()
+{
+  if (!started_response) { // errored out before execute() wrote anything
+    start_response();
+  }
+}
+
+
+// AddUserToGroup
+class RGWAddUserToGroup_IAM : public RGWOp {
+  bufferlist post_body;
+  RGWGroupInfo group;
+  std::unique_ptr<rgw::sal::User> user;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWAddUserToGroup_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "add_user_to_group"; }
+  RGWOpType get_type() override { return RGW_OP_ADD_USER_TO_GROUP; }
+};
+
+int RGWAddUserToGroup_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    group.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  const std::string username = s->info.args.get("UserName");
+  if (!validate_iam_user_name(username, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  rgw::sal::Attrs attrs_ignored;
+  RGWObjVersionTracker objv_ignored;
+  int r = driver->load_group_by_name(this, y, group.account_id, name,
+                                     group, attrs_ignored, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  if (r < 0) {
+    return r;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  r = driver->load_account_user_by_name(this, y, group.account_id,
+                                        tenant, username, &user);
+  if (r == -ENOENT) {
+    s->err.message = "No such UserName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWAddUserToGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(group);
+  const rgw::ARN arn{resource_name, "group", group.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamAddUserToGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWAddUserToGroup_IAM::forward_to_master(optional_yield y,
+                                             const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("UserName");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWAddUserToGroup_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  const bool inserted = info.group_ids.insert(group.id).second;
+  if (inserted) {
+    constexpr bool exclusive = false;
+    op_ret = user->store_user(this, y, exclusive, &old_info);
+  }
+}
+
+void RGWAddUserToGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "AddUserToGroupResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /AddUserToGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// RemoveUserFromGroup
+class RGWRemoveUserFromGroup_IAM : public RGWOp {
+  bufferlist post_body;
+  RGWGroupInfo group;
+  std::unique_ptr<rgw::sal::User> user;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWRemoveUserFromGroup_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "remove_user_from_group"; }
+  RGWOpType get_type() override { return RGW_OP_REMOVE_USER_FROM_GROUP; }
+};
+
+int RGWRemoveUserFromGroup_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    group.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  const std::string username = s->info.args.get("UserName");
+  if (!validate_iam_user_name(username, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  rgw::sal::Attrs attrs_ignored;
+  RGWObjVersionTracker objv_ignored;
+  int r = driver->load_group_by_name(this, y, group.account_id, name,
+                                     group, attrs_ignored, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  if (r < 0) {
+    return r;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  r = driver->load_account_user_by_name(this, y, group.account_id,
+                                        tenant, username, &user);
+  if (r == -ENOENT) {
+    s->err.message = "No such UserName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWRemoveUserFromGroup_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(group);
+  const rgw::ARN arn{resource_name, "group", group.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamRemoveUserFromGroup, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWRemoveUserFromGroup_IAM::forward_to_master(optional_yield y,
+                                                  const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("UserName");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWRemoveUserFromGroup_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  if (auto id = info.group_ids.find(group.id);
+      id != info.group_ids.end()) {
+    info.group_ids.erase(id);
+
+    constexpr bool exclusive = false;
+    op_ret = user->store_user(this, y, exclusive, &old_info);
+  }
+}
+
+void RGWRemoveUserFromGroup_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "RemoveUserFromGroupResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /RemoveUserFromGroupResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListGroupsForUser
+class RGWListGroupsForUser_IAM : public RGWOp {
+  rgw_account_id account_id;
+  std::string marker;
+  int max_items = 100;
+  std::unique_ptr<rgw::sal::User> user;
+
+ 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 "list_groups_for_user"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_GROUPS_FOR_USER; }
+};
+
+int RGWListGroupsForUser_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  marker = s->info.args.get("Marker");
+
+  int r = s->info.args.get_int("MaxItems", &max_items, max_items);
+  if (r < 0 || max_items > 1000) {
+    s->err.message = "Invalid value for MaxItems";
+    return -EINVAL;
+  }
+
+  const std::string username = s->info.args.get("UserName");
+  if (!validate_iam_user_name(username, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  r = driver->load_account_user_by_name(this, y, account_id,
+                                        tenant, username, &user);
+  if (r == -ENOENT) {
+    s->err.message = "No such UserName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWListGroupsForUser_IAM::verify_permission(optional_yield y)
+{
+  const RGWUserInfo& info = user->get_info();
+  const std::string resource_name = string_cat_reserve(info.path, info.display_name);
+  const rgw::ARN arn{resource_name, "user", account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamListGroupsForUser, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListGroupsForUser_IAM::execute(optional_yield y)
+{
+  rgw::sal::GroupList listing;
+  listing.next_marker = marker;
+
+  op_ret = user->list_groups(this, y, marker, max_items, listing);
+  if (op_ret == -ENOENT) {
+    op_ret = 0;
+  } else if (op_ret < 0) {
+    return;
+  }
+
+  dump_start(s); // <?xml block ?>
+  Formatter* f = s->formatter;
+  Formatter::ObjectSection response{*f, "ListGroupsForUserResponse", RGW_REST_IAM_XMLNS};
+  {
+    Formatter::ObjectSection result{*f, "ListGroupsForUserResult"};
+    {
+      Formatter::ArraySection groups{*f, "Groups"};
+      for (const auto& info : listing.groups) {
+        Formatter::ObjectSection result{*f, "member"};
+        dump_iam_group(info, s->formatter);
+      } // /member
+    } // /Groups
+    const bool truncated = !listing.next_marker.empty();
+    f->dump_bool("IsTruncated", truncated);
+    if (truncated) {
+      f->dump_string("Marker", listing.next_marker);
+    }
+  } // /ListGroupsForUserResult
+  Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+  f->dump_string("RequestId", s->trans_id);
+  // /ResponseMetadata
+  // /ListGroupsForUserResponse
+}
+
+void RGWListGroupsForUser_IAM::send_response()
+{
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// PutGroupPolicy
+class RGWPutGroupPolicy_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string policy_name;
+  std::string policy_document;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWPutGroupPolicy_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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_group_policy"; }
+  RGWOpType get_type() override { return RGW_OP_PUT_GROUP_POLICY; }
+};
+
+int RGWPutGroupPolicy_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_name = s->info.args.get("PolicyName");
+  if (!validate_iam_policy_name(policy_name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_document = s->info.args.get("PolicyDocument");
+  if (policy_document.empty()) {
+    s->err.message = "Missing required element PolicyDocument";
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  int r = driver->load_group_by_name(this, y, info.account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWPutGroupPolicy_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamPutGroupPolicy, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWPutGroupPolicy_IAM::forward_to_master(optional_yield y,
+                                                const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("PolicyName");
+  s->info.args.remove("PolicyDocument");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWPutGroupPolicy_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  try {
+    // validate the document
+    const rgw::IAM::Policy p(
+      s->cct, s->user->get_tenant(), policy_document,
+      s->cct->_conf.get_val<bool>("rgw_policy_reject_invalid_principals"));
+  } catch (rgw::IAM::PolicyParseException& e) {
+    s->err.message = std::move(e.msg);
+    op_ret = -ERR_MALFORMED_DOC;
+    return;
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y] {
+        std::map<std::string, std::string> policies;
+        if (auto p = attrs.find(RGW_ATTR_IAM_POLICY); p != attrs.end()) try {
+          decode(policies, p->second);
+        } catch (const buffer::error& err) {
+          ldpp_dout(this, 0) << "ERROR: failed to decode group policies" << dendl;
+          return -EIO;
+        }
+
+        policies[policy_name] = policy_document;
+
+        constexpr size_t GROUP_POLICIES_MAX_NUM = 100;
+        if (policies.size() > GROUP_POLICIES_MAX_NUM) {
+          s->err.message = fmt::format("Group policy limit {} exceeded",
+                                       GROUP_POLICIES_MAX_NUM);
+          return -ERR_LIMIT_EXCEEDED;
+        }
+
+        bufferlist bl;
+        encode(policies, bl);
+        attrs[RGW_ATTR_IAM_POLICY] = std::move(bl);
+
+        constexpr bool exclusive = false;
+        return driver->store_group(this, y, info, attrs, objv, exclusive, &info);
+      });
+}
+
+void RGWPutGroupPolicy_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "PutGroupPolicyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /PutGroupPolicyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// GetGroupPolicy
+class RGWGetGroupPolicy_IAM : public RGWOp {
+  std::string policy_name;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+
+ 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_group_policy"; }
+  RGWOpType get_type() override { return RGW_OP_GET_GROUP_POLICY; }
+};
+
+int RGWGetGroupPolicy_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_name = s->info.args.get("PolicyName");
+  if (!validate_iam_policy_name(policy_name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  RGWObjVersionTracker objv_ignored;
+  int r = driver->load_group_by_name(this, y, info.account_id, name,
+                                     info, attrs, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWGetGroupPolicy_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamGetGroupPolicy, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWGetGroupPolicy_IAM::execute(optional_yield y)
+{
+  std::map<std::string, std::string> policies;
+  if (auto p = attrs.find(RGW_ATTR_IAM_POLICY); p != attrs.end()) try {
+    decode(policies, p->second);
+  } catch (const buffer::error& err) {
+    ldpp_dout(this, 0) << "ERROR: failed to decode group policies" << dendl;
+    op_ret = -EIO;
+    return;
+  }
+
+  auto policy = policies.find(policy_name);
+  if (policy == policies.end()) {
+    s->err.message = "No such PolicyName on the group";
+    op_ret = -ERR_NO_SUCH_ENTITY;
+    return;
+  }
+
+  dump_start(s); // <?xml block ?>
+  Formatter* f = s->formatter;
+  Formatter::ObjectSection response{*f, "GetGroupPolicyResponse", RGW_REST_IAM_XMLNS};
+  {
+    Formatter::ObjectSection result{*f, "GetGroupPolicyResult"};
+    encode_json("GroupName", info.name, f);
+    encode_json("PolicyName", policy_name, f);
+    encode_json("PolicyDocument", policy->second, f);
+    // /GetGroupPolicyResult
+  }
+  Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+  f->dump_string("RequestId", s->trans_id);
+  // /ResponseMetadata
+  // /GetGroupPolicyResponse
+}
+
+void RGWGetGroupPolicy_IAM::send_response()
+{
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// DeleteGroupPolicy
+class RGWDeleteGroupPolicy_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string policy_name;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWDeleteGroupPolicy_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "detach_group_policy"; }
+  RGWOpType get_type() override { return RGW_OP_DETACH_GROUP_POLICY; }
+};
+
+int RGWDeleteGroupPolicy_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_name = s->info.args.get("PolicyName");
+  if (!validate_iam_policy_name(policy_name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  int r = driver->load_group_by_name(this, y, info.account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWDeleteGroupPolicy_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamDeleteGroupPolicy, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWDeleteGroupPolicy_IAM::forward_to_master(optional_yield y,
+                                                const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("PolicyName");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWDeleteGroupPolicy_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y, &site] {
+        std::map<std::string, std::string> policies;
+        if (auto it = attrs.find(RGW_ATTR_IAM_POLICY); it != attrs.end()) try {
+          decode(policies, it->second);
+        } catch (buffer::error& err) {
+          ldpp_dout(this, 0) << "ERROR: failed to decode user policies" << dendl;
+          return -EIO;
+        }
+
+        auto i = policies.find(policy_name);
+        if (i == policies.end()) {
+          if (!site.is_meta_master()) {
+            return 0; // delete succeeded on the master
+          }
+          return -ERR_NO_SUCH_ENTITY;
+        }
+        policies.erase(i);
+
+        bufferlist bl;
+        encode(policies, bl);
+        attrs[RGW_ATTR_IAM_POLICY] = std::move(bl);
+
+        constexpr bool exclusive = false;
+        return driver->store_group(this, y, info, attrs, objv, exclusive, &info);
+      });
+}
+
+void RGWDeleteGroupPolicy_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "DeleteGroupPolicyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /DeleteGroupPolicyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListGroupPolicies
+class RGWListGroupPolicies_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string marker;
+  int max_items = 100;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+
+ 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 "list_group_policies"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_GROUP_POLICIES; }
+};
+
+int RGWListGroupPolicies_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  marker = s->info.args.get("Marker");
+
+  int r = s->info.args.get_int("MaxItems", &max_items, max_items);
+  if (r < 0 || max_items > 1000) {
+    s->err.message = "Invalid value for MaxItems";
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  RGWObjVersionTracker objv_ignored;
+  r = driver->load_group_by_name(this, y, info.account_id, name,
+                                 info, attrs, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWListGroupPolicies_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamListGroupPolicies, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListGroupPolicies_IAM::execute(optional_yield y)
+{
+  std::map<std::string, std::string> policies;
+  if (auto p = attrs.find(RGW_ATTR_IAM_POLICY); p != attrs.end()) try {
+    decode(policies, p->second);
+  } catch (const buffer::error& err) {
+    ldpp_dout(this, 0) << "ERROR: failed to decode user policies" << dendl;
+    op_ret = -EIO;
+  }
+
+  dump_start(s); // <?xml block ?>
+  Formatter* f = s->formatter;
+  Formatter::ObjectSection response{*f, "ListGroupPoliciesResponse", RGW_REST_IAM_XMLNS};
+  {
+    Formatter::ObjectSection result{*f, "ListGroupPoliciesResult"};
+    auto policy = policies.lower_bound(marker);
+    {
+      Formatter::ArraySection names{*f, "PolicyNames"};
+      for (; policy != policies.end() && max_items > 0; ++policy, --max_items) {
+        encode_json("member", policy->first, f);
+      }
+    } // /PolicyNames
+    const bool is_truncated = (policy != policies.end());
+    encode_json("IsTruncated", is_truncated, f);
+    if (is_truncated) {
+      encode_json("Marker", policy->first, f);
+    }
+  } // /ListUserPoliciesResult
+  Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+  f->dump_string("RequestId", s->trans_id);
+  // /ResponseMetadata
+  // /ListGroupPoliciesResponse
+}
+
+void RGWListGroupPolicies_IAM::send_response()
+{
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// AttachGroupPolicy
+class RGWAttachGroupPolicy_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string policy_arn;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWAttachGroupPolicy_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "attach_group_policy"; }
+  RGWOpType get_type() override { return RGW_OP_ATTACH_GROUP_POLICY; }
+};
+
+int RGWAttachGroupPolicy_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_arn = s->info.args.get("PolicyArn");
+  if (!validate_iam_policy_arn(policy_arn, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  int r = driver->load_group_by_name(this, y, info.account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWAttachGroupPolicy_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamAttachGroupPolicy, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWAttachGroupPolicy_IAM::forward_to_master(optional_yield y,
+                                                const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("PolicyArn");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWAttachGroupPolicy_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  // validate the policy arn
+  try {
+    const auto p = rgw::IAM::get_managed_policy(s->cct, policy_arn);
+    if (!p) {
+      op_ret = -ERR_NO_SUCH_ENTITY;
+      s->err.message = "The requested PolicyArn is not recognized";
+      return;
+    }
+  } catch (const rgw::IAM::PolicyParseException& e) {
+    ldpp_dout(this, 5) << "failed to parse policy: " << e.what() << dendl;
+    s->err.message = e.what();
+    op_ret = -ERR_MALFORMED_DOC;
+    return;
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y] {
+        rgw::IAM::ManagedPolicies policies;
+        if (auto it = attrs.find(RGW_ATTR_MANAGED_POLICY); it != attrs.end()) try {
+          decode(policies, it->second);
+        } catch (buffer::error& err) {
+          ldpp_dout(this, 0) << "ERROR: failed to decode user policies" << dendl;
+          return -EIO;
+        }
+
+        if (!policies.arns.insert(policy_arn).second) {
+          return 0; // nothing to do, return success
+        }
+
+        bufferlist bl;
+        encode(policies, bl);
+        attrs[RGW_ATTR_MANAGED_POLICY] = std::move(bl);
+
+        constexpr bool exclusive = false;
+        return driver->store_group(this, y, info, attrs, objv, exclusive, &info);
+      });
+}
+
+void RGWAttachGroupPolicy_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "AttachGroupPolicyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /AttachGroupPolicyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// DetachGroupPolicy
+class RGWDetachGroupPolicy_IAM : public RGWOp {
+  bufferlist post_body;
+  std::string policy_arn;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  RGWObjVersionTracker objv;
+
+  int forward_to_master(optional_yield y, const rgw::SiteConfig& site);
+ public:
+  explicit RGWDetachGroupPolicy_IAM(const ceph::bufferlist& post_body)
+    : post_body(post_body) {}
+
+  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 "detach_group_policy"; }
+  RGWOpType get_type() override { return RGW_OP_DETACH_GROUP_POLICY; }
+};
+
+int RGWDetachGroupPolicy_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  policy_arn = s->info.args.get("PolicyArn");
+  if (!validate_iam_policy_arn(policy_arn, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  int r = driver->load_group_by_name(this, y, info.account_id, name,
+                                     info, attrs, objv);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWDetachGroupPolicy_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamDetachGroupPolicy, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+int RGWDetachGroupPolicy_IAM::forward_to_master(optional_yield y,
+                                                const rgw::SiteConfig& site)
+{
+  RGWXMLDecoder::XMLParser parser;
+  if (!parser.init()) {
+    ldpp_dout(this, 0) << "ERROR: failed to initialize xml parser" << dendl;
+    return -EINVAL;
+  }
+
+  s->info.args.remove("GroupName");
+  s->info.args.remove("PolicyArn");
+  s->info.args.remove("Action");
+  s->info.args.remove("Version");
+
+  int r = forward_iam_request_to_master(this, site, s->user->get_info(),
+                                        post_body, parser, s->info, y);
+  if (r < 0) {
+    ldpp_dout(this, 20) << "ERROR: forward_iam_request_to_master failed with error code: " << r << dendl;
+    return r;
+  }
+  return 0;
+}
+
+void RGWDetachGroupPolicy_IAM::execute(optional_yield y)
+{
+  const rgw::SiteConfig& site = *s->penv.site;
+  if (!site.is_meta_master()) {
+    op_ret = forward_to_master(y, site);
+    if (op_ret) {
+      return;
+    }
+  }
+
+  op_ret = retry_raced_group_write(this, y, driver, info, attrs, objv,
+      [this, y, &site] {
+        rgw::IAM::ManagedPolicies policies;
+        if (auto it = attrs.find(RGW_ATTR_MANAGED_POLICY); it != attrs.end()) try {
+          decode(policies, it->second);
+        } catch (const buffer::error& err) {
+          ldpp_dout(this, 0) << "ERROR: failed to decode user policies" << dendl;
+          return -EIO;
+        }
+
+        auto i = policies.arns.find(policy_arn);
+        if (i == policies.arns.end()) {
+          if (!site.is_meta_master()) {
+            return 0; // delete succeeded on the master
+          }
+          return -ERR_NO_SUCH_ENTITY;
+        }
+        policies.arns.erase(i);
+
+        bufferlist bl;
+        encode(policies, bl);
+        attrs[RGW_ATTR_MANAGED_POLICY] = std::move(bl);
+
+        constexpr bool exclusive = false;
+        return driver->store_group(this, y, info, attrs, objv, exclusive, &info);
+      });
+}
+
+void RGWDetachGroupPolicy_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "DetachGroupPolicyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /DetachGroupPolicyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListAttachedGroupPolicies
+class RGWListAttachedGroupPolicies_IAM : public RGWOp {
+  bufferlist post_body;
+  RGWGroupInfo info;
+  rgw::sal::Attrs attrs;
+  std::string marker;
+  int max_items = 100;
+
+ 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 "list_attached_group_policies"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_ATTACHED_GROUP_POLICIES; }
+};
+
+int RGWListAttachedGroupPolicies_IAM::init_processing(optional_yield y)
+{
+  if (const auto* id = std::get_if<rgw_account_id>(&s->owner.id); id) {
+    info.account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string name = s->info.args.get("GroupName");
+  if (!validate_iam_group_name(name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  marker = s->info.args.get("Marker");
+
+  int r = s->info.args.get_int("MaxItems", &max_items, max_items);
+  if (r < 0 || max_items > 1000) {
+    s->err.message = "Invalid value for MaxItems";
+    return -EINVAL;
+  }
+
+  // look up group by GroupName
+  RGWObjVersionTracker objv_ignored;
+  r = driver->load_group_by_name(this, y, info.account_id, name,
+                                 info, attrs, objv_ignored);
+  if (r == -ENOENT) {
+    s->err.message = "No such GroupName in the account";
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWListAttachedGroupPolicies_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "group", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamListAttachedGroupPolicies, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListAttachedGroupPolicies_IAM::execute(optional_yield y)
+{
+  rgw::IAM::ManagedPolicies policies;
+  if (auto p = attrs.find(RGW_ATTR_MANAGED_POLICY); p != attrs.end()) try {
+    decode(policies, p->second);
+  } catch (const buffer::error& err) {
+    ldpp_dout(this, 0) << "ERROR: failed to decode user policies" << dendl;
+    op_ret = -EIO;
+  }
+
+
+  dump_start(s); // <?xml block ?>
+  Formatter* f = s->formatter;
+  Formatter::ObjectSection response{*f, "ListAttachedGroupPoliciesResponse", RGW_REST_IAM_XMLNS};
+  {
+    Formatter::ObjectSection result{*f, "ListAttachedGroupPoliciesResult"};
+
+    auto policy = policies.arns.lower_bound(marker);
+    {
+      Formatter::ArraySection arr{*f, "AttachedPolicies"};
+      for (; policy != policies.arns.end() && max_items > 0; ++policy, --max_items) {
+        Formatter::ObjectSection result{*f, "member"};
+        std::string_view arn = *policy;
+        if (auto p = arn.find('/'); p != arn.npos) {
+          encode_json("PolicyName", arn.substr(p + 1), f);
+        }
+        encode_json("PolicyArn", arn, f);
+      }
+    } // /AttachedPolicies
+    const bool is_truncated = (policy != policies.arns.end());
+    encode_json("IsTruncated", is_truncated, f);
+    if (is_truncated) {
+      encode_json("Marker", *policy, f);
+    }
+    // /ListAttachedUserPoliciesResult
+  }
+  Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+  f->dump_string("RequestId", s->trans_id);
+  // /ResponseMetadata
+  // /ListAttachedGroupPoliciesResponse
+}
+
+void RGWListAttachedGroupPolicies_IAM::send_response()
+{
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+RGWOp* make_iam_create_group_op(const ceph::bufferlist& post_body) {
+  return new RGWCreateGroup_IAM(post_body);
+}
+RGWOp* make_iam_get_group_op(const ceph::bufferlist&) {
+  return new RGWGetGroup_IAM;
+}
+RGWOp* make_iam_update_group_op(const ceph::bufferlist& post_body) {
+  return new RGWUpdateGroup_IAM(post_body);
+}
+RGWOp* make_iam_delete_group_op(const ceph::bufferlist& post_body) {
+  return new RGWDeleteGroup_IAM(post_body);
+}
+RGWOp* make_iam_list_groups_op(const ceph::bufferlist&) {
+  return new RGWListGroups_IAM;
+}
+
+RGWOp* make_iam_add_user_to_group_op(const ceph::bufferlist& post_body) {
+  return new RGWAddUserToGroup_IAM(post_body);
+}
+RGWOp* make_iam_remove_user_from_group_op(const ceph::bufferlist& post_body) {
+  return new RGWRemoveUserFromGroup_IAM(post_body);
+}
+RGWOp* make_iam_list_groups_for_user_op(const ceph::bufferlist& unused) {
+  return new RGWListGroupsForUser_IAM;
+}
+
+RGWOp* make_iam_put_group_policy_op(const ceph::bufferlist& post_body) {
+  return new RGWPutGroupPolicy_IAM(post_body);
+}
+RGWOp* make_iam_get_group_policy_op(const ceph::bufferlist& unused) {
+  return new RGWGetGroupPolicy_IAM;
+}
+RGWOp* make_iam_delete_group_policy_op(const ceph::bufferlist& post_body) {
+  return new RGWDeleteGroupPolicy_IAM(post_body);
+}
+RGWOp* make_iam_list_group_policies_op(const ceph::bufferlist& unused) {
+  return new RGWListGroupPolicies_IAM;
+}
+RGWOp* make_iam_attach_group_policy_op(const ceph::bufferlist& post_body) {
+  return new RGWAttachGroupPolicy_IAM(post_body);
+}
+RGWOp* make_iam_detach_group_policy_op(const ceph::bufferlist& post_body) {
+  return new RGWDetachGroupPolicy_IAM(post_body);
+}
+RGWOp* make_iam_list_attached_group_policies_op(const ceph::bufferlist& unused) {
+  return new RGWListAttachedGroupPolicies_IAM();
+}
+
diff --git a/src/rgw/rgw_rest_iam_group.h b/src/rgw/rgw_rest_iam_group.h
new file mode 100644 (file)
index 0000000..861b7e0
--- /dev/null
@@ -0,0 +1,40 @@
+// -*- 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 "include/buffer_fwd.h"
+
+class RGWOp;
+
+// IAM Group op factory functions
+RGWOp* make_iam_create_group_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_get_group_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_update_group_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_delete_group_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_list_groups_op(const ceph::bufferlist& unused);
+
+RGWOp* make_iam_add_user_to_group_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_remove_user_from_group_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_list_groups_for_user_op(const ceph::bufferlist& unused);
+
+// IAM GroupPolicy op factory functions
+RGWOp* make_iam_put_group_policy_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_get_group_policy_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_delete_group_policy_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_list_group_policies_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_attach_group_policy_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_detach_group_policy_op(const ceph::bufferlist& post_body);
+RGWOp* make_iam_list_attached_group_policies_op(const ceph::bufferlist& unused);
index f03762422e1a54424f7a050d0dab774092aac416..983c43eccc831a27cbf1fd26397b37e509bda0b6 100644 (file)
@@ -107,6 +107,12 @@ using rgw::IAM::iamListOIDCProviders;
 using rgw::IAM::iamListRoleTags;
 using rgw::IAM::iamListUsers;
 using rgw::IAM::iamListAccessKeys;
+using rgw::IAM::iamGetGroup;
+using rgw::IAM::iamListGroups;
+using rgw::IAM::iamListGroupsForUser;
+using rgw::IAM::iamGetGroupPolicy;
+using rgw::IAM::iamListGroupPolicies;
+using rgw::IAM::iamListAttachedGroupPolicies;
 using rgw::IAM::iamSimulateCustomPolicy;
 using rgw::IAM::iamSimulatePrincipalPolicy;
 using rgw::IAM::snsGetTopicAttributes;
@@ -809,6 +815,12 @@ TEST_F(ManagedPolicyTest, IAMReadOnlyAccess)
   act[iamListRoleTags] = 1;
   act[iamListUsers] = 1;
   act[iamListAccessKeys] = 1;
+  act[iamGetGroup] = 1;
+  act[iamListGroups] = 1;
+  act[iamListGroupsForUser] = 1;
+  act[iamGetGroupPolicy] = 1;
+  act[iamListGroupPolicies] = 1;
+  act[iamListAttachedGroupPolicies] = 1;
   act[iamSimulateCustomPolicy] = 1;
   act[iamSimulatePrincipalPolicy] = 1;