From 5a176d3f838899d65a3a931b78b38d82a7629055 Mon Sep 17 00:00:00 2001 From: "Adam C. Emerson" Date: Wed, 4 Mar 2026 17:43:39 -0500 Subject: [PATCH] rgw/policy: Add rgw-policy-test command line tool Annotated evaluation of a policy. Signed-off-by: Adam C. Emerson --- ceph.spec.in | 2 + debian/ceph-common.install | 2 + doc/man/8/CMakeLists.txt | 1 + doc/man/8/rgw-policy-test.rst | 65 +++++++++++++ doc/man_index.rst | 1 + src/rgw/CMakeLists.txt | 6 ++ src/rgw/rgw_iam_policy.cc | 175 +++++++++++++++++++++------------- src/rgw/rgw_iam_policy.h | 5 + src/rgw/rgw_poltester.cc | 167 ++++++++++++++++++++++++++++++++ 9 files changed, 358 insertions(+), 66 deletions(-) create mode 100644 doc/man/8/rgw-policy-test.rst create mode 100644 src/rgw/rgw_poltester.cc diff --git a/ceph.spec.in b/ceph.spec.in index e7032069af6..761e90200b9 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -2620,8 +2620,10 @@ fi %{_bindir}/radosgw-es %{_bindir}/radosgw-object-expirer %{_bindir}/rgw-policy-check +%{_bindir}/rgw-policy-test %{_mandir}/man8/radosgw.8* %{_mandir}/man8/rgw-policy-check.8* +%{_mandir}/man8/rgw-policy-test.8* %dir %{_localstatedir}/lib/ceph/radosgw %{_unitdir}/ceph-radosgw@.service %{_unitdir}/ceph-radosgw.target diff --git a/debian/ceph-common.install b/debian/ceph-common.install index cc5632e805f..86821692b33 100755 --- a/debian/ceph-common.install +++ b/debian/ceph-common.install @@ -23,6 +23,7 @@ usr/bin/rgw-gap-list usr/bin/rgw-gap-list-comparator usr/bin/rgw-orphan-list usr/bin/rgw-policy-check +usr/bin/rgw-policy-test usr/bin/rgw-restore-bucket-index usr/bin/rbd usr/bin/rbdmap @@ -44,6 +45,7 @@ usr/share/man/man8/mount.ceph.8 usr/share/man/man8/rados.8 usr/share/man/man8/radosgw-admin.8 usr/share/man/man8/rgw-policy-check.8 +usr/share/man/man8/rgw-policy-test.8 usr/share/man/man8/rgw-gap-list.8 usr/share/man/man8/rbd.8 usr/share/man/man8/rbdmap.8 diff --git a/doc/man/8/CMakeLists.txt b/doc/man/8/CMakeLists.txt index 772b822beb0..6d63e89f4d3 100644 --- a/doc/man/8/CMakeLists.txt +++ b/doc/man/8/CMakeLists.txt @@ -61,6 +61,7 @@ if(WITH_RADOSGW) rgw-orphan-list.rst rgw-gap-list.rst rgw-policy-check.rst + rgw-policy-test.rst ceph-diff-sorted.rst rgw-restore-bucket-index.rst) endif() diff --git a/doc/man/8/rgw-policy-test.rst b/doc/man/8/rgw-policy-test.rst new file mode 100644 index 00000000000..4f35c8a1700 --- /dev/null +++ b/doc/man/8/rgw-policy-test.rst @@ -0,0 +1,65 @@ +:orphan: + +=================================================== +rgw-policy-test -- test evaluation of bucket policy +=================================================== + +.. program:: rgw-policy-test + +Synopsis +======== + +| **rgw-policy-test** + [-e *key*=*value*]... [-t *tenant*] [-R resource] [-I identity] *action* *filename* + + +Description +=========== + +This utility reads a policy from a file or stdin and evaluates it +against the supplied identity, resource, action, and request context +(the key/value pairs evaluated by conditions.) The utility will print +a trace of evaluating the policy and the effect of its evaluation. + +On error it will print a message and return non-zero. + +Options +======= + +.. option:: -t *tenant*, --tenant *tenant* + + Specify *tenant* as the tenant. If not provided, the default + (empty) tenant is used. + +.. option:: -e *key*=*value*, --environment *key*=*value* + + Add (*key*, *value*) to the environment for evaluation. May be + specified multiple times. + +.. option:: -I *SCHEMA:STRING*, --identity *SCHEMA:STRING* + + Specify *identity* as the identity whose access is to be checked. + +.. option:: -R *ARN*, --resource *ARN* + + Specify *ARN* as the resource for which to check access. + +.. option:: *action* + + Use *action* as the action to check. + +.. option:: *filename* + + Read the policy from *filename*, or - for standard input. + +Availability +============ + +**rgw-policy-test** is part of Ceph, a massively scalable, open-source, +distributed storage system. Please refer to the Ceph documentation at +https://docs.ceph.com/ for more information. + +See also +======== + +:doc:`radosgw `\(8) diff --git a/doc/man_index.rst b/doc/man_index.rst index ca1353f911e..442e79da74f 100644 --- a/doc/man_index.rst +++ b/doc/man_index.rst @@ -48,4 +48,5 @@ man/8/ceph-immutable-object-cache man/8/ceph-diff-sorted man/8/rgw-policy-check + man/8/rgw-policy-test man/8/rgw-restore-bucket-index diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index 875c5b21397..c80a7a457c0 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -668,6 +668,12 @@ add_executable(rgw-policy-check ${radosgw_polparser_srcs}) target_link_libraries(rgw-policy-check ${rgw_libs}) install(TARGETS rgw-policy-check DESTINATION bin) +set(radosgw_poltester_srcs + rgw_poltester.cc) +add_executable(rgw-policy-test ${radosgw_poltester_srcs}) +target_link_libraries(rgw-policy-test ${rgw_libs}) +install(TARGETS rgw-policy-test DESTINATION bin) + set(librgw_srcs librgw.cc) add_library(rgw SHARED ${librgw_srcs}) diff --git a/src/rgw/rgw_iam_policy.cc b/src/rgw/rgw_iam_policy.cc index ac513b85d69..18b3404249f 100644 --- a/src/rgw/rgw_iam_policy.cc +++ b/src/rgw/rgw_iam_policy.cc @@ -10,6 +10,7 @@ #include #include + #include #include @@ -344,6 +345,79 @@ const Keyword top[1]{{"", TokenKind::pseudo, TokenID::Top, 0, false, const Keyword cond_key[1]{{"", TokenKind::cond_key, TokenID::CondKey, 0, true, false}}; +namespace { +boost::optional +parse_principal_(const struct Keyword* w, std::string&& s, + string* errmsg) { + if ((w->id == TokenID::AWS) && (s == "*")) { + // Wildcard! + return Principal::wildcard(); + } else if (w->id == TokenID::CanonicalUser) { + // Do nothing for now. + if (errmsg) + *errmsg = "RGW does not support canonical users."; + return boost::none; + } else if (w->id == TokenID::AWS || w->id == TokenID::Federated) { + // AWS and Federated ARNs + if (auto a = ARN::parse(s)) { + if (a->resource == "root") { + return Principal::account(std::move(a->account)); + } + + static const char rx_str[] = "([^/]*)/(.*)"; + static const regex rx(rx_str, sizeof(rx_str) - 1, + std::regex_constants::ECMAScript | + std::regex_constants::optimize); + smatch match; + if (regex_match(a->resource, match, rx) && match.size() == 3) { + if (match[1] == "user") { + return Principal::user(std::move(a->account), + match[2]); + } + + if (match[1] == "role") { + return Principal::role(std::move(a->account), + match[2]); + } + + if (match[1] == "oidc-provider") { + return Principal::oidc_provider(std::move(match[2])); + } + if (match[1] == "assumed-role") { + return Principal::assumed_role(std::move(a->account), match[2]); + } + } + } else if (std::none_of(s.begin(), s.end(), + [](const char& c) { + return (c == ':') || (c == '/'); + })) { + // Since tenants are simply prefixes, there's no really good + // way to see if one exists or not. So we return the thing and + // let them try to match against it. + return Principal::account(std::move(s)); + } + if (errmsg) + *errmsg = + fmt::format( + "`{}` is not a supported AWS or Federated ARN. Supported ARNs are " + "forms like: " + "`arn:aws:iam::tenant:root` or a bare tenant name for a tenant, " + "`arn:aws:iam::tenant:role/role-name` for a role, " + "`arn:aws:sts::tenant:assumed-role/role-name/role-session-name` " + "for an assumed role, " + "`arn:aws:iam::tenant:user/user-name` for a user, " + "`arn:aws:iam::tenant:oidc-provider/idp-url` for OIDC.", s); + } else if (w->id == TokenID::Service) { + return Principal::service(std::move(s)); + } + + if (errmsg) + *errmsg = fmt::format("RGW does not support principals of type `{}`.", + w->name); + return boost::none; +} +} + struct ParseState { PolicyParser* pp; const Keyword* w; @@ -643,72 +717,7 @@ bool ParseState::key(const char* s, size_t l) { // which will make all of this ever so much nicer. boost::optional ParseState::parse_principal(string&& s, string* errmsg) { - if ((w->id == TokenID::AWS) && (s == "*")) { - // Wildcard! - return Principal::wildcard(); - } else if (w->id == TokenID::CanonicalUser) { - // Do nothing for now. - if (errmsg) - *errmsg = "RGW does not support canonical users."; - return boost::none; - } else if (w->id == TokenID::AWS || w->id == TokenID::Federated) { - // AWS and Federated ARNs - if (auto a = ARN::parse(s)) { - if (a->resource == "root") { - return Principal::account(std::move(a->account)); - } - - static const char rx_str[] = "([^/]*)/(.*)"; - static const regex rx(rx_str, sizeof(rx_str) - 1, - std::regex_constants::ECMAScript | - std::regex_constants::optimize); - smatch match; - if (regex_match(a->resource, match, rx) && match.size() == 3) { - if (match[1] == "user") { - return Principal::user(std::move(a->account), - match[2]); - } - - if (match[1] == "role") { - return Principal::role(std::move(a->account), - match[2]); - } - - if (match[1] == "oidc-provider") { - return Principal::oidc_provider(std::move(match[2])); - } - if (match[1] == "assumed-role") { - return Principal::assumed_role(std::move(a->account), match[2]); - } - } - } else if (std::none_of(s.begin(), s.end(), - [](const char& c) { - return (c == ':') || (c == '/'); - })) { - // Since tenants are simply prefixes, there's no really good - // way to see if one exists or not. So we return the thing and - // let them try to match against it. - return Principal::account(std::move(s)); - } - if (errmsg) - *errmsg = - fmt::format( - "`{}` is not a supported AWS or Federated ARN. Supported ARNs are " - "forms like: " - "`arn:aws:iam::tenant:root` or a bare tenant name for a tenant, " - "`arn:aws:iam::tenant:role/role-name` for a role, " - "`arn:aws:sts::tenant:assumed-role/role-name/role-session-name` " - "for an assumed role, " - "`arn:aws:iam::tenant:user/user-name` for a user, " - "`arn:aws:iam::tenant:oidc-provider/idp-url` for OIDC.", s); - } else if (w->id == TokenID::Service) { - return Principal::service(std::move(s)); - } - - if (errmsg) - *errmsg = fmt::format("RGW does not support principals of type `{}`.", - w->name); - return boost::none; + return ::rgw::IAM::parse_principal_(w, std::move(s), errmsg); } bool ParseState::do_string(CephContext* cct, const char* s, size_t l) { @@ -2144,5 +2153,39 @@ bool is_public(const Policy& p, const LogOut& eval_log) return std::any_of(p.statements.begin(), p.statements.end(), IsPublicStatement(eval_log)); } + +boost::optional parse_principal(std::string&& s, string* errmsg) { + keyword_hash tokens; + auto colon = s.find(':') ; + if (colon == s.npos) { + if (errmsg) { + *errmsg = "Identities are of the form SCHEMA:STRING"; + } + return boost::none; + } + const auto w = tokens.lookup(s.data(), colon); + if (!w || w->kind != TokenKind::princ_type) { + if (errmsg) { + *errmsg = fmt::format("`{}` is not a valid identity schema", + std::string_view{s.data(), colon}); + } + return boost::none; + } + s.erase(0, colon + 1); + return parse_principal_(w, std::move(s), errmsg); +} + +// This function is only use from the policy testing tool, so it makes +// no sense for it to handle wildcards. +boost::optional +parse_action(std::string_view s) +{ + for (const auto& [key, n] : actpairs) { + if (boost::iequals(key, s)) { + return rgw::IAM::action_t(n); + } + } + return boost::none; +} } // namespace IAM } // namespace rgw diff --git a/src/rgw/rgw_iam_policy.h b/src/rgw/rgw_iam_policy.h index 313e6438e49..a5a2b631e84 100644 --- a/src/rgw/rgw_iam_policy.h +++ b/src/rgw/rgw_iam_policy.h @@ -222,6 +222,9 @@ enum action_t { allCount }; +boost::optional +parse_principal(std::string&& s, std::string* errmsg); + using Action_t = std::bitset; using NotAction_t = Action_t; @@ -840,6 +843,8 @@ inline bool is_public(const DoutPrefixProvider* dpp, const Policy& p) } return b; } + +boost::optional parse_action(std::string_view s); } } diff --git a/src/rgw/rgw_poltester.cc b/src/rgw/rgw_poltester.cc new file mode 100644 index 00000000000..f71a1e61eb5 --- /dev/null +++ b/src/rgw/rgw_poltester.cc @@ -0,0 +1,167 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*- +// vim: ts=8 sw=2 sts=2 expandtab + +#include +#include +#include +#include +#include +#include + +#include + +#include "include/buffer.h" + +#include "common/ceph_argparse.h" +#include "common/common_init.h" + +#include "global/global_init.h" + +#include "rgw/rgw_auth.h" +#include "rgw/rgw_iam_policy.h" + +using namespace std::literals; + +namespace buffer = ceph::buffer; + +using rgw::auth::Identity; + +void +evaluate(CephContext* cct, const std::string* tenant, + const std::string& fname, std::istream& in, + const std::unordered_multimap& environment, + boost::optional ida, uint64_t action, + boost::optional resource) +{ + buffer::list bl; + bl.append(in); + try { + auto p = rgw::IAM::Policy( + cct, tenant, bl.to_str(), + cct->_conf.get_val("rgw_policy_reject_invalid_principals")); + auto effect = p.eval(std::cout, environment, ida, action, resource); + std::cout << effect << std::endl; + } catch (const rgw::IAM::PolicyParseException& e) { + std::cerr << fname << ": " << e.what() << std::endl; + throw; + } catch (const std::exception& e) { + std::cerr << fname << ": caught exception: " << e.what() << std::endl; + throw; + } +} + +int helpful_exit(std::string_view cmdname) +{ + std::cerr << cmdname << " -h for usage" << std::endl; + return 1; +} + +void usage(std::string_view cmdname) +{ + std::cout << "usage: " << cmdname << " [options...] ACTION POLICY" << std::endl; + std::cout << "options:" << std::endl; + std::cout << + " -t | --tenant=TENANT\ttenant owning the resource the policy governs\n"; + std::cout << + " -e | --environment=KEY=VALUE\tPair to set in the environment\n"; + std::cout << " -I | --identity=IDENTITY\tIdentity against which to test\n"; + std::cout << " -R | --resource=ARN\tResource to test access to\n\n"; + std::cout << " ACTION\tAction to test against, e.g. s3:GetObject\n"; + std::cout << " POLICY\tFilename of the policy or - for standard input\n"; + std::cout.flush(); +} + +// This has an uncaught exception. Even if the exception is caught, the program +// would need to be terminated, so the warning is simply suppressed. +// coverity[root_function:SUPPRESS] +int main(int argc, const char* argv[]) +{ + std::string_view cmdname = argv[0]; + std::string tenant{}; // Explicitly defaulted to the empty tenant if none is provided. + std::unordered_multimap environment; + boost::optional identity_; + boost::optional identity; + uint64_t action = 0; + boost::optional resource_; + boost::optional resource; + + auto args = argv_to_vec(argc, argv); + if (ceph_argparse_need_usage(args)) { + usage(cmdname); + return 0; + } + + auto cct = global_init(nullptr, args, CEPH_ENTITY_TYPE_CLIENT, + CODE_ENVIRONMENT_UTILITY, + CINIT_FLAG_NO_DAEMON_ACTIONS | + CINIT_FLAG_NO_MON_CONFIG); + common_init_finish(cct.get()); + std::string val; + for (std::vector::iterator i = args.begin(); i != args.end(); ) { + if (ceph_argparse_double_dash(args, i)) { + break; + } else if (ceph_argparse_witharg(args, i, &val, "--tenant", "-t", + (char*)nullptr)) { + tenant = std::move(val); + } else if (ceph_argparse_witharg(args, i, &val, "--environment", "-e", + (char*)nullptr)) { + auto equal = val.find('='); + if (equal == val.npos || equal == 0) { + return helpful_exit(cmdname); + } + std::string k{val.data(), equal}; + std::string v{val.data() + equal + 1}; + environment.insert({std::move(k), std::move(v)}); + } else if (ceph_argparse_witharg(args, i, &val, "--resource", "-R", + (char*)nullptr)) { + resource_ = rgw::ARN::parse(val); + if (!resource_) { + return helpful_exit(cmdname); + } + resource = *resource_; + } else if (ceph_argparse_witharg(args, i, &val, "--identity", "-I", + (char*)nullptr)) { + std::string errmsg; + auto principal = rgw::IAM::parse_principal(std::move(val), &errmsg); + if (!principal) { + std::cerr << errmsg << std::endl; + return helpful_exit(cmdname); + } + identity_.emplace(std::move(*principal)); + identity = *identity_; + } else { + ++i; + } + } + + if (args.size() != 2) { + return helpful_exit(cmdname); + } + + if (auto act = rgw::IAM::parse_action(args[0]); act) { + action = *act; + } else { + std::cerr << args[0] << " is not a valid action." << std::endl; + return helpful_exit(cmdname); + } + + try { + if (args[1] == "-"s) { + evaluate(cct.get(), &tenant, "(stdin)", std::cin, + environment, identity, action, resource); + } else { + std::ifstream in; + in.open(args[1], std::ifstream::in); + if (!in.is_open()) { + std::cerr << "Can't read " << args[1] << std::endl; + return 1; + } + evaluate(cct.get(), &tenant, args[1], in, environment, + identity, action, resource); + } + } catch (const std::exception&) { + return 1; + } + + return 0; +} -- 2.47.3