From: Adam C. Emerson Date: Thu, 25 Jun 2026 20:48:08 +0000 (-0400) Subject: rgw/policy: Allow logging policy evaluation X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=7bdb627a85c18ea9bb24f3239e21d0aaddf1dc0e;p=ceph.git rgw/policy: Allow logging policy evaluation When the `rgw_copious_policy_logging` option is set to true, a detailed trace of policy evaluation is logged. Assisted-by: Claude Opus 4.6 (I wanted to see how it performed on rebase conflict resolution.) Assisted-by: Claude Fable 5 (I figured I should have it look at the policy evaluation code for bugs while it's included, and it found a few mistakes in output.) Signed-off-by: Adam C. Emerson --- diff --git a/src/common/options/rgw.yaml.in b/src/common/options/rgw.yaml.in index 921069cfdd0..227a184b138 100644 --- a/src/common/options/rgw.yaml.in +++ b/src/common/options/rgw.yaml.in @@ -4851,6 +4851,7 @@ options: default: 11 services: - rgw + - name: rgw_usage_log_key_transition type: bool level: advanced @@ -4891,3 +4892,14 @@ options: desc: When not empty, this value is returned as a response header Access-Control-Expose-Headers. services: - rgw + +- name: rgw_copious_policy_logging + type: bool + level: advanced + desc: Log the details of each policy evaluation in extreme verbosity + default: false + services: + - rgw + flags: + - runtime + with_legacy: true diff --git a/src/rgw/rgw_bucket_logging.cc b/src/rgw/rgw_bucket_logging.cc index 597584ce6f2..7d9585a5ec8 100644 --- a/src/rgw/rgw_bucket_logging.cc +++ b/src/rgw/rgw_bucket_logging.cc @@ -1102,7 +1102,7 @@ int verify_target_bucket_policy(const DoutPrefixProvider* dpp, rgw::IAM::Environment env; const auto arn_it = env.emplace("aws:SourceArn", std::move(source_bucket_arn)); const auto acct_it = env.emplace("aws:SourceAccount", std::move(source_account)); - if (policy.eval(env, ident, rgw::IAM::s3PutObject, target_resource_arn) != rgw::IAM::Effect::Allow) { + if (policy.eval(dpp, env, ident, rgw::IAM::s3PutObject, target_resource_arn) != rgw::IAM::Effect::Allow) { ldpp_dout(dpp, 1) << "ERROR: logging bucket: '" << target_bucket_id << "' must have a bucket policy that allows logging service principal to put objects in the following resource ARN: '" << target_resource_arn.to_string() << "' from source bucket ARN: '" << arn_it->second << diff --git a/src/rgw/rgw_common.cc b/src/rgw/rgw_common.cc index 88366087ca9..f58119686c9 100644 --- a/src/rgw/rgw_common.cc +++ b/src/rgw/rgw_common.cc @@ -1152,10 +1152,11 @@ Effect eval_or_pass(const DoutPrefixProvider* dpp, const uint64_t op, const ARN& resource, boost::optional princ_type=boost::none) { - if (!policy) + if (!policy) { return Effect::Pass; - else - return policy->eval(env, id, op, resource, princ_type); + } else { + return policy->eval(dpp, env, id, op, resource, princ_type); + } } Effect eval_identity_or_session_policies(const DoutPrefixProvider* dpp, @@ -1376,7 +1377,7 @@ bool verify_bucket_permission(const DoutPrefixProvider* dpp, // If RestrictPublicBuckets is enabled and the bucket policy allows public access, // deny the request if the requester is not in the bucket owner account if (s->public_access_block.RestrictPublicBuckets && - bucket_policy && rgw::IAM::is_public(*bucket_policy) && + bucket_policy && rgw::IAM::is_public(dpp, *bucket_policy) && !s->identity->is_owner_of(s->bucket_info.owner)) { ldpp_dout(dpp, 10) << __func__ << ": public policies are blocked by the RestrictPublicBuckets block public access setting" << dendl; return false; @@ -1543,7 +1544,7 @@ bool verify_object_permission(const DoutPrefixProvider* dpp, struct perm_state_b // If RestrictPublicBuckets is enabled and the bucket policy allows public access, // deny the request if the requester is not in the bucket owner account if (ps->public_access_block.RestrictPublicBuckets && - bucket_policy && rgw::IAM::is_public(*bucket_policy) && + bucket_policy && rgw::IAM::is_public(dpp, *bucket_policy) && !ps->identity->is_owner_of(ps->bucket_info.owner)) { ldpp_dout(dpp, 10) << __func__ << ": public policies are blocked by the RestrictPublicBuckets block public access setting" << dendl; return false; diff --git a/src/rgw/rgw_iam_policy.cc b/src/rgw/rgw_iam_policy.cc index e42001c8361..ac513b85d69 100644 --- a/src/rgw/rgw_iam_policy.cc +++ b/src/rgw/rgw_iam_policy.cc @@ -1,29 +1,26 @@ // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*- // vim: ts=8 sw=2 sts=2 expandtab ft=cpp - #include #include #include #include -#include #include +#include +#include + #include #include #include "rapidjson/reader.h" -#include "include/expected.hpp" - #include "rgw_auth.h" #include "rgw_iam_policy.h" -namespace { -constexpr int dout_subsys = ceph_subsys_rgw; -} +inline constexpr int dout_subsys = ceph_subsys_rgw; using std::dec; using std::hex; @@ -50,6 +47,19 @@ using rapidjson::StringStream; using rgw::auth::Principal; +using namespace std::literals; + +template +struct fmt::formatter> : fmt::formatter { + template + auto format(const boost::optional& opt, FormatContext& ctx) const { + if (opt) { + return fmt::formatter::format(*opt, ctx); + } + return fmt::format_to(ctx.out(), "--"); + } +}; + namespace rgw { namespace IAM { #include "rgw_iam_policy_keywords.frag.cc" @@ -59,8 +69,6 @@ struct actpair { const uint64_t bit; }; - - static const actpair actpairs[] = {{ "s3:AbortMultipartUpload", s3AbortMultipartUpload }, { "s3:CreateBucket", s3CreateBucket }, @@ -227,6 +235,108 @@ static const actpair actpairs[] = { "organizations:ListTargetsForPolicy", organizationsListTargetsForPolicy}, }; +namespace { +const char* condop_string(const TokenID t) { + switch (t) { + case TokenID::StringEquals: + return "StringEquals"; + + case TokenID::StringNotEquals: + return "StringNotEquals"; + + case TokenID::StringEqualsIgnoreCase: + return "StringEqualsIgnoreCase"; + + case TokenID::StringNotEqualsIgnoreCase: + return "StringNotEqualsIgnoreCase"; + + case TokenID::StringLike: + return "StringLike"; + + case TokenID::StringNotLike: + return "StringNotLike"; + + // Numeric! + case TokenID::NumericEquals: + return "NumericEquals"; + + case TokenID::NumericNotEquals: + return "NumericNotEquals"; + + case TokenID::NumericLessThan: + return "NumericLessThan"; + + case TokenID::NumericLessThanEquals: + return "NumericLessThanEquals"; + + case TokenID::NumericGreaterThan: + return "NumericGreaterThan"; + + case TokenID::NumericGreaterThanEquals: + return "NumericGreaterThanEquals"; + + case TokenID::DateEquals: + return "DateEquals"; + + case TokenID::DateNotEquals: + return "DateNotEquals"; + + case TokenID::DateLessThan: + return "DateLessThan"; + + case TokenID::DateLessThanEquals: + return "DateLessThanEquals"; + + case TokenID::DateGreaterThan: + return "DateGreaterThan"; + + case TokenID::DateGreaterThanEquals: + return "DateGreaterThanEquals"; + + case TokenID::Bool: + return "Bool"; + + case TokenID::BinaryEquals: + return "BinaryEquals"; + + case TokenID::IpAddress: + return "IpAddress"; + + case TokenID::NotIpAddress: + return "NotIpAddress"; + + case TokenID::ArnEquals: + return "ArnEquals"; + + case TokenID::ArnNotEquals: + return "ArnNotEquals"; + + case TokenID::ArnLike: + return "ArnLike"; + + case TokenID::ArnNotLike: + return "ArnNotLike"; + + case TokenID::Null: + return "Null"; + + default: + return "InvalidConditionOperator"; + } +} +} + +template +inline std::ostream& +operator<<(std::ostream& out, const boost::optional& t) +{ + if (!t) + out << "--"; + else + out << ' ' << *t; + return out; +} + struct PolicyParser; const Keyword top[1]{{"", TokenKind::pseudo, TokenID::Top, 0, false, @@ -873,20 +983,28 @@ static bool arn_like(const std::string& input, const std::string& pattern) return match_policy(pattern, input, MATCH_POLICY_ARN); } -bool Condition::eval(const Environment& env) const { +bool Condition::eval(const Environment& env, const LogOut& eval_log) const { std::vector runtime_vals; auto i = env.find(key); if (op == TokenID::Null) { const std::string value = (i == env.end() ? "true" : "false"); - return typed_any(std::equal_to{}, as_bool, value, vals); + eval_log.format("Null check. key `{}` {}.", key, + i == env.end() ? "is not present"sv : + "is present"sv); + return typed_any(std::equal_to{}, as_bool, value, vals, eval_log); } if (i == env.end()) { if (op == TokenID::ForAllValuesStringEquals || op == TokenID::ForAllValuesStringEqualsIgnoreCase || op == TokenID::ForAllValuesStringLike) { + eval_log.format( + "Evaluating `{}` when key `{}` is not present. Vacuously true.", + condop_string(op), key); return true; } else { + eval_log.format("Evaluating `{}` when key `{}` is not present. Returning {}.", + condop_string(op), key, ifexists ? "true" : "false"); return ifexists; } } @@ -901,113 +1019,131 @@ bool Condition::eval(const Environment& env) const { } } const auto& s = i->second; - - const auto& itr = env.equal_range(key); - + const auto itr = env.equal_range(key); + // Printable range because for fmt::format. + const std::ranges::subrange prange(itr.first, itr.second); + eval_log.format("Evaluating `{}` for `[{}]` against `[{}]`", + condop_string(op), fmt::join(prange, ","), + fmt::join(isruntime ? runtime_vals : vals, ",")); switch (op) { // String! case TokenID::ForAnyValueStringEquals: case TokenID::StringEquals: - return multimap_any(std::equal_to(), itr, isruntime? runtime_vals : vals); + return multimap_any(std::equal_to(), itr, + isruntime ? runtime_vals : vals, + eval_log); case TokenID::StringNotEquals: - return multimap_none(std::equal_to(), - itr, isruntime? runtime_vals : vals); + return multimap_none(std::equal_to(), itr, + isruntime ? runtime_vals : vals, eval_log); case TokenID::ForAnyValueStringEqualsIgnoreCase: case TokenID::StringEqualsIgnoreCase: - return multimap_any(ci_equal_to(), itr, isruntime? runtime_vals : vals); + return multimap_any(ci_equal_to(), itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::StringNotEqualsIgnoreCase: - return multimap_none(ci_equal_to(), itr, isruntime? runtime_vals : vals); + return multimap_none(ci_equal_to(), itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::ForAnyValueStringLike: case TokenID::StringLike: - return multimap_any(string_like(), itr, isruntime? runtime_vals : vals); + return multimap_any(string_like(), itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::StringNotLike: - return multimap_none(string_like(), itr, isruntime? runtime_vals : vals); + return multimap_none(string_like(), itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::ForAllValuesStringEquals: - return multimap_all(std::equal_to(), itr, isruntime? runtime_vals : vals); + return multimap_all(std::equal_to(), itr, + isruntime ? runtime_vals : vals, + eval_log); case TokenID::ForAllValuesStringLike: - return multimap_all(string_like(), itr, isruntime? runtime_vals : vals); + return multimap_all(string_like(), itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::ForAllValuesStringEqualsIgnoreCase: - return multimap_all(ci_equal_to(), itr, isruntime? runtime_vals : vals); + return multimap_all(ci_equal_to(), itr, isruntime ? runtime_vals : vals, + eval_log); // Numeric case TokenID::NumericEquals: - return typed_any(std::equal_to(), as_number, s, vals); + return typed_any(std::equal_to(), as_number, s, vals, eval_log); case TokenID::NumericNotEquals: - return typed_none(std::equal_to(), - as_number, s, vals); + return typed_none(std::equal_to(), as_number, s, vals, eval_log); case TokenID::NumericLessThan: - return typed_any(std::less(), as_number, s, vals); + return typed_any(std::less(), as_number, s, vals, eval_log); case TokenID::NumericLessThanEquals: - return typed_any(std::less_equal(), as_number, s, vals); + return typed_any(std::less_equal(), as_number, s, vals, eval_log); case TokenID::NumericGreaterThan: - return typed_any(std::greater(), as_number, s, vals); + return typed_any(std::greater(), as_number, s, vals, eval_log); case TokenID::NumericGreaterThanEquals: - return typed_any(std::greater_equal(), as_number, s, - vals); + return typed_any(std::greater_equal(), as_number, s, vals, + eval_log); // Date! case TokenID::DateEquals: - return typed_any(std::equal_to(), as_date, s, vals); + return typed_any(std::equal_to(), as_date, s, vals, + eval_log); case TokenID::DateNotEquals: return typed_none(std::equal_to(), - as_date, s, vals); + as_date, s, vals, eval_log); case TokenID::DateLessThan: - return typed_any(std::less(), as_date, s, vals); + return typed_any(std::less(), as_date, s, vals, eval_log); case TokenID::DateLessThanEquals: - return typed_any(std::less_equal(), as_date, s, vals); + return typed_any(std::less_equal(), as_date, s, vals, + eval_log); case TokenID::DateGreaterThan: - return typed_any(std::greater(), as_date, s, vals); + return typed_any(std::greater(), as_date, s, vals, + eval_log); case TokenID::DateGreaterThanEquals: return typed_any(std::greater_equal(), as_date, s, - vals); + vals, eval_log); // Bool! case TokenID::Bool: - return typed_any(std::equal_to(), as_bool, s, vals); + return typed_any(std::equal_to(), as_bool, s, vals, eval_log); // Binary! case TokenID::BinaryEquals: return typed_any(std::equal_to(), as_binary, s, - vals); + vals, eval_log); // IP Address! case TokenID::IpAddress: - return typed_any(std::equal_to(), as_network, s, vals); + return typed_any(std::equal_to(), as_network, s, vals, eval_log); case TokenID::NotIpAddress: return typed_none(std::equal_to(), - as_network, s, vals); + as_network, s, vals, eval_log); // Amazon Resource Names! // The ArnEquals and ArnLike condition operators behave identically. case TokenID::ArnEquals: case TokenID::ArnLike: - return multimap_any(arn_like, itr, isruntime? runtime_vals : vals); + return multimap_any(arn_like, itr, isruntime ? runtime_vals : vals, + eval_log); case TokenID::ArnNotEquals: case TokenID::ArnNotLike: - return multimap_none(arn_like, itr, isruntime? runtime_vals : vals); + return multimap_none(arn_like, itr, isruntime ? runtime_vals : vals, + eval_log); default: + eval_log.format("Unknown operation: Returning false."); return false; } } @@ -1074,96 +1210,6 @@ boost::optional Condition::as_network(const string& s) { return m; } -namespace { -const char* condop_string(const TokenID t) { - switch (t) { - case TokenID::StringEquals: - return "StringEquals"; - - case TokenID::StringNotEquals: - return "StringNotEquals"; - - case TokenID::StringEqualsIgnoreCase: - return "StringEqualsIgnoreCase"; - - case TokenID::StringNotEqualsIgnoreCase: - return "StringNotEqualsIgnoreCase"; - - case TokenID::StringLike: - return "StringLike"; - - case TokenID::StringNotLike: - return "StringNotLike"; - - // Numeric! - case TokenID::NumericEquals: - return "NumericEquals"; - - case TokenID::NumericNotEquals: - return "NumericNotEquals"; - - case TokenID::NumericLessThan: - return "NumericLessThan"; - - case TokenID::NumericLessThanEquals: - return "NumericLessThanEquals"; - - case TokenID::NumericGreaterThan: - return "NumericGreaterThan"; - - case TokenID::NumericGreaterThanEquals: - return "NumericGreaterThanEquals"; - - case TokenID::DateEquals: - return "DateEquals"; - - case TokenID::DateNotEquals: - return "DateNotEquals"; - - case TokenID::DateLessThan: - return "DateLessThan"; - - case TokenID::DateLessThanEquals: - return "DateLessThanEquals"; - - case TokenID::DateGreaterThan: - return "DateGreaterThan"; - - case TokenID::DateGreaterThanEquals: - return "DateGreaterThanEquals"; - - case TokenID::Bool: - return "Bool"; - - case TokenID::BinaryEquals: - return "BinaryEquals"; - - case TokenID::IpAddress: - return "case TokenID::IpAddress"; - - case TokenID::NotIpAddress: - return "NotIpAddress"; - - case TokenID::ArnEquals: - return "ArnEquals"; - - case TokenID::ArnNotEquals: - return "ArnNotEquals"; - - case TokenID::ArnLike: - return "ArnLike"; - - case TokenID::ArnNotLike: - return "ArnNotLike"; - - case TokenID::Null: - return "Null"; - - default: - return "InvalidConditionOperator"; - } -} - template ostream& print_array(ostream& m, Iterator begin, Iterator end) { if (begin == end) { @@ -1184,8 +1230,6 @@ ostream& print_dict(ostream& m, Iterator begin, Iterator end) { return m; } -} - ostream& operator <<(ostream& m, const Condition& c) { m << condop_string(c.op); if (c.ifexists) { @@ -1196,46 +1240,82 @@ ostream& operator <<(ostream& m, const Condition& c) { return m << " }"; } -Effect Statement::eval(const Environment& e, - boost::optional ida, - uint64_t act, boost::optional res, boost::optional princ_type) const { - - if (eval_principal(e, ida, princ_type) == Effect::Deny) { +Effect +Statement::eval( + const Environment& e, + boost::optional ida, + uint64_t act, + boost::optional res, + const LogOut& eval_log, + boost::optional princ_type) const { + + if (eval_principal(e, ida, eval_log, princ_type) == Effect::Deny) { + eval_log.format("Passing."); return Effect::Pass; } if (res && resource.empty() && notresource.empty()) { + eval_log.format("A resource was specified but both Resource and NotResource " + "were empty. Passing."); return Effect::Pass; } if (!res && (!resource.empty() || !notresource.empty())) { + eval_log.format("No resource was specified, but Resource or " + "NotResource was non-empty. Passing."); return Effect::Pass; } if (!resource.empty() && res) { - if (!std::any_of(resource.begin(), resource.end(), - [&res](const ARN& pattern) { - return pattern.match(*res); - })) { + eval_log.format("Checking Resource for {}:", res); + if (!std::any_of( + resource.begin(), resource.end(), + [&res, &eval_log](const ARN& pattern) { + if (pattern.match(*res)) { + eval_log.format("Matched `{}`.", pattern); + return true; + } + return false; + })) { + eval_log.format("No match in Resource. Passing."); return Effect::Pass; } } else if (!notresource.empty() && res) { - if (std::any_of(notresource.begin(), notresource.end(), - [&res](const ARN& pattern) { - return pattern.match(*res); + eval_log.format("Checking NotResource for {}:", res); + if (std::any_of( + notresource.begin(), notresource.end(), + [&res, &eval_log](const ARN& pattern) { + if (pattern.match(*res)) { + eval_log.format("Matched `{}`.", pattern); + return true; + } + return false; })) { + eval_log.format("Match found in NotResource. Passing."); return Effect::Pass; } } - if (!(action[act] == 1) || (notaction[act] == 1)) { + if (!(action[act] == 1)) { + eval_log.format("{} not found in Action. Passing.", + action_bit_string(action_t(act))); return Effect::Pass; } - if (std::all_of(conditions.begin(), - conditions.end(), - [&e](const Condition& c) { return c.eval(e);})) { + if (notaction[act] == 1) { + eval_log.format("{} found in NotAction. Passing.", + action_bit_string(action_t(act))); + return Effect::Pass; + } + + eval_log.format("Evaluating conditions:"); + if (std::all_of(conditions.begin(), conditions.end(), + [&e, &eval_log](const Condition& c) { + return c.eval(e, eval_log); + })) { + eval_log.format("Returning {}.", effect); return effect; } + eval_log.format("Passing."); return Effect::Pass; } @@ -1248,50 +1328,76 @@ static bool is_identity(const auth::Identity& ida, }); } -Effect Statement::eval_principal(const Environment& e, - boost::optional ida, boost::optional princ_type) const { +Effect +Statement::eval_principal( + const Environment& e, + boost::optional ida, + const LogOut& eval_log, + boost::optional princ_type) const { if (princ_type) { *princ_type = PolicyPrincipal::Other; } + eval_log.format("Evaluating identity `{}`:", ida); if (ida) { if (princ.empty() && noprinc.empty()) { + eval_log.format("Principal empty and NotPrincipal empty: Denying."); return Effect::Deny; } - if (ida->get_identity_type() != TYPE_ROLE && !princ.empty() && !is_identity(*ida, princ)) { + if (ida->get_identity_type() != TYPE_ROLE && + !princ.empty() && !is_identity(*ida, princ)) { + eval_log.format("Identity is not role, Principal is not empty, and " + "the identity is not in Principal."); return Effect::Deny; } + eval_log.format("Checking type of Principal match."); if (ida->get_identity_type() == TYPE_ROLE && !princ.empty()) { bool princ_matched = false; - for (auto p : princ) { // Check each principal to determine the type of the one that has matched + // Check each principal to determine the type of the one that has matched + for (auto p : princ) { if (ida->is_identity(p)) { if (p.is_assumed_role() || p.is_user()) { - if (princ_type) *princ_type = PolicyPrincipal::Session; + if (princ_type) { + eval_log.format("Setting principal type to Session."); + *princ_type = PolicyPrincipal::Session; + } } else { - if (princ_type) *princ_type = PolicyPrincipal::Role; + if (princ_type) { + eval_log.format("Setting principal type to Role."); + *princ_type = PolicyPrincipal::Role; + } } princ_matched = true; } } if (!princ_matched) { + eval_log.format("No match in Principal, Denying."); return Effect::Deny; } } else if (!noprinc.empty() && is_identity(*ida, noprinc)) { + eval_log.format("Match in NotPrincipal, Denying."); return Effect::Deny; } } return Effect::Allow; } -Effect Statement::eval_conditions(const Environment& e) const { - if (std::all_of(conditions.begin(), - conditions.end(), - [&e](const Condition& c) { return c.eval(e);})) { - return Effect::Allow; - } +Effect Statement::eval_conditions(const Environment& e, + const LogOut& eval_log) const { + if (std::all_of( + conditions.begin(), conditions.end(), + [&e, &eval_log](const Condition& c) { + eval_log.format("Evaluating condition `{}`:", c); + return c.eval(e, eval_log); + })) { + eval_log.format("Returning Allow."); + return Effect::Allow; + } + + eval_log.format("Returning Deny."); return Effect::Deny; } -const char* action_bit_string(action_t action) { +std::string_view action_bit_string(action_t action) { switch (action) { case s3GetObject: return "s3:GetObject"; @@ -1783,11 +1889,23 @@ const char* action_bit_string(action_t action) { return "organizations:ListTargetsForPolicy"; case s3All: + return "s3:*"; + case s3objectlambdaAll: + return "s3-object-lambda:*"; + case iamAll: + return "iam:*"; + case stsAll: + return "sts:*"; + case snsAll: + return "sns:*"; + case organizationsAll: + return "organizations:*"; + case allCount: return "{invalidSentinel}"; } @@ -1903,45 +2021,70 @@ Policy::Policy(CephContext* cct, const string* tenant, } Effect Policy::eval(const Environment& e, - boost::optional ida, - std::uint64_t action, boost::optional resource, - boost::optional princ_type) const { + boost::optional ida, + std::uint64_t action, + boost::optional resource, + const LogOut& eval_log, + boost::optional princ_type) const +{ auto allowed = false; + eval_log.format("Evaluating policy:\n```\n{}\n```\n" + "Environment: {}\n" + "Principal: {}\n" + "Action: {}\n" + "Resource: {}", + text, e, ida, action_bit_string(action_t(action)), + resource); + for (auto& s : statements) { - auto g = s.eval(e, ida, action, resource, princ_type); + eval_log.format("Evaluating statement `{}`:", s); + auto g = s.eval(e, ida, action, resource, eval_log, princ_type); if (g == Effect::Deny) { + eval_log.format("Denying."); return g; } else if (g == Effect::Allow) { allowed = true; } } + eval_log.format("{}", allowed ? "Allowing." : "Passing."); return allowed ? Effect::Allow : Effect::Pass; } -Effect Policy::eval_principal(const Environment& e, - boost::optional ida, boost::optional princ_type) const { +Effect +Policy::eval_principal( + const Environment& e, + boost::optional ida, + const LogOut& eval_log, + boost::optional princ_type) const { auto allowed = false; for (auto& s : statements) { - auto g = s.eval_principal(e, ida, princ_type); + eval_log.format("Evaluating identity `{}` in statement `{}`:", ida, s); + auto g = s.eval_principal(e, ida, eval_log, princ_type); if (g == Effect::Deny) { + eval_log.format("Denying."); return g; } else if (g == Effect::Allow) { allowed = true; } } + eval_log.format("{}", allowed ? "Allowing." : "Denying."); return allowed ? Effect::Allow : Effect::Deny; } -Effect Policy::eval_conditions(const Environment& e) const { +Effect Policy::eval_conditions(const Environment& e, + const LogOut& eval_log) const { auto allowed = false; for (auto& s : statements) { - auto g = s.eval_conditions(e); + eval_log.format("Evaluating conditions in statement `{}`", s); + auto g = s.eval_conditions(e, eval_log); if (g == Effect::Deny) { + eval_log.format("Denying."); return g; } else if (g == Effect::Allow) { allowed = true; } } + eval_log.format("{}", allowed ? "Allowing." : "Denying."); return allowed ? Effect::Allow : Effect::Deny; } @@ -1976,11 +2119,18 @@ static const Environment iam_all_env = { struct IsPublicStatement { - bool operator() (const Statement &s) const { + const LogOut& eval_log; + + IsPublicStatement(const LogOut& eval_log) : + eval_log(eval_log) + {} + + bool operator()(const Statement& s) const { if (s.effect == Effect::Allow) { for (const auto& p : s.princ) { if (p.is_wildcard()) { - return s.eval_conditions(iam_all_env) == Effect::Allow; + eval_log.format("Evaluating conditions in statement `{}`:", s); + return s.eval_conditions(iam_all_env, eval_log) == Effect::Allow; } } } @@ -1989,10 +2139,10 @@ struct IsPublicStatement }; -bool is_public(const Policy& p) +bool is_public(const Policy& p, const LogOut& eval_log) { - return std::any_of(p.statements.begin(), p.statements.end(), IsPublicStatement()); + return std::any_of(p.statements.begin(), p.statements.end(), + IsPublicStatement(eval_log)); } - } // namespace IAM } // namespace rgw diff --git a/src/rgw/rgw_iam_policy.h b/src/rgw/rgw_iam_policy.h index 85ed19f94c7..313e6438e49 100644 --- a/src/rgw/rgw_iam_policy.h +++ b/src/rgw/rgw_iam_policy.h @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -18,6 +20,7 @@ #include #include +#include #include "common/ceph_time.h" #include "common/iso_8601.h" @@ -339,7 +342,7 @@ inline int op_to_perm(std::uint64_t op) { } } -const char* action_bit_string(action_t action); +std::string_view action_bit_string(action_t action); enum class PolicyPrincipal { Role, @@ -369,6 +372,61 @@ inline bool operator ==(const MaskedIP& l, const MaskedIP& r) { return (l.addr >> shift) == (r.addr >> shift); } +class LogOut { + template + struct overloaded : Ts... { + using Ts::operator()...; + }; + + mutable std::variant log; + std::string prefix; + +public: + LogOut() : log(std::monostate{}) {} + LogOut(std::nullptr_t) : log(std::monostate{}) {} + LogOut(std::ostream& m) : log(&m) {} + LogOut(const DoutPrefixProvider* dpp) + : log(dpp) { + if (!dpp || !dpp->get_cct()->_conf->rgw_copious_policy_logging) { + log = std::monostate{}; + } else { + std::ostringstream ostr; + dpp->gen_prefix(ostr); + prefix = std::move(ostr).str(); + } + } + + operator bool() const { return !std::holds_alternative(log); }; + void vformat(fmt::string_view fmt, fmt::format_args args) const { + std::visit( + overloaded{ + [](std::monostate) {}, + [&fmt, &args](std::ostream* o) { + assert(o); // We're constructing from a reference so `o` can't be null. + fmt::vprint(*o, fmt, args); + *o << std::endl; + }, + [&fmt, &args, this](const DoutPrefixProvider* dpp) { + assert(dpp); // If we're given null, we switch to monostate + + // The user has explicitly turned on policy logging, so + // we don't bother with the conditional logging + // business. + ceph::logging::StringEntry e(0, ceph_subsys_rgw, prefix); + fmt::vformat_to(e.get_inserter(), fmt, args); + dpp->get_cct()->_log->submit_entry(std::move(e)); + return; + } + }, log); + } + template + void format(fmt::format_string fmt, Args&&... args) const + { + vformat(fmt, fmt::make_format_args(args...)); + } +}; + + struct Condition { TokenID op; // Originally I was going to use a perfect hash table, but Marcus @@ -387,8 +445,7 @@ struct Condition { Condition() = default; Condition(TokenID op, const char* s, std::size_t len, bool ifexists) : op(op), key(s, len), ifexists(ifexists) {} - - bool eval(const Environment& e) const; + bool eval(const Environment& e, const LogOut& eval_log) const; static boost::optional as_number(const std::string& s) { std::size_t p = 0; @@ -485,89 +542,120 @@ struct Condition { } }; - using unordered_multimap_it_pair = std::pair ::const_iterator, std::unordered_multimap::const_iterator>; + using unordered_multimap_it_pair = std::pair< + std::unordered_multimap::const_iterator, + std::unordered_multimap::const_iterator>; template - static bool multimap_all(F&& f, const unordered_multimap_it_pair& it, - const std::vector& v) { + static bool multimap_all(F&& f, + const unordered_multimap_it_pair& it, + const std::vector& v, + const LogOut& eval_log) { for (auto itr = it.first; itr != it.second; itr++) { bool matched = false; + std::string failmatch; for (const auto& d : v) { if (f(itr->second, d)) { - matched = true; + matched = true; + } else if (eval_log && failmatch.empty()) { + failmatch = fmt::format("({}, {})", itr->second, d); + } + } + if (!matched) { + if (failmatch.empty()) { + eval_log.format("Values matched against were empty."); + } else { + eval_log.format("Predicate false for {}", failmatch); + } + return false; } - } - if (!matched) - return false; } return true; } - template - static bool multimap_any(F&& f, const unordered_multimap_it_pair& it, - const std::vector& v) { + template + static bool + multimap_any(F&& f, + const unordered_multimap_it_pair& it, + const std::vector& v, + const LogOut& eval_log) { for (auto itr = it.first; itr != it.second; itr++) { for (const auto& d : v) { if (f(itr->second, d)) { - return true; + eval_log.format("Predicate true for ({}, {})", itr->second, d); + return true; + } } - } } + eval_log.format("No predicate true. Returning false."); return false; } template static bool multimap_none(F&& f, const unordered_multimap_it_pair& it, - const std::vector& v) { + const std::vector& v, + const LogOut& eval_log) { for (auto itr = it.first; itr != it.second; itr++) { for (const auto& d : v) { if (f(itr->second, d)) { + eval_log.format("Predicate true for ({}, {})", itr->second, d); return false; } } } + eval_log.format("No predicate true. Returning true."); return true; } template static bool typed_any(F&& f, X&& x, const std::string& c, - const std::vector& v) { - auto xc = std::forward(x)(c); + const std::vector& v, + const LogOut& eval_log) { + auto xc = x(c); if (!xc) { + eval_log.format("Failed to convert `{}`. Returning false.", c); return false; } for (const auto& d : v) { auto xd = x(d); if (!xd) { + eval_log.format("Failed to convert `{}`. Skipping.", d); continue; } if (f(*xc, *xd)) { + eval_log.format("Predicate true for ({}, {})", *xc, *xd); return true; } } + eval_log.format("No predicate true for `{}`. Returning false.", *xc); return false; } template static bool typed_none(F&& f, X&& x, const std::string& c, - const std::vector& v) { - auto xc = std::forward(x)(c); + const std::vector& v, + const LogOut& eval_log) { + auto xc = x(c); if (!xc) { + eval_log.format("Failed to convert `{}`. Returning false.", c); return false; } for (const auto& d : v) { auto xd = x(d); if (!xd) { + eval_log.format("Failed to convert `{}`. Skipping.", d); continue; } if (f(*xc, *xd)) { + eval_log.format("Predicate true for ({}, {})", *xc, *xd); return false; } } + eval_log.format("No predicate true for `{}`. Returning true.", *xc); return true; } @@ -607,13 +695,21 @@ struct Statement { std::vector conditions; Effect eval(const Environment& e, - boost::optional ida, - std::uint64_t action, boost::optional resource, boost::optional princ_type=boost::none) const; - - Effect eval_principal(const Environment& e, - boost::optional ida, boost::optional princ_type=boost::none) const; - - Effect eval_conditions(const Environment& e) const; + boost::optional ida, + std::uint64_t action, + boost::optional resource, + const LogOut& eval_log, + boost::optional princ_type=boost::none) const; + + Effect eval_principal( + const Environment& e, + boost::optional ida, + const LogOut& eval_log, + boost::optional princ_type = boost::none) const; + + Effect + eval_conditions(const Environment& e, + const LogOut& eval_log) const; }; std::ostream& operator <<(std::ostream& m, const Statement& s); @@ -652,14 +748,39 @@ struct Policy { std::string text, bool reject_invalid_principals); + Effect eval(std::ostream& out, const Environment& e, + boost::optional ida, + std::uint64_t action, + boost::optional resource, + boost::optional princ_type = boost::none) const { + return eval(e, ida, action, resource, out, princ_type); + } + Effect eval(const Environment& e, - boost::optional ida, - std::uint64_t action, boost::optional resource, boost::optional princ_type=boost::none) const; + boost::optional ida, + std::uint64_t action, + boost::optional resource, + boost::optional princ_type = boost::none) const { + return eval(e, ida, action, resource, {}, princ_type); + } + + Effect eval(const DoutPrefixProvider* dpp, + const Environment& e, + boost::optional ida, + std::uint64_t action, + boost::optional resource, + boost::optional princ_type = boost::none) const { + return eval(e, ida, action, resource, dpp, princ_type); + } - Effect eval_principal(const Environment& e, - boost::optional ida, boost::optional princ_type=boost::none) const; + Effect eval_principal( + const Environment& e, + boost::optional ida, + const LogOut& eval_log, + boost::optional princ_type = boost::none) const; - Effect eval_conditions(const Environment& e) const; + Effect eval_conditions(const Environment& e, + const LogOut& eval_log) const; template bool has_conditional(const std::string& conditional, F p) const { @@ -693,10 +814,36 @@ struct Policy { bool has_partial_conditional_value(const std::string& c) const { return has_conditional_value(c, Condition::ci_starts_with()); } + +private: + + Effect eval(const Environment& e, + boost::optional ida, + std::uint64_t action, + boost::optional resource, + const LogOut& eval_log, + boost::optional princ_type =boost::none) const; + }; std::ostream& operator <<(std::ostream& m, const Policy& p); -bool is_public(const Policy& p); +bool is_public(const Policy& p, const LogOut& eval_log); +inline bool is_public(const DoutPrefixProvider* dpp, const Policy& p) +{ + bool b = false; + if (dpp) { + bool copious_logging = dpp->get_cct()->_conf->rgw_copious_policy_logging; + b = is_public(p, {copious_logging ? dpp : nullptr}); + } else { + b = is_public(p, nullptr); + } + return b; } } +} + +template <> struct fmt::formatter : ostream_formatter {}; +template <> struct fmt::formatter : ostream_formatter {}; +template <> struct fmt::formatter : ostream_formatter {}; +template <> struct fmt::formatter : ostream_formatter {}; diff --git a/src/rgw/rgw_iam_policy_keywords.h b/src/rgw/rgw_iam_policy_keywords.h index dc04bff0bba..ada69a64e2f 100644 --- a/src/rgw/rgw_iam_policy_keywords.h +++ b/src/rgw/rgw_iam_policy_keywords.h @@ -3,8 +3,11 @@ #pragma once -namespace rgw { -namespace IAM { +#include + +#include + +namespace rgw::IAM { enum class TokenKind { pseudo, top, statement, cond_op, cond_key, version_key, effect_key, @@ -127,6 +130,25 @@ enum class Effect { Pass }; +inline std::string_view to_string(Effect e) +{ + using enum Effect; + switch (e) { + case Allow: + return "Allow"; + case Pass: + return "Pass"; + case Deny: + return "Deny"; + } + return "Unknown Effect"; +} + +inline std::ostream& operator <<(std::ostream& m, const Effect& e) +{ + return m << to_string(e); +} + enum class Type { string, number, @@ -137,5 +159,13 @@ enum class Type { arn, null }; -} -} +} // namespace rgw::IAM + +template<> +struct fmt::formatter : formatter { + template + auto format(const rgw::IAM::Effect& e, FormatContext& ctx) const { + auto s = to_string(e); + return formatter::format(s, ctx); + } +}; diff --git a/src/rgw/rgw_op.cc b/src/rgw/rgw_op.cc index 9ff10491975..207637417a2 100644 --- a/src/rgw/rgw_op.cc +++ b/src/rgw/rgw_op.cc @@ -9359,7 +9359,7 @@ void RGWPutBucketPolicy::execute(optional_yield y) s->cct->_conf.get_val("rgw_policy_reject_invalid_principals")); rgw::sal::Attrs attrs(s->bucket_attrs); if (s->public_access_block.BlockPublicPolicy && - rgw::IAM::is_public(p)) { + rgw::IAM::is_public(this, p)) { op_ret = -EACCES; return; } @@ -9906,7 +9906,8 @@ int RGWGetBucketPolicyStatus::verify_permission(optional_yield y) void RGWGetBucketPolicyStatus::execute(optional_yield y) { - isPublic = (s->iam_policy && rgw::IAM::is_public(*s->iam_policy)) || s->bucket_acl.is_public(this); + isPublic = (s->iam_policy && rgw::IAM::is_public(this, *s->iam_policy)) || + s->bucket_acl.is_public(this); } int RGWPutBucketPublicAccessBlock::verify_permission(optional_yield y) diff --git a/src/rgw/rgw_rest_sts.cc b/src/rgw/rgw_rest_sts.cc index 676312d75db..d9b3f7ff952 100644 --- a/src/rgw/rgw_rest_sts.cc +++ b/src/rgw/rgw_rest_sts.cc @@ -857,7 +857,7 @@ int RGWREST_STS::verify_permission(optional_yield y) const rgw::IAM::Policy p(s->cct, policy_tenant, policy, false); if (!s->principal_tags.empty()) { - auto res = p.eval(s->env, *s->auth.identity, rgw::IAM::stsTagSession, boost::none); + auto res = p.eval(this, s->env, *s->auth.identity, rgw::IAM::stsTagSession, boost::none); if (res != rgw::IAM::Effect::Allow) { ldout(s->cct, 0) << "evaluating policy for stsTagSession returned deny/pass" << dendl; return -EPERM; @@ -870,7 +870,7 @@ int RGWREST_STS::verify_permission(optional_yield y) op = rgw::IAM::stsAssumeRole; } - auto res = p.eval(s->env, *s->auth.identity, op, boost::none); + auto res = p.eval(this, s->env, *s->auth.identity, op, boost::none); if (res != rgw::IAM::Effect::Allow) { ldout(s->cct, 0) << "evaluating policy for op: " << op << " returned deny/pass" << dendl; return -EPERM; diff --git a/src/test/rgw/test_rgw_iam_policy.cc b/src/test/rgw/test_rgw_iam_policy.cc index 2ccdc577089..af0d6aab8c9 100644 --- a/src/test/rgw/test_rgw_iam_policy.cc +++ b/src/test/rgw/test_rgw_iam_policy.cc @@ -271,18 +271,15 @@ TEST_F(PolicyTest, Eval1) { ARN arn1(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); - EXPECT_EQ(p.eval(e, none, s3ListBucket, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(e, none, s3ListBucket, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); - EXPECT_EQ(p.eval(e, none, s3PutBucketAcl, arn2), - Effect::Pass); + EXPECT_EQ(p.eval(e, none, s3PutBucketAcl, arn2), Effect::Pass); ARN arn3(Partition::aws, Service::s3, "", arbitrary_tenant, "erroneous_bucket"); - EXPECT_EQ(p.eval(e, none, s3ListBucket, arn3), - Effect::Pass); + EXPECT_EQ(p.eval(e, none, s3ListBucket, arn3), Effect::Pass); } @@ -341,29 +338,22 @@ TEST_F(PolicyTest, Eval2) { for (auto i = 0ULL; i < s3All; ++i) { ARN arn1(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket"); - EXPECT_EQ(p.eval(e, trueacct, i, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(e, trueacct, i, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket/myobject"); - EXPECT_EQ(p.eval(e, trueacct, i, arn2), - Effect::Allow); + EXPECT_EQ(p.eval(e, trueacct, i, arn2), Effect::Allow); ARN arn3(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket"); - EXPECT_EQ(p.eval(e, notacct, i, arn3), - Effect::Pass); + EXPECT_EQ(p.eval(e, notacct, i, arn3), Effect::Pass); ARN arn4(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket/myobject"); - EXPECT_EQ(p.eval(e, notacct, i, arn4), - Effect::Pass); + EXPECT_EQ(p.eval(e, notacct, i, arn4), Effect::Pass); ARN arn5(Partition::aws, Service::s3, "", arbitrary_tenant, "notyourbucket"); - EXPECT_EQ(p.eval(e, trueacct, i, arn5), - Effect::Pass); + EXPECT_EQ(p.eval(e, trueacct, i, arn5), Effect::Pass); ARN arn6(Partition::aws, Service::s3, "", arbitrary_tenant, "notyourbucket/notyourobject"); - EXPECT_EQ(p.eval(e, trueacct, i, arn6), - Effect::Pass); - + EXPECT_EQ(p.eval(e, trueacct, i, arn6), Effect::Pass); } } @@ -535,13 +525,11 @@ TEST_F(PolicyTest, Eval3) { ARN arn1(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket"); - EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket"); - EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, arn2), - Effect::Allow); + EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, arn2), Effect::Allow); for (auto op = 0ULL; op < s3All; ++op) { @@ -550,20 +538,16 @@ TEST_F(PolicyTest, Eval3) { } ARN arn3(Partition::aws, Service::s3, "", arbitrary_tenant, "confidential-data"); - EXPECT_EQ(p.eval(em, none, op, arn3), - Effect::Pass); + EXPECT_EQ(p.eval(em, none, op, arn3), Effect::Pass); ARN arn4(Partition::aws, Service::s3, "", arbitrary_tenant, "confidential-data"); - EXPECT_EQ(p.eval(tr, none, op, arn4), - s3allow[op] ? Effect::Allow : Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, arn4), s3allow[op] ? Effect::Allow : Effect::Pass); ARN arn5(Partition::aws, Service::s3, "", arbitrary_tenant, "confidential-data"); - EXPECT_EQ(p.eval(fa, none, op, arn5), - Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, arn5), Effect::Pass); ARN arn6(Partition::aws, Service::s3, "", arbitrary_tenant, "confidential-data/moo"); - EXPECT_EQ(p.eval(em, none, op, arn6), - Effect::Pass); + EXPECT_EQ(p.eval(em, none, op, arn6), Effect::Pass); ARN arn7(Partition::aws, Service::s3, "", arbitrary_tenant, "confidential-data/moo"); EXPECT_EQ(p.eval(tr, none, op, arn7), @@ -574,16 +558,13 @@ TEST_F(PolicyTest, Eval3) { Effect::Pass); ARN arn9(Partition::aws, Service::s3, "", arbitrary_tenant, "really-confidential-data"); - EXPECT_EQ(p.eval(em, none, op, arn9), - Effect::Pass); + EXPECT_EQ(p.eval(em, none, op, arn9), Effect::Pass); ARN arn10(Partition::aws, Service::s3, "", arbitrary_tenant, "really-confidential-data"); - EXPECT_EQ(p.eval(tr, none, op, arn10), - Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, arn10), Effect::Pass); ARN arn11(Partition::aws, Service::s3, "", arbitrary_tenant, "really-confidential-data"); - EXPECT_EQ(p.eval(fa, none, op, arn11), - Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, arn11), Effect::Pass); ARN arn12(Partition::aws, Service::s3, "", arbitrary_tenant, "really-confidential-data/moo"); @@ -596,7 +577,6 @@ TEST_F(PolicyTest, Eval3) { "", arbitrary_tenant, "really-confidential-data/moo"); EXPECT_EQ(p.eval(fa, none, op, arn14), Effect::Pass); - } } @@ -636,13 +616,11 @@ TEST_F(PolicyTest, Eval4) { ARN arn1(Partition::aws, Service::iam, "", arbitrary_tenant, "role/example_role"); - EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::iam, "", arbitrary_tenant, "role/example_role"); - EXPECT_EQ(p.eval(e, none, iamDeleteRole, arn2), - Effect::Pass); + EXPECT_EQ(p.eval(e, none, iamDeleteRole, arn2), Effect::Pass); } TEST_F(PolicyTest, Parse5) { @@ -681,18 +659,15 @@ TEST_F(PolicyTest, Eval5) { ARN arn1(Partition::aws, Service::iam, "", arbitrary_tenant, "role/example_role"); - EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::iam, "", arbitrary_tenant, "role/example_role"); - EXPECT_EQ(p.eval(e, none, s3ListBucket, arn2), - Effect::Pass); + EXPECT_EQ(p.eval(e, none, s3ListBucket, arn2), Effect::Pass); ARN arn3(Partition::aws, Service::iam, "", "", "role/example_role"); - EXPECT_EQ(p.eval(e, none, iamCreateRole, arn3), - Effect::Pass); + EXPECT_EQ(p.eval(e, none, iamCreateRole, arn3), Effect::Pass); } TEST_F(PolicyTest, Parse6) { @@ -731,13 +706,11 @@ TEST_F(PolicyTest, Eval6) { ARN arn1(Partition::aws, Service::iam, "", arbitrary_tenant, "user/A"); - EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), - Effect::Allow); + EXPECT_EQ(p.eval(e, none, iamCreateRole, arn1), Effect::Allow); ARN arn2(Partition::aws, Service::iam, "", arbitrary_tenant, "user/A"); - EXPECT_EQ(p.eval(e, none, s3ListBucket, arn2), - Effect::Allow); + EXPECT_EQ(p.eval(e, none, s3ListBucket, arn2), Effect::Allow); } TEST_F(PolicyTest, Parse7) { @@ -786,18 +759,15 @@ TEST_F(PolicyTest, Eval7) { ARN arn1(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket/*"); - EXPECT_EQ(p.eval(e, subacct, s3ListBucket, arn1), - Effect::Allow); - + EXPECT_EQ(p.eval(e, subacct, s3ListBucket, arn1), Effect::Allow); + ARN arn2(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket/*"); - EXPECT_EQ(p.eval(e, parentacct, s3ListBucket, arn2), - Effect::Pass); + EXPECT_EQ(p.eval(e, parentacct, s3ListBucket, arn2), Effect::Pass); ARN arn3(Partition::aws, Service::s3, "", arbitrary_tenant, "mybucket/*"); - EXPECT_EQ(p.eval(e, sub2acct, s3ListBucket, arn3), - Effect::Pass); + EXPECT_EQ(p.eval(e, sub2acct, s3ListBucket, arn3), Effect::Pass); } @@ -1283,43 +1253,43 @@ TEST_F(IPPolicyTest, EvalIPAddress) { ARN arn1(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(allowp.eval(e, trueacct, s3ListBucket, arn1), - Effect::Pass); + Effect::Pass); ARN arn2(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket/myobject"); EXPECT_EQ(fullp.eval(e, trueacct, s3ListBucket, arn2), - Effect::Pass); + Effect::Pass); ARN arn3(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(allowp.eval(allowedIP, trueacct, s3ListBucket, arn3), - Effect::Allow); + Effect::Allow); ARN arn4(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(allowp.eval(blocklistedIPv6, trueacct, s3ListBucket, arn4), - Effect::Pass); + Effect::Pass); ARN arn5(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(denyp.eval(allowedIP, trueacct, s3ListBucket, arn5), - Effect::Deny); + Effect::Deny); ARN arn6(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket/myobject"); EXPECT_EQ(denyp.eval(allowedIP, trueacct, s3ListBucket, arn6), - Effect::Deny); + Effect::Deny); ARN arn7(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(denyp.eval(blocklistedIP, trueacct, s3ListBucket, arn7), - Effect::Pass); + Effect::Pass); ARN arn8(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket/myobject"); EXPECT_EQ(denyp.eval(blocklistedIP, trueacct, s3ListBucket, arn8), - Effect::Pass); + Effect::Pass); ARN arn9(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket"); EXPECT_EQ(denyp.eval(blocklistedIPv6, trueacct, s3ListBucket, arn9), - Effect::Pass); + Effect::Pass); ARN arn10(Partition::aws, Service::s3, "", arbitrary_tenant, "example_bucket/myobject"); EXPECT_EQ(denyp.eval(blocklistedIPv6, trueacct, s3ListBucket, arn10), @@ -1555,20 +1525,20 @@ TEST(Condition, ArnLike) Condition ArnLike{TokenID::ArnLike, key.data(), key.size(), false}; ArnLike.vals.push_back("arn:aws:s3:::bucket"); - EXPECT_FALSE(ArnLike.eval({})); - EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::bucket"}})); - EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::BUCKET"}})); - EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::user"}})); + EXPECT_FALSE(ArnLike.eval({}, nullptr)); + EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::bucket"}}, nullptr)); + EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::BUCKET"}}, nullptr)); + EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::user"}}, nullptr)); } { Condition ArnLike{TokenID::ArnLike, key.data(), key.size(), false}; ArnLike.vals.push_back("arn:aws:s3:::b*"); - EXPECT_FALSE(ArnLike.eval({})); - EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::b"}})); - EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::bucket"}})); - EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::BUCKET"}})); - EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::user"}})); + EXPECT_FALSE(ArnLike.eval({}, nullptr)); + EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::b"}}, nullptr)); + EXPECT_TRUE(ArnLike.eval({{key, "arn:aws:s3:::bucket"}}, nullptr)); + EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::BUCKET"}}, nullptr)); + EXPECT_FALSE(ArnLike.eval({{key, "arn:aws:s3:::user"}}, nullptr)); } } @@ -1589,7 +1559,7 @@ protected: TEST_F(ConditionTest, StringNotEqualsLogic) { std::string key = "aws:UserName"; - + // Test case: value matches one of multiple condition values // Should return false because value equals at least one condition value { @@ -1599,11 +1569,11 @@ TEST_F(ConditionTest, StringNotEqualsLogic) stringNotEquals.vals.push_back("charlie"); // Input "bob" matches second condition value, should return false - EXPECT_FALSE(stringNotEquals.eval({{key, "bob"}})); - // Input "alice" matches first condition value, should return false - EXPECT_FALSE(stringNotEquals.eval({{key, "alice"}})); + EXPECT_FALSE(stringNotEquals.eval({{key, "bob"}}, nullptr)); + // Input "alice" matches first condition value, should return false + EXPECT_FALSE(stringNotEquals.eval({{key, "alice"}}, nullptr)); } - + // Test case: value doesn't match any condition values // Should return true because value differs from all condition values { @@ -1613,7 +1583,7 @@ TEST_F(ConditionTest, StringNotEqualsLogic) stringNotEquals.vals.push_back("charlie"); // Input "david" doesn't match any condition value, should return true - EXPECT_TRUE(stringNotEquals.eval({{key, "david"}})); + EXPECT_TRUE(stringNotEquals.eval({{key, "david"}}, nullptr)); } } @@ -1630,11 +1600,11 @@ TEST_F(ConditionTest, NumericNotEqualsLogic) numericNotEquals.vals.push_back("30"); // Input "20" matches second condition value, should return false - EXPECT_FALSE(numericNotEquals.eval({{key, "20"}})); + EXPECT_FALSE(numericNotEquals.eval({{key, "20"}}, nullptr)); // Input "10" matches first condition value, should return false - EXPECT_FALSE(numericNotEquals.eval({{key, "10"}})); + EXPECT_FALSE(numericNotEquals.eval({{key, "10"}}, nullptr)); } - + // Test case: value doesn't match any condition values // Should return true because value differs from all condition values { @@ -1644,14 +1614,14 @@ TEST_F(ConditionTest, NumericNotEqualsLogic) numericNotEquals.vals.push_back("30"); // Input "40" doesn't match any condition value, should return true - EXPECT_TRUE(numericNotEquals.eval({{key, "40"}})); + EXPECT_TRUE(numericNotEquals.eval({{key, "40"}}, nullptr)); } } TEST_F(ConditionTest, DateNotEqualsLogic) { std::string key = "aws:CurrentTime"; - + // Test case: value matches one of multiple condition values // Should return false because value equals at least one condition value { @@ -1661,9 +1631,9 @@ TEST_F(ConditionTest, DateNotEqualsLogic) dateNotEquals.vals.push_back("2023-12-01T00:00:00Z"); // Input matches second condition value, should return false - EXPECT_FALSE(dateNotEquals.eval({{key, "2023-06-01T00:00:00Z"}})); + EXPECT_FALSE(dateNotEquals.eval({{key, "2023-06-01T00:00:00Z"}}, nullptr)); } - + // Test case: value doesn't match any condition values // Should return true because value differs from all condition values { @@ -1673,14 +1643,14 @@ TEST_F(ConditionTest, DateNotEqualsLogic) dateNotEquals.vals.push_back("2023-12-01T00:00:00Z"); // Input doesn't match any condition value, should return true - EXPECT_TRUE(dateNotEquals.eval({{key, "2024-01-01T00:00:00Z"}})); + EXPECT_TRUE(dateNotEquals.eval({{key, "2024-01-01T00:00:00Z"}}, nullptr)); } } TEST_F(ConditionTest, NotIpAddressLogic) { std::string key = "aws:SourceIp"; - + // Test case: value matches one of multiple condition values // Should return false because value equals at least one condition value { @@ -1690,11 +1660,11 @@ TEST_F(ConditionTest, NotIpAddressLogic) notIpAddress.vals.push_back("172.16.0.1"); // Input matches second condition value, should return false - EXPECT_FALSE(notIpAddress.eval({{key, "10.0.0.1"}})); + EXPECT_FALSE(notIpAddress.eval({{key, "10.0.0.1"}}, nullptr)); // Input matches first condition value, should return false - EXPECT_FALSE(notIpAddress.eval({{key, "192.168.1.1"}})); + EXPECT_FALSE(notIpAddress.eval({{key, "192.168.1.1"}}, nullptr)); } - + // Test case: value doesn't match any condition values // Should return true because value differs from all condition values { @@ -1704,14 +1674,14 @@ TEST_F(ConditionTest, NotIpAddressLogic) notIpAddress.vals.push_back("172.16.0.1"); // Input doesn't match any condition value, should return true - EXPECT_TRUE(notIpAddress.eval({{key, "8.8.8.8"}})); + EXPECT_TRUE(notIpAddress.eval({{key, "8.8.8.8"}}, nullptr)); } } TEST_F(ConditionTest, ArnNotEqualsLogic) { std::string key = "aws:SourceArn"; - + // Test case: value matches one of multiple condition values // Should return false because value equals at least one condition value { @@ -1721,9 +1691,9 @@ TEST_F(ConditionTest, ArnNotEqualsLogic) arnNotEquals.vals.push_back("arn:aws:s3:::bucket3"); // Input matches second condition value, should return false - EXPECT_FALSE(arnNotEquals.eval({{key, "arn:aws:s3:::bucket2"}})); + EXPECT_FALSE(arnNotEquals.eval({{key, "arn:aws:s3:::bucket2"}}, nullptr)); } - + // Test case: value doesn't match any condition values // Should return true because value differs from all condition values { @@ -1733,14 +1703,15 @@ TEST_F(ConditionTest, ArnNotEqualsLogic) arnNotEquals.vals.push_back("arn:aws:s3:::bucket3"); // Input doesn't match any condition value, should return true - EXPECT_TRUE(arnNotEquals.eval({{key, "arn:aws:s3:::other-bucket"}})); + EXPECT_TRUE(arnNotEquals.eval({{key, "arn:aws:s3:::other-bucket"}}, + nullptr)); } } TEST_F(ConditionTest, StringNotLikeLogic) { std::string key = "s3:prefix"; - + // Test case: value matches one of multiple condition patterns // Should return false because value matches at least one condition pattern { @@ -1750,11 +1721,11 @@ TEST_F(ConditionTest, StringNotLikeLogic) stringNotLike.vals.push_back("temp/*"); // Input matches second condition pattern, should return false - EXPECT_FALSE(stringNotLike.eval({{key, "admin/config.txt"}})); + EXPECT_FALSE(stringNotLike.eval({{key, "admin/config.txt"}}, nullptr)); // Input matches first condition pattern, should return false - EXPECT_FALSE(stringNotLike.eval({{key, "user/profile.jpg"}})); + EXPECT_FALSE(stringNotLike.eval({{key, "user/profile.jpg"}}, nullptr)); } - + // Test case: value doesn't match any condition patterns // Should return true because value differs from all condition patterns { @@ -1764,7 +1735,7 @@ TEST_F(ConditionTest, StringNotLikeLogic) stringNotLike.vals.push_back("temp/*"); // Input doesn't match any condition pattern, should return true - EXPECT_TRUE(stringNotLike.eval({{key, "public/document.pdf"}})); + EXPECT_TRUE(stringNotLike.eval({{key, "public/document.pdf"}}, nullptr)); } } @@ -1777,8 +1748,8 @@ TEST_F(ConditionTest, Null) Condition isNull{TokenID::Null, key.data(), key.size(), false}; isNull.vals.push_back("true"); - EXPECT_TRUE(isNull.eval({})); - EXPECT_FALSE(isNull.eval({{key, "admin/config.txt"}})); + EXPECT_TRUE(isNull.eval({}, nullptr)); + EXPECT_FALSE(isNull.eval({{key, "admin/config.txt"}}, nullptr)); } { @@ -1786,8 +1757,8 @@ TEST_F(ConditionTest, Null) Condition notNull{TokenID::Null, key.data(), key.size(), false}; notNull.vals.push_back("false"); - EXPECT_FALSE(notNull.eval({})); - EXPECT_TRUE(notNull.eval({{key, "admin/config.txt"}})); + EXPECT_FALSE(notNull.eval({}, nullptr)); + EXPECT_TRUE(notNull.eval({{key, "admin/config.txt"}}, nullptr)); } } @@ -1799,32 +1770,32 @@ TEST_F(ConditionTest, KeyStoneRoleStringEquals) cond.vals.push_back("testrole"); // No roles - EXPECT_FALSE(cond.eval({})); + EXPECT_FALSE(cond.eval({}, nullptr)); // Single matching role - EXPECT_TRUE(cond.eval({{key, "testrole"}})); + EXPECT_TRUE(cond.eval({{key, "testrole"}}, nullptr)); // Single non-matching role - EXPECT_FALSE(cond.eval({{key, "member"}})); + EXPECT_FALSE(cond.eval({{key, "member"}}, nullptr)); //Multiple roles in env,one matches Environment multi_env; multi_env.emplace(key, "member"); multi_env.emplace(key, "testrole"); multi_env.emplace(key, "reader"); - EXPECT_TRUE(cond.eval(multi_env)); + EXPECT_TRUE(cond.eval(multi_env, nullptr)); //Multiple roles in env, no match Environment no_match_env; no_match_env.emplace(key, "member"); no_match_env.emplace(key, "reader"); - EXPECT_FALSE(cond.eval(no_match_env)); + EXPECT_FALSE(cond.eval(no_match_env, nullptr)); // Multiple identical roles (redundancy check) Environment duplicate_env; duplicate_env.emplace(key, "testrole"); duplicate_env.emplace(key, "testrole"); - EXPECT_TRUE(cond.eval(duplicate_env)); + EXPECT_TRUE(cond.eval(duplicate_env, nullptr)); } TEST_F(ConditionTest, KeyStoneRoleNotStringEquals) @@ -1835,26 +1806,26 @@ TEST_F(ConditionTest, KeyStoneRoleNotStringEquals) cond.vals.push_back("admin"); // No roles - EXPECT_FALSE(cond.eval({})); + EXPECT_FALSE(cond.eval({}, nullptr)); // Role matches - EXPECT_FALSE(cond.eval({{key, "admin"}})); + EXPECT_FALSE(cond.eval({{key, "admin"}}, nullptr)); // Role doesn't match - EXPECT_TRUE(cond.eval({{key, "member"}})); + EXPECT_TRUE(cond.eval({{key, "member"}}, nullptr)); // Multiple roles in env, one matches -> false Environment multi_env; multi_env.emplace(key, "member"); multi_env.emplace(key, "admin"); EXPECT_FALSE(multi_env.count(key) == 0); - EXPECT_FALSE(cond.eval(multi_env)); + EXPECT_FALSE(cond.eval(multi_env, nullptr)); // Multiple roles, none match -> true Environment no_match_env; no_match_env.emplace(key, "member"); no_match_env.emplace(key, "reader"); - EXPECT_TRUE(cond.eval(no_match_env)); + EXPECT_TRUE(cond.eval(no_match_env, nullptr)); } TEST_F(ConditionTest, KeyStoneRolePolicyParsing) @@ -1893,18 +1864,18 @@ TEST_F(ConditionTest, KeyStoneRolePolicyParsing) // Eval with matching role in environment Environment match_env; match_env.emplace("keystone:role", "testrole"); - EXPECT_TRUE(p->statements[0].conditions[0].eval(match_env)); + EXPECT_TRUE(p->statements[0].conditions[0].eval(match_env, nullptr)); // Eval with non-matching role Environment nomatch_env; nomatch_env.emplace("keystone:role", "member"); - EXPECT_FALSE(p->statements[0].conditions[0].eval(nomatch_env)); + EXPECT_FALSE(p->statements[0].conditions[0].eval(nomatch_env, nullptr)); // Eval with multiple roles, one matching Environment multi_env; multi_env.emplace("keystone:role", "member"); multi_env.emplace("keystone:role", "testrole"); - EXPECT_TRUE(p->statements[0].conditions[0].eval(multi_env)); + EXPECT_TRUE(p->statements[0].conditions[0].eval(multi_env, nullptr)); } TEST_F(ConditionTest, KeystoneUserIdStringEquals) @@ -1913,7 +1884,7 @@ TEST_F(ConditionTest, KeystoneUserIdStringEquals) Condition cond{TokenID::StringEquals, key.data(), key.size(), false}; cond.vals.push_back("user-123"); - EXPECT_FALSE(cond.eval({})); - EXPECT_TRUE(cond.eval({{key, "user-123"}})); - EXPECT_FALSE(cond.eval({{key, "user-456"}})); -} \ No newline at end of file + EXPECT_FALSE(cond.eval({}, nullptr)); + EXPECT_TRUE(cond.eval({{key, "user-123"}}, nullptr)); + EXPECT_FALSE(cond.eval({{key, "user-456"}}, nullptr)); +}