mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
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:
parent
3a1686cf29
commit
5a176d3f83
@ -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
|
||||
|
||||
2
debian/ceph-common.install
vendored
2
debian/ceph-common.install
vendored
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
65
doc/man/8/rgw-policy-test.rst
Normal file
65
doc/man/8/rgw-policy-test.rst
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
167
src/rgw/rgw_poltester.cc
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user