// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab /* * Ceph - scalable distributed file system * * Copyright (C) 2013 Inktank * * 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 #include #include #include #include #include #include #include #include "MgrCap.h" #include "include/stringify.h" #include "include/ipaddr.h" #include "common/debug.h" #include "common/Formatter.h" #include #include #include "include/ceph_assert.h" static inline bool is_not_alnum_space(char c) { return !(isalpha(c) || isdigit(c) || (c == '-') || (c == '_')); } static std::string maybe_quote_string(const std::string& str) { if (find_if(str.begin(), str.end(), is_not_alnum_space) == str.end()) return str; return std::string("\"") + str + std::string("\""); } #define dout_subsys ceph_subsys_mgr std::ostream& operator<<(std::ostream& out, const mgr_rwxa_t& p) { if (p == MGR_CAP_ANY) return out << "*"; if (p & MGR_CAP_R) out << "r"; if (p & MGR_CAP_W) out << "w"; if (p & MGR_CAP_X) out << "x"; return out; } std::ostream& operator<<(std::ostream& out, const MgrCapGrantConstraint& c) { switch (c.match_type) { case MgrCapGrantConstraint::MATCH_TYPE_EQUAL: out << "="; break; case MgrCapGrantConstraint::MATCH_TYPE_PREFIX: out << " prefix "; break; case MgrCapGrantConstraint::MATCH_TYPE_REGEX: out << " regex "; break; default: break; } out << maybe_quote_string(c.value); return out; } std::ostream& operator<<(std::ostream& out, const MgrCapGrant& m) { if (!m.profile.empty()) { out << "profile " << maybe_quote_string(m.profile); } else { out << "allow"; if (!m.service.empty()) { out << " service " << maybe_quote_string(m.service); } else if (!m.module.empty()) { out << " module " << maybe_quote_string(m.module); } else if (!m.command.empty()) { out << " command " << maybe_quote_string(m.command); } } if (!m.arguments.empty()) { out << (!m.profile.empty() ? "" : " with"); for (auto& [key, constraint] : m.arguments) { out << " " << maybe_quote_string(key) << constraint; } } if (m.allow != 0) { out << " " << m.allow; } if (m.network.size()) { out << " network " << m.network; } return out; } // // fusion lets us easily populate structs via the qi parser. typedef std::map kvmap; BOOST_FUSION_ADAPT_STRUCT(MgrCapGrant, (std::string, service) (std::string, module) (std::string, profile) (std::string, command) (kvmap, arguments) (mgr_rwxa_t, allow) (std::string, network)) BOOST_FUSION_ADAPT_STRUCT(MgrCapGrantConstraint, (MgrCapGrantConstraint::MatchType, match_type) (std::string, value)) // void MgrCapGrant::parse_network() { network_valid = ::parse_network(network.c_str(), &network_parsed, &network_prefix); } void MgrCapGrant::expand_profile(std::ostream *err) const { // only generate this list once if (!profile_grants.empty()) { return; } if (profile == "read-only") { // grants READ-ONLY caps MGR-wide profile_grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_R}}); return; } if (profile == "read-write") { // grants READ-WRITE caps MGR-wide profile_grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W}}); return; } if (profile == "crash") { profile_grants.push_back({{}, {}, {}, "crash post", {}, {}}); return; } if (profile == "osd") { // this is a documented profile (so we need to accept it as valid), but it // currently doesn't do anything return; } if (profile == "mds") { // this is a documented profile (so we need to accept it as valid), but it // currently doesn't do anything return; } if (profile == "rbd" || profile == "rbd-read-only") { Arguments filtered_arguments; for (auto& [key, constraint] : arguments) { if (key == "pool" || key == "namespace") { filtered_arguments[key] = std::move(constraint); } else { if (err != nullptr) { *err << "profile '" << profile << "' does not recognize key '" << key << "'"; } return; } } mgr_rwxa_t perms = mgr_rwxa_t{MGR_CAP_R}; if (profile == "rbd") { perms = mgr_rwxa_t{MGR_CAP_R | MGR_CAP_W}; } // allow all 'rbd_support' commands (restricted by optional // pool/namespace constraints) profile_grants.push_back({{}, "rbd_support", {}, {}, std::move(filtered_arguments), perms}); return; } if (err != nullptr) { *err << "unrecognized profile '" << profile << "'"; } } bool MgrCapGrant::validate_arguments( const std::map& args) const { for (auto& [key, constraint] : arguments) { auto q = args.find(key); // argument must be present if a constraint exists if (q == args.end()) { return false; } switch (constraint.match_type) { case MgrCapGrantConstraint::MATCH_TYPE_EQUAL: if (constraint.value != q->second) return false; break; case MgrCapGrantConstraint::MATCH_TYPE_PREFIX: if (q->second.find(constraint.value) != 0) return false; break; case MgrCapGrantConstraint::MATCH_TYPE_REGEX: try { std::regex pattern(constraint.value, std::regex::extended); if (!std::regex_match(q->second, pattern)) { return false; } } catch(const std::regex_error&) { return false; } break; default: return false; } } return true; } mgr_rwxa_t MgrCapGrant::get_allowed( CephContext *cct, EntityName name, const std::string& s, const std::string& m, const std::string& c, const std::map& args) const { if (!profile.empty()) { expand_profile(nullptr); mgr_rwxa_t a; for (auto& grant : profile_grants) { a = a | grant.get_allowed(cct, name, s, m, c, args); } return a; } if (!service.empty()) { if (service != s) { return mgr_rwxa_t{}; } return allow; } if (!module.empty()) { if (module != m) { return mgr_rwxa_t{}; } // don't test module arguments when validating a specific command if (c.empty() && !validate_arguments(args)) { return mgr_rwxa_t{}; } return allow; } if (!command.empty()) { if (command != c) { return mgr_rwxa_t{}; } if (!validate_arguments(args)) { return mgr_rwxa_t{}; } return mgr_rwxa_t{MGR_CAP_ANY}; } return allow; } std::ostream& operator<<(std::ostream&out, const MgrCap& m) { bool first = true; for (auto& grant : m.grants) { if (!first) { out << ", "; } first = false; out << grant; } return out; } bool MgrCap::is_allow_all() const { for (auto& grant : grants) { if (grant.is_allow_all()) { return true; } } return false; } void MgrCap::set_allow_all() { grants.clear(); grants.push_back({{}, {}, {}, {}, {}, mgr_rwxa_t{MGR_CAP_ANY}}); text = "allow *"; } bool MgrCap::is_capable( CephContext *cct, EntityName name, const std::string& service, const std::string& module, const std::string& command, const std::map& command_args, bool op_may_read, bool op_may_write, bool op_may_exec, const entity_addr_t& addr) const { if (cct) { ldout(cct, 20) << "is_capable service=" << service << " " << "module=" << module << " " << "command=" << command << (op_may_read ? " read":"") << (op_may_write ? " write":"") << (op_may_exec ? " exec":"") << " addr " << addr << " on cap " << *this << dendl; } mgr_rwxa_t allow; for (auto& grant : grants) { if (cct) ldout(cct, 20) << " allow so far " << allow << ", doing grant " << grant << dendl; if (grant.network.size() && (!grant.network_valid || !network_contains(grant.network_parsed, grant.network_prefix, addr))) { continue; } if (grant.is_allow_all()) { if (cct) { ldout(cct, 20) << " allow all" << dendl; } return true; } // check enumerated caps allow = allow | grant.get_allowed(cct, name, service, module, command, command_args); if ((!op_may_read || (allow & MGR_CAP_R)) && (!op_may_write || (allow & MGR_CAP_W)) && (!op_may_exec || (allow & MGR_CAP_X))) { if (cct) { ldout(cct, 20) << " match" << dendl; } return true; } } return false; } void MgrCap::encode(ceph::buffer::list& bl) const { // remain backwards compatible w/ MgrCap ENCODE_START(4, 4, bl); encode(text, bl); ENCODE_FINISH(bl); } void MgrCap::decode(ceph::buffer::list::const_iterator& bl) { // remain backwards compatible w/ MgrCap std::string s; DECODE_START(4, bl); decode(s, bl); DECODE_FINISH(bl); parse(s, NULL); } void MgrCap::dump(ceph::Formatter *f) const { f->dump_string("text", text); } void MgrCap::generate_test_instances(std::list& ls) { ls.push_back(new MgrCap); ls.push_back(new MgrCap); ls.back()->parse("allow *"); ls.push_back(new MgrCap); ls.back()->parse("allow rwx"); ls.push_back(new MgrCap); ls.back()->parse("allow service foo x"); ls.push_back(new MgrCap); ls.back()->parse("allow command bar x"); ls.push_back(new MgrCap); ls.back()->parse("allow service foo r, allow command bar x"); ls.push_back(new MgrCap); ls.back()->parse("allow command bar with k1=v1 x"); ls.push_back(new MgrCap); ls.back()->parse("allow command bar with k1=v1 k2=v2 x"); ls.push_back(new MgrCap); ls.back()->parse("allow module bar with k1=v1 k2=v2 x"); ls.push_back(new MgrCap); ls.back()->parse("profile rbd pool=rbd"); } // grammar namespace qi = boost::spirit::qi; namespace ascii = boost::spirit::ascii; namespace phoenix = boost::phoenix; template struct MgrCapParser : qi::grammar { MgrCapParser() : MgrCapParser::base_type(mgrcap) { using qi::char_; using qi::int_; using qi::ulong_long; using qi::lexeme; using qi::alnum; using qi::_val; using qi::_1; using qi::_2; using qi::_3; using qi::eps; using qi::lit; quoted_string %= lexeme['"' >> +(char_ - '"') >> '"'] | lexeme['\'' >> +(char_ - '\'') >> '\'']; unquoted_word %= +char_("a-zA-Z0-9_./-"); str %= quoted_string | unquoted_word; network_str %= +char_("/.:a-fA-F0-9]["); spaces = +(lit(' ') | lit('\n') | lit('\t')); // key <=|prefix|regex> value[ ...] str_match = -spaces >> lit('=') >> -spaces >> qi::attr(MgrCapGrantConstraint::MATCH_TYPE_EQUAL) >> str; str_prefix = spaces >> lit("prefix") >> spaces >> qi::attr(MgrCapGrantConstraint::MATCH_TYPE_PREFIX) >> str; str_regex = spaces >> lit("regex") >> spaces >> qi::attr(MgrCapGrantConstraint::MATCH_TYPE_REGEX) >> str; kv_pair = str >> (str_match | str_prefix | str_regex); kv_map %= kv_pair >> *(spaces >> kv_pair); // command := command[=]cmd [k1=v1 k2=v2 ...] command_match = -spaces >> lit("allow") >> spaces >> lit("command") >> (lit('=') | spaces) >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::string()) >> str >> -(spaces >> lit("with") >> spaces >> kv_map) >> qi::attr(0) >> -(spaces >> lit("network") >> spaces >> network_str); // service foo rwxa service_match %= -spaces >> lit("allow") >> spaces >> lit("service") >> (lit('=') | spaces) >> str >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::map()) >> spaces >> rwxa >> -(spaces >> lit("network") >> spaces >> network_str); // module foo rwxa module_match %= -spaces >> lit("allow") >> spaces >> lit("module") >> (lit('=') | spaces) >> qi::attr(std::string()) >> str >> qi::attr(std::string()) >> qi::attr(std::string()) >> -(spaces >> lit("with") >> spaces >> kv_map) >> spaces >> rwxa >> -(spaces >> lit("network") >> spaces >> network_str); // profile foo profile_match %= -spaces >> -(lit("allow") >> spaces) >> lit("profile") >> (lit('=') | spaces) >> qi::attr(std::string()) >> qi::attr(std::string()) >> str >> qi::attr(std::string()) >> -(spaces >> kv_map) >> qi::attr(0) >> -(spaces >> lit("network") >> spaces >> network_str); // rwxa rwxa_match %= -spaces >> lit("allow") >> spaces >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::string()) >> qi::attr(std::map()) >> rwxa >> -(spaces >> lit("network") >> spaces >> network_str); // rwxa := * | [r][w][x] rwxa = (lit("*")[_val = MGR_CAP_ANY]) | (lit("all")[_val = MGR_CAP_ANY]) | ( eps[_val = 0] >> ( lit('r')[_val |= MGR_CAP_R] || lit('w')[_val |= MGR_CAP_W] || lit('x')[_val |= MGR_CAP_X] ) ); // grant := allow ... grant = -spaces >> (rwxa_match | profile_match | service_match | module_match | command_match) >> -spaces; // mgrcap := grant [grant ...] grants %= (grant % (*lit(' ') >> (lit(';') | lit(',')) >> *lit(' '))); mgrcap = grants [_val = phoenix::construct(_1)]; } qi::rule spaces; qi::rule rwxa; qi::rule quoted_string; qi::rule unquoted_word; qi::rule str, network_str; qi::rule str_match, str_prefix, str_regex; qi::rule()> kv_pair; qi::rule()> kv_map; qi::rule rwxa_match; qi::rule command_match; qi::rule service_match; qi::rule module_match; qi::rule profile_match; qi::rule grant; qi::rule()> grants; qi::rule mgrcap; }; bool MgrCap::parse(const std::string& str, std::ostream *err) { auto iter = str.begin(); auto end = str.end(); MgrCapParser exp; bool r = qi::parse(iter, end, exp, *this); if (r && iter == end) { text = str; std::stringstream profile_err; for (auto& g : grants) { g.parse_network(); if (!g.profile.empty()) { g.expand_profile(&profile_err); } } if (!profile_err.str().empty()) { if (err != nullptr) { *err << "mgr capability parse failed during profile evaluation: " << profile_err.str(); } return false; } return true; } // Make sure no grants are kept after parsing failed! grants.clear(); if (err) { if (iter != end) *err << "mgr capability parse failed, stopped at '" << std::string(iter, end) << "' of '" << str << "'"; else *err << "mgr capability parse failed, stopped at end of '" << str << "'"; } return false; }