rgw/policy: Add rgw-policy-test command line tool

Annotated evaluation of a policy.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
This commit is contained in:
Adam C. Emerson 2026-03-04 17:43:39 -05:00
parent 3a1686cf29
commit 5a176d3f83
No known key found for this signature in database
9 changed files with 358 additions and 66 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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 <radosgw>`\(8)

View File

@ -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

View File

@ -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})

View File

@ -10,6 +10,7 @@
#include <fmt/ranges.h>
#include <fmt/std.h>
#include <arpa/inet.h>
#include <experimental/iterator>
@ -344,6 +345,79 @@ const Keyword top[1]{{"<Top>", TokenKind::pseudo, TokenID::Top, 0, false,
const Keyword cond_key[1]{{"<Condition Key>", TokenKind::cond_key,
TokenID::CondKey, 0, true, false}};
namespace {
boost::optional<Principal>
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<Principal> 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<Principal> 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<action_t>
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

View File

@ -222,6 +222,9 @@ enum action_t {
allCount
};
boost::optional<rgw::auth::Principal>
parse_principal(std::string&& s, std::string* errmsg);
using Action_t = std::bitset<allCount>;
using NotAction_t = Action_t;
@ -840,6 +843,8 @@ inline bool is_public(const DoutPrefixProvider* dpp, const Policy& p)
}
return b;
}
boost::optional<action_t> parse_action(std::string_view s);
}
}

167
src/rgw/rgw_poltester.cc Normal file
View File

@ -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 <exception>
#include <fstream>
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <boost/optional.hpp>
#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<std::string, std::string>& environment,
boost::optional<const rgw::auth::Identity&> ida, uint64_t action,
boost::optional<const rgw::ARN&> resource)
{
buffer::list bl;
bl.append(in);
try {
auto p = rgw::IAM::Policy(
cct, tenant, bl.to_str(),
cct->_conf.get_val<bool>("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<std::string, std::string> environment;
boost::optional<rgw::auth::PrincipalIdentity> identity_;
boost::optional<const rgw::auth::Identity&> identity;
uint64_t action = 0;
boost::optional<rgw::ARN> resource_;
boost::optional<const rgw::ARN&> 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<const char*>::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;
}