Annotated evaluation of a policy.
Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
%{_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
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()
--- /dev/null
+: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 <radosgw>`\(8)
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})
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
{
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(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;
// 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) {
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});
+ }
+ }
+ s.erase(0, colon);
+ return parse_principal_(w, std::move(s), errmsg);
+}
+
+boost::optional<uint64_t>
+parse_action(std::string_view s)
+{
+ for (const auto& [key, n] : actpairs) {
+ if (key == s) {
+ return n;
+ }
+ }
+ return boost::none;
+}
+
} // namespace IAM
} // namespace rgw
return m;
}
+boost::optional<rgw::auth::Principal>
+parse_principal(std::string&& s, std::string* errmsg);
using Action_t = std::bitset<allCount>;
};
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;
}
return b;
}
+
+boost::optional<uint64_t> parse_action(std::string_view s);
}
}
--- /dev/null
+// -*- 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 <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+#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<RGWAccountInfo>& get_account() const override {
+ ceph_abort();
+ static std::optional<RGWAccountInfo> 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<rgw::ARN> 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<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(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<std::string> tenant;
+ std::unordered_multimap<std::string, std::string> environment;
+ boost::optional<FakeIdentity> 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 || 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;
+}