]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
rgw/iam: add IAM AccessKey apis
authorCasey Bodley <cbodley@redhat.com>
Wed, 20 Dec 2023 20:01:00 +0000 (15:01 -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/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_user.cc
src/rgw/rgw_rest_iam_user.h

index be12f50a20dc72f0556594d5a411c9bf5505cad1..5f55ba21f714cad2c148b53251d0eca3b7017450 100644 (file)
@@ -504,6 +504,10 @@ bool is_non_s3_op(RGWOpType op_type)
   case RGW_OP_UPDATE_USER:
   case RGW_OP_DELETE_USER:
   case RGW_OP_LIST_USERS:
+  case RGW_OP_CREATE_ACCESS_KEY:
+  case RGW_OP_UPDATE_ACCESS_KEY:
+  case RGW_OP_DELETE_ACCESS_KEY:
+  case RGW_OP_LIST_ACCESS_KEYS:
     return true;
   default:
     return false;
index 8d71ec5c688ca0462ce06a634e290c80b3bc4a06..0a954a3c95427ed1fabb0b47fffb75f926b26825 100644 (file)
@@ -158,6 +158,10 @@ static const actpair actpairs[] =
  { "iam:UpdateUser", iamUpdateUser},
  { "iam:DeleteUser", iamDeleteUser},
  { "iam:ListUsers", iamListUsers},
+ { "iam:CreateAccessKey", iamCreateAccessKey},
+ { "iam:UpdateAccessKey", iamUpdateAccessKey},
+ { "iam:DeleteAccessKey", iamDeleteAccessKey},
+ { "iam:ListAccessKeys", iamListAccessKeys},
  { "sts:AssumeRole", stsAssumeRole},
  { "sts:AssumeRoleWithWebIdentity", stsAssumeRoleWithWebIdentity},
  { "sts:GetSessionToken", stsGetSessionToken},
@@ -1474,6 +1478,18 @@ const char* action_bit_string(uint64_t action) {
   case iamListUsers:
     return "iam:ListUsers";
 
+  case iamCreateAccessKey:
+    return "iam:CreateAccessKey";
+
+  case iamUpdateAccessKey:
+    return "iam:UpdateAccessKey";
+
+  case iamDeleteAccessKey:
+    return "iam:DeleteAccessKey";
+
+  case iamListAccessKeys:
+    return "iam:ListAccessKeys";
+
   case stsAssumeRole:
     return "sts:AssumeRole";
 
index bf4983695c3d890675079d8112bfa2f5bac5bfce..232896a97f44b4afd0ec6f20bbc3cc228fa39e38 100644 (file)
@@ -139,6 +139,10 @@ enum {
   iamUpdateUser,
   iamDeleteUser,
   iamListUsers,
+  iamCreateAccessKey,
+  iamUpdateAccessKey,
+  iamDeleteAccessKey,
+  iamListAccessKeys,
   iamAll,
 
   stsAssumeRole,
index 128479143e0569fa6d00305907ffe5eb75da2039..71456f755c82bbfb9539a5dcad8636dd712c979d 100644 (file)
@@ -90,6 +90,10 @@ enum RGWOpType {
   RGW_OP_UPDATE_USER,
   RGW_OP_DELETE_USER,
   RGW_OP_LIST_USERS,
+  RGW_OP_CREATE_ACCESS_KEY,
+  RGW_OP_UPDATE_ACCESS_KEY,
+  RGW_OP_DELETE_ACCESS_KEY,
+  RGW_OP_LIST_ACCESS_KEYS,
   /* rgw specific */
   RGW_OP_ADMIN_SET_METADATA,
   RGW_OP_GET_OBJ_LAYOUT,
index c391761ccd67db3208cf3887517955a2a0db75d4..a3a7cee6a23e8959c57719f6a8cc229e292f214d 100644 (file)
@@ -45,6 +45,10 @@ static const std::unordered_map<std::string_view, op_generator> op_generators =
   {"UpdateUser", make_iam_update_user_op},
   {"DeleteUser", make_iam_delete_user_op},
   {"ListUsers", make_iam_list_users_op},
+  {"CreateAccessKey", make_iam_create_access_key_op},
+  {"UpdateAccessKey", make_iam_update_access_key_op},
+  {"DeleteAccessKey", make_iam_delete_access_key_op},
+  {"ListAccessKeys", make_iam_list_access_keys_op},
 };
 
 bool RGWHandler_REST_IAM::action_exists(const req_state* s) 
index 54bbcc10433130507e9197dceb4b07c24143e6ec..9194d98f98aa2e465525f62f6375705c2fee04da 100644 (file)
@@ -593,6 +593,494 @@ void RGWListUsers_IAM::send_response()
 }
 
 
+void dump_access_key(const RGWAccessKey& key, Formatter* f)
+{
+  encode_json("AccessKeyId", key.id, f);
+  encode_json("Status", key.active ? "Active" : "Inactive", f);
+  encode_json("CreateDate", key.create_date, f);
+}
+
+// CreateAccessKey
+class RGWCreateAccessKey_IAM : public RGWOp {
+  std::unique_ptr<rgw::sal::User> user;
+  RGWAccessKey key;
+ 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_access_key"; }
+  RGWOpType get_type() override { return RGW_OP_CREATE_ACCESS_KEY; }
+};
+
+int RGWCreateAccessKey_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()) {
+    // 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;
+  }
+  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();
+  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 RGWCreateAccessKey_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::iamCreateAccessKey, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWCreateAccessKey_IAM::execute(optional_yield y)
+{
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  std::optional<int> max_keys;
+  {
+    // read account's access key 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_access_keys >= 0) { // max < 0 means unlimited
+      max_keys = account.max_access_keys;
+    }
+  }
+
+  if (rgw_generate_access_key(this, y, driver, key.id) < 0) {
+    s->err.message = "failed to generate s3 access key";
+    op_ret = -ERR_INTERNAL_ERROR;
+    return;
+  }
+  rgw_generate_secret_key(get_cct(), key.key);
+  key.create_date = ceph::real_clock::now();
+  info.access_keys[key.id] = key;
+
+  // check the current count against account limit
+  if (max_keys && std::cmp_greater(info.access_keys.size(), *max_keys)) {
+    s->err.message = fmt::format("Access key limit {} exceeded", *max_keys);
+    op_ret = -ERR_LIMIT_EXCEEDED;
+    return;
+  }
+
+  constexpr bool exclusive = false;
+  op_ret = user->store_user(this, y, exclusive, &old_info);
+}
+
+void RGWCreateAccessKey_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "CreateAccessKeyResponse", RGW_REST_IAM_XMLNS};
+    {
+      Formatter::ObjectSection result{*f, "CreateAccessKeyResult"};
+      Formatter::ObjectSection accesskey{*f, "AccessKey"};
+      encode_json("UserName", user->get_display_name(), f);
+      dump_access_key(key, f);
+      encode_json("SecretAccessKey", key.key, f);
+      // /AccessKey
+      // /CreateAccessKeyResult
+    }
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /CreateAccessKeyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// UpdateAccessKey
+class RGWUpdateAccessKey_IAM : public RGWOp {
+  std::string access_key_id;
+  bool new_status = false;
+  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_access_key"; }
+  RGWOpType get_type() override { return RGW_OP_UPDATE_ACCESS_KEY; }
+};
+
+int RGWUpdateAccessKey_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;
+  }
+
+  access_key_id = s->info.args.get("AccessKeyId");
+  if (access_key_id.empty()) {
+    s->err.message = "Missing required element AccessKeyId";
+    return -EINVAL;
+  }
+
+  const std::string status = s->info.args.get("Status");
+  if (status == "Active") {
+    new_status = true;
+  } else if (status == "Inactive") {
+    new_status = false;
+  } else {
+    if (status.empty()) {
+      s->err.message = "Missing required element Status";
+    } else {
+      s->err.message = "Invalid value for Status";
+    }
+    return -EINVAL;
+  }
+
+  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;
+  }
+  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();
+  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 RGWUpdateAccessKey_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::iamUpdateAccessKey, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWUpdateAccessKey_IAM::execute(optional_yield y)
+{
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  auto key = info.access_keys.find(access_key_id);
+  if (key == info.access_keys.end()) {
+    s->err.message = "No such AccessKeyId in the user";
+    op_ret = -ERR_NO_SUCH_ENTITY;
+    return;
+  }
+
+  if (key->second.active == new_status) {
+    return; // nothing to do, return success
+  }
+
+  key->second.active = new_status;
+
+  constexpr bool exclusive = false;
+  op_ret = user->store_user(this, y, exclusive, &old_info);
+}
+
+void RGWUpdateAccessKey_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "UpdateAccessKeyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /UpdateAccessKeyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+// DeleteAccessKey
+class RGWDeleteAccessKey_IAM : public RGWOp {
+  std::string access_key_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 "delete_access_key"; }
+  RGWOpType get_type() override { return RGW_OP_DELETE_ACCESS_KEY; }
+};
+
+int RGWDeleteAccessKey_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;
+  }
+
+  access_key_id = s->info.args.get("AccessKeyId");
+  if (access_key_id.empty()) {
+    s->err.message = "Missing required element AccessKeyId";
+    return -EINVAL;
+  }
+
+  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;
+  }
+  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();
+  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 RGWDeleteAccessKey_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::iamDeleteAccessKey, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWDeleteAccessKey_IAM::execute(optional_yield y)
+{
+  RGWUserInfo& info = user->get_info();
+  RGWUserInfo old_info = info;
+
+  auto key = info.access_keys.find(access_key_id);
+  if (key == info.access_keys.end()) {
+    s->err.message = "No such AccessKeyId in the user";
+    op_ret = -ERR_NO_SUCH_ENTITY;
+    return;
+  }
+
+  info.access_keys.erase(key);
+
+  constexpr bool exclusive = false;
+  op_ret = user->store_user(this, y, exclusive, &old_info);
+}
+
+void RGWDeleteAccessKey_IAM::send_response()
+{
+  if (!op_ret) {
+    dump_start(s); // <?xml block ?>
+    Formatter* f = s->formatter;
+    Formatter::ObjectSection response{*f, "DeleteAccessKeyResponse", RGW_REST_IAM_XMLNS};
+    Formatter::ObjectSection metadata{*f, "ResponseMetadata"};
+    f->dump_string("RequestId", s->trans_id);
+    // /ResponseMetadata
+    // /DeleteAccessKeyResponse
+  }
+
+  set_req_state_err(s, op_ret);
+  dump_errno(s);
+  end_header(s, this);
+}
+
+
+// ListAccessKeys
+class RGWListAccessKeys_IAM : public RGWOp {
+  std::unique_ptr<rgw::sal::User> user;
+  std::string marker;
+  int max_items = 100;
+
+  bool started_response = false;
+  void start_response();
+ 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_access_keys"; }
+  RGWOpType get_type() override { return RGW_OP_LIST_ACCESS_KEYS; }
+};
+
+int RGWListAccessKeys_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;
+  }
+
+  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 (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;
+  }
+  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) {
+    return -ERR_NO_SUCH_ENTITY;
+  }
+  return r;
+}
+
+int RGWListAccessKeys_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::iamListAccessKeys, true)) {
+    return 0;
+  }
+  return -EACCES;
+}
+
+void RGWListAccessKeys_IAM::execute(optional_yield y)
+{
+  start_response();
+  started_response = true;
+
+  dump_start(s); // <?xml block ?>
+
+  Formatter* f = s->formatter;
+  f->open_object_section_in_ns("ListAccessKeysResponse", RGW_REST_IAM_XMLNS);
+  f->open_object_section("ListAccessKeysResult");
+  encode_json("UserName", user->get_display_name(), f);
+  f->open_array_section("AccessKeyMetadata");
+
+  const RGWUserInfo& info = user->get_info();
+
+  auto key = info.access_keys.lower_bound(marker);
+  for (int i = 0; i < max_items && key != info.access_keys.end(); ++i, ++key) {
+    f->open_object_section("member");
+    encode_json("UserName", user->get_display_name(), f);
+    dump_access_key(key->second, f);
+    f->close_section(); // member
+  }
+
+  f->close_section(); // AccessKeyMetadata
+
+  const bool truncated = (key != info.access_keys.end());
+  f->dump_bool("IsTruncated", truncated);
+  if (truncated) {
+    f->dump_string("Marker", key->second.id);
+  }
+
+  f->close_section(); // ListAccessKeysResult
+  f->close_section(); // ListAccessKeysResponse
+  rgw_flush_formatter_and_reset(s, f);
+}
+
+void RGWListAccessKeys_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);
+}
+
+void RGWListAccessKeys_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;
 }
@@ -608,3 +1096,16 @@ RGWOp* make_iam_delete_user_op(const ceph::bufferlist&) {
 RGWOp* make_iam_list_users_op(const ceph::bufferlist&) {
   return new RGWListUsers_IAM;
 }
+
+RGWOp* make_iam_create_access_key_op(const ceph::bufferlist& unused) {
+  return new RGWCreateAccessKey_IAM;
+}
+RGWOp* make_iam_update_access_key_op(const ceph::bufferlist& unused) {
+  return new RGWUpdateAccessKey_IAM;
+}
+RGWOp* make_iam_delete_access_key_op(const ceph::bufferlist& unused) {
+  return new RGWDeleteAccessKey_IAM;
+}
+RGWOp* make_iam_list_access_keys_op(const ceph::bufferlist& unused) {
+  return new RGWListAccessKeys_IAM;
+}
index 06d5ce6b580864101fb6cac8403d9d3dc0114daf..ed7e960b229e2aa3b01f905c7dcfcad8492cffb2 100644 (file)
@@ -25,3 +25,9 @@ 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);
+
+// AccessKey op factory functions
+RGWOp* make_iam_create_access_key_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_update_access_key_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_delete_access_key_op(const ceph::bufferlist& unused);
+RGWOp* make_iam_list_access_keys_op(const ceph::bufferlist& unused);