mgr: support optional arguments for module and profile caps

This allows an optional, arbitrary key/value constraint clauses to
be appended to "profile XYZ" and "allow module XYZ" caps. A module
can then provide additional validatation against these meta-arguments.

Example:
profile rbd pool=rbd
allow module rbd_support with pool=rbd

Signed-off-by: Jason Dillaman <dillaman@redhat.com>
This commit is contained in:
Jason Dillaman 2019-10-11 12:44:01 -04:00
parent 3463613bd4
commit cb534e0049
4 changed files with 88 additions and 59 deletions

View File

@ -149,7 +149,7 @@ Capability syntax follows the form::
mgr 'allow {access-spec} [network {network/prefix}]'
mgr 'profile {name} [network {network/prefix}]'
mgr 'profile {name} [{key1} {match-type} {value1} ...] [network {network/prefix}]'
Manager capabilities can also be specified for specific commands,
all commands exported by a built-in manager service, or all commands
@ -159,7 +159,7 @@ Capability syntax follows the form::
mgr 'allow service {service-name} {access-spec} [network {network/prefix}]'
mgr 'allow module {module-name} {access-spec} [network {network/prefix}]'
mgr 'allow module {module-name} [with {key1} {match-type} {value1} ...] {access-spec} [network {network/prefix}]'
The ``{access-spec}`` syntax is as follows: ::

View File

@ -86,18 +86,19 @@ ostream& operator<<(ostream& out, const MgrCapGrant& m) {
out << " module " << maybe_quote_string(m.module);
} else if (!m.command.empty()) {
out << " command " << maybe_quote_string(m.command);
if (!m.command_args.empty()) {
out << " with";
for (auto& [key, constraint] : m.command_args) {
out << " " << maybe_quote_string(key) << constraint;
}
}
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;
@ -115,7 +116,7 @@ BOOST_FUSION_ADAPT_STRUCT(MgrCapGrant,
(std::string, module)
(std::string, profile)
(std::string, command)
(kvmap, command_args)
(kvmap, arguments)
(mgr_rwxa_t, allow)
(std::string, network))
@ -155,15 +156,52 @@ void MgrCapGrant::expand_profile() const {
}
}
bool MgrCapGrant::validate_arguments(
const std::map<std::string, std::string>& 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<std::string, std::string>& c_args) const {
const std::map<std::string, std::string>& args) const {
if (!profile.empty()) {
expand_profile();
mgr_rwxa_t a;
for (auto& grant : profile_grants) {
a = a | grant.get_allowed(cct, name, s, m, c, c_args);
a = a | grant.get_allowed(cct, name, s, m, c, args);
}
return a;
}
@ -179,6 +217,11 @@ mgr_rwxa_t MgrCapGrant::get_allowed(
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;
}
@ -186,38 +229,9 @@ mgr_rwxa_t MgrCapGrant::get_allowed(
if (command != c) {
return mgr_rwxa_t{};
}
for (auto& [key, constraint] : command_args) {
auto q = c_args.find(key);
// argument must be present if a constraint exists
if (q == c_args.end()) {
if (!validate_arguments(args)) {
return mgr_rwxa_t{};
}
switch (constraint.match_type) {
case MgrCapGrantConstraint::MATCH_TYPE_EQUAL:
if (constraint.value != q->second)
return mgr_rwxa_t{};
break;
case MgrCapGrantConstraint::MATCH_TYPE_PREFIX:
if (q->second.find(constraint.value) != 0)
return mgr_rwxa_t{};
break;
case MgrCapGrantConstraint::MATCH_TYPE_REGEX:
try {
std::regex pattern(constraint.value, std::regex::extended);
if (!std::regex_match(q->second, pattern)) {
return mgr_rwxa_t{};
}
} catch(const std::regex_error&) {
return mgr_rwxa_t{};
}
break;
default:
break;
}
}
return mgr_rwxa_t{MGR_CAP_ANY};
}
@ -345,6 +359,10 @@ void MgrCap::generate_test_instances(list<MgrCap*>& ls) {
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
@ -412,7 +430,7 @@ struct MgrCapParser : qi::grammar<Iterator, MgrCap()> {
>> str
>> qi::attr(std::string())
>> qi::attr(std::string())
>> qi::attr(map<std::string, MgrCapGrantConstraint>())
>> -(spaces >> lit("with") >> spaces >> kv_map)
>> spaces >> rwxa
>> -(spaces >> lit("network") >> spaces >> network_str);
@ -423,7 +441,7 @@ struct MgrCapParser : qi::grammar<Iterator, MgrCap()> {
>> qi::attr(std::string())
>> str
>> qi::attr(std::string())
>> qi::attr(std::map<std::string, MgrCapGrantConstraint>())
>> -(spaces >> kv_map)
>> qi::attr(0)
>> -(spaces >> lit("network") >> spaces >> network_str);

View File

@ -63,26 +63,30 @@ struct MgrCapGrant {
* - a service allow ('allow service mds rw')
* - this will match against a specific service and the r/w/x flags.
*
* - a module allow ('allow module rbd_support rw')
* - a module allow ('allow module rbd_support rw, allow module rbd_support with pool=rbd rw')
* - this will match against a specific python add-on module and the r/w/x
* flags.
*
* - a profile ('profile read-only')
* - a profile ('profile read-only, profile rbd pool=rbd')
* - this will match against specific MGR-enforced semantics of what
* this type of user should need to do. examples include 'read-write',
* 'read-only', 'crash'.
*
* - a command ('allow command foo', 'allow command bar with arg1=val1 arg2 prefix val2')
* this includes the command name (the prefix string), and a set
* of key/value pairs that constrain use of that command. if no pairs
* are specified, any arguments are allowed; if a pair is specified, that
* argument must be present and equal or match a prefix.
* this includes the command name (the prefix string)
*
* The command, module, and profile caps can also accept an optional
* key/value map. If not provided, all command arguments and module
* meta-arguments are allowed. If a key/value pair is specified, that
* argument must be present and must match the provided constraint.
*/
typedef std::map<std::string, MgrCapGrantConstraint> Arguments;
std::string service;
std::string module;
std::string profile;
std::string command;
std::map<std::string, MgrCapGrantConstraint> command_args;
Arguments arguments;
// restrict by network
std::string network;
@ -107,13 +111,16 @@ struct MgrCapGrant {
std::string&& module,
std::string&& profile,
std::string&& command,
std::map<std::string, MgrCapGrantConstraint>&& command_args,
Arguments&& arguments,
mgr_rwxa_t allow)
: service(std::move(service)), module(std::move(module)),
profile(std::move(profile)), command(std::move(command)),
command_args(std::move(command_args)), allow(allow) {
arguments(std::move(arguments)), allow(allow) {
}
bool validate_arguments(
const std::map<std::string, std::string>& arguments) const;
/**
* check if given request parameters match our constraints
*
@ -122,7 +129,7 @@ struct MgrCapGrant {
* @param service service (if any)
* @param module module (if any)
* @param command command (if any)
* @param command_args command args (if any)
* @param arguments profile/module/command args (if any)
* @return bits we allow
*/
mgr_rwxa_t get_allowed(
@ -131,7 +138,7 @@ struct MgrCapGrant {
const std::string& service,
const std::string& module,
const std::string& command,
const std::map<std::string, std::string>& command_args) const;
const std::map<std::string, std::string>& arguments) const;
bool is_allow_all() const {
return (allow == MGR_CAP_ANY &&
@ -168,7 +175,7 @@ struct MgrCap {
* @param service service name
* @param module module name
* @param command command id
* @param command_args
* @param arguments
* @param op_may_read whether the operation may need to read
* @param op_may_write whether the operation may need to write
* @param op_may_exec whether the operation may exec
@ -179,7 +186,7 @@ struct MgrCap {
const std::string& service,
const std::string& module,
const std::string& command,
const std::map<std::string, std::string>& command_args,
const std::map<std::string, std::string>& arguments,
bool op_may_read, bool op_may_write, bool op_may_exec,
const entity_addr_t& addr) const;

View File

@ -57,12 +57,14 @@ const char *parse_good[] = {
"allow module=foo x",
"allow module foo_foo r",
"allow module \" foo \" w",
"allow module foo with arg1=value1 x",
"allow command abc with arg=foo arg2=bar, allow service foo r",
"allow command abc.def with arg=foo arg2=bar, allow service foo r",
"allow command \"foo bar\" with arg=\"baz\"",
"allow command \"foo bar\" with arg=\"baz.xx\"",
"allow command \"foo bar\" with arg = \"baz.xx\"",
"profile osd",
"profile rbd pool=ABC namespace=NS",
"profile \"mds-bootstrap\", profile foo",
"allow * network 1.2.3.4/24",
"allow * network ::1/128",
@ -95,6 +97,7 @@ const char *parse_identity[] = {
"profile osd-bootstrap",
"profile mds-bootstrap, allow *",
"profile \"foo bar\", allow *",
"profile rbd namespace=NS pool=ABC",
"allow command abc",
"allow command \"a b c\"",
"allow command abc with arg=foo",
@ -111,6 +114,7 @@ const char *parse_identity[] = {
"allow service \" foo \" w, allow service bar r",
"allow module foo x",
"allow module \" foo_foo \" r",
"allow module foo with arg1=value1 x",
"allow command abc with arg=foo arg2=bar, allow service foo r",
0
};