From c3a6f7588a357d3b6cffcd162b7d7b10187a2b51 Mon Sep 17 00:00:00 2001 From: Yehuda Sadeh Date: Thu, 9 Nov 2017 18:31:26 -0500 Subject: [PATCH] rgw: mfa - initial work Signed-off-by: Yehuda Sadeh --- CMakeLists.txt | 1 + cmake/modules/Findliboath.cmake | 22 ++++++ src/rgw/rgw_common.h | 15 +++- src/rgw/rgw_json_enc.cc | 3 +- src/rgw/rgw_op.h | 2 + src/rgw/rgw_rest_s3.cc | 118 +++++++++++++++++++++++--------- 6 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 cmake/modules/Findliboath.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 34c5796a47dc9..47309814a76bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -395,6 +395,7 @@ if(WITH_RADOSGW) message(WARNING "disabling WITH_RADOSGW_BEAST_FRONTEND, which depends on WITH_BOOST_CONTEXT") set(WITH_RADOSGW_BEAST_FRONTEND OFF) endif() + find_package(liboath 2.4 REQUIRED) # https://curl.haxx.se/docs/install.html mentions the # configure flags for various ssl backends diff --git a/cmake/modules/Findliboath.cmake b/cmake/modules/Findliboath.cmake new file mode 100644 index 0000000000000..ae5446a8ee9f9 --- /dev/null +++ b/cmake/modules/Findliboath.cmake @@ -0,0 +1,22 @@ +# CMake module to search for liboath headers +# +# If it's found it sets LIBOATH_FOUND to TRUE +# and following variables are set: +# LIBOATH_INCLUDE_DIR +# LIBOATH_LIBRARY +find_path(LIBOATH_INCLUDE_DIR + oath.h + PATHS + /usr/include + /usr/local/include + /usr/include/liboath) +find_library(LIBOATH_LIBRARY NAMES oath liboath PATHS + /usr/local/lib + /usr/lib) + +# handle the QUIETLY and REQUIRED arguments and set UUID_FOUND to TRUE if +# all listed variables are TRUE +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(oath DEFAULT_MSG LIBOATH_LIBRARY LIBOATH_INCLUDE_DIR) + +mark_as_advanced(LIBOATH_LIBRARY LIBOATH_INCLUDE_DIR) diff --git a/src/rgw/rgw_common.h b/src/rgw/rgw_common.h index fdba4ef14a6e3..b1adef0ccb4cb 100644 --- a/src/rgw/rgw_common.h +++ b/src/rgw/rgw_common.h @@ -634,6 +634,7 @@ struct RGWUserInfo map temp_url_keys; RGWQuotaInfo user_quota; uint32_t type; + map mfa_devices; RGWUserInfo() : auid(0), @@ -653,7 +654,7 @@ struct RGWUserInfo } void encode(bufferlist& bl) const { - ENCODE_START(19, 9, bl); + ENCODE_START(20, 9, bl); encode(auid, bl); string access_key; string secret_key; @@ -694,10 +695,11 @@ struct RGWUserInfo encode(user_id.tenant, bl); encode(admin, bl); encode(type, bl); + encode(mfa_devices, bl); ENCODE_FINISH(bl); } void decode(bufferlist::iterator& bl) { - DECODE_START_LEGACY_COMPAT_LEN_32(19, 9, 9, bl); + DECODE_START_LEGACY_COMPAT_LEN_32(20, 9, 9, bl); if (struct_v >= 2) decode(auid, bl); else auid = CEPH_AUTH_UID_DEFAULT; string access_key; @@ -770,6 +772,9 @@ struct RGWUserInfo if (struct_v >= 19) { decode(type, bl); } + if (struct_v >= 20) { + ::decode(mfa_devices, bl); + } DECODE_FINISH(bl); } void dump(Formatter *f) const; @@ -1178,6 +1183,7 @@ enum RGWBucketFlags { BUCKET_VERSIONED = 0x2, BUCKET_VERSIONS_SUSPENDED = 0x4, BUCKET_DATASYNC_DISABLED = 0X8, + BUCKET_MFA_ENABLED = 0X10, }; enum RGWBucketIndexType { @@ -1345,8 +1351,9 @@ struct RGWBucketInfo void decode_json(JSONObj *obj); bool versioned() const { return (flags & BUCKET_VERSIONED) != 0; } - int versioning_status() { return flags & (BUCKET_VERSIONED | BUCKET_VERSIONS_SUSPENDED); } + int versioning_status() { return flags & (BUCKET_VERSIONED | BUCKET_VERSIONS_SUSPENDED | BUCKET_MFA_ENABLED); } bool versioning_enabled() { return versioning_status() == BUCKET_VERSIONED; } + bool mfa_enabled() { return versioning_status() == BUCKET_MFA_ENABLED; } bool datasync_flag_enabled() const { return (flags & BUCKET_DATASYNC_DISABLED) == 0; } bool has_swift_versioning() const { @@ -1881,6 +1888,8 @@ struct req_state { string req_id; string trans_id; + bool mfa_verified; + req_state(CephContext* _cct, RGWEnv* e, RGWUserInfo* u); ~req_state(); diff --git a/src/rgw/rgw_json_enc.cc b/src/rgw/rgw_json_enc.cc index 60a1212d739b1..36bbb3531d32c 100644 --- a/src/rgw/rgw_json_enc.cc +++ b/src/rgw/rgw_json_enc.cc @@ -474,6 +474,7 @@ void RGWUserInfo::dump(Formatter *f) const break; } encode_json("type", user_source_type, f); + encode_json("mfa_devices", mfa_devices, f); } @@ -543,6 +544,7 @@ void RGWUserInfo::decode_json(JSONObj *obj) } else if (user_source_type == "none") { type = TYPE_NONE; } + JSONDecoder::decode_json("mfa_devices", mfa_devices, obj); } void RGWQuotaInfo::dump(Formatter *f) const @@ -781,7 +783,6 @@ void RGWBucketInfo::decode_json(JSONObj *obj) { int rs; JSONDecoder::decode_json("reshard_status", rs, obj); reshard_status = (cls_rgw_reshard_status)rs; - JSONDecoder::decode_json("new_bucket_instance_id",new_bucket_instance_id, obj); } void rgw_obj_key::dump(Formatter *f) const diff --git a/src/rgw/rgw_op.h b/src/rgw/rgw_op.h index b4ee8e499592f..bbf03f058f53d 100644 --- a/src/rgw/rgw_op.h +++ b/src/rgw/rgw_op.h @@ -766,6 +766,7 @@ public: }; enum BucketVersionStatus { + VersioningStatusInvalid = -1, VersioningNotChanged = 0, VersioningEnabled = 1, VersioningSuspended =2, @@ -774,6 +775,7 @@ enum BucketVersionStatus { class RGWSetBucketVersioning : public RGWOp { protected: int versioning_status; + bool mfa_status{false}; bufferlist in_data; public: RGWSetBucketVersioning() : versioning_status(VersioningNotChanged) {} diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc index e2054a7412204..0d256e752f91e 100644 --- a/src/rgw/rgw_rest_s3.cc +++ b/src/rgw/rgw_rest_s3.cc @@ -14,6 +14,8 @@ #include #include +#include + #include "rgw_rest.h" #include "rgw_rest_s3.h" #include "rgw_rest_s3website.h" @@ -879,37 +881,33 @@ void RGWGetBucketVersioning_ObjStore_S3::send_response() rgw_flush_formatter_and_reset(s, s->formatter); } -class RGWSetBucketVersioningParser : public RGWXMLParser -{ - XMLObj *alloc_obj(const char *el) override { - return new XMLObj; - } - -public: - RGWSetBucketVersioningParser() {} - ~RGWSetBucketVersioningParser() override {} - - int get_versioning_status(int *status) { - XMLObj *config = find_first("VersioningConfiguration"); - if (!config) - return -EINVAL; - - *status = VersioningNotChanged; - - XMLObj *field = config->find_first("Status"); - if (!field) - return 0; +struct ver_config_status { + int status{VersioningSuspended}; + int mfa_status{0}; + + void decode_xml(XMLObj *obj) { +dout(0) << __FILE__ << ":" << __LINE__ << dendl; + string status_str; + string mfa_str; + RGWXMLDecoder::decode_xml("Status", status_str, obj); +dout(0) << __FILE__ << ":" << __LINE__ << dendl; + if (status_str == "Enabled") { + status = VersioningEnabled; + } else if (status_str != "Suspended") { + status = VersioningStatusInvalid; + } - *status = VersioningSuspended; - string& s = field->get_data(); +dout(0) << __FILE__ << ":" << __LINE__ << " status_str=" << status_str << dendl; - if (stringcasecmp(s, "Enabled") == 0) { - *status = VersioningEnabled; - } else if (stringcasecmp(s, "Suspended") != 0) { - return -EINVAL; + RGWXMLDecoder::decode_xml("MfaDelete", mfa_str, obj); +dout(0) << __FILE__ << ":" << __LINE__ << " mfa_str=" << mfa_str << dendl; + if (mfa_str == "Enabled") { + mfa_status = 1; + } else if (mfa_str == "Disabled") { + mfa_status = 0; + } else { + mfa_status = -EINVAL; } - - return 0; } }; @@ -930,12 +928,10 @@ int RGWSetBucketVersioning_ObjStore_S3::get_params() return r; } - RGWSetBucketVersioningParser parser; - + RGWXMLDecoder::XMLParser parser; if (!parser.init()) { ldout(s->cct, 0) << "ERROR: failed to initialize parser" << dendl; - r = -EIO; - return r; + return -EIO; } if (!parser.parse(data, len, 1)) { @@ -944,13 +940,25 @@ int RGWSetBucketVersioning_ObjStore_S3::get_params() return r; } + ver_config_status status_conf; + + RGWXMLDecoder::decode_xml("VersioningConfiguration", status_conf, &parser); + if (!store->is_meta_master()) { /* only need to keep this data around if we're not meta master */ in_data.append(data, len); } - r = parser.get_versioning_status(&versioning_status); - + versioning_status = status_conf.status; + if (versioning_status == VersioningStatusInvalid) { + r = -EINVAL; + } + + if (status_conf.mfa_status >= 0) { + mfa_status = (bool)status_conf.mfa_status; + } else { + r = -EINVAL; + } return r; } @@ -3216,6 +3224,42 @@ int RGWHandler_REST_S3::init_from_header(struct req_state* s, return 0; } +static int verify_mfa(RGWRados *store, RGWUserInfo *user, const string& mfa_str, bool *verified) +{ + vector params; + get_str_vec(mfa_str, " ", params); + + if (params.size() != 2) { + ldout(store->ctx(), 5) << "NOTICE: invalid mfa string provided: " << mfa_str << dendl; + return -EINVAL; + } + + string& serial = params[0]; + string& otp = params[1]; + + auto i = user->mfa_devices.find(serial); + if (i == user->mfa_devices.end()) { + ldout(store->ctx(), 5) << "NOTICE: user does not have mfa device with serial=" << serial << dendl; + return -EACCES; + } + + string& seed = i->second; + + utime_t now = ceph_clock_now(); + + int result = oath_totp_validate3(seed.c_str(), seed.size(), now.sec(), + 30 /* step size */, 0, 2 /* window */, + NULL, NULL, otp.c_str()); + if (result == OATH_INVALID_OTP) { + ldout(cct, 5) << "NOTICE: totp token failed to validate" << dendl; + return -EACCES; + } + + *verified = true; + + return 0; +} + int RGWHandler_REST_S3::postauth_init() { struct req_init_state *t = &s->init_state; @@ -3250,6 +3294,12 @@ int RGWHandler_REST_S3::postauth_init() if (ret) return ret; } + + const char *mfa = s->info.env->get("HTTP_X_AMZ_MFA"); + if (mfa) { + ret = verify_mfa(store, s->user, string(mfa), &s->mfa_verified); + } + return 0; } -- 2.39.5