diff --git a/qa/suites/rados/singleton/all/mon-config-key-caps.yaml b/qa/suites/rados/singleton/all/mon-config-key-caps.yaml new file mode 100644 index 0000000000000..0b0b95c52e080 --- /dev/null +++ b/qa/suites/rados/singleton/all/mon-config-key-caps.yaml @@ -0,0 +1,17 @@ +roles: +- - mon.a + - mgr.x + - osd.0 + - osd.1 + - osd.2 + - client.0 +tasks: +- install: +- ceph: + log-whitelist: + - overall HEALTH_ + - \(AUTH_BAD_CAPS\) +- workunit: + clients: + all: + - mon/test_config_key_caps.sh diff --git a/qa/workunits/mon/test_config_key_caps.sh b/qa/workunits/mon/test_config_key_caps.sh new file mode 100755 index 0000000000000..77b4b53b701d1 --- /dev/null +++ b/qa/workunits/mon/test_config_key_caps.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash + +set -x +set -e + +tmp=$(mktemp -d -p /tmp test_mon_config_key_caps.XXXXX) +entities=() + +function cleanup() +{ + set +e + set +x + if [[ -e $tmp/keyring ]] && [[ -e $tmp/keyring.orig ]]; then + grep '\[.*\..*\]' $tmp/keyring.orig > $tmp/entities.orig + for e in $(grep '\[.*\..*\]' $tmp/keyring | \ + diff $tmp/entities.orig - | \ + sed -n 's/^.*\[\(.*\..*\)\]/\1/p'); + do + ceph auth rm $e 2>&1 >& /dev/null + done + fi + #rm -fr $tmp +} + +trap cleanup 0 # cleanup on exit + +function expect_false() +{ + set -x + if "$@"; then return 1; else return 0; fi +} + +# for cleanup purposes +ceph auth export -o $tmp/keyring.orig + +k=$tmp/keyring + +# setup a few keys +ceph config-key ls +ceph config-key set daemon-private/osd.123/test-foo +ceph config-key set mgr/test-foo +ceph config-key set device/test-foo +ceph config-key set test/foo + +allow_aa=client.allow_aa +allow_bb=client.allow_bb +allow_cc=client.allow_cc + +mgr_a=mgr.a +mgr_b=mgr.b +osd_a=osd.100 +osd_b=osd.200 + +prefix_aa=client.prefix_aa +prefix_bb=client.prefix_bb +prefix_cc=client.prefix_cc +match_aa=client.match_aa +match_bb=client.match_bb + +fail_aa=client.fail_aa +fail_bb=client.fail_bb +fail_cc=client.fail_cc +fail_dd=client.fail_dd +fail_ee=client.fail_ee +fail_ff=client.fail_ff +fail_gg=client.fail_gg +fail_writes=client.fail_writes + +ceph auth get-or-create $allow_aa mon 'allow *' +ceph auth get-or-create $allow_bb mon 'allow service config-key rwx' +ceph auth get-or-create $allow_cc mon 'allow command "config-key get"' + +ceph auth get-or-create $mgr_a mon 'allow profile mgr' +ceph auth get-or-create $mgr_b mon 'allow profile mgr' +ceph auth get-or-create $osd_a mon 'allow profile osd' +ceph auth get-or-create $osd_b mon 'allow profile osd' + +ceph auth get-or-create $prefix_aa mon \ + "allow command \"config-key get\" with key prefix client/$prefix_aa" + +cap="allow command \"config-key set\" with key prefix client/" +cap="$cap,allow command \"config-key get\" with key prefix client/$prefix_bb" +ceph auth get-or-create $prefix_bb mon "$cap" + +cap="allow command \"config-key get\" with key prefix client/" +cap="$cap, allow command \"config-key set\" with key prefix client/" +cap="$cap, allow command \"config-key ls\"" +ceph auth get-or-create $prefix_cc mon "$cap" + +cap="allow command \"config-key get\" with key=client/$match_aa/foo" +ceph auth get-or-create $match_aa mon "$cap" +cap="allow command \"config-key get\" with key=client/$match_bb/foo" +cap="$cap,allow command \"config-key set\" with key=client/$match_bb/foo" +ceph auth get-or-create $match_bb mon "$cap" + +ceph auth get-or-create $fail_aa mon 'allow rx' +ceph auth get-or-create $fail_bb mon 'allow r,allow w' +ceph auth get-or-create $fail_cc mon 'allow rw' +ceph auth get-or-create $fail_dd mon 'allow rwx' +ceph auth get-or-create $fail_ee mon 'allow profile bootstrap-rgw' +ceph auth get-or-create $fail_ff mon 'allow profile bootstrap-rbd' +# write commands will require rw; wx is not enough +ceph auth get-or-create $fail_gg mon 'allow service config-key wx' +# read commands will only require 'r'; 'rx' should be enough. +ceph auth get-or-create $fail_writes mon 'allow service config-key rx' + +# grab keyring +ceph auth export -o $k + +# keys will all the caps can do whatever +for c in $allow_aa $allow_bb $allow_cc $mgr_a $mgr_b; do + ceph -k $k --name $c config-key get daemon-private/osd.123/test-foo + ceph -k $k --name $c config-key get mgr/test-foo + ceph -k $k --name $c config-key get device/test-foo + ceph -k $k --name $c config-key get test/foo +done + +for c in $osd_a $osd_b; do + ceph -k $k --name $c config-key put daemon-private/$c/test-foo + ceph -k $k --name $c config-key get daemon-private/$c/test-foo + expect_false ceph -k $k --name $c config-key ls + expect_false ceph -k $k --name $c config-key get mgr/test-foo + expect_false ceph -k $k --name $c config-key get device/test-foo + expect_false ceph -k $k --name $c config-key get test/foo +done + +expect_false ceph -k $k --name $osd_a get daemon-private/$osd_b/test-foo +expect_false ceph -k $k --name $osd_b get daemon-private/$osd_a/test-foo + +expect_false ceph -k $k --name $prefix_aa \ + config-key ls +expect_false ceph -k $k --name $prefix_aa \ + config-key get daemon-private/osd.123/test-foo +expect_false ceph -k $k --name $prefix_aa \ + config-key set test/bar +expect_false ceph -k $k --name $prefix_aa \ + config-key set client/$prefix_aa/foo + +# write something so we can read, use a custom entity +ceph -k $k --name $allow_bb config-key set client/$prefix_aa/foo +ceph -k $k --name $prefix_aa config-key get client/$prefix_aa/foo +# check one writes to the other's prefix, the other is able to read +ceph -k $k --name $prefix_bb config-key set client/$prefix_aa/bar +ceph -k $k --name $prefix_aa config-key get client/$prefix_aa/bar + +ceph -k $k --name $prefix_bb config-key set client/$prefix_bb/foo +ceph -k $k --name $prefix_bb config-key get client/$prefix_bb/foo + +expect_false ceph -k $k --name $prefix_bb config-key get client/$prefix_aa/bar +expect_false ceph -k $k --name $prefix_bb config-key ls +expect_false ceph -k $k --name $prefix_bb \ + config-key get daemon-private/osd.123/test-foo +expect_false ceph -k $k --name $prefix_bb config-key get mgr/test-foo +expect_false ceph -k $k --name $prefix_bb config-key get device/test-foo +expect_false ceph -k $k --name $prefix_bb config-key get test/bar +expect_false ceph -k $k --name $prefix_bb config-key set test/bar + +ceph -k $k --name $prefix_cc config-key set client/$match_aa/foo +ceph -k $k --name $prefix_cc config-key set client/$match_bb/foo +ceph -k $k --name $prefix_cc config-key get client/$match_aa/foo +ceph -k $k --name $prefix_cc config-key get client/$match_bb/foo +expect_false ceph -k $k --name $prefix_cc config-key set other/prefix +expect_false ceph -k $k --name $prefix_cc config-key get mgr/test-foo +ceph -k $k --name $prefix_cc config-key ls >& /dev/null + +ceph -k $k --name $match_aa config-key get client/$match_aa/foo +expect_false ceph -k $k --name $match_aa config-key get client/$match_bb/foo +expect_false ceph -k $k --name $match_aa config-key set client/$match_aa/foo +ceph -k $k --name $match_bb config-key get client/$match_bb/foo +ceph -k $k --name $match_bb config-key set client/$match_bb/foo +expect_false ceph -k $k --name $match_bb config-key get client/$match_aa/foo +expect_false ceph -k $k --name $match_bb config-key set client/$match_aa/foo + +keys=(daemon-private/osd.123/test-foo + mgr/test-foo + device/test-foo + test/foo + client/$prefix_aa/foo + client/$prefix_bb/foo + client/$match_aa/foo + client/$match_bb/foo +) +# expect these all to fail accessing config-key +for c in $fail_aa $fail_bb $fail_cc \ + $fail_dd $fail_ee $fail_ff \ + $fail_gg; do + for m in get set; do + for key in ${keys[*]} client/$prefix_aa/foo client/$prefix_bb/foo; do + expect_false ceph -k $k --name $c config-key $m $key + done + done +done + +# fail writes but succeed on reads +expect_false ceph -k $k --name $fail_writes config-key set client/$match_aa/foo +expect_false ceph -k $k --name $fail_writes config-key set test/foo +ceph -k $k --name $fail_writes config-key ls +ceph -k $k --name $fail_writes config-key get client/$match_aa/foo +ceph -k $k --name $fail_writes config-key get daemon-private/osd.123/test-foo + +echo "OK" diff --git a/src/common/options.cc.orig b/src/common/options.cc.orig new file mode 100644 index 0000000000000..dc052fe8039c2 --- /dev/null +++ b/src/common/options.cc.orig @@ -0,0 +1,7196 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "acconfig.h" +#include "options.h" +#include "common/Formatter.h" + +// Helpers for validators +#include "include/stringify.h" +#include +#include +#include + +// Definitions for enums +#include "common/perf_counters.h" + +// rbd feature validation +#include "librbd/Features.h" + +namespace { +class printer : public boost::static_visitor<> { + ostream& out; +public: + explicit printer(ostream& os) + : out(os) {} + template + void operator()(const T& v) const { + out << v; + } + void operator()(boost::blank blank) const { + return; + } + void operator()(bool v) const { + out << (v ? "true" : "false"); + } + void operator()(double v) const { + out << std::fixed << v << std::defaultfloat; + } + void operator()(const Option::size_t& v) const { + out << v.value; + } + void operator()(const std::chrono::seconds v) const { + out << v.count(); + } +}; +} + +ostream& operator<<(ostream& os, const Option::value_t& v) { + printer p{os}; + v.apply_visitor(p); + return os; +} + +void Option::dump_value(const char *field_name, + const Option::value_t &v, Formatter *f) const +{ + if (boost::get(&v)) { + // This should be nil but Formatter doesn't allow it. + f->dump_string(field_name, ""); + return; + } + switch (type) { + case TYPE_INT: + f->dump_int(field_name, boost::get(v)); break; + case TYPE_UINT: + f->dump_unsigned(field_name, boost::get(v)); break; + case TYPE_STR: + f->dump_string(field_name, boost::get(v)); break; + case TYPE_FLOAT: + f->dump_float(field_name, boost::get(v)); break; + case TYPE_BOOL: + f->dump_bool(field_name, boost::get(v)); break; + default: + f->dump_stream(field_name) << v; break; + } +} + +int Option::pre_validate(std::string *new_value, std::string *err) const +{ + if (validator) { + return validator(new_value, err); + } else { + return 0; + } +} + +int Option::validate(const Option::value_t &new_value, std::string *err) const +{ + // Generic validation: min + if (!boost::get(&(min))) { + if (new_value < min) { + std::ostringstream oss; + oss << "Value '" << new_value << "' is below minimum " << min; + *err = oss.str(); + return -EINVAL; + } + } + + // Generic validation: max + if (!boost::get(&(max))) { + if (new_value > max) { + std::ostringstream oss; + oss << "Value '" << new_value << "' exceeds maximum " << max; + *err = oss.str(); + return -EINVAL; + } + } + + // Generic validation: enum + if (!enum_allowed.empty() && type == Option::TYPE_STR) { + auto found = std::find(enum_allowed.begin(), enum_allowed.end(), + boost::get(new_value)); + if (found == enum_allowed.end()) { + std::ostringstream oss; + oss << "'" << new_value << "' is not one of the permitted " + "values: " << joinify(enum_allowed.begin(), + enum_allowed.end(), + std::string(", ")); + *err = oss.str(); + return -EINVAL; + } + } + + return 0; +} + +namespace { +template +std::chrono::seconds +do_parse_duration(const char* unit, string val, + size_t start, size_t* new_start) +{ + auto found = val.find(unit, start); + if (found == val.npos) { + *new_start = start; + return Duration{0}; + } + val[found] = '\0'; + string err; + char* s = &val[start]; + auto intervals = strict_strtoll(s, 10, &err); + if (!err.empty()) { + throw invalid_argument(s); + } + auto secs = chrono::duration_cast(Duration{intervals}); + *new_start = found + strlen(unit); + return secs; +} + +std::chrono::seconds parse_duration(const std::string& s) +{ + using namespace std::chrono; + auto secs = 0s; + size_t start = 0; + size_t new_start = 0; + using days_t = duration>; + auto v = s; + v.erase(std::remove_if(begin(v), end(v), + [](char c){ return std::isspace(c);}), end(v)); + if (auto delta = do_parse_duration("days", v, start, &new_start); + delta.count()) { + start = new_start; + secs += delta; + } + if (auto delta = do_parse_duration("hours", v, start, &new_start); + delta.count()) { + start = new_start; + secs += delta; + } + if (auto delta = do_parse_duration("minutes", v, start, &new_start); + delta.count()) { + start = new_start; + secs += delta; + } + if (auto delta = do_parse_duration("seconds", v, start, &new_start); + delta.count()) { + start = new_start; + secs += delta; + } + if (new_start == 0) { + string err; + if (auto delta = std::chrono::seconds{strict_strtoll(s.c_str(), 10, &err)}; + err.empty()) { + secs += delta; + } else { + throw invalid_argument(err); + } + } + return secs; +} +} // anonymous namespace + +int Option::parse_value( + const std::string& raw_val, + value_t *out, + std::string *error_message, + std::string *normalized_value) const +{ + std::string val = raw_val; + + int r = pre_validate(&val, error_message); + if (r != 0) { + return r; + } + + if (type == Option::TYPE_INT) { + int64_t f = strict_si_cast(val.c_str(), error_message); + if (!error_message->empty()) { + return -EINVAL; + } + *out = f; + } else if (type == Option::TYPE_UINT) { + uint64_t f = strict_si_cast(val.c_str(), error_message); + if (!error_message->empty()) { + return -EINVAL; + } + *out = f; + } else if (type == Option::TYPE_STR) { + *out = val; + } else if (type == Option::TYPE_FLOAT) { + double f = strict_strtod(val.c_str(), error_message); + if (!error_message->empty()) { + return -EINVAL; + } else { + *out = f; + } + } else if (type == Option::TYPE_BOOL) { + if (strcasecmp(val.c_str(), "false") == 0) { + *out = false; + } else if (strcasecmp(val.c_str(), "true") == 0) { + *out = true; + } else { + int b = strict_strtol(val.c_str(), 10, error_message); + if (!error_message->empty()) { + return -EINVAL; + } + *out = (bool)!!b; + } + } else if (type == Option::TYPE_ADDR) { + entity_addr_t addr; + if (!addr.parse(val.c_str())){ + return -EINVAL; + } + *out = addr; + } else if (type == Option::TYPE_UUID) { + uuid_d uuid; + if (!uuid.parse(val.c_str())) { + return -EINVAL; + } + *out = uuid; + } else if (type == Option::TYPE_SIZE) { + Option::size_t sz{strict_iecstrtoll(val.c_str(), error_message)}; + if (!error_message->empty()) { + return -EINVAL; + } + *out = sz; + } else if (type == Option::TYPE_SECS) { + try { + *out = parse_duration(val); + } catch (const invalid_argument&) { + return -EINVAL; + } + } else { + ceph_abort(); + } + + r = validate(*out, error_message); + if (r != 0) { + return r; + } + + if (normalized_value) { + *normalized_value = to_str(*out); + } + return 0; +} + +void Option::dump(Formatter *f) const +{ + f->dump_string("name", name); + + f->dump_string("type", type_to_str(type)); + + f->dump_string("level", level_to_str(level)); + + f->dump_string("desc", desc); + f->dump_string("long_desc", long_desc); + + dump_value("default", value, f); + dump_value("daemon_default", daemon_value, f); + + f->open_array_section("tags"); + for (const auto t : tags) { + f->dump_string("tag", t); + } + f->close_section(); + + f->open_array_section("services"); + for (const auto s : services) { + f->dump_string("service", s); + } + f->close_section(); + + f->open_array_section("see_also"); + for (const auto sa : see_also) { + f->dump_string("see_also", sa); + } + f->close_section(); + + if (type == TYPE_STR) { + f->open_array_section("enum_values"); + for (const auto &ea : enum_allowed) { + f->dump_string("enum_value", ea); + } + f->close_section(); + } + + dump_value("min", min, f); + dump_value("max", max, f); + + f->dump_bool("can_update_at_runtime", can_update_at_runtime()); +} + +std::string Option::to_str(const Option::value_t& v) +{ + return stringify(v); +} + +void Option::print(ostream *out) const +{ + *out << name << " - " << desc << "\n"; + *out << " (" << type_to_str(type) << ", " << level_to_str(level) << ")\n"; + if (!boost::get(&daemon_value)) { + *out << " Default (non-daemon): " << stringify(value) << "\n"; + *out << " Default (daemon): " << stringify(daemon_value) << "\n"; + } else { + *out << " Default: " << stringify(value) << "\n"; + } + if (!enum_allowed.empty()) { + *out << " Possible values: "; + for (auto& i : enum_allowed) { + *out << " " << stringify(i); + } + *out << "\n"; + } + if (!boost::get(&min)) { + *out << " Minimum: " << stringify(min) << "\n" + << " Maximum: " << stringify(max) << "\n"; + } + *out << " Can update at runtime: " + << (can_update_at_runtime() ? "true" : "false") << "\n"; + if (!services.empty()) { + *out << " Services: " << services << "\n"; + } + if (!tags.empty()) { + *out << " Tags: " << tags << "\n"; + } + if (!see_also.empty()) { + *out << " See also: " << see_also << "\n"; + } + + if (long_desc.size()) { + *out << "\n" << long_desc << "\n"; + } +} + +constexpr unsigned long long operator"" _min (unsigned long long min) { + return min * 60; +} +constexpr unsigned long long operator"" _hr (unsigned long long hr) { + return hr * 60 * 60; +} +constexpr unsigned long long operator"" _day (unsigned long long day) { + return day * 60 * 60 * 24; +} +constexpr unsigned long long operator"" _K (unsigned long long n) { + return n << 10; +} +constexpr unsigned long long operator"" _M (unsigned long long n) { + return n << 20; +} +constexpr unsigned long long operator"" _G (unsigned long long n) { + return n << 30; +} + +std::vector