]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
rgw/iam: add initial IAM User apis
authorCasey Bodley <cbodley@redhat.com>
Mon, 18 Dec 2023 01:33:06 +0000 (20:33 -0500)
committerCasey Bodley <cbodley@redhat.com>
Wed, 10 Apr 2024 17:09:14 +0000 (13:09 -0400)
Signed-off-by: Casey Bodley <cbodley@redhat.com>
src/rgw/CMakeLists.txt
src/rgw/rgw_auth_s3.cc
src/rgw/rgw_common.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_user.cc [new file with mode: 0644]
src/rgw/rgw_rest_iam_user.h [new file with mode: 0644]

index 84a5bc9e56c97f1278c7dedb01871ccb8215d6f9..cc22cfacd2902a787505b9bd19689845514458f8 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_user.cc
   rgw_rest_s3.cc
   rgw_rest_pubsub.cc
   rgw_rest_zero.cc
index 54dcbec50f990082895c690788af217dff3f0381..be12f50a20dc72f0556594d5a411c9bf5505cad1 100644 (file)
@@ -498,6 +498,12 @@ bool is_non_s3_op(RGWOpType op_type)
   case RGW_OP_LIST_ROLE_TAGS:
   case RGW_OP_UNTAG_ROLE:
   case RGW_OP_UPDATE_ROLE:
+
+  case RGW_OP_CREATE_USER:
+  case RGW_OP_GET_USER:
+  case RGW_OP_UPDATE_USER:
+  case RGW_OP_DELETE_USER:
+  case RGW_OP_LIST_USERS:
     return true;
   default:
     return false;
index 7ba3089f7f417e5787416eb6eb4024b14ef698cb..05b58e3a2d953dfc6d535a5a3957fd6047c70b04 100644 (file)
@@ -81,7 +81,7 @@ rgw_http_errors rgw_http_s3_errors({
     { ERR_INVALID_WEBSITE_ROUTING_RULES_ERROR, {400, "InvalidRequest" }},
     { ERR_INVALID_ENCRYPTION_ALGORITHM, {400, "InvalidEncryptionAlgorithmError" }},
     { ERR_INVALID_RETENTION_PERIOD,{400, "InvalidRetentionPeriod"}},
-    { ERR_LIMIT_EXCEEDED, {400, "LimitExceeded" }},
+    { ERR_LIMIT_EXCEEDED, {409, "LimitExceeded" }},
     { ERR_LENGTH_REQUIRED, {411, "MissingContentLength" }},
     { EACCES, {403, "AccessDenied" }},
     { EPERM, {403, "AccessDenied" }},
@@ -135,6 +135,7 @@ rgw_http_errors rgw_http_s3_errors({
     { ERR_NO_SUCH_BUCKET_ENCRYPTION_CONFIGURATION, {404, "ServerSideEncryptionConfigurationNotFoundError"}},
     { ERR_NO_SUCH_PUBLIC_ACCESS_BLOCK_CONFIGURATION, {404, "NoSuchPublicAccessBlockConfiguration"}},
     { ERR_ACCOUNT_EXISTS, {409, "AccountAlreadyExists"}},
+    { ECANCELED, {409, "ConcurrentModification"}},
 });
 
 rgw_http_errors rgw_http_swift_errors({
index 813b78f161e11651c52c4cb78257a2bf345066bd..8d71ec5c688ca0462ce06a634e290c80b3bc4a06 100644 (file)
@@ -153,6 +153,11 @@ static const actpair actpairs[] =
  { "iam:ListRoleTags", iamListRoleTags},
  { "iam:UntagRole", iamUntagRole},
  { "iam:UpdateRole", iamUpdateRole},
+ { "iam:CreateUser", iamCreateUser},
+ { "iam:GetUser", iamGetUser},
+ { "iam:UpdateUser", iamUpdateUser},
+ { "iam:DeleteUser", iamDeleteUser},
+ { "iam:ListUsers", iamListUsers},
  { "sts:AssumeRole", stsAssumeRole},
  { "sts:AssumeRoleWithWebIdentity", stsAssumeRoleWithWebIdentity},
  { "sts:GetSessionToken", stsGetSessionToken},
@@ -1454,6 +1459,21 @@ const char* action_bit_string(uint64_t action) {
   case iamUpdateRole:
     return "iam:UpdateRole";
 
+  case iamCreateUser:
+    return "iam:CreateUser";
+
+  case iamGetUser:
+    return "iam:GetUser";
+
+  case iamUpdateUser:
+    return "iam:UpdateUser";
+
+  case iamDeleteUser:
+    return "iam:DeleteUser";
+
+  case iamListUsers:
+    return "iam:ListUsers";
+
   case stsAssumeRole:
     return "sts:AssumeRole";
 
index c6d3bc9ad77a9020484edfeef181592053ee3e7f..bf4983695c3d890675079d8112bfa2f5bac5bfce 100644 (file)
@@ -134,6 +134,11 @@ enum {
   iamListRoleTags,
   iamUntagRole,
   iamUpdateRole,
+  iamCreateUser,
+  iamGetUser,
+  iamUpdateUser,
+  iamDeleteUser,
+  iamListUsers,
   iamAll,
 
   stsAssumeRole,
index 53ef18696de478a9ac0805295f5b74c55809033a..128479143e0569fa6d00305907ffe5eb75da2039 100644 (file)
@@ -85,6 +85,11 @@ enum RGWOpType {
   RGW_OP_LIST_ROLE_TAGS,
   RGW_OP_UNTAG_ROLE,
   RGW_OP_UPDATE_ROLE,
+  RGW_OP_CREATE_USER,
+  RGW_OP_GET_USER,
+  RGW_OP_UPDATE_USER,
+  RGW_OP_DELETE_USER,
+  RGW_OP_LIST_USERS,
   /* rgw specific */
   RGW_OP_ADMIN_SET_METADATA,
   RGW_OP_GET_OBJ_LAYOUT,
index b9e8779c10a472c80eee12c5692468976c5f9241..c391761ccd67db3208cf3887517955a2a0db75d4 100644 (file)
@@ -1,6 +1,7 @@
 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
 // vim: ts=8 sw=2 smarttab ft=cpp
 
+#include <regex>
 #include <boost/tokenizer.hpp>
 
 #include "rgw_auth_s3.h"
@@ -9,6 +10,7 @@
 #include "rgw_rest_role.h"
 #include "rgw_rest_user_policy.h"
 #include "rgw_rest_oidc_provider.h"
+#include "rgw_rest_iam_user.h"
 
 #define dout_context g_ceph_context
 #define dout_subsys ceph_subsys_rgw
@@ -37,7 +39,12 @@ static const std::unordered_map<std::string_view, op_generator> op_generators =
   {"TagRole", [](const bufferlist& bl_post_body) -> RGWOp* {return new RGWTagRole(bl_post_body);}},
   {"ListRoleTags", [](const bufferlist& bl_post_body) -> RGWOp* {return new RGWListRoleTags;}},
   {"UntagRole", [](const bufferlist& bl_post_body) -> RGWOp* {return new RGWUntagRole(bl_post_body);}},
-  {"UpdateRole", [](const bufferlist& bl_post_body) -> RGWOp* {return new RGWUpdateRole(bl_post_body);}}
+  {"UpdateRole", [](const bufferlist& bl_post_body) -> RGWOp* {return new RGWUpdateRole(bl_post_body);}},
+  {"CreateUser", make_iam_create_user_op},
+  {"GetUser", make_iam_get_user_op},
+  {"UpdateUser", make_iam_update_user_op},
+  {"DeleteUser", make_iam_delete_user_op},
+  {"ListUsers", make_iam_list_users_op},
 };
 
 bool RGWHandler_REST_IAM::action_exists(const req_state* s) 
@@ -88,3 +95,54 @@ RGWRESTMgr_IAM::get_handler(rgw::sal::Driver* driver,
   bufferlist bl;
   return new RGWHandler_REST_IAM(auth_registry, bl);
 }
+
+static constexpr size_t MAX_USER_NAME_LEN = 64;
+
+bool validate_iam_user_name(const std::string& name, std::string& err)
+{
+  if (name.empty()) {
+    err = "Missing required element UserName";
+    return false;
+  }
+  if (name.size() > MAX_USER_NAME_LEN) {
+    err = "UserName too long";
+    return false;
+  }
+  const std::regex pattern("[\\w+=,.@-]+");
+  if (!std::regex_match(name, pattern)) {
+    err = "UserName 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)
+{
+  if (path.size() > MAX_PATH_LEN) {
+    err = "Path too long";
+    return false;
+  }
+  const std::regex pattern("(/[!-~]+/)|(/)");
+  if (!std::regex_match(path, pattern)) {
+    err = "Path contains invalid characters";
+    return false;
+  }
+  return true;
+}
+
+std::string iam_user_arn(const RGWUserInfo& info)
+{
+  if (info.type == TYPE_ROOT) {
+    return fmt::format("arn:aws:iam::{}:root", info.account_id);
+  }
+  std::string_view acct = !info.account_id.empty()
+      ? info.account_id : info.user_id.tenant;
+  std::string_view path = info.path;
+  if (path.empty()) {
+    path = "/";
+  }
+  return fmt::format("arn:aws:iam::{}:user{}{}",
+                     acct, path, info.display_name);
+}
index c1edd201e37035359ce95cb79fa2dbb656034e03..e2cac242c8bf099582c28cdfaf9870dcafbdf1f5 100644 (file)
@@ -8,6 +8,13 @@
 #include "rgw_rest.h"
 
 
+struct RGWUserInfo;
+
+bool validate_iam_user_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);
+
 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_user.cc b/src/rgw/rgw_rest_iam_user.cc
new file mode 100644 (file)
index 0000000..54bbcc1
--- /dev/null
@@ -0,0 +1,610 @@
+// -*- 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_user.h"
+
+#include <utility>
+#include "include/buffer.h"
+#include "common/errno.h"
+#include "rgw_arn.h"
+#include "rgw_common.h"
+#include "rgw_op.h"
+#include "rgw_rest.h"
+#include "rgw_rest_iam.h"
+
+
+static std::string make_resource_name(const RGWUserInfo& info)
+{
+  std::string_view path = info.path;
+  if (path.empty()) {
+    path = "/";
+  }
+  return string_cat_reserve(path, info.display_name);
+}
+
+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);
+  encode_json("CreateDate", info.create_date, f);
+}
+
+
+// CreateUser
+class RGWCreateUser_IAM : public RGWOp {
+  RGWUserInfo info;
+ 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 "create_user"; }
+  RGWOpType get_type() override { return RGW_OP_CREATE_USER; }
+};
+
+int RGWCreateUser_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.display_name = s->info.args.get("UserName");
+  if (!validate_iam_user_name(info.display_name, s->err.message)) {
+    return -EINVAL;
+  }
+
+  // TODO: Tags
+  return 0;
+}
+
+int RGWCreateUser_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "user", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamCreateUser, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWCreateUser_IAM::execute(optional_yield y)
+{
+  // check the current user 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;
+    return;
+  }
+
+  if (account.max_users >= 0) { // max_users < 0 means unlimited
+    uint32_t count = 0;
+    op_ret = driver->count_account_users(this, y, info.account_id, count);
+    if (op_ret < 0) {
+      ldpp_dout(this, 4) << "failed to count users for iam account "
+          << info.account_id << ": " << cpp_strerror(op_ret) << dendl;
+      return;
+    }
+    if (std::cmp_greater_equal(count, account.max_users)) {
+      s->err.message = fmt::format("User limit {} exceeded",
+                                   account.max_users);
+      op_ret = ERR_LIMIT_EXCEEDED;
+      return;
+    }
+  }
+
+  // generate user id
+  uuid_d uuid;
+  uuid.generate_random();
+  info.user_id.id = uuid.to_string();
+  info.user_id.tenant = s->auth.identity->get_tenant();
+
+  info.create_date = ceph::real_clock::now();
+
+  std::unique_ptr<rgw::sal::User> user = driver->get_user(info.user_id);
+  user->get_info() = info;
+
+  constexpr bool exclusive = true;
+  op_ret = user->store_user(this, y, exclusive, nullptr);
+}
+
+void RGWCreateUser_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "CreateUserResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "CreateUserResult"};
+      Formatter::ObjectSection user{*f, "User"};
+      dump_iam_user(info, f);
+      // /User
+      // /CreateUserResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /CreateUserResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// GetUser
+class RGWGetUser_IAM : public RGWOp {
+  rgw_account_id account_id;
+  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 "get_user"; }
+  RGWOpType get_type() override { return RGW_OP_GET_USER; }
+};
+
+int RGWGetUser_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) {
+    account_id = *id;
+  } else {
+    return -ERR_METHOD_NOT_ALLOWED;
+  }
+
+  const std::string username = s->info.args.get("UserName");
+  if (username.empty()) {
+    // If you do not specify a user name, IAM determines the user name
+    // implicitly based on the AWS access key ID signing the request.
+    // This operation works for access keys under the AWS account.
+    // Consequently, you can use this operation to manage AWS account
+    // root user credentials.
+    user = s->user->clone();
+    return 0;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  int 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 RGWGetUser_IAM::verify_permission(optional_yield y)
+{
+  const RGWUserInfo& info = user->get_info();
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "user", account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamGetUser, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWGetUser_IAM::execute(optional_yield y)
+{
+}
+
+void RGWGetUser_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "GetUserResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "GetUserResult"};
+      Formatter::ObjectSection User{*f, "User"};
+      dump_iam_user(user->get_info(), f);
+      // /User
+      // /GetUserResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /GetUserResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// UpdateUser
+class RGWUpdateUser_IAM : public RGWOp {
+  std::string new_path;
+  std::string new_username;
+  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 "update_user"; }
+  RGWOpType get_type() override { return RGW_OP_UPDATE_USER; }
+};
+
+int RGWUpdateUser_IAM::init_processing(optional_yield y)
+{
+  // use account id from authenticated user/role. with AssumeRole, this may not
+  // match the account of s->user
+  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_username = s->info.args.get("NewUserName");
+  if (!new_username.empty() &&
+      !validate_iam_user_name(new_username, s->err.message)) {
+    return -EINVAL;
+  }
+
+  const std::string username = s->info.args.get("UserName");
+  if (username.empty()) {
+    s->err.message = "Missing required element UserName";
+    return -EINVAL;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  int 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 RGWUpdateUser_IAM::verify_permission(optional_yield y)
+{
+  const RGWUserInfo& info = user->get_info();
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "user", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamUpdateUser, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWUpdateUser_IAM::execute(optional_yield y)
+{
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  if (!new_path.empty()) {
+    info.path = new_path;
+  }
+  if (!new_username.empty()) {
+    info.display_name = new_username;
+  }
+
+  constexpr bool exclusive = false;
+  op_ret = user->store_user(this, y, exclusive, &old_info);
+}
+
+void RGWUpdateUser_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "UpdateUserResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "UpdateUserResult"};
+      Formatter::ObjectSection User{*f, "User"};
+      dump_iam_user(user->get_info(), f);
+      // /User
+      // /UpdateUserResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /UpdateUserResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// DeleteUser
+class RGWDeleteUser_IAM : public RGWOp {
+  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 "delete_user"; }
+  RGWOpType get_type() override { return RGW_OP_DELETE_USER; }
+};
+
+int RGWDeleteUser_IAM::init_processing(optional_yield y)
+{
+  // use account id from authenticated user/role. with AssumeRole, this may not
+  // match the account of s->user
+  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 username = s->info.args.get("UserName");
+  if (username.empty()) {
+    s->err.message = "Missing required element UserName";
+    return -EINVAL;
+  }
+
+  // look up user by UserName
+  const std::string& tenant = s->auth.identity->get_tenant();
+  int 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 RGWDeleteUser_IAM::verify_permission(optional_yield y)
+{
+  const RGWUserInfo& info = user->get_info();
+  const std::string resource_name = make_resource_name(info);
+  const rgw::ARN arn{resource_name, "user", info.account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamDeleteUser, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWDeleteUser_IAM::execute(optional_yield y)
+{
+  // verify that all user resources are removed first
+  const RGWUserInfo& info = user->get_info();
+  if (!info.access_keys.empty()) {
+    s->err.message = "The user cannot be deleted until its AccessKeys are removed";
+    op_ret = -ERR_DELETE_CONFLICT;
+    return;
+  }
+
+  const auto& attrs = user->get_attrs();
+  if (auto p = attrs.find(RGW_ATTR_USER_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 user policies" << dendl;
+      op_ret = -EIO;
+      return;
+    }
+
+    if (!policies.empty()) {
+      s->err.message = "The user cannot be deleted until all user policies are removed";
+      op_ret = -ERR_DELETE_CONFLICT;
+      return;
+    }
+  }
+
+  op_ret = user->remove_user(this, y);
+}
+
+void RGWDeleteUser_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "DeleteUserResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /DeleteUserResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListUsers
+class RGWListUsers_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<RGWUserInfo> users);
+ 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_users"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_USERS; }
+};
+
+int RGWListUsers_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) {
+    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 RGWListUsers_IAM::verify_permission(optional_yield y)
+{
+  const std::string resource_name = "";
+  const rgw::ARN arn{resource_name, "user", account_id, true};
+  if (verify_user_permission(this, s, arn, rgw::IAM::iamListUsers, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListUsers_IAM::execute(optional_yield y)
+{
+  const std::string& tenant = s->auth.identity->get_tenant();
+
+  rgw::sal::UserList listing;
+  listing.next_marker = marker;
+
+  op_ret = driver->list_account_users(this, y, account_id, tenant,
+                                      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.users);
+
+  if (!started_response) {
+    started_response = true;
+    start_response();
+  }
+  end_response(listing.next_marker);
+}
+
+void RGWListUsers_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("ListUsersResponse", RGW_REST_IAM_XMLNS);
+  s->formatter->open_object_section("ListUsersResult");
+  s->formatter->open_array_section("Users");
+}
+
+void RGWListUsers_IAM::end_response(std::string_view next_marker)
+{
+  s->formatter->close_section(); // Users
+
+  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(); // ListUsersResult
+  s->formatter->close_section(); // ListUsersResponse
+  rgw_flush_formatter_and_reset(s, s->formatter);
+}
+
+void RGWListUsers_IAM::send_response_data(std::span<RGWUserInfo> users)
+{
+  if (!started_response) {
+    started_response = true;
+    start_response();
+  }
+
+  for (const auto& info : users) {
+    s->formatter->open_object_section("member");
+    dump_iam_user(info, s->formatter);
+    s->formatter->close_section(); // member
+  }
+
+  // flush after each chunk
+  rgw_flush_formatter(s, s->formatter);
+}
+
+void RGWListUsers_IAM::send_response()
+{
+  if (!started_response) { // errored out before execute() wrote anything
+    start_response();
+  }
+}
+
+
+RGWOp* make_iam_create_user_op(const ceph::bufferlist&) {
+  return new RGWCreateUser_IAM;
+}
+RGWOp* make_iam_get_user_op(const ceph::bufferlist&) {
+  return new RGWGetUser_IAM;
+}
+RGWOp* make_iam_update_user_op(const ceph::bufferlist&) {
+  return new RGWUpdateUser_IAM;
+}
+RGWOp* make_iam_delete_user_op(const ceph::bufferlist&) {
+  return new RGWDeleteUser_IAM;
+}
+RGWOp* make_iam_list_users_op(const ceph::bufferlist&) {
+  return new RGWListUsers_IAM;
+}
diff --git a/src/rgw/rgw_rest_iam_user.h b/src/rgw/rgw_rest_iam_user.h
new file mode 100644 (file)
index 0000000..06d5ce6
--- /dev/null
@@ -0,0 +1,27 @@
+// -*- 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 User op factory functions
+RGWOp* make_iam_create_user_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_get_user_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_update_user_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_delete_user_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_list_users_op(const ceph::bufferlist& unused);