]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
rgw/policy: Add rgw-policy-test command line tool
authorAdam C. Emerson <aemerson@redhat.com>
Thu, 23 Oct 2025 18:22:19 +0000 (14:22 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Mon, 15 Dec 2025 17:28:50 +0000 (12:28 -0500)
Annotated evaluation of a policy.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
ceph.spec.in
doc/man/8/CMakeLists.txt
doc/man/8/rgw-policy-test.rst [new file with mode: 0644]
src/rgw/CMakeLists.txt
src/rgw/rgw_iam_policy.cc
src/rgw/rgw_iam_policy.h
src/rgw/rgw_poltester.cc [new file with mode: 0644]

index 45435898907e72a6c51b17662e3e3b81003e940a..cb7b48fed3275091d82bc2a0d804540e04c540aa 100644 (file)
@@ -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
index 772b822beb02f676a9402ac1c0dbcf467bcf41ed..6d63e89f4d31be0459aff83fab37e49e79f787b7 100644 (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()
diff --git a/doc/man/8/rgw-policy-test.rst b/doc/man/8/rgw-policy-test.rst
new file mode 100644 (file)
index 0000000..d27e41a
--- /dev/null
@@ -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 <radosgw>`\(8)
index 9c7fe7432abd8fedb05335fe7cb4b74d5cff02c5..4c7ca20c1acb8a8213af1ba99378322382fb060c 100644 (file)
@@ -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})
index c665f1370ee4e5038742c0f59df03a2e95901f92..2eaca4e557282af55aa12190115822ee793feb7d 100644 (file)
@@ -340,20 +340,6 @@ operator<<(std::ostream& out, const boost::optional<T>& 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]{{"<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(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<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) {
@@ -2176,5 +2170,37 @@ bool is_public(const Policy& p, maybeout eval_log)
                      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
index c1a3a7e801d5029c5115b108479c0e173d68c933..67ed8daa2b64564db145cb9a62256bdf219b0820 100644 (file)
@@ -313,6 +313,8 @@ inline const maybeout& lendl(const maybeout& m)
   return m;
 }
 
+boost::optional<rgw::auth::Principal>
+parse_principal(std::string&& s, std::string* errmsg);
 
 
 using Action_t = std::bitset<allCount>;
@@ -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<uint64_t> parse_action(std::string_view s);
 }
 }
diff --git a/src/rgw/rgw_poltester.cc b/src/rgw/rgw_poltester.cc
new file mode 100644 (file)
index 0000000..37d34b5
--- /dev/null
@@ -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 <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;
+}