From be2b7e6b01e568ba906a7f422111aa15d8fa9f10 Mon Sep 17 00:00:00 2001 From: Shilpa Jagannath Date: Wed, 19 Jun 2019 15:04:03 +0530 Subject: [PATCH] This commit deals with renaming user. Under the hood, the attributes of an existing user are moved under the new user namespace specified by "--new-uid". It calls bucket link and bucket chown to link the buckets and objects to the new user namespace. Access and secret keys of the user(and the subusers) are preserved. Usage: "radosgw-admin user rename --uid=<> --new-uid=<>" Signed-off-by: Shilpa Jagannath --- doc/man/8/radosgw-admin.rst | 11 ++ src/rgw/rgw_admin.cc | 25 ++++ src/rgw/rgw_bucket.cc | 177 ++++++++++++++------------- src/rgw/rgw_bucket.h | 3 +- src/rgw/rgw_user.cc | 234 ++++++++++++++++++++++++++++++++++++ src/rgw/rgw_user.h | 14 +++ 6 files changed, 379 insertions(+), 85 deletions(-) diff --git a/doc/man/8/radosgw-admin.rst b/doc/man/8/radosgw-admin.rst index 7cc9c26cc0c32..1fdb7f48eea79 100644 --- a/doc/man/8/radosgw-admin.rst +++ b/doc/man/8/radosgw-admin.rst @@ -35,6 +35,9 @@ which are as follows: Display information of a user, and any potentially available subusers and keys. +:command:`user rename` + Renames a user. + :command:`user rm` Remove a user. @@ -467,6 +470,10 @@ Options The radosgw user ID. +.. option:: --new-uid=uid + + ID of the new user. Used with 'user rename' command. + .. option:: --subuser= Name of the subuser. @@ -895,6 +902,10 @@ Generate a new user:: Remove a user:: $ radosgw-admin user rm --uid=johnny + +Rename a user:: + + $ radosgw-admin user rename --uid=johny --new-uid=joe Remove a user and all associated buckets with their contents:: diff --git a/src/rgw/rgw_admin.cc b/src/rgw/rgw_admin.cc index 6a2ad126bfb1b..dc6019955a3de 100644 --- a/src/rgw/rgw_admin.cc +++ b/src/rgw/rgw_admin.cc @@ -84,6 +84,7 @@ void usage() cout << " user create create a new user\n" ; cout << " user modify modify user\n"; cout << " user info get user info\n"; + cout << " user rename rename user\n"; cout << " user rm remove user\n"; cout << " user suspend suspend a user\n"; cout << " user enable re-enable user after suspension\n"; @@ -243,6 +244,7 @@ void usage() cout << "options:\n"; cout << " --tenant= tenant name\n"; cout << " --uid= user id\n"; + cout << " --new-uid= new user id\n"; cout << " --subuser= subuser name\n"; cout << " --access-key= S3 access key\n"; cout << " --email= user's email address\n"; @@ -390,6 +392,7 @@ enum { OPT_USER_CREATE, OPT_USER_INFO, OPT_USER_MODIFY, + OPT_USER_RENAME, OPT_USER_RM, OPT_USER_SUSPEND, OPT_USER_ENABLE, @@ -644,6 +647,8 @@ static int get_cmd(const char *cmd, const char *prev_cmd, const char *prev_prev_ return OPT_USER_INFO; if (strcmp(cmd, "modify") == 0) return OPT_USER_MODIFY; + if (strcmp(cmd, "rename") == 0) + return OPT_USER_RENAME; if (strcmp(cmd, "rm") == 0) return OPT_USER_RM; if (strcmp(cmd, "suspend") == 0) @@ -2748,6 +2753,7 @@ int main(int argc, const char **argv) rgw_user user_id; string tenant; + rgw_user new_user_id; std::string access_key, secret_key, user_email, display_name; std::string bucket_name, pool_name, object; rgw_pool pool; @@ -2907,6 +2913,8 @@ int main(int argc, const char **argv) break; } else if (ceph_argparse_witharg(args, i, &val, "-i", "--uid", (char*)NULL)) { user_id.from_str(val); + } else if (ceph_argparse_witharg(args, i, &val, "-i", "--new-uid", (char*)NULL)) { + new_user_id.from_str(val); } else if (ceph_argparse_witharg(args, i, &val, "--tenant", (char*)NULL)) { tenant = val; } else if (ceph_argparse_witharg(args, i, &val, "--access-key", (char*)NULL)) { @@ -3313,6 +3321,11 @@ int main(int argc, const char **argv) } user_id.tenant = tenant; } + + if (!new_user_id.empty() && !tenant.empty()) { + new_user_id.tenant = tenant; + } + /* check key parameter conflict */ if ((!access_key.empty()) && gen_access_key) { cerr << "ERROR: key parameter conflict, --access-key & --gen-access-key" << std::endl; @@ -4933,6 +4946,10 @@ int main(int argc, const char **argv) if (!user_email.empty()) user_op.set_user_email(user_email); + if (!user_id.empty()) { + user_op.set_new_user_id(new_user_id); + } + if (!access_key.empty()) user_op.set_access_key(access_key); @@ -5054,6 +5071,14 @@ int main(int argc, const char **argv) } output_user_info = false; + break; + case OPT_USER_RENAME: + ret = user.rename(user_op, &err_msg); + if (ret < 0) { + cerr << "could not rename user: " << err_msg << std::endl; + return -ret; + } + break; case OPT_USER_ENABLE: case OPT_USER_SUSPEND: diff --git a/src/rgw/rgw_bucket.cc b/src/rgw/rgw_bucket.cc index e1792ad30d07d..be0897b8c6bda 100644 --- a/src/rgw/rgw_bucket.cc +++ b/src/rgw/rgw_bucket.cc @@ -190,6 +190,90 @@ int rgw_bucket_sync_user_stats(RGWRados *store, const string& tenant_name, const return 0; } +int rgw_bucket_chown(RGWRados* const store, RGWUserInfo& user_info, RGWBucketInfo& bucket_info, const string& marker, map& attrs) +{ + RGWObjectCtx obj_ctx(store); + std::vector objs; + map common_prefixes; + + RGWRados::Bucket target(store, bucket_info); + RGWRados::Bucket::List list_op(&target); + + list_op.params.list_versions = true; + list_op.params.allow_unordered = true; + list_op.params.marker = marker; + + bool is_truncated = false; + int count = 0; + int max_entries = 1000; + + //Loop through objects and update object acls to point to bucket owner + + do { + objs.clear(); + int ret = list_op.list_objects(max_entries, &objs, &common_prefixes, &is_truncated); + if (ret < 0) { + ldout(store->ctx(), 0) << "ERROR: list objects failed: " << cpp_strerror(-ret) << dendl; + return ret; + } + + list_op.params.marker = list_op.get_next_marker(); + count += objs.size(); + + for (const auto& obj : objs) { + + const rgw_obj r_obj(bucket_info.bucket, obj.key); + ret = get_obj_attrs(store, obj_ctx, bucket_info, r_obj, attrs); + if (ret < 0){ + ldout(store->ctx(), 0) << "ERROR: failed to read object " << obj.key.name << cpp_strerror(-ret) << dendl; + continue; + } + const auto& aiter = attrs.find(RGW_ATTR_ACL); + if (aiter == attrs.end()) { + ldout(store->ctx(), 0) << "ERROR: no acls found for object " << obj.key.name << " .Continuing with next object." << dendl; + continue; + } else { + bufferlist& bl = aiter->second; + RGWAccessControlPolicy policy(store->ctx()); + ACLOwner owner; + try { + decode(policy, bl); + owner = policy.get_owner(); + } catch (buffer::error& err) { + ldout(store->ctx(), 0) << "ERROR: decode policy failed" << err << dendl; + return -EIO; + } + + //Get the ACL from the policy + RGWAccessControlList& acl = policy.get_acl(); + + //Remove grant that is set to old owner + acl.remove_canon_user_grant(owner.get_id()); + + //Create a grant and add grant + ACLGrant grant; + grant.set_canon(bucket_info.owner, user_info.display_name, RGW_PERM_FULL_CONTROL); + acl.add_grant(&grant); + + //Update the ACL owner to the new user + owner.set_id(bucket_info.owner); + owner.set_name(user_info.display_name); + policy.set_owner(owner); + + bl.clear(); + encode(policy, bl); + + ret = modify_obj_attr(store, obj_ctx, bucket_info, r_obj, RGW_ATTR_ACL, bl); + if (ret < 0) { + ldout(store->ctx(), 0) << "ERROR: modify attr failed " << cpp_strerror(-ret) << dendl; + return ret; + } + } + } cerr << count << " objects processed in " << bucket_info.bucket.name << " .Next marker " << list_op.params.marker.name << std::endl; + } while(is_truncated); + return 0; +} + int rgw_link_bucket(RGWRados* const store, const rgw_user& user_id, rgw_bucket& bucket, @@ -826,7 +910,7 @@ int RGWBucket::init(RGWRados *storage, RGWBucketAdminOpState& op_state, if (!bucket_name.empty()) { ceph::real_time mtime; int r = store->get_bucket_info(obj_ctx, bucket_tenant, bucket_name, - bucket_info, &mtime, pattrs); + bucket_info, &mtime, null_yield, pattrs); if (r < 0) { set_err_msg(err_msg, "failed to fetch bucket info for bucket=" + bucket_name); ldout(store->ctx(), 0) << "could not get bucket info for bucket=" << bucket_name << dendl; @@ -939,7 +1023,7 @@ int RGWBucket::link(RGWBucketAdminOpState& op_state, store->get_bucket_instance_obj(bucket, obj_bucket_instance); auto inst_sysobj = obj_ctx.get_obj(obj_bucket_instance); - r = sysobj.wop() + r = inst_sysobj.wop() .set_objv_tracker(&objv_tracker) .write_attr(RGW_ATTR_ACL, aclbl, null_yield); if (r < 0) { @@ -997,20 +1081,15 @@ int RGWBucket::link(RGWBucketAdminOpState& op_state, int RGWBucket::chown(RGWBucketAdminOpState& op_state, map& attrs, const string& marker, std::string *err_msg) { - //after bucket link rgw_bucket& bucket = op_state.get_bucket(); tenant = bucket.tenant; bucket_name = bucket.name; - std::vector objs; - map common_prefixes; - RGWObjectCtx obj_ctx(store); - RGWBucketInfo bucket_info; RGWSysObjectCtx sys_ctx = store->svc.sysobj->init_obj_ctx(); - int ret = store->get_bucket_info(sys_ctx, tenant, bucket_name, bucket_info, NULL, &attrs); + int ret = store->get_bucket_info(sys_ctx, tenant, bucket_name, bucket_info, NULL, null_yield, &attrs); if (ret < 0) { set_err_msg(err_msg, "bucket info failed: tenant: " + tenant + "bucket_name: " + bucket_name + " " + cpp_strerror(-ret)); return ret; @@ -1023,82 +1102,12 @@ int RGWBucket::chown(RGWBucketAdminOpState& op_state, return ret; } - RGWRados::Bucket target(store, bucket_info); - RGWRados::Bucket::List list_op(&target); - - list_op.params.list_versions = true; - list_op.params.allow_unordered = true; - list_op.params.marker = marker; - - bool is_truncated = false; - int count = 0; - int max_entries = 1000; - - //Loop through objects and update object acls to point to bucket owner - - do { - objs.clear(); - ret = list_op.list_objects(max_entries, &objs, &common_prefixes, &is_truncated); - if (ret < 0) { - set_err_msg(err_msg, "list objects failed: " + cpp_strerror(-ret)); - return ret; - } - - list_op.params.marker = list_op.get_next_marker(); - count += objs.size(); - - for (const auto& obj : objs) { - - const rgw_obj r_obj(bucket_info.bucket, obj.key); - ret = get_obj_attrs(store, obj_ctx, bucket_info, r_obj, attrs); - if (ret < 0){ - set_err_msg(err_msg, "failed to read object " + obj.key.name + "with " + cpp_strerror(-ret)); - continue; - } - const auto& aiter = attrs.find(RGW_ATTR_ACL); - if (aiter == attrs.end()) { - set_err_msg(err_msg, "no acls found for object " + obj.key.name + " .Continuing with next object." + cpp_strerror(-ret)); - continue; - } else { - bufferlist& bl = aiter->second; - RGWAccessControlPolicy policy(store->ctx()); - ACLOwner owner; - try { - decode(policy, bl); - owner = policy.get_owner(); - } catch (buffer::error& err) { - set_err_msg(err_msg, "decode policy failed: "); - return -EIO; - } - - //Get the ACL from the policy - RGWAccessControlList& acl = policy.get_acl(); - - //Remove grant that is set to old owner - acl.remove_canon_user_grant(owner.get_id()); - - //Create a grant and add grant - ACLGrant grant; - grant.set_canon(bucket_info.owner, user_info.display_name, RGW_PERM_FULL_CONTROL); - acl.add_grant(&grant); - - //Update the ACL owner to the new user - owner.set_id(bucket_info.owner); - owner.set_name(user_info.display_name); - policy.set_owner(owner); - - bl.clear(); - encode(policy, bl); - - ret = modify_obj_attr(store, obj_ctx, bucket_info, r_obj, RGW_ATTR_ACL, bl); - if (ret < 0) { - set_err_msg(err_msg, "modify attr failed: " + cpp_strerror(-ret)); - return ret; - } - } - } cerr << count << " objects processed, next marker " << list_op.params.marker.name << std::endl; - } while(is_truncated); - return 0; + ret = rgw_bucket_chown(store, user_info, bucket_info, marker, attrs); + if (ret < 0) { + set_err_msg(err_msg, "Failed to change object ownership" + cpp_strerror(-ret)); + } + + return ret; } int RGWBucket::unlink(RGWBucketAdminOpState& op_state, std::string *err_msg) diff --git a/src/rgw/rgw_bucket.h b/src/rgw/rgw_bucket.h index 202eee0e903cb..8a4c1a6d30dbc 100644 --- a/src/rgw/rgw_bucket.h +++ b/src/rgw/rgw_bucket.h @@ -215,7 +215,8 @@ extern int rgw_link_bucket(RGWRados* store, rgw_ep_info *pinfo = nullptr); extern int rgw_unlink_bucket(RGWRados *store, const rgw_user& user_id, const string& tenant_name, const string& bucket_name, bool update_entrypoint = true); - +extern int rgw_bucket_chown(RGWRados* const store, RGWUserInfo& user_info, RGWBucketInfo& bucket_info, + const string& marker, map& attrs); extern int rgw_remove_object(RGWRados *store, const RGWBucketInfo& bucket_info, const rgw_bucket& bucket, rgw_obj_key& key); extern int rgw_remove_bucket(RGWRados *store, rgw_bucket& bucket, bool delete_children, optional_yield y); extern int rgw_remove_bucket_bypass_gc(RGWRados *store, rgw_bucket& bucket, int concurrent_max, optional_yield y); diff --git a/src/rgw/rgw_user.cc b/src/rgw/rgw_user.cc index e93ff9ca7ca4f..b351073671751 100644 --- a/src/rgw/rgw_user.cc +++ b/src/rgw/rgw_user.cc @@ -1936,6 +1936,219 @@ int RGWUser::check_op(RGWUserAdminOpState& op_state, std::string *err_msg) return 0; } +int RGWUser::execute_user_rename(RGWUserAdminOpState& op_state, std::string *err_msg) +{ + + // unlink buckets from existing user + rgw_user& old_uid = op_state.get_user_id(); + RGWUserInfo old_user_info = op_state.get_user_info(); + + bool is_truncated = false; + string marker; + CephContext *cct = store->ctx(); + size_t max_buckets = cct->_conf->rgw_list_buckets_max_chunk; + vector read_buckets; + do { + RGWUserBuckets buckets; + int ret = rgw_read_user_buckets(store, old_uid, buckets, marker, string(), + max_buckets, false, &is_truncated); + if (ret < 0) { + set_err_msg(err_msg, "unable to read user bucket info"); + return ret; + } + + map& m = buckets.get_buckets(); + + std::map::iterator it; + for (it = m.begin(); it != m.end(); ++it) { + RGWBucketEnt obj = it->second; + read_buckets.push_back(obj.bucket); //Save the list of all buckets of the user to be able to use after bucket unlink + ret = rgw_unlink_bucket(store, old_uid, (obj.bucket.tenant), (obj.bucket.name)); + if (ret < 0) { + set_err_msg(err_msg, "error unlinking bucket " + cpp_strerror(-ret)); + return ret; + } + + marker = it->first; + } + + } while (is_truncated); + + // delete old user + std::string subprocess_msg; + int ret = execute_remove(op_state, &subprocess_msg); + if (ret < 0) { + set_err_msg(err_msg, "unable to remove existing user, " + subprocess_msg); + return ret; + } + + // create new user with old user's attributes + rgw_user& uid = op_state.get_new_uid(); + + if (old_uid.tenant != uid.tenant) { + set_err_msg(err_msg, "Users have to be under the same tenant namespace " + + old_uid.tenant + "!=" + uid.tenant); + } + + string display_name = old_user_info.display_name; + RGWUserAdminOpState new_op_state; + new_op_state.set_user_id(uid); + + ret = execute_rename(new_op_state, old_user_info, &subprocess_msg); + if (ret < 0) { + set_err_msg(err_msg, "unable to create new user, " + subprocess_msg); + return ret; + } + + RGWUserInfo user_info; + ret = rgw_get_user_info_by_uid(store, uid, user_info); + if (ret < 0) { + set_err_msg(err_msg, "failed to fetch user info"); + return ret; + } + + + // link bucket and objects to new user + ACLOwner owner; + RGWAccessControlPolicy policy_instance; + policy_instance.create_default(user_info.user_id, display_name); + owner = policy_instance.get_owner(); + bufferlist aclbl; + policy_instance.encode(aclbl); + + map attrs; + for (vector::iterator iter = read_buckets.begin(); iter != read_buckets.end(); ++iter) { + + RGWBucketInfo bucket_info; + rgw_bucket bucket = *iter; + RGWSysObjectCtx sys_ctx = store->svc.sysobj->init_obj_ctx(); + + ret = store->get_bucket_info(sys_ctx, bucket.tenant, bucket.name, + bucket_info, NULL, null_yield, &attrs); + if (ret < 0) { + set_err_msg(err_msg, "failed to fetch bucket info for bucket= " + bucket.name); + return ret; + } + + RGWObjVersionTracker objv_tracker; + RGWObjVersionTracker old_version = bucket_info.objv_tracker; + + ret = store->set_bucket_owner(bucket, owner); + if (ret < 0) { + set_err_msg(err_msg, "failed to set bucket owner: " + cpp_strerror(-ret)); + return ret; + } + + const rgw_pool& root_pool = store->svc.zone->get_zone_params().domain_root; + std::string bucket_entry; + rgw_make_bucket_entry_name(bucket.tenant, bucket.name, bucket_entry); + rgw_raw_obj obj(root_pool, bucket_entry); + auto obj_ctx = store->svc.sysobj->init_obj_ctx(); + auto sysobj = obj_ctx.get_obj(obj); + rgw_raw_obj obj_bucket_instance; + + store->get_bucket_instance_obj(bucket, obj_bucket_instance); + auto inst_sysobj = obj_ctx.get_obj(obj_bucket_instance); + ret = inst_sysobj.wop() + .set_objv_tracker(&objv_tracker) + .write_attr(RGW_ATTR_ACL, aclbl, null_yield); + if (ret < 0) { + set_err_msg(err_msg, "failed to set new acl on bucket " + bucket.name); + return ret; + } + + RGWBucketEntryPoint ep; + ep.bucket = bucket_info.bucket; + ep.owner = user_info.user_id; + ep.creation_time = bucket_info.creation_time; + ep.linked = true; + map ep_attrs; + rgw_ep_info ep_data{ep, ep_attrs}; + + ret = rgw_link_bucket(store, user_info.user_id, bucket_info.bucket, + ceph::real_time(), true, &ep_data); + if (ret < 0) { + set_err_msg(err_msg, "failed to link bucket " + bucket.name + " to new user"); + return ret; + } + + RGWBucketInfo new_bucket_info; + ret = store->get_bucket_info(sys_ctx, bucket.tenant, bucket.name, + new_bucket_info, NULL, null_yield, &attrs); + if (ret < 0) { + set_err_msg(err_msg, "failed to fetch bucket info for bucket= " + bucket.name); + return ret; + } + + ret = rgw_bucket_chown(store, user_info, new_bucket_info, marker, attrs); + if (ret < 0) { + set_err_msg(err_msg, "failed to run bucket chown" + cpp_strerror(-ret)); + return ret; + } + } + return 0; +} + +int RGWUser:: execute_rename(RGWUserAdminOpState& op_state, RGWUserInfo& old_user_info, std::string *err_msg) +{ + std::string subprocess_msg; + int ret = 0; + bool defer_user_update = true; + + rgw_user& user_id = op_state.get_user_id(); + + RGWUserInfo user_info; + user_info = old_user_info; + user_info.user_id = user_id; + + // update swift_keys with new user id + auto modify_keys = user_info.swift_keys; + map::iterator it; + + user_info.swift_keys.clear(); + + for (it = modify_keys.begin(); it != modify_keys.end(); it++) { + + RGWAccessKey old_key; + old_key = it->second; + + std::string id; + user_id.to_str(id); + id.append(":"); + id.append(old_key.subuser); + + old_key.id = id; + user_info.swift_keys[id] = old_key; + } + + op_state.set_user_info(user_info); + op_state.set_populated(); + op_state.set_initialized(); + + // update the helper objects + ret = init_members(op_state); + if (ret < 0) { + set_err_msg(err_msg, "unable to initialize user"); + return ret; + } + + // see if we need to add an access key + if (op_state.has_key_op()) { + ret = keys.add(op_state, &subprocess_msg, defer_user_update); + if (ret < 0) { + set_err_msg(err_msg, "unable to create access key, " + subprocess_msg); + return ret; + } + } + + ret = update(op_state, err_msg); + if (ret < 0) + return ret; + + return 0; +} + + int RGWUser::execute_add(RGWUserAdminOpState& op_state, std::string *err_msg) { std::string subprocess_msg; @@ -2063,6 +2276,7 @@ int RGWUser::execute_add(RGWUserAdminOpState& op_state, std::string *err_msg) return 0; } + int RGWUser::add(RGWUserAdminOpState& op_state, std::string *err_msg) { std::string subprocess_msg; @@ -2083,6 +2297,26 @@ int RGWUser::add(RGWUserAdminOpState& op_state, std::string *err_msg) return 0; } +int RGWUser::rename(RGWUserAdminOpState& op_state, std::string *err_msg) +{ + std::string subprocess_msg; + int ret; + + ret = check_op(op_state, &subprocess_msg); + if (ret < 0) { + set_err_msg(err_msg, "unable to parse parameters, " + subprocess_msg); + return ret; + } + + ret = execute_user_rename(op_state, &subprocess_msg); + if (ret < 0) { + set_err_msg(err_msg, "unable to rename user, " + subprocess_msg); + return ret; + } + + return 0; +} + int RGWUser::execute_remove(RGWUserAdminOpState& op_state, std::string *err_msg, optional_yield y) { int ret; diff --git a/src/rgw/rgw_user.h b/src/rgw/rgw_user.h index 84a5f0b8dd29b..02805d9d521d7 100644 --- a/src/rgw/rgw_user.h +++ b/src/rgw/rgw_user.h @@ -159,6 +159,7 @@ struct RGWUserAdminOpState { rgw_user user_id; std::string user_email; std::string display_name; + rgw_user new_user_id; int32_t max_buckets; __u8 suspended; __u8 admin; @@ -257,6 +258,13 @@ struct RGWUserAdminOpState { user_id = id; } + void set_new_user_id(rgw_user& id) { + if (id.empty()) + return; + + new_user_id = id; + } + void set_user_email(std::string& email) { /* always lowercase email address */ boost::algorithm::to_lower(email); @@ -446,6 +454,7 @@ struct RGWUserAdminOpState { std::string get_caps() { return caps; } std::string get_user_email() { return user_email; } std::string get_display_name() { return display_name; } + rgw_user& get_new_uid() { return new_user_id; } map& get_temp_url_keys() { return temp_url_keys; } RGWUserInfo& get_user_info() { return info; } @@ -673,6 +682,8 @@ private: int execute_remove(RGWUserAdminOpState& op_state, std::string *err_msg, optional_yield y); int execute_modify(RGWUserAdminOpState& op_state, std::string *err_msg); + int execute_user_rename(RGWUserAdminOpState& op_state, std::string *err_msg); + int execute_rename(RGWUserAdminOpState& op_state, RGWUserInfo& old_user_info, std::string *err_msg); public: RGWUser(); @@ -693,8 +704,11 @@ public: /* API Contracted Methods */ int add(RGWUserAdminOpState& op_state, std::string *err_msg = NULL); + int remove(RGWUserAdminOpState& op_state, optional_yield y, std::string *err_msg = NULL); + int rename(RGWUserAdminOpState& op_state, std::string *err_msg = NULL); + /* remove an already populated RGWUser */ int remove(std::string *err_msg = NULL); -- 2.39.5