From: Adam C. Emerson Date: Thu, 23 Oct 2025 18:22:19 +0000 (-0400) Subject: rgw/policy: Add rgw-policy-test command line tool X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0fbe9cc13c208e160498ff9cff4b3811c023129c;p=ceph-ci.git rgw/policy: Add rgw-policy-test command line tool Annotated evaluation of a policy. Signed-off-by: Adam C. Emerson --- diff --git a/ceph.spec.in b/ceph.spec.in index 45435898907..cb7b48fed32 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -2251,8 +2251,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/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..d27e41a6da7 --- /dev/null +++ b/doc/man/8/rgw-policy-test.rst @@ -0,0 +1,66 @@ +:orphan: + +=================================================== +rgw-policy-test -- test evaluation of bucket policy +=================================================== + +.. program:: rgw-policy-test + +Synopsis +======== + +| **rgw-policy-check** + [-e *key*=*value*]... [-t *tenant*] [-r resource] [-i identity] *action* *filename* + + +Description +=========== + +This program reads a policy from a file or stdin and evaluates it +against the supplied identity, resource, action, and environment. The +program 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* + + Specify *tenant* as the tenant. This is required by the + policy parsing logic and is used to construct the internal + state representation of the policy. + +.. option: -e *key*=*value* + + Add (*key*, *value*) to the environment for evaluation. May be + specified multiple times. + +.. option: -i *identity* + + Specify *identity* as the identity whose access is to be checked. + +.. option: -r *ARN* + + Specify *ARN* as the resource to check access for. + +.. *action* + + Use *action* as the action to check. + +.. *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/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index 9c7fe7432ab..4c7ca20c1ac 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -592,6 +592,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 c665f1370ee..2eaca4e5572 100644 --- a/src/rgw/rgw_iam_policy.cc +++ b/src/rgw/rgw_iam_policy.cc @@ -340,20 +340,6 @@ operator<<(std::ostream& out, const boost::optional& t) return out; } -inline std::ostream& -operator<<(std::ostream& out, const Effect& e) -{ - switch (e) { - case Effect::Allow: - return out << "Allow"; - case Effect::Pass: - return out << "Pass"; - case Effect::Deny: - return out << "Deny"; - } - return out << "Unknown Effect"; -} - void maybeout::lendl_() const { @@ -378,6 +364,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(std::string{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; @@ -677,72 +736,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) { @@ -2176,5 +2170,37 @@ bool is_public(const Policy& p, maybeout eval_log) 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}); + } + } + s.erase(0, colon); + return parse_principal_(w, std::move(s), errmsg); +} + +boost::optional +parse_action(std::string_view s) +{ + for (const auto& [key, n] : actpairs) { + if (key == s) { + return 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 c1a3a7e801d..67ed8daa2b6 100644 --- a/src/rgw/rgw_iam_policy.h +++ b/src/rgw/rgw_iam_policy.h @@ -313,6 +313,8 @@ inline const maybeout& lendl(const maybeout& m) return m; } +boost::optional +parse_principal(std::string&& s, std::string* errmsg); using Action_t = std::bitset; @@ -755,6 +757,19 @@ struct Statement { }; std::ostream& operator <<(std::ostream& m, const Statement& s); +inline std::ostream& +operator<<(std::ostream& out, const Effect& e) +{ + switch (e) { + case Effect::Allow: + return out << "Allow"; + case Effect::Pass: + return out << "Pass"; + case Effect::Deny: + return out << "Deny"; + } + return out << "Unknown Effect"; +} struct PolicyParseException : public std::exception { rapidjson::ParseResult pr; @@ -883,5 +898,7 @@ 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..37d34b5b04d --- /dev/null +++ b/src/rgw/rgw_poltester.cc @@ -0,0 +1,243 @@ +// -*- 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::Principal; +using rgw::auth::Identity; + +class FakeIdentity : public Identity { + const Principal id; +public: + + explicit FakeIdentity(Principal&& id) : id(std::move(id)) {} + + ACLOwner get_aclowner() const override { + ceph_abort(); + return {}; + } + + uint32_t get_perms_from_aclspec(const DoutPrefixProvider* dpp, const aclspec_t& aclspec) const override { + ceph_abort(); + return 0; + }; + + bool is_admin() const override { + ceph_abort(); + return false; + } + + bool is_owner_of(const rgw_owner& owner) const override { + ceph_abort(); + return false; + } + + bool is_root() const override { + ceph_abort(); + return false; + } + + virtual uint32_t get_perm_mask() const override { + ceph_abort(); + return 0; + } + + std::string get_acct_name() const override { + abort(); + return {}; + } + + std::string get_subuser() const override { + abort(); + return {}; + } + + const std::string& get_tenant() const override { + ceph_abort(); + static std::string empty; + return empty; + } + + const std::optional& get_account() const override { + ceph_abort(); + static std::optional empty; + return empty; + } + + void to_str(std::ostream& out) const override { + out << id; + } + + bool is_identity(const Principal& p) const override { + return id.is_wildcard() || p.is_wildcard() || p == id; + } + + uint32_t get_identity_type() const override { + return TYPE_RGW; + } + + std::optional get_caller_identity() const override { + return std::nullopt; + } +}; + +// Returns true on success +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(environment, ida, action, resource, &std::cout); + 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 to test against\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::optional tenant; + 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 || equal == val.size() - 1) { + return helpful_exit(cmdname); + } + std::string key{val.data(), equal}; + std::string val{val.data() + equal + 1}; + environment.insert({std::move(key), std::move(val)}); + } 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 ? &(*tenant) : nullptr, "(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 ? &(*tenant) : nullptr, args[1], in, environment, + identity, action, resource); + } + } catch (const std::exception&) { + return 1; + } + + return 0; +}