From: Adam C. Emerson Date: Tue, 9 Aug 2016 16:28:07 +0000 (-0400) Subject: rgw: Add basic support for IAM policies X-Git-Tag: v12.1.0~10^2~88^2~4 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=69a5eebd8a45d68cd67566545d741029a4399858;p=ceph.git rgw: Add basic support for IAM policies This support is currently incomplete but should provide a starting point. Signed-off-by: Adam C. Emerson --- diff --git a/ceph.spec.in b/ceph.spec.in index 339adc627bf5..40704d68b53b 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -97,6 +97,7 @@ BuildRequires: selinux-policy-devel BuildRequires: /usr/share/selinux/devel/policyhelp %endif BuildRequires: bc +BuildRequires: gperf BuildRequires: cmake BuildRequires: cryptsetup BuildRequires: fuse-devel diff --git a/debian/control b/debian/control index eb91c40b65f0..43b582b4e0dc 100644 --- a/debian/control +++ b/debian/control @@ -9,6 +9,7 @@ Uploaders: Ken Dreyer , Alfredo Deza Build-Depends: bc, btrfs-tools, + gperf, cmake, cpio, cryptsetup-bin | cryptsetup, diff --git a/install-deps.sh b/install-deps.sh index 90347622e29e..42c34e68f434 100755 --- a/install-deps.sh +++ b/install-deps.sh @@ -22,6 +22,7 @@ export LC_ALL=C # the following is vulnerable to i18n if [ x`uname`x = xFreeBSDx ]; then $SUDO pkg install -yq \ devel/git \ + devel/gperf \ devel/gmake \ devel/cmake \ devel/yasm \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 01f3de06eeec..387a7d30731b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -335,6 +335,7 @@ add_library(crush_objs OBJECT ${crush_srcs}) add_subdirectory(json_spirit) include_directories("${CMAKE_SOURCE_DIR}/src/xxHash") +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/src/rapidjson/include") set(GMOCK_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/src/googletest/googletest/include/gmock") diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index ede95a5bd8e7..53583652acdf 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -25,6 +25,15 @@ add_custom_target(civetweb_h "${CMAKE_BINARY_DIR}/src/include/civetweb" COMMENT "keep civetweb.h up-to-date") +function(gperf_generate input output) + add_custom_command( + OUTPUT ${output} + COMMAND gperf ${input} > ${output} + DEPENDS ${input} + COMMENT "Generate ${output}" + ) +endfunction() + set(rgw_a_srcs rgw_acl.cc rgw_acl_s3.cc @@ -103,7 +112,15 @@ set(rgw_a_srcs rgw_xml_enc.cc rgw_torrent.cc rgw_crypt.cc - rgw_crypt_sanitize.cc) + rgw_crypt_sanitize.cc + rgw_iam_policy.cc) + +gperf_generate(${CMAKE_SOURCE_DIR}/src/rgw/rgw_iam_policy_keywords.gperf + rgw_iam_policy_keywords.frag.cc) +set_source_files_properties(rgw_iam_policy.cc PROPERTIES + OBJECT_DEPENDS ${CMAKE_BINARY_DIR}/src/rgw/rgw_iam_policy_keywords.frag.cc + COMPILE_FLAGS -I${CMAKE_BINARY_DIR}/src/rgw) + if (WITH_RADOSGW_FCGI_FRONTEND) list(APPEND rgw_a_srcs rgw_fcgi.cc) @@ -115,7 +132,9 @@ add_dependencies(rgw_a civetweb_h) target_include_directories(rgw_a PUBLIC "../Beast/include" - ${FCGI_INCLUDE_DIR}) + ${FCGI_INCLUDE_DIR} + "../rapidjson/include" + ) target_compile_definitions(rgw_a PUBLIC BOOST_COROUTINES_NO_DEPRECATION_WARNING) target_link_libraries(rgw_a librados cls_lock_client cls_rgw_client cls_refcount_client diff --git a/src/rgw/rgw_basic_types.cc b/src/rgw/rgw_basic_types.cc index 5ebf1cfe4460..c16d920f21b0 100644 --- a/src/rgw/rgw_basic_types.cc +++ b/src/rgw/rgw_basic_types.cc @@ -1,6 +1,13 @@ +#include +#include +#include + #include "rgw_basic_types.h" #include "common/ceph_json.h" +using std::string; +using std::stringstream; + void decode_json_obj(rgw_user& val, JSONObj *obj) { string s = obj->get_data(); @@ -12,3 +19,24 @@ void encode_json(const char *name, const rgw_user& val, Formatter *f) string s = val.to_str(); f->dump_string(name, s); } + +namespace rgw { +namespace auth { +ostream& operator <<(ostream& m, const Principal& p) { + if (p.is_wildcard()) { + return m << "*"; + } + + m << "arn:aws:iam:" << p.get_tenant() << ":"; + if (p.is_tenant()) { + return m << "root"; + } + return m << (p.is_user() ? "user/" : "role/") << p.get_id(); +} +string to_string(const Principal& p) { + stringstream s; + s << p; + return s.str(); +} +} +} diff --git a/src/rgw/rgw_basic_types.h b/src/rgw/rgw_basic_types.h index 4e76fa8c07e0..31e9d3a32acb 100644 --- a/src/rgw/rgw_basic_types.h +++ b/src/rgw/rgw_basic_types.h @@ -177,6 +177,9 @@ public: return (t < o.t) || ((t == o.t) && (u < o.u)); } }; + +std::ostream& operator <<(std::ostream& m, const Principal& p); +std::string to_string(const Principal& p); } } diff --git a/src/rgw/rgw_common.cc b/src/rgw/rgw_common.cc index cb9040d64ab7..f0a34a5e22bb 100644 --- a/src/rgw/rgw_common.cc +++ b/src/rgw/rgw_common.cc @@ -1756,7 +1756,9 @@ int match(const string& pattern, const string& input, uint32_t flag) string substr_pattern = pattern.substr(last_pos_pattern, cur_pos_pattern); int res; - if (flag & MATCH_POLICY_ACTION || flag & MATCH_POLICY_ARN) { + if (substr_pattern == "*") { + res = 1; + } else if (flag & MATCH_POLICY_ACTION || flag & MATCH_POLICY_ARN) { res = match_internal(substr_pattern, substr_input, &matchignorecase); } else { res = match_internal(substr_pattern, substr_input, &matchcase); @@ -1767,7 +1769,7 @@ int match(const string& pattern, const string& input, uint32_t flag) if (cur_pos_pattern == string::npos && cur_pos_input == string::npos) return 1; else if ((cur_pos_pattern == string::npos && cur_pos_input != string::npos) || - (cur_pos_pattern != string::npos && cur_pos_input == string::npos)) + (cur_pos_pattern != string::npos && cur_pos_input == string::npos)) return 0; last_pos_pattern = cur_pos_pattern + 1; diff --git a/src/rgw/rgw_common.h b/src/rgw/rgw_common.h index c615008e2713..3fb453938922 100644 --- a/src/rgw/rgw_common.h +++ b/src/rgw/rgw_common.h @@ -1349,12 +1349,14 @@ namespace rgw { namespace s3 { class RGWGetPolicyV2Extractor; } + class Completer; } namespace io { class BasicClient; } } + struct req_info { RGWEnv *env; RGWHTTPArgs args; diff --git a/src/rgw/rgw_iam_policy.cc b/src/rgw/rgw_iam_policy.cc new file mode 100644 index 000000000000..c12e243f1e00 --- /dev/null +++ b/src/rgw/rgw_iam_policy.cc @@ -0,0 +1,1514 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + + +#include +#include +#include +#include +#include + +#include "rapidjson/reader.h" + +#include "rgw_auth.h" +#include "rgw_iam_policy.h" + +namespace { +constexpr int dout_subsys = ceph_subsys_rgw; +} + +using std::bitset; +using std::find; +using std::int64_t; +using std::move; +using std::pair; +using std::regex; +using std::regex_match; +using std::size_t; +using std::smatch; +using std::string; +using std::stringstream; +using std::ostream; +using std::uint16_t; +using std::uint64_t; +using std::unordered_map; + +using boost::container::flat_set; +using boost::none; +using boost::optional; + +using rapidjson::BaseReaderHandler; +using rapidjson::UTF8; +using rapidjson::SizeType; +using rapidjson::Reader; +using rapidjson::kParseCommentsFlag; +using rapidjson::kParseNumbersAsStringsFlag; +using rapidjson::StringStream; +using rapidjson::ParseResult; + +using rgw::auth::Principal; + +namespace rgw { +namespace IAM { +#include "rgw_iam_policy_keywords.frag.cc" + +struct actpair { + const char* name; + const uint64_t bit; +}; + +namespace { +optional to_partition(const smatch::value_type& p, + bool wildcards) { + if (p == "aws") { + return Partition::aws; + } else if (p == "aws-cn") { + return Partition::aws_cn; + } else if (p == "aws-us-gov") { + return Partition::aws_us_gov; + } else if (p == "*" && wildcards) { + return Partition::wildcard; + } else { + return none; + } + + ceph_abort(); +} + +optional to_service(const smatch::value_type& s, + bool wildcards) { + static const unordered_map services = { + { "acm", Service::acm }, + { "apigateway", Service::apigateway }, + { "appstream", Service::appstream }, + { "artifact", Service::artifact }, + { "autoscaling", Service::autoscaling }, + { "aws-marketplace", Service::aws_marketplace }, + { "aws-marketplace-management", + Service::aws_marketplace_management }, + { "aws-portal", Service::aws_portal }, + { "cloudformation", Service::cloudformation }, + { "cloudfront", Service::cloudfront }, + { "cloudhsm", Service::cloudhsm }, + { "cloudsearch", Service::cloudsearch }, + { "cloudtrail", Service::cloudtrail }, + { "cloudwatch", Service::cloudwatch }, + { "codebuild", Service::codebuild }, + { "codecommit", Service::codecommit }, + { "codedeploy", Service::codedeploy }, + { "codepipeline", Service::codepipeline }, + { "cognito-identity", Service::cognito_identity }, + { "cognito-idp", Service::cognito_idp }, + { "cognito-sync", Service::cognito_sync }, + { "config", Service::config }, + { "datapipeline", Service::datapipeline }, + { "devicefarm", Service::devicefarm }, + { "directconnect", Service::directconnect }, + { "dms", Service::dms }, + { "ds", Service::ds }, + { "dynamodb", Service::dynamodb }, + { "ec2", Service::ec2 }, + { "ecr", Service::ecr }, + { "ecs", Service::ecs }, + { "elasticache", Service::elasticache }, + { "elasticbeanstalk", Service::elasticbeanstalk }, + { "elasticfilesystem", Service::elasticfilesystem }, + { "elasticloadbalancing", Service::elasticloadbalancing }, + { "elasticmapreduce", Service::elasticmapreduce }, + { "elastictranscoder", Service::elastictranscoder }, + { "es", Service::es }, + { "events", Service::events }, + { "firehose", Service::firehose }, + { "gamelift", Service::gamelift }, + { "glacier", Service::glacier }, + { "health", Service::health }, + { "iam", Service::iam }, + { "importexport", Service::importexport }, + { "inspector", Service::inspector }, + { "iot", Service::iot }, + { "kinesis", Service::kinesis }, + { "kinesisanalytics", Service::kinesisanalytics }, + { "kms", Service::kms }, + { "lambda", Service::lambda }, + { "lightsail", Service::lightsail }, + { "logs", Service::logs }, + { "machinelearning", Service::machinelearning }, + { "mobileanalytics", Service::mobileanalytics }, + { "mobilehub", Service::mobilehub }, + { "opsworks", Service::opsworks }, + { "opsworks-cm", Service::opsworks_cm }, + { "polly", Service::polly }, + { "rds", Service::rds }, + { "redshift", Service::redshift }, + { "route53", Service::route53 }, + { "route53domains", Service::route53domains }, + { "s3", Service::s3 }, + { "sdb", Service::sdb }, + { "servicecatalog", Service::servicecatalog }, + { "ses", Service::ses }, + { "sns", Service::sns }, + { "sqs", Service::sqs }, + { "ssm", Service::ssm }, + { "states", Service::states }, + { "storagegateway", Service::storagegateway }, + { "sts", Service::sts }, + { "support", Service::support }, + { "swf", Service::swf }, + { "trustedadvisor", Service::trustedadvisor }, + { "waf", Service::waf }, + { "workmail", Service::workmail }, + { "workspaces", Service::workspaces }}; + + if (wildcards && s == "*") { + return Service::wildcard; + } + + auto i = services.find(s); + if (i == services.end()) { + return none; + } else { + return i->second; + } +} +} + +ARN::ARN(const rgw_obj& o) + : partition(Partition::aws), + service(Service::s3), + region(), + account(o.bucket.tenant), + resource(o.bucket.name) +{ + resource.push_back('/'); + resource.append(o.key.name); +} + +ARN::ARN(const rgw_bucket& b) + : partition(Partition::aws), + service(Service::s3), + region(), + account(b.tenant), + resource(b.name) { } + +ARN::ARN(const rgw_bucket& b, const string& o) + : partition(Partition::aws), + service(Service::s3), + region(), + account(b.tenant), + resource(b.name) { + resource.push_back('/'); + resource.append(o); +} + +optional ARN::parse(const string& s, bool wildcards) { + static const regex rx_wild("arn:([^:]*):([^:]*):([^:]*):([^:]*):([^:]*)", + std::regex_constants::ECMAScript | + std::regex_constants::optimize); + static const regex rx_no_wild( + "arn:([^:*]*):([^:*]*):([^:*]*):([^:*]*):([^:*]*)", + std::regex_constants::ECMAScript | + std::regex_constants::optimize); + + smatch match; + + if ((s == "*") && wildcards) { + return ARN(Partition::wildcard, Service::wildcard, "*", "*", "*"); + } else if (regex_match(s, match, wildcards ? rx_wild : rx_no_wild)) { + ceph_assert(match.size() == 6); + + ARN a; + { + auto p = to_partition(match[1], wildcards); + if (!p) + return none; + + a.partition = *p; + } + { + auto s = to_service(match[2], wildcards); + if (!s) { + return none; + } + a.service = *s; + } + + a.region = match[3]; + a.account = match[4]; + a.resource = match[5]; + + return a; + } + return none; +} + +string ARN::to_string() const { + string s; + + if (partition == Partition::aws) { + s.append("aws:"); + } else if (partition == Partition::aws_cn) { + s.append("aws-cn:"); + } else if (partition == Partition::aws_us_gov) { + s.append("aws-us-gov:"); + } else { + s.append("*:"); + } + + static const unordered_map services = { + { Service::acm, "acm" }, + { Service::apigateway, "apigateway" }, + { Service::appstream, "appstream" }, + { Service::artifact, "artifact" }, + { Service::autoscaling, "autoscaling" }, + { Service::aws_marketplace, "aws-marketplace" }, + { Service::aws_marketplace_management, "aws-marketplace-management" }, + { Service::aws_portal, "aws-portal" }, + { Service::cloudformation, "cloudformation" }, + { Service::cloudfront, "cloudfront" }, + { Service::cloudhsm, "cloudhsm" }, + { Service::cloudsearch, "cloudsearch" }, + { Service::cloudtrail, "cloudtrail" }, + { Service::cloudwatch, "cloudwatch" }, + { Service::codebuild, "codebuild" }, + { Service::codecommit, "codecommit" }, + { Service::codedeploy, "codedeploy" }, + { Service::codepipeline, "codepipeline" }, + { Service::cognito_identity, "cognito-identity" }, + { Service::cognito_idp, "cognito-idp" }, + { Service::cognito_sync, "cognito-sync" }, + { Service::config, "config" }, + { Service::datapipeline, "datapipeline" }, + { Service::devicefarm, "devicefarm" }, + { Service::directconnect, "directconnect" }, + { Service::dms, "dms" }, + { Service::ds, "ds" }, + { Service::dynamodb, "dynamodb" }, + { Service::ec2, "ec2" }, + { Service::ecr, "ecr" }, + { Service::ecs, "ecs" }, + { Service::elasticache, "elasticache" }, + { Service::elasticbeanstalk, "elasticbeanstalk" }, + { Service::elasticfilesystem, "elasticfilesystem" }, + { Service::elasticloadbalancing, "elasticloadbalancing" }, + { Service::elasticmapreduce, "elasticmapreduce" }, + { Service::elastictranscoder, "elastictranscoder" }, + { Service::es, "es" }, + { Service::events, "events" }, + { Service::firehose, "firehose" }, + { Service::gamelift, "gamelift" }, + { Service::glacier, "glacier" }, + { Service::health, "health" }, + { Service::iam, "iam" }, + { Service::importexport, "importexport" }, + { Service::inspector, "inspector" }, + { Service::iot, "iot" }, + { Service::kinesis, "kinesis" }, + { Service::kinesisanalytics, "kinesisanalytics" }, + { Service::kms, "kms" }, + { Service::lambda, "lambda" }, + { Service::lightsail, "lightsail" }, + { Service::logs, "logs" }, + { Service::machinelearning, "machinelearning" }, + { Service::mobileanalytics, "mobileanalytics" }, + { Service::mobilehub, "mobilehub" }, + { Service::opsworks, "opsworks" }, + { Service::opsworks_cm, "opsworks-cm" }, + { Service::polly, "polly" }, + { Service::rds, "rds" }, + { Service::redshift, "redshift" }, + { Service::route53, "route53" }, + { Service::route53domains, "route53domains" }, + { Service::s3, "s3" }, + { Service::sdb, "sdb" }, + { Service::servicecatalog, "servicecatalog" }, + { Service::ses, "ses" }, + { Service::sns, "sns" }, + { Service::sqs, "sqs" }, + { Service::ssm, "ssm" }, + { Service::states, "states" }, + { Service::storagegateway, "storagegateway" }, + { Service::sts, "sts" }, + { Service::support, "support" }, + { Service::swf, "swf" }, + { Service::trustedadvisor, "trustedadvisor" }, + { Service::waf, "waf" }, + { Service::workmail, "workmail" }, + { Service::workspaces, "workspaces" }}; + + auto i = services.find(service); + if (i != services.end()) { + s.append(i->second); + } else { + s.push_back('*'); + } + s.push_back(':'); + + s.append(region); + s.push_back(':'); + + s.append(account); + s.push_back(':'); + + s.append(resource); + + return s; +} + +bool operator ==(const ARN& l, const ARN& r) { + return ((l.partition == r.partition) && + (l.service == r.service) && + (l.region == r.region) && + (l.account == r.account) && + (l.resource == r.resource)); +} +bool operator <(const ARN& l, const ARN& r) { + return ((l.partition < r.partition) || + (l.service < r.service) || + (l.region < r.region) || + (l.account < r.account) || + (l.resource < r.resource)); +} + +// The candidate is not allowed to have wildcards. The only way to +// do that sanely would be to use unification rather than matching. +bool ARN::match(const ARN& candidate) const { + if ((candidate.partition == Partition::wildcard) || + (partition != candidate.partition && partition + != Partition::wildcard)) { + return false; + } + + if ((candidate.service == Service::wildcard) || + (service != candidate.service && service != Service::wildcard)) { + return false; + } + + if (!::match(region, candidate.region, MATCH_POLICY_ARN)) { + return false; + } + + if (!::match(account, candidate.account, MATCH_POLICY_ARN)) { + return false; + } + + if (!::match(resource, candidate.resource, MATCH_POLICY_ARN)) { + return false; + } + + return true; +} + +static const actpair actpairs[] = +{{ "s3:AbortMultipartUpload", s3AbortMultipartUpload }, + { "s3:CreateBucket", s3CreateBucket }, + { "s3:DeleteBucketPolicy", s3DeleteBucketPolicy }, + { "s3:DeleteBucket", s3DeleteBucket }, + { "s3:DeleteBucketWebsite", s3DeleteBucketWebsite }, + { "s3:DeleteObject", s3DeleteObject }, + { "s3:DeleteObjectVersion", s3DeleteObjectVersion }, + { "s3:DeleteReplicationConfiguration", s3DeleteReplicationConfiguration }, + { "s3:GetAccelerateConfiguration", s3GetAccelerateConfiguration }, + { "s3:GetBucketAcl", s3GetBucketAcl }, + { "s3:GetBucketCORS", s3GetBucketCORS }, + { "s3:GetBucketLocation", s3GetBucketLocation }, + { "s3:GetBucketLogging", s3GetBucketLogging }, + { "s3:GetBucketNotification", s3GetBucketNotification }, + { "s3:GetBucketPolicy", s3GetBucketPolicy }, + { "s3:GetBucketRequestPayment", s3GetBucketRequestPayment }, + { "s3:GetBucketTagging", s3GetBucketTagging }, + { "s3:GetBucketVersioning", s3GetBucketVersioning }, + { "s3:GetBucketWebsite", s3GetBucketWebsite }, + { "s3:GetLifecycleConfiguration", s3GetLifecycleConfiguration }, + { "s3:GetObjectAcl", s3GetObjectAcl }, + { "s3:GetObject", s3GetObject }, + { "s3:GetObjectTorrent", s3GetObjectTorrent }, + { "s3:GetObjectVersionAcl", s3GetObjectVersionAcl }, + { "s3:GetObjectVersion", s3GetObjectVersion }, + { "s3:GetObjectVersionTorrent", s3GetObjectVersionTorrent }, + { "s3:GetReplicationConfiguration", s3GetReplicationConfiguration }, + { "s3:ListAllMyBuckets", s3ListAllMyBuckets }, + { "s3:ListBucketMultiPartUploads", s3ListBucketMultiPartUploads }, + { "s3:ListBucket", s3ListBucket }, + { "s3:ListBucketVersions", s3ListBucketVersions }, + { "s3:ListMultipartUploadParts", s3ListMultipartUploadParts }, + { "s3:PutAccelerateConfiguration", s3PutAccelerateConfiguration }, + { "s3:PutBucketAcl", s3PutBucketAcl }, + { "s3:PutBucketCORS", s3PutBucketCORS }, + { "s3:PutBucketLogging", s3PutBucketLogging }, + { "s3:PutBucketNotification", s3PutBucketNotification }, + { "s3:PutBucketPolicy", s3PutBucketPolicy }, + { "s3:PutBucketRequestPayment", s3PutBucketRequestPayment }, + { "s3:PutBucketTagging", s3PutBucketTagging }, + { "s3:PutBucketVersioning", s3PutBucketVersioning }, + { "s3:PutBucketWebsite", s3PutBucketWebsite }, + { "s3:PutLifecycleConfiguration", s3PutLifecycleConfiguration }, + { "s3:PutObjectAcl", s3PutObjectAcl }, + { "s3:PutObject", s3PutObject }, + { "s3:PutObjectVersionAcl", s3PutObjectVersionAcl }, + { "s3:PutReplicationConfiguration", s3PutReplicationConfiguration }, + { "s3:RestoreObject", s3RestoreObject }}; + +struct PolicyParser; + +const Keyword top[1]{"", TokenKind::pseudo, TokenID::Top, 0, false, + false}; +const Keyword cond_key[1]{"", TokenKind::cond_key, + TokenID::CondKey, 0, true, false}; + +struct ParseState { + PolicyParser* pp; + const Keyword* w; + + bool arraying = false; + bool objecting = false; + + void reset(); + + ParseState(PolicyParser* pp, const Keyword* w) + : pp(pp), w(w) {} + + bool obj_start(); + + bool obj_end(); + + bool array_start() { + if (w->arrayable && !arraying) { + arraying = true; + return true; + } + return false; + } + + bool array_end(); + + bool key(const char* s, size_t l); + bool do_string(CephContext* cct, const char* s, size_t l); + bool number(const char* str, size_t l); +}; + +// If this confuses you, look up the Curiously Recurring Template Pattern +struct PolicyParser : public BaseReaderHandler, PolicyParser> { + keyword_hash tokens; + std::vector s; + CephContext* cct; + const string& tenant; + Policy& policy; + + uint32_t seen = 0; + + uint32_t dex(TokenID in) const { + switch (in) { + case TokenID::Version: + return 0x1; + case TokenID::Id: + return 0x2; + case TokenID::Statement: + return 0x4; + case TokenID::Sid: + return 0x8; + case TokenID::Effect: + return 0x10; + case TokenID::Principal: + return 0x20; + case TokenID::NotPrincipal: + return 0x40; + case TokenID::Action: + return 0x80; + case TokenID::NotAction: + return 0x100; + case TokenID::Resource: + return 0x200; + case TokenID::NotResource: + return 0x400; + case TokenID::Condition: + return 0x800; + case TokenID::AWS: + return 0x1000; + case TokenID::Federated: + return 0x2000; + case TokenID::Service: + return 0x4000; + case TokenID::CanonicalUser: + return 0x8000; + default: + ceph_abort(); + } + } + bool test(TokenID in) { + return seen & dex(in); + } + void set(TokenID in) { + seen |= dex(in); + } + void set(std::initializer_list l) { + for (auto in : l) { + seen |= dex(in); + } + } + void reset(TokenID in) { + seen &= ~dex(in); + } + void reset(std::initializer_list l) { + for (auto in : l) { + seen &= ~dex(in); + } + } + + PolicyParser(CephContext* cct, const string& tenant, Policy& policy) + : cct(cct), tenant(tenant), policy(policy) {} + PolicyParser(const PolicyParser& policy) = delete; + + bool StartObject() { + if (s.empty()) { + s.push_back({this, top}); + s.back().objecting = true; + return true; + } + + return s.back().obj_start(); + } + bool EndObject(SizeType memberCount) { + if (s.empty()) { + return false; + } + + return s.back().obj_end(); + } + bool Key(const char* str, SizeType length, bool copy) { + if (s.empty()) { + return false; + } + + return s.back().key(str, length); + } + + bool String(const char* str, SizeType length, bool copy) { + if (s.empty()) { + return false; + } + + return s.back().do_string(cct, str, length); + } + bool RawNumber(const char* str, SizeType length, bool copy) { + if (s.empty()) { + return false; + } + + return s.back().number(str, length); + } + bool StartArray() { + if (s.empty()) { + return false; + } + + return s.back().array_start(); + } + bool EndArray(SizeType) { + if (s.empty()) { + return false; + } + + return s.back().array_end(); + } + + bool Default() { + return false; + } +}; + + +// I really despise this misfeature of C++. +// +bool ParseState::obj_end() { + if (objecting) { + objecting = false; + if (!arraying) { + pp->s.pop_back(); + } else { + reset(); + } + return true; + } + return false; +} + +bool ParseState::key(const char* s, size_t l) { + auto k = pp->tokens.lookup(s, l); + + if (!k) { + if (w->kind == TokenKind::cond_op) { + auto& t = pp->policy.statements.back(); + pp->s.emplace_back(pp, cond_key); + t.conditions.emplace_back(w->id, s, l); + return true; + } else { + return false; + } + } + + // If the token we're going with belongs within the condition at the + // top of the stack and we haven't already encountered it, push it + // on the stack + + // Top + if ((((w->id == TokenID::Top) && (k->kind == TokenKind::top)) || + // Statement + ((w->id == TokenID::Statement) && (k->kind == TokenKind::statement)) || + + /// Principal + ((w->id == TokenID::Principal || w->id == TokenID::NotPrincipal) && + (k->kind == TokenKind::princ_type))) && + + // Check that it hasn't been encountered. Note that this + // conjoins with the run of disjunctions above. + !pp->test(k->id)) { + pp->set(k->id); + pp->s.emplace_back(pp, k); + return true; + } else if ((w->id == TokenID::Condition) && + (k->kind == TokenKind::cond_op)) { + pp->s.emplace_back(pp, k); + return true; + } + return false; +} + +// I should just rewrite a few helper functions to use iterators, +// which will make all of this ever so much nicer. +static optional parse_principal(CephContext* cct, TokenID t, + string&& s) { + // Wildcard! + if ((t == TokenID::AWS) && (s == "*")) { + return Principal::wildcard(); + + // Do nothing for now. + } else if (t == TokenID::CanonicalUser) { + + // AWS ARNs + } else if (t == TokenID::AWS) { + auto a = ARN::parse(s); + if (!a) { + 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::tenant(std::move(s)); + } + } + + if (a->resource == "root") { + return Principal::tenant(std::move(a->account)); + } + + static const regex rx("([^/]*)/(.*)", + std::regex_constants::ECMAScript | + std::regex_constants::optimize); + smatch match; + if (regex_match(a->resource, match, rx)) { + ceph_assert(match.size() == 2); + + 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]); + } + } + } + + ldout(cct, 0) << "Supplied principal is discarded: " << s << dendl; + return boost::none; +} + +bool ParseState::do_string(CephContext* cct, const char* s, size_t l) { + auto k = pp->tokens.lookup(s, l); + Policy& p = pp->policy; + Statement* t = p.statements.empty() ? nullptr : &(p.statements.back()); + + // Top level! + if ((w->id == TokenID::Version) && k && + k->kind == TokenKind::version_key) { + p.version = static_cast(k->specific); + } else if (w->id == TokenID::Id) { + p.id = string(s, l); + + // Statement + + } else if (w->id == TokenID::Sid) { + t->sid.emplace(s, l); + } else if ((w->id == TokenID::Effect) && + k->kind == TokenKind::effect_key) { + t->effect = static_cast(k->specific); + } else if (w->id == TokenID::Principal && s && *s == '*') { + t->princ.emplace(Principal::wildcard()); + } else if (w->id == TokenID::NotPrincipal && s && *s == '*') { + t->noprinc.emplace(Principal::wildcard()); + } else if ((w->id == TokenID::Action) || + (w->id == TokenID::NotAction)) { + for (auto& p : actpairs) { + if (match({s, l}, p.name, MATCH_POLICY_ACTION)) { + (w->id == TokenID::Action ? t->action : t->notaction) |= p.bit; + } + } + } else if (w->id == TokenID::Resource || w->id == TokenID::NotResource) { + auto a = ARN::parse({s, l}, true); + // You can't specify resources for someone ELSE'S account. + if (a && (a->account.empty() || a->account == pp->tenant || + a->account == "*")) { + if (a->account.empty() || a->account == "*") + a->account = pp->tenant; + (w->id == TokenID::Resource ? t->resource : t->notresource) + .emplace(std::move(*a)); + } + else + ldout(cct, 0) << "Supplied resource is discarded: " << string(s, l) + << dendl; + } else if (w->kind == TokenKind::cond_key) { + auto& t = pp->policy.statements.back(); + t.conditions.back().vals.emplace_back(s, l); + + // Principals + + } else if (w->kind == TokenKind::princ_type) { + ceph_assert(pp->s.size() > 1); + auto& pri = pp->s[pp->s.size() - 2].w->id == TokenID::Principal ? + t->princ : t->noprinc; + + auto o = parse_principal(pp->cct, w->id, string(s, l)); + if (o) + pri.emplace(std::move(*o)); + + // Failure + + } else { + return false; + } + + if (!arraying) { + pp->s.pop_back(); + } + + return true; +} + +bool ParseState::number(const char* s, size_t l) { + // Top level! + if (w->kind == TokenKind::cond_key) { + auto& t = pp->policy.statements.back(); + t.conditions.back().vals.emplace_back(s, l); + + // Failure + + } else { + return false; + } + + if (!arraying) { + pp->s.pop_back(); + } + + return true; +} + +void ParseState::reset() { + pp->reset({TokenID::Sid, TokenID::Effect, TokenID::Principal, + TokenID::NotPrincipal, TokenID::Action, TokenID::NotAction, + TokenID::Resource, TokenID::NotResource, TokenID::Condition}); +} + +bool ParseState::obj_start() { + if (w->objectable && !objecting) { + objecting = true; + if (w->id == TokenID::Statement) { + pp->policy.statements.push_back({}); + } + + return true; + } + + return false; +} + + +bool ParseState::array_end() { + if (arraying && !objecting) { + pp->s.pop_back(); + return true; + } + + return false; +} + +ostream& operator <<(ostream& m, const MaskedIP& ip) { + // I have a theory about why std::bitset is the way it is. + if (ip.v6) { + for (int i = 15; i >= 0; --i) { + uint8_t b = 0; + for (int j = 7; j >= 0; --j) { + b |= (ip.addr[(i * 8) + j] << j); + } + m << hex << b; + if (i != 0) { + m << "::"; + } + } + } else { + // It involves Satan. + for (int i = 3; i >= 0; --i) { + uint8_t b = 0; + for (int j = 7; j >= 0; --j) { + b |= (ip.addr[(i * 8) + j] << j); + } + m << b; + if (i != 0) { + m << "."; + } + } + } + m << "/" << ip.prefix; + // It would explain a lot + return m; +} + +string to_string(const MaskedIP& m) { + stringstream ss; + ss << m; + return ss.str(); +} + +bool Condition::eval(const Environment& env) const { + auto i = env.find(key); + if (op == TokenID::Null) { + return i == env.end() ? true : false; + } + + if (i == env.end()) { + return false; + } + const auto& s = i->second; + + switch (op) { + // String! + case TokenID::StringEquals: + return orrible(std::equal_to(), s, vals); + + case TokenID::StringNotEquals: + return orrible(std::not2(std::equal_to()), + s, vals); + + case TokenID::StringEqualsIgnoreCase: + return orrible(ci_equal_to(), s, vals); + + case TokenID::StringNotEqualsIgnoreCase: + return orrible(std::not2(ci_equal_to()), s, vals); + + // Implement actual StringLike with wildcarding later + case TokenID::StringLike: + return orrible(std::equal_to(), s, vals); + case TokenID::StringNotLike: + return orrible(std::not2(std::equal_to()), + s, vals); + + // Numeric + case TokenID::NumericEquals: + return shortible(std::equal_to(), as_number, s, vals); + + case TokenID::NumericNotEquals: + return shortible(std::not2(std::equal_to()), + as_number, s, vals); + + + case TokenID::NumericLessThan: + return shortible(std::less(), as_number, s, vals); + + + case TokenID::NumericLessThanEquals: + return shortible(std::less_equal(), as_number, s, vals); + + case TokenID::NumericGreaterThan: + return shortible(std::greater(), as_number, s, vals); + + case TokenID::NumericGreaterThanEquals: + return shortible(std::greater_equal(), as_number, s, vals); + + // Date! + case TokenID::DateEquals: + return shortible(std::equal_to(), as_date, s, vals); + + case TokenID::DateNotEquals: + return shortible(std::not2(std::equal_to()), + as_date, s, vals); + + case TokenID::DateLessThan: + return shortible(std::less(), as_date, s, vals); + + + case TokenID::DateLessThanEquals: + return shortible(std::less_equal(), as_date, s, vals); + + case TokenID::DateGreaterThan: + return shortible(std::greater(), as_date, s, vals); + + case TokenID::DateGreaterThanEquals: + return shortible(std::greater_equal(), as_date, s, + vals); + + // Bool! + case TokenID::Bool: + return shortible(std::equal_to(), as_bool, s, vals); + + // Binary! + case TokenID::BinaryEquals: + return shortible(std::equal_to(), as_binary, s, + vals); + + // IP Address! + case TokenID::IpAddress: + return shortible(std::equal_to(), as_network, s, vals); + + case TokenID::NotIpAddress: + return shortible(std::not2(std::equal_to()), as_network, s, + vals); + +#if 0 + // Amazon Resource Names! (Does S3 need this?) + TokenID::ArnEquals, TokenID::ArnNotEquals, TokenID::ArnLike, + TokenID::ArnNotLike, +#endif + + default: + return false; + } +} + +optional Condition::as_network(const string& s) { + MaskedIP m; + if (s.empty()) { + return none; + } + + m.v6 = s.find(':'); + auto slash = s.find('/'); + if (slash == string::npos) { + m.prefix = m.v6 ? 128 : 32; + } else { + char* end = 0; + m.prefix = strtoul(s.data() + slash + 1, &end, 10); + if (*end != 0 || (m.v6 && m.prefix > 128) || + (!m.v6 && m.prefix > 32)) { + return none; + } + } + + string t; + auto p = &s; + + if (slash != string::npos) { + t.assign(s, 0, slash); + p = &t; + } + + if (m.v6) { + struct sockaddr_in6 a; + if (inet_pton(AF_INET6, p->c_str(), static_cast(&a)) != 1) { + return none; + } + + m.addr |= Address(a.sin6_addr.s6_addr[0]) << 0; + m.addr |= Address(a.sin6_addr.s6_addr[1]) << 8; + m.addr |= Address(a.sin6_addr.s6_addr[2]) << 16; + m.addr |= Address(a.sin6_addr.s6_addr[3]) << 24; + m.addr |= Address(a.sin6_addr.s6_addr[4]) << 32; + m.addr |= Address(a.sin6_addr.s6_addr[5]) << 40; + m.addr |= Address(a.sin6_addr.s6_addr[6]) << 48; + m.addr |= Address(a.sin6_addr.s6_addr[7]) << 56; + m.addr |= Address(a.sin6_addr.s6_addr[8]) << 64; + m.addr |= Address(a.sin6_addr.s6_addr[9]) << 72; + m.addr |= Address(a.sin6_addr.s6_addr[10]) << 80; + m.addr |= Address(a.sin6_addr.s6_addr[11]) << 88; + m.addr |= Address(a.sin6_addr.s6_addr[12]) << 96; + m.addr |= Address(a.sin6_addr.s6_addr[13]) << 104; + m.addr |= Address(a.sin6_addr.s6_addr[14]) << 112; + m.addr |= Address(a.sin6_addr.s6_addr[15]) << 120; + } else { + struct sockaddr_in a; + if (inet_pton(AF_INET, p->c_str(), static_cast(&a)) != 1) { + return none; + } + m.addr = ntohl(a.sin_addr.s_addr); + } + + return none; +} + +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) { + m << "["; + } else { + auto beforelast = end - 1; + m << "[ "; + for (auto i = begin; i != end; ++i) { + m << *i; + if (i != beforelast) { + m << ", "; + } else { + m << " "; + } + } + } + m << "]"; + return m; +} +} + +ostream& operator <<(ostream& m, const Condition& c) { + m << "{ " << condop_string(c.op) << ": { " << c.key; + if (c.ifexists) { + m << "IfExists"; + } + print_array(m, c.vals.cbegin(), c.vals.cend()); + return m << "}"; +} + +string to_string(const Condition& c) { + stringstream ss; + ss << c; + return ss.str(); +} + +Effect Statement::eval(const Environment& e, + optional ida, + uint64_t act, const ARN& res) const { + if (ida && (!ida->is_identity(princ) || ida->is_identity(noprinc))) { + return Effect::Pass; + } + + + if (!std::any_of(resource.begin(), resource.end(), + [&res](const ARN& pattern) { + return pattern.match(res); + }) || + (std::any_of(notresource.begin(), notresource.end(), + [&res](const ARN& pattern) { + return pattern.match(res); + }))) { + return Effect::Pass; + } + + if (!(action & act) || (notaction & act)) { + return Effect::Pass; + } + + if (std::all_of(conditions.begin(), + conditions.end(), + [&e](const Condition& c) { return c.eval(e);})) { + return effect; + } + + return Effect::Pass; +} + +namespace { +const char* action_bit_string(uint64_t action) { + switch (action) { + case s3GetObject: + return "s3:GetObject"; + + case s3GetObjectVersion: + return "s3:GetObjectVersion"; + + case s3PutObject: + return "s3:PutObject"; + + case s3GetObjectAcl: + return "s3:GetObjectAcl"; + + case s3GetObjectVersionAcl: + return "s3:GetObjectVersionAcl"; + + case s3PutObjectAcl: + return "s3:PutObjectAcl"; + + case s3PutObjectVersionAcl: + return "s3:PutObjectVersionAcl"; + + case s3DeleteObject: + return "s3:DeleteObject"; + + case s3DeleteObjectVersion: + return "s3:DeleteObjectVersion"; + + case s3ListMultipartUploadParts: + return "s3:ListMultipartUploadParts"; + + case s3AbortMultipartUpload: + return "s3:AbortMultipartUpload"; + + case s3GetObjectTorrent: + return "s3:GetObjectTorrent"; + + case s3GetObjectVersionTorrent: + return "s3:GetObjectVersionTorrent"; + + case s3RestoreObject: + return "s3:RestoreObject"; + + case s3CreateBucket: + return "s3:CreateBucket"; + + case s3DeleteBucket: + return "s3:DeleteBucket"; + + case s3ListBucket: + return "s3:ListBucket"; + + case s3ListBucketVersions: + return "s3:ListBucketVersions"; + case s3ListAllMyBuckets: + return "s3:ListAllMyBuckets"; + + case s3ListBucketMultiPartUploads: + return "s3:ListBucketMultiPartUploads"; + + case s3GetAccelerateConfiguration: + return "s3:GetAccelerateConfiguration"; + + case s3PutAccelerateConfiguration: + return "s3:PutAccelerateConfiguration"; + + case s3GetBucketAcl: + return "s3:GetBucketAcl"; + + case s3PutBucketAcl: + return "s3:PutBucketAcl"; + + case s3GetBucketCORS: + return "s3:GetBucketCORS"; + + case s3PutBucketCORS: + return "s3:PutBucketCORS"; + + case s3GetBucketVersioning: + return "s3:GetBucketVersioning"; + + case s3PutBucketVersioning: + return "s3:PutBucketVersioning"; + + case s3GetBucketRequestPayment: + return "s3:GetBucketRequestPayment"; + + case s3PutBucketRequestPayment: + return "s3:PutBucketRequestPayment"; + + case s3GetBucketLocation: + return "s3:GetBucketLocation"; + + case s3GetBucketPolicy: + return "s3:GetBucketPolicy"; + + case s3DeleteBucketPolicy: + return "s3:DeleteBucketPolicy"; + + case s3PutBucketPolicy: + return "s3:PutBucketPolicy"; + + case s3GetBucketNotification: + return "s3:GetBucketNotification"; + + case s3PutBucketNotification: + return "s3:PutBucketNotification"; + + case s3GetBucketLogging: + return "s3:GetBucketLogging"; + + case s3PutBucketLogging: + return "s3:PutBucketLogging"; + + case s3GetBucketTagging: + return "s3:GetBucketTagging"; + + case s3PutBucketTagging: + return "s3:PutBucketTagging"; + + case s3GetBucketWebsite: + return "s3:GetBucketWebsite"; + + case s3PutBucketWebsite: + return "s3:PutBucketWebsite"; + + case s3DeleteBucketWebsite: + return "s3:DeleteBucketWebsite"; + + case s3GetLifecycleConfiguration: + return "s3:GetLifecycleConfiguration"; + + case s3PutLifecycleConfiguration: + return "s3:PutLifecycleConfiguration"; + + case s3PutReplicationConfiguration: + return "s3:PutReplicationConfiguration"; + + case s3GetReplicationConfiguration: + return "s3:GetReplicationConfiguration"; + + case s3DeleteReplicationConfiguration: + return "s3:DeleteReplicationConfiguration"; + } + return "s3Invalid"; +} + +ostream& print_actions(ostream& m, const uint64_t a) { + bool begun = false; + m << "[ "; + for (auto i = 0U; i < s3Count; ++i) { + if (a & (1 << i)) { + if (begun) { + m << ", "; + } else { + begun = true; + } + m << action_bit_string(1 << i); + } + } + if (begun) { + m << " ]"; + } else { + m << "]"; + } + return m; +} +} + +ostream& operator <<(ostream& m, const Statement& s) { + m << "{ "; + if (s.sid) { + m << "Sid: " << *s.sid << ", "; + } + if (!s.princ.empty()) { + m << "Principal: "; + print_array(m, s.princ.cbegin(), s.princ.cend()); + m << ", "; + } + if (!s.noprinc.empty()) { + m << "NotPrincipal: "; + print_array(m, s.noprinc.cbegin(), s.noprinc.cend()); + m << ", "; + } + + m << "Effect: " << + (s.effect == Effect::Allow ? + (const char*) "Allow" : + (const char*) "Deny"); + + if (s.action || s.notaction || !s.resource.empty() || + !s.notresource.empty() || !s.conditions.empty()) { + m << ", "; + } + + if (s.action) { + m << "Action: "; + print_actions(m, s.action); + + if (s.notaction || !s.resource.empty() || + !s.notresource.empty() || !s.conditions.empty()) { + m << ", "; + } + } + + if (s.notaction) { + m << "NotAction: "; + print_actions(m, s.notaction); + + if (!s.resource.empty() || !s.notresource.empty() || + !s.conditions.empty()) { + m << ", "; + } + } + + if (!s.resource.empty()) { + m << "Resource: "; + print_array(m, s.resource.cbegin(), s.resource.cend()); + + if (!s.notresource.empty() || !s.conditions.empty()) { + m << ", "; + } + } + + if (!s.notresource.empty()) { + m << "NotResource: "; + print_array(m, s.notresource.cbegin(), s.notresource.cend()); + + if (!s.conditions.empty()) { + m << ", "; + } + } + + if (!s.conditions.empty()) { + m << "Condition: "; + print_array(m, s.conditions.cbegin(), s.conditions.cend()); + } + + return m << " }"; +} + +string to_string(const Statement& s) { + stringstream m; + m << s; + return m.str(); +} + +Policy::Policy(CephContext* cct, const string& tenant, + const bufferlist& _text) + : text(_text.to_str()) { + StringStream ss(text.data()); + PolicyParser pp(cct, tenant, *this); + auto pr = Reader{}.Parse(ss, pp); + if (!pr) { + throw PolicyParseException(std::move(pr)); + } +} + +Effect Policy::eval(const Environment& e, + optional ida, + std::uint64_t action, const ARN& resource) const { + auto allowed = false; + for (auto& s : statements) { + auto g = s.eval(e, ida, action, resource); + if (g == Effect::Deny) { + return g; + } else if (g == Effect::Allow) { + allowed = true; + } + } + return allowed ? Effect::Allow : Effect::Pass; +} + +ostream& operator <<(ostream& m, const Policy& p) { + m << "{ Version: " + << (p.version == Version::v2008_10_17 ? "2008-10-17" : "2012-10-17"); + + if (p.id || !p.statements.empty()) { + m << ", "; + } + + if (p.id) { + m << "Id: " << *p.id; + if (!p.statements.empty()) { + m << ", "; + } + } + + if (!p.statements.empty()) { + m << "Statements: "; + print_array(m, p.statements.cbegin(), p.statements.cend()); + m << ", "; + } + return m << " }"; +} + +string to_string(const Policy& p) { + stringstream s; + s << p; + return s.str(); +} + +} +} diff --git a/src/rgw/rgw_iam_policy.h b/src/rgw/rgw_iam_policy.h new file mode 100644 index 000000000000..4429a5743096 --- /dev/null +++ b/src/rgw/rgw_iam_policy.h @@ -0,0 +1,466 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_RGW_IAM_POLICY_H +#define CEPH_RGW_IAM_POLICY_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common/ceph_time.h" +#include "common/iso_8601.h" + +#include "rapidjson/error/error.h" +#include "rapidjson/error/en.h" + +#include "fnmatch.h" + +#include "rgw_basic_types.h" +#include "rgw_iam_policy_keywords.h" + +#include "include/assert.h" // razzin' frazzin' ...grrr. + +class RGWRados; +namespace rgw { +namespace auth { +class Identity; +} +} +struct rgw_obj; +struct rgw_bucket; + +namespace rgw { +namespace IAM { +static constexpr std::uint64_t s3None = 0; +static constexpr std::uint64_t s3GetObject = 1ULL << 0; +static constexpr std::uint64_t s3GetObjectVersion = 1ULL << 1; +static constexpr std::uint64_t s3PutObject = 1ULL << 2; +static constexpr std::uint64_t s3GetObjectAcl = 1ULL << 3; +static constexpr std::uint64_t s3GetObjectVersionAcl = 1ULL << 4; +static constexpr std::uint64_t s3PutObjectAcl = 1ULL << 5; +static constexpr std::uint64_t s3PutObjectVersionAcl = 1ULL << 6; +static constexpr std::uint64_t s3DeleteObject = 1ULL << 7; +static constexpr std::uint64_t s3DeleteObjectVersion = 1ULL << 8; +static constexpr std::uint64_t s3ListMultipartUploadParts = 1ULL << 9; +static constexpr std::uint64_t s3AbortMultipartUpload = 1ULL << 10; +static constexpr std::uint64_t s3GetObjectTorrent = 1ULL << 11; +static constexpr std::uint64_t s3GetObjectVersionTorrent = 1ULL << 12; +static constexpr std::uint64_t s3RestoreObject = 1ULL << 13; +static constexpr std::uint64_t s3CreateBucket = 1ULL << 14; +static constexpr std::uint64_t s3DeleteBucket = 1ULL << 15; +static constexpr std::uint64_t s3ListBucket = 1ULL << 16; +static constexpr std::uint64_t s3ListBucketVersions = 1ULL << 17; +static constexpr std::uint64_t s3ListAllMyBuckets = 1ULL << 18; +static constexpr std::uint64_t s3ListBucketMultiPartUploads = 1ULL << 19; +static constexpr std::uint64_t s3GetAccelerateConfiguration = 1ULL << 20; +static constexpr std::uint64_t s3PutAccelerateConfiguration = 1ULL << 21; +static constexpr std::uint64_t s3GetBucketAcl = 1ULL << 22; +static constexpr std::uint64_t s3PutBucketAcl = 1ULL << 23; +static constexpr std::uint64_t s3GetBucketCORS = 1ULL << 24; +static constexpr std::uint64_t s3PutBucketCORS = 1ULL << 25; +static constexpr std::uint64_t s3GetBucketVersioning = 1ULL << 26; +static constexpr std::uint64_t s3PutBucketVersioning = 1ULL << 27; +static constexpr std::uint64_t s3GetBucketRequestPayment = 1ULL << 28; +static constexpr std::uint64_t s3PutBucketRequestPayment = 1ULL << 29; +static constexpr std::uint64_t s3GetBucketLocation = 1ULL << 30; +static constexpr std::uint64_t s3GetBucketPolicy = 1ULL << 31; +static constexpr std::uint64_t s3DeleteBucketPolicy = 1ULL << 32; +static constexpr std::uint64_t s3PutBucketPolicy = 1ULL << 33; +static constexpr std::uint64_t s3GetBucketNotification = 1ULL << 34; +static constexpr std::uint64_t s3PutBucketNotification = 1ULL << 35; +static constexpr std::uint64_t s3GetBucketLogging = 1ULL << 36; +static constexpr std::uint64_t s3PutBucketLogging = 1ULL << 37; +static constexpr std::uint64_t s3GetBucketTagging = 1ULL << 38; +static constexpr std::uint64_t s3PutBucketTagging = 1ULL << 39; +static constexpr std::uint64_t s3GetBucketWebsite = 1ULL << 40; +static constexpr std::uint64_t s3PutBucketWebsite = 1ULL << 41; +static constexpr std::uint64_t s3DeleteBucketWebsite = 1ULL << 42; +static constexpr std::uint64_t s3GetLifecycleConfiguration = 1ULL << 43; +static constexpr std::uint64_t s3PutLifecycleConfiguration = 1ULL << 44; +static constexpr std::uint64_t s3PutReplicationConfiguration = 1ULL << 45; +static constexpr std::uint64_t s3GetReplicationConfiguration = 1ULL << 46; +static constexpr std::uint64_t s3DeleteReplicationConfiguration = 1ULL << 47; +static constexpr std::uint64_t s3Count = 48; +static constexpr std::uint64_t s3All = (1ULL << s3Count) - 1; + +namespace { +inline int op_to_perm(std::uint64_t op) { + switch (op) { + case s3GetObject: + case s3GetObjectTorrent: + case s3GetObjectVersion: + case s3GetObjectVersionTorrent: + case s3ListAllMyBuckets: + case s3ListBucket: + case s3ListBucketMultiPartUploads: + case s3ListBucketVersions: + case s3ListMultipartUploadParts: + return RGW_PERM_READ; + + case s3AbortMultipartUpload: + case s3CreateBucket: + case s3DeleteBucket: + case s3DeleteObject: + case s3DeleteObjectVersion: + case s3PutObject: + case s3RestoreObject: + return RGW_PERM_WRITE; + + case s3GetAccelerateConfiguration: + case s3GetBucketAcl: + case s3GetBucketCORS: + case s3GetBucketLocation: + case s3GetBucketLogging: + case s3GetBucketNotification: + case s3GetBucketPolicy: + case s3GetBucketRequestPayment: + case s3GetBucketTagging: + case s3GetBucketVersioning: + case s3GetBucketWebsite: + case s3GetLifecycleConfiguration: + case s3GetObjectAcl: + case s3GetObjectVersionAcl: + case s3GetReplicationConfiguration: + return RGW_PERM_READ_ACP; + + case s3DeleteBucketPolicy: + case s3DeleteBucketWebsite: + case s3DeleteReplicationConfiguration: + case s3PutAccelerateConfiguration: + case s3PutBucketAcl: + case s3PutBucketCORS: + case s3PutBucketLogging: + case s3PutBucketNotification: + case s3PutBucketPolicy: + case s3PutBucketRequestPayment: + case s3PutBucketTagging: + case s3PutBucketVersioning: + case s3PutBucketWebsite: + case s3PutLifecycleConfiguration: + case s3PutObjectAcl: + case s3PutObjectVersionAcl: + case s3PutReplicationConfiguration: + return RGW_PERM_WRITE_ACP; + + case s3All: + return RGW_PERM_FULL_CONTROL; + } + return RGW_PERM_INVALID; +} +} + +using Environment = boost::container::flat_map; + +enum struct Partition { + aws, aws_cn, aws_us_gov, wildcard + // If we wanted our own ARNs for principal type unique to us + // (maybe to integrate better with Swift) or for anything else we + // provide that doesn't map onto S3, we could add an 'rgw' + // partition type. +}; + +enum struct Service { + apigateway, appstream, artifact, autoscaling, aws_portal, acm, + cloudformation, cloudfront, cloudhsm, cloudsearch, cloudtrail, + cloudwatch, events, logs, codebuild, codecommit, codedeploy, + codepipeline, cognito_idp, cognito_identity, cognito_sync, + config, datapipeline, dms, devicefarm, directconnect, + ds, dynamodb, ec2, ecr, ecs, ssm, elasticbeanstalk, elasticfilesystem, + elasticloadbalancing, elasticmapreduce, elastictranscoder, elasticache, + es, gamelift, glacier, health, iam, importexport, inspector, iot, + kms, kinesisanalytics, firehose, kinesis, lambda, lightsail, + machinelearning, aws_marketplace, aws_marketplace_management, + mobileanalytics, mobilehub, opsworks, opsworks_cm, polly, + redshift, rds, route53, route53domains, sts, servicecatalog, + ses, sns, sqs, s3, swf, sdb, states, storagegateway, support, + trustedadvisor, waf, workmail, workspaces, wildcard +}; + +struct ARN { + Partition partition; + Service service; + std::string region; + // Once we refity tenant, we should probably use that instead of a + // string. + std::string account; + std::string resource; + + ARN() + : partition(Partition::wildcard), service(Service::wildcard) {} + ARN(Partition partition, Service service, std::string region, + std::string account, std::string resource) + : partition(partition), service(service), region(std::move(region)), + account(std::move(account)), resource(std::move(resource)) {} + ARN(const rgw_obj& o); + ARN(const rgw_bucket& b); + ARN(const rgw_bucket& b, const std::string& o); + + static boost::optional parse(const std::string& s, + bool wildcard = false); + std::string to_string() const; + + // `this` is the pattern + bool match(const ARN& candidate) const; +}; + +inline std::string to_string(const ARN& a) { + return a.to_string(); +} + +inline std::ostream& operator <<(std::ostream& m, const ARN& a) { + return m << to_string(a); +} + +bool operator ==(const ARN& l, const ARN& r); +bool operator <(const ARN& l, const ARN& r); + +using Address = std::bitset<128>; +struct MaskedIP { + bool v6; + Address addr; + // Since we're mapping IPv6 to IPv4 addresses, we may want to + // consider making the prefix always be in terms of a v6 address + // and just use the v6 bit to rewrite it as a v4 prefix for + // output. + unsigned int prefix; +}; + +std::ostream& operator <<(std::ostream& m, const MaskedIP& ip); +string to_string(const MaskedIP& m); + +inline bool operator ==(const MaskedIP& l, const MaskedIP& r) { + auto shift = std::max((l.v6 ? 128 : 32) - l.prefix, + (r.v6 ? 128 : 32) - r.prefix); + ceph_assert(shift > 0); + return (l.addr >> shift) == (r.addr >> shift); +} + +struct Condition { + TokenID op; + // Originally I was going to use a perfect hash table, but Marcus + // says keys are to be added at run-time not compile time. + + // In future development, use symbol internment. + std::string key; + bool ifexists = false; + // Much to my annoyance there is no actual way to do this in a + // typed way that is compatible with AWS. I know this because I've + // seen examples where the same value is used as a string in one + // context and a date in another. + std::vector vals; + + Condition() = default; + Condition(TokenID op, const char* s, std::size_t len) : op(op) { + static constexpr char ifexistr[] = "IfExists"; + auto l = static_cast(memmem(static_cast(s), len, + static_cast(ifexistr), + sizeof(ifexistr) -1)); + if (l && ((l + sizeof(ifexistr) - 1 == (s + len)))) { + ifexists = true; + key.assign(s, static_cast(l) - s); + } else { + key.assign(s, len); + } + } + + bool eval(const Environment& e) const; + + static boost::optional as_number(const std::string& s) { + std::size_t p = 0; + + try { + double d = std::stod(s, &p); + if (p < s.length()) { + return boost::none; + } + + return d; + } catch (const std::logic_error& e) { + return boost::none; + } + } + + static boost::optional as_date(const std::string& s) { + std::size_t p = 0; + + try { + double d = std::stod(s, &p); + if (p == s.length()) { + return ceph::real_time( + std::chrono::seconds(static_cast(d)) + + std::chrono::nanoseconds( + static_cast((d - static_cast(d)) + * 1000000000))); + } + + return from_iso_8601(boost::string_ref(s), false); + } catch (const std::logic_error& e) { + return boost::none; + } + } + + static boost::optional as_bool(const std::string& s) { + std::size_t p = 0; + + if (s.empty() || boost::iequals(s, "false")) { + return false; + } + + try { + double d = std::stod(s, &p); + if (p == s.length()) { + return !((d == +0.0) || (d = -0.0) || std::isnan(d)); + } + } catch (const std::logic_error& e) { + // Fallthrough + } + + return true; + } + + static boost::optional as_binary(const std::string& s) { + // In a just world + ceph::bufferlist base64; + // I could populate a bufferlist + base64.push_back(buffer::create_static( + s.length(), + const_cast(s.data()))); // Yuck + // From a base64 encoded std::string. + ceph::bufferlist bin; + + try { + base64.decode_base64(bin); + } catch (const ceph::buffer::malformed_input& e) { + return boost::none; + } + return bin; + } + + static boost::optional as_network(const std::string& s); + + + struct ci_equal_to : public std::binary_function { + bool operator ()(const std::string& s1, + const std::string& s2) const { + return boost::iequals(s1, s2); + } + }; + + + template + static bool orrible(F&& f, const std::string& c, + const std::vector& v) { + for (const auto& d : v) { + if (std::forward(f)(c, d)) { + return true; + } + } + return false; + } + + template + static bool shortible(F&& f, X& x, const std::string& c, + const std::vector& v) { + auto xc = std::forward(x)(c); + if (!xc) { + return false; + } + + for (const auto& d : v) { + auto xd = std::forward(x)(d); + if (!xd) { + continue; + } + + if (std::forward(f)(*xc, *xd)) { + return true; + } + } + return false; + } +}; + +std::ostream& operator <<(std::ostream& m, const Condition& c); + +std::string to_string(const Condition& c); + +struct Statement { + boost::optional sid = boost::none; + + boost::container::flat_set princ; + boost::container::flat_set noprinc; + + // Every statement MUST provide an effect. I just initialize it to + // deny as defensive programming. + Effect effect = Effect::Deny; + + std::uint64_t action = 0; + std::uint64_t notaction = 0; + + boost::container::flat_set resource; + boost::container::flat_set notresource; + + std::vector conditions; + + Effect eval(const Environment& e, + boost::optional ida, + std::uint64_t action, const ARN& resource) const; +}; + +std::ostream& operator <<(ostream& m, const Statement& s); +std::string to_string(const Statement& s); + +struct PolicyParseException : public std::exception { + rapidjson::ParseResult pr; + + PolicyParseException(rapidjson::ParseResult&& pr) + : pr(pr) { } + const char* what() const noexcept override { + return rapidjson::GetParseError_En(pr.Code()); + } +}; + +struct Policy { + std::string text; + Version version = Version::v2008_10_17; + boost::optional id = boost::none; + + std::vector statements; + + Policy(CephContext* cct, const std::string& tenant, + const bufferlist& text); + + Effect eval(const Environment& e, + boost::optional ida, + std::uint64_t action, const ARN& resource) const; +}; + +std::ostream& operator <<(ostream& m, const Policy& p); +std::string to_string(const Policy& p); +} +} + +namespace std { +template<> +struct hash<::rgw::IAM::Service> { + size_t operator()(const ::rgw::IAM::Service& s) const noexcept { + // Invoke a default-constructed hash object for int. + return hash()(static_cast(s)); + } +}; +} + +#endif diff --git a/src/rgw/rgw_iam_policy_keywords.gperf b/src/rgw/rgw_iam_policy_keywords.gperf new file mode 100644 index 000000000000..d37fa6aff653 --- /dev/null +++ b/src/rgw/rgw_iam_policy_keywords.gperf @@ -0,0 +1,127 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +%language=C++ +%define class-name keyword_hash +%define lookup-function-name lookup +%struct-type +struct Keyword { + const char* name; + TokenKind kind; + TokenID id; + uint64_t specific; + bool arrayable; + bool objectable; +}; +%% +# Top-level +# +Version, TokenKind::top, TokenID::Version, 0, false, false +Id, TokenKind::top, TokenID::Id, 0, false, false +Statement, TokenKind::top, TokenID::Statement, 0, true, true +# +# Statement level +# +Sid, TokenKind::statement, TokenID::Sid, 0, false, false +Effect, TokenKind::statement, TokenID::Effect, 0, false, false +Principal, TokenKind::statement, TokenID::Principal, 0, false, true +NotPrincipal, TokenKind::statement, TokenID::NotPrincipal, 0, true, true +Action, TokenKind::statement, TokenID::Action, 0, true, false +NotAction, TokenKind::statement, TokenID::NotAction, 0, true, false +Resource, TokenKind::statement, TokenID::Resource, 0, true, false +NotResource, TokenKind::statement, TokenID::NotResource, 0, true, false +Condition, TokenKind::statement, TokenID::Condition, 0, true, true +# +# Condition operators +# +# String +StringEquals, TokenKind::cond_op, TokenID::StringEquals, (uint64_t) Type::string, true, true +StringNotEquals, TokenKind::cond_op, TokenID::StringNotEquals, (uint64_t) Type::string, true, true +StringEqualsIgnoreCase, TokenKind::cond_op, TokenID::StringEqualsIgnoreCase, (uint64_t) Type::string, true, true +StringNotEqualsIgnoreCase, TokenKind::cond_op, TokenID::StringNotEqualsIgnoreCase, (uint64_t) Type::string, true, true +StringLike, TokenKind::cond_op, TokenID::StringLike, (uint64_t) Type::string, true, true, +StringNotLike, TokenKind::cond_op, TokenID::StringNotLike, (uint64_t) Type::string, true, true +# Numeric +NumericEquals, TokenKind::cond_op, TokenID::NumericEquals, (uint64_t) Type::number, true, true +NumericNotEquals, TokenKind::cond_op, TokenID::NumericNotEquals, (uint64_t) Type::number, true, true +NumericLessThan, TokenKind::cond_op, TokenID::NumericLessThan, (uint64_t) Type::number, true, true +NumericLessThanEquals, TokenKind::cond_op, TokenID::NumericLessThanEquals, (uint64_t) Type::number, true, true +NumericGreaterThan, TokenKind::cond_op, TokenID::NumericGreaterThan, (uint64_t) Type::number, true, true +NumericGreaterThanEquals, TokenKind::cond_op, TokenID::NumericGreaterThanEquals, (uint64_t) Type::number, true, true +# Date +DateEquals, TokenKind::cond_op, TokenID::DateEquals, (uint64_t) Type::date, true, true +DateNotEquals, TokenKind::cond_op, TokenID::DateNotEquals, (uint64_t) Type::date, true, true +DateLessThan, TokenKind::cond_op, TokenID::DateLessThan, (uint64_t) Type::date, true, true +DateLessThanEquals, TokenKind::cond_op, TokenID::DateLessThanEquals, (uint64_t) Type::date, true, true +DateGreaterThan, TokenKind::cond_op, TokenID::DateGreaterThan, (uint64_t) Type::date, true, true +DateGreaterThanEquals, TokenKind::cond_op, TokenID::DateGreaterThanEquals, (uint64_t) Type::date, true, true +# Bool +Bool, TokenKind::cond_op, TokenID::Bool, (uint64_t) Type::boolean, true, true +# Binary +BinaryEquals, TokenKind::cond_op, TokenID::BinaryEquals, (uint64_t) Type::binary, true, true +# IP Address +IpAddress, TokenKind::cond_op, TokenID::IpAddress, (uint64_t) Type::ipaddr, true, true +NotIpAddress, TokenKind::cond_op, TokenID::NotIpAddress, (uint64_t) Type::ipaddr, true, true +# Amazon Resource Names +ArnEquals, TokenKind::cond_op, TokenID::ArnEquals, (uint64_t) Type::arn, true, true +ArnNotEquals, TokenKind::cond_op, TokenID::ArnNotEquals, (uint64_t) Type::arn, true, true +ArnLike, TokenKind::cond_op, TokenID::ArnLike, (uint64_t) Type::arn, true, true +ArnNotLike, TokenKind::cond_op, TokenID::ArnNotLike, (uint64_t) Type::arn, true, true +# Null +Null, TokenKind::cond_op, TokenID::Null, (uint64_t) Type::null, true, true +# +# Condition keys +# +# AWS +#aws:CurrentTime, TokenKind::cond_key, TokenID::awsCurrentTime, (uint64_t) Type::date, true, false +#aws:EpochTime, TokenKind::cond_key, TokenID::awsEpochTime, (uint64_t) Type::date, true, false +#aws:TokenIssueTime, TokenKind::cond_key, TokenID::awsTokenIssueTime, (uint64_t) Type::date, true, false +#aws:MultiFactorAuthPresent, TokenKind::cond_key, TokenID::awsMultiFactorAuthPresent, (uint64_t) Type::boolean, true, false +#aws:MultiFactorAuthAge, TokenKind::cond_key, TokenID::awsMultiFactorAuthAge, (uint64_t) Type::number, true, false +#aws:PrincipalType, TokenKind::cond_key, TokenID::awsPrincipalType, (uint64_t) Type::string, true, false +#aws:Referer, TokenKind::cond_key, TokenID::awsReferer, (uint64_t) Type::string, true, false +#aws:SecureTransport, TokenKind::cond_key, TokenID::awsSecureTransport, (uint64_t) Type::boolean, true, false +#aws:SourceArn, TokenKind::cond_key, TokenID::awsSourceArn, (uint64_t) Type::arn, true, false +#aws:SourceIp, TokenKind::cond_key, TokenID::awsSourceIp, (uint64_t) Type::ipaddr, true, false +#aws:SourceVpc, TokenKind::cond_key, TokenID::awsSourceVpc, (uint64_t) Type::string, true, false +#aws:SourceVpce, TokenKind::cond_key, TokenID::awsSourceVpce, (uint64_t) Type::string, true, false +#aws:UserAgent, TokenKind::cond_key, TokenID::awsUserAgent, (uint64_t) Type::string, true, false +#aws:userid, TokenKind::cond_key, TokenID::awsuserid, (uint64_t) Type::string, true, false +#aws:username, TokenKind::cond_key, TokenID::awsusername, (uint64_t) Type::string, true, false +# S3 +#s3:x-amz-acl, TokenKind::cond_key, TokenID::s3x_amz_acl, (uint64_t) Type::string, true, false +#s3:x-amz-grant-read, TokenKind::cond_key, TokenID::s3x_amz_grant_permission, (uint64_t) Type::boolean, true, false +#s3:x-amz-grant-write, TokenKind::cond_key, TokenID::s3x_amz_grant_permission, (uint64_t) Type::boolean, true, false +#s3:x-amz-grant-read-acp, TokenKind::cond_key, TokenID::s3x_amz_grant_permission, (uint64_t) Type::boolean, true, false +#s3:x-amz-grant-write-acp, TokenKind::cond_key, TokenID::s3x_amz_grant_permission, (uint64_t) Type::boolean, true, false +#s3:x-amz-grant-full-control, TokenKind::cond_key, TokenID::s3x_amz_grant_permission, (uint64_t) Type::boolean, true, false +#s3:x-amz-copy-source, TokenKind::cond_key, TokenID::s3x_amz_copy_source, (uint64_t) Type::string, true, false +#s3:x-amz-server-side-encryption, TokenKind::cond_key, TokenID::s3x_amz_server_side_encryption, (uint64_t) Type::boolean, true, false +#s3:x-amz-server-side-encryption-aws-kms-key-id, TokenKind::cond_key, TokenID::s3x_amz_server_side_encryption_aws_kms_key_id, (uint64_t) Type::arn, true, false +#s3:x-amz-metadata-directive, TokenKind::cond_key, TokenID::s3x_amz_metadata_directive, (uint64_t) Type::string, true, false +#s3:x-amz-storage-class, TokenKind::cond_key, TokenID::s3x_amz_storage_class, (uint64_t) Type::string, true, false +#s3:VersionId, TokenKind::cond_key, TokenID::s3VersionId, (uint64_t) Type::string, true, false +#s3:LocationConstraint, TokenKind::cond_key, TokenID::s3LocationConstraint, (uint64_t) Type::string, true, false +#s3:prefix, TokenKind::cond_key, TokenID::s3prefix, (uint64_t) Type::string, true, false +#s3:delimiter, TokenKind::cond_key, TokenID::s3delimiter, (uint64_t) Type::string, true, false +#s3:max-keys, TokenKind::cond_key, TokenID::s3max_keys, (uint64_t) Type::number, true, false +#s3:signatureversion, TokenKind::cond_key, TokenID::s3signatureversion, (uint64_t) Type::string, true, false +#s3:authType, TokenKind::cond_key, TokenID::s3authType, (uint64_t) Type::string, true, false +#s3:signatureAge, TokenKind::cond_key, TokenID::s3signatureAge, (uint64_t) Type::number, true, false +#s3:x-amz-content-sha256, TokenKind::cond_key, TokenID::s3x_amz_content_sha256, (uint64_t) Type::string, true, false +# +# Version Keywords +# +2008-10-17, TokenKind::version_key, TokenID::v2008_10_17, (uint64_t) Version::v2008_10_17, false, false +2012-10-17, TokenKind::version_key, TokenID::v2012_10_17, (uint64_t) Version::v2012_10_17, false, false +# +# Effect Keywords +# +Allow, TokenKind::effect_key, TokenID::Allow, (uint64_t) Effect::Allow, false, false +Deny, TokenKind::effect_key, TokenID::Deny, (uint64_t) Effect::Deny, false, false +# +# Principal types +# +AWS, TokenKind::princ_type, TokenID::AWS, 0, true, false +Federated, TokenKind::princ_type, TokenID::Federated, 0, true, false +Service, TokenKind::princ_type, TokenID::Service, 0, true, false +CanonicalUser, TokenKind::princ_type, TokenID::CanonicalUser, 0, true, false diff --git a/src/rgw/rgw_iam_policy_keywords.h b/src/rgw/rgw_iam_policy_keywords.h new file mode 100644 index 000000000000..a0cd34b6286e --- /dev/null +++ b/src/rgw/rgw_iam_policy_keywords.h @@ -0,0 +1,139 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_RGW_POLICY_S3V2_KEYWORDS_H +#define CEPH_RGW_POLICY_S3V2_KEYWORDS_H + +namespace rgw { +namespace IAM { + +enum class TokenKind { + pseudo, top, statement, cond_op, cond_key, version_key, effect_key, + princ_type +}; + +enum class TokenID { + /// Pseudo-token + Top, + + /// Top-level tokens + Version, Id, Statement, + + /// Statement level tokens + Sid, Effect, Principal, NotPrincipal, Action, NotAction, + Resource, NotResource, Condition, + + /// Condition Operators! + /// Any of these, except Null, can have an IfExists variant. + + // String! + StringEquals, StringNotEquals, StringEqualsIgnoreCase, + StringNotEqualsIgnoreCase, StringLike, StringNotLike, + + // Numeric! + NumericEquals, NumericNotEquals, NumericLessThan, NumericLessThanEquals, + NumericGreaterThan, NumericGreaterThanEquals, + + // Date! + DateEquals, DateNotEquals, DateLessThan, DateLessThanEquals, + DateGreaterThan, DateGreaterThanEquals, + + // Bool! + Bool, + + // Binary! + BinaryEquals, + + // IP Address! + IpAddress, NotIpAddress, + + // Amazon Resource Names! (Does S3 need this?) + ArnEquals, ArnNotEquals, ArnLike, ArnNotLike, + + // Null! + Null, + +#if 0 // Keys are done at runtime now + + /// Condition Keys! + awsCurrentTime, + awsEpochTime, + awsTokenIssueTime, + awsMultiFactorAuthPresent, + awsMultiFactorAuthAge, + awsPrincipalType, + awsReferer, + awsSecureTransport, + awsSourceArn, + awsSourceIp, + awsSourceVpc, + awsSourceVpce, + awsUserAgent, + awsuserid, + awsusername, + s3x_amz_acl, + s3x_amz_grant_permission, + s3x_amz_copy_source, + s3x_amz_server_side_encryption, + s3x_amz_server_side_encryption_aws_kms_key_id, + s3x_amz_metadata_directive, + s3x_amz_storage_class, + s3VersionId, + s3LocationConstraint, + s3prefix, + s3delimiter, + s3max_keys, + s3signatureversion, + s3authType, + s3signatureAge, + s3x_amz_content_sha256, +#else + CondKey, +#endif + + /// + /// Versions! + /// + v2008_10_17, + v2012_10_17, + + /// + /// Effects! + /// + Allow, + Deny, + + /// Principal Types! + AWS, + Federated, + Service, + CanonicalUser +}; + + +enum class Version { + v2008_10_17, + v2012_10_17 +}; + + +enum class Effect { + Allow, + Deny, + Pass +}; + +enum class Type { + string, + number, + date, + boolean, + binary, + ipaddr, + arn, + null +}; +} +} + +#endif // CEPH_RGW_POLICY_S3V2_KEYWORDS_H diff --git a/src/test/rgw/CMakeLists.txt b/src/test/rgw/CMakeLists.txt index b5a03e0baadf..c85bda788265 100644 --- a/src/test/rgw/CMakeLists.txt +++ b/src/test/rgw/CMakeLists.txt @@ -105,5 +105,29 @@ target_link_libraries(unittest_rgw_crypto ${UNITTEST_LIBS} ${CRYPTO_LIBS} ) -set_target_properties(unittest_rgw_crypto PROPERTIES COMPILE_FLAGS - ${UNITTEST_CXX_FLAGS}) +set_target_properties(unittest_rgw_crypto PROPERTIES COMPILE_FLAGS$ {UNITTEST_CXX_FLAGS}) + +# ceph_test_rgw_iam_policy +set(test_rgw_iam_policy_srcs test_rgw_iam_policy.cc) +add_executable(ceph_test_rgw_iam_policy + ${test_rgw_iam_policy_srcs} + ) +target_link_libraries(ceph_test_rgw_iam_policy + rgw_a + cls_rgw_client + cls_lock_client + cls_refcount_client + cls_log_client + cls_statelog_client + cls_version_client + cls_replica_log_client + cls_user_client + librados + global + ${CURL_LIBRARIES} + ${EXPAT_LIBRARIES} + ${CMAKE_DL_LIBS} + ${UNITTEST_LIBS} + ${CRYPTO_LIBS} + ) +set_target_properties(ceph_test_rgw_iam_policy PROPERTIES COMPILE_FLAGS ${UNITTEST_CXX_FLAGS}) diff --git a/src/test/rgw/test_rgw_iam_policy.cc b/src/test/rgw/test_rgw_iam_policy.cc new file mode 100644 index 000000000000..cc512e0b0c3f --- /dev/null +++ b/src/test/rgw/test_rgw_iam_policy.cc @@ -0,0 +1,507 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2015 Red Hat + * + * This is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1, as published by the Free Software + * Foundation. See file COPYING. + * + */ + +#include + +#include +#include + +#include + +#include "common/code_environment.h" +#include "common/ceph_context.h" +#include "global/global_init.h" +#include "rgw/rgw_auth.h" +#include "rgw/rgw_iam_policy.h" + + +using std::string; +using std::vector; + +using boost::container::flat_set; +using boost::intrusive_ptr; +using boost::make_optional; +using boost::none; +using boost::optional; + +using rgw::auth::Identity; +using rgw::auth::Principal; + +using rgw::IAM::ARN; +using rgw::IAM::Effect; +using rgw::IAM::Environment; +using rgw::IAM::Partition; +using rgw::IAM::Policy; +using rgw::IAM::s3All; +using rgw::IAM::s3Count; +using rgw::IAM::s3GetAccelerateConfiguration; +using rgw::IAM::s3GetBucketAcl; +using rgw::IAM::s3GetBucketCORS; +using rgw::IAM::s3GetBucketLocation; +using rgw::IAM::s3GetBucketLogging; +using rgw::IAM::s3GetBucketNotification; +using rgw::IAM::s3GetBucketPolicy; +using rgw::IAM::s3GetBucketRequestPayment; +using rgw::IAM::s3GetBucketTagging; +using rgw::IAM::s3GetBucketVersioning; +using rgw::IAM::s3GetBucketWebsite; +using rgw::IAM::s3GetLifecycleConfiguration; +using rgw::IAM::s3GetObject; +using rgw::IAM::s3GetObjectAcl; +using rgw::IAM::s3GetObjectVersionAcl; +using rgw::IAM::s3GetObjectTorrent; +using rgw::IAM::s3GetObjectVersion; +using rgw::IAM::s3GetObjectVersionTorrent; +using rgw::IAM::s3GetReplicationConfiguration; +using rgw::IAM::s3ListAllMyBuckets; +using rgw::IAM::s3ListBucket; +using rgw::IAM::s3ListBucket; +using rgw::IAM::s3ListBucketMultiPartUploads; +using rgw::IAM::s3ListBucketVersions; +using rgw::IAM::s3ListMultipartUploadParts; +using rgw::IAM::s3None; +using rgw::IAM::s3PutBucketAcl; +using rgw::IAM::s3PutBucketPolicy; +using rgw::IAM::Service; +using rgw::IAM::TokenID; +using rgw::IAM::Version; + +class FakeIdentity : public Identity { + const Principal id; +public: + + FakeIdentity(Principal&& id) : id(std::move(id)) {} + uint32_t get_perms_from_aclspec(const aclspec_t& aclspec) const override { + abort(); + return 0; + }; + + bool is_admin_of(const rgw_user& uid) const override { + abort(); + return false; + } + + bool is_owner_of(const rgw_user& uid) const override { + abort(); + return false; + } + + virtual uint32_t get_perm_mask() const override { + abort(); + return 0; + } + + void to_str(std::ostream& out) const override { + abort(); + } + + bool is_identity(const flat_set& ids) const override { + return ids.find(id) != ids.end(); + } +}; + +class PolicyTest : public ::testing::Test { +protected: + intrusive_ptr cct; + static const string arbitrary_tenant; + static string example1; + static string example2; + static string example3; +public: + PolicyTest() { + cct = new CephContext(CEPH_ENTITY_TYPE_CLIENT); + } +}; + +TEST_F(PolicyTest, Parse1) { + optional p; + + ASSERT_NO_THROW(p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example1))); + ASSERT_TRUE(p); + + EXPECT_EQ(p->text, example1); + EXPECT_EQ(p->version, Version::v2012_10_17); + EXPECT_FALSE(p->id); + EXPECT_FALSE(p->statements[0].sid); + EXPECT_FALSE(p->statements.empty()); + EXPECT_EQ(p->statements.size(), 1U); + EXPECT_TRUE(p->statements[0].princ.empty()); + EXPECT_TRUE(p->statements[0].noprinc.empty()); + EXPECT_EQ(p->statements[0].effect, Effect::Allow); + EXPECT_EQ(p->statements[0].action, s3ListBucket); + EXPECT_EQ(p->statements[0].notaction, s3None); + ASSERT_FALSE(p->statements[0].resource.empty()); + ASSERT_EQ(p->statements[0].resource.size(), 1U); + EXPECT_EQ(p->statements[0].resource.begin()->partition, Partition::aws); + EXPECT_EQ(p->statements[0].resource.begin()->service, Service::s3); + EXPECT_TRUE(p->statements[0].resource.begin()->region.empty()); + EXPECT_EQ(p->statements[0].resource.begin()->account, arbitrary_tenant); + EXPECT_EQ(p->statements[0].resource.begin()->resource, "example_bucket"); + EXPECT_TRUE(p->statements[0].notresource.empty()); + EXPECT_TRUE(p->statements[0].conditions.empty()); +} + +TEST_F(PolicyTest, Eval1) { + auto p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example1)); + Environment e; + + EXPECT_EQ(p.eval(e, none, s3ListBucket, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "example_bucket")), + Effect::Allow); + + EXPECT_EQ(p.eval(e, none, s3PutBucketAcl, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "example_bucket")), + Effect::Pass); + + EXPECT_EQ(p.eval(e, none, s3ListBucket, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "erroneous_bucket")), + Effect::Pass); + +} + +TEST_F(PolicyTest, Parse2) { + optional p; + + ASSERT_NO_THROW(p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example2))); + ASSERT_TRUE(p); + + EXPECT_EQ(p->text, example2); + EXPECT_EQ(p->version, Version::v2012_10_17); + EXPECT_EQ(*p->id, "S3-Account-Permissions"); + ASSERT_FALSE(p->statements.empty()); + EXPECT_EQ(p->statements.size(), 1U); + EXPECT_EQ(*p->statements[0].sid, "1"); + EXPECT_FALSE(p->statements[0].princ.empty()); + EXPECT_EQ(p->statements[0].princ.size(), 1U); + EXPECT_EQ(*p->statements[0].princ.begin(), + Principal::tenant("ACCOUNT-ID-WITHOUT-HYPHENS")); + EXPECT_TRUE(p->statements[0].noprinc.empty()); + EXPECT_EQ(p->statements[0].effect, Effect::Allow); + EXPECT_EQ(p->statements[0].action, s3All); + EXPECT_EQ(p->statements[0].notaction, s3None); + ASSERT_FALSE(p->statements[0].resource.empty()); + ASSERT_EQ(p->statements[0].resource.size(), 2U); + EXPECT_EQ(p->statements[0].resource.begin()->partition, Partition::aws); + EXPECT_EQ(p->statements[0].resource.begin()->service, Service::s3); + EXPECT_TRUE(p->statements[0].resource.begin()->region.empty()); + EXPECT_EQ(p->statements[0].resource.begin()->account, arbitrary_tenant); + EXPECT_EQ(p->statements[0].resource.begin()->resource, "mybucket"); + EXPECT_EQ((p->statements[0].resource.begin() + 1)->partition, + Partition::aws); + EXPECT_EQ((p->statements[0].resource.begin() + 1)->service, + Service::s3); + EXPECT_TRUE((p->statements[0].resource.begin() + 1)->region.empty()); + EXPECT_EQ((p->statements[0].resource.begin() + 1)->account, + arbitrary_tenant); + EXPECT_EQ((p->statements[0].resource.begin() + 1)->resource, "mybucket/*"); + EXPECT_TRUE(p->statements[0].notresource.empty()); + EXPECT_TRUE(p->statements[0].conditions.empty()); +} + +TEST_F(PolicyTest, Eval2) { + auto p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example2)); + Environment e; + + auto trueacct = FakeIdentity( + Principal::tenant("ACCOUNT-ID-WITHOUT-HYPHENS")); + + auto notacct = FakeIdentity( + Principal::tenant("some-other-account")); + for (auto i = 0ULL; i < s3Count; ++i) { + EXPECT_EQ(p.eval(e, trueacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket")), + Effect::Allow); + EXPECT_EQ(p.eval(e, trueacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket/myobject")), + Effect::Allow); + + EXPECT_EQ(p.eval(e, notacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket")), + Effect::Pass); + EXPECT_EQ(p.eval(e, notacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket/myobject")), + Effect::Pass); + + EXPECT_EQ(p.eval(e, trueacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "notyourbucket")), + Effect::Pass); + EXPECT_EQ(p.eval(e, trueacct, 1ULL << i, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "notyourbucket/notyourobject")), + Effect::Pass); + + } +} + +TEST_F(PolicyTest, Parse3) { + optional p; + + ASSERT_NO_THROW(p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example3))); + ASSERT_TRUE(p); + + EXPECT_EQ(p->text, example3); + EXPECT_EQ(p->version, Version::v2012_10_17); + EXPECT_FALSE(p->id); + ASSERT_FALSE(p->statements.empty()); + EXPECT_EQ(p->statements.size(), 3U); + + EXPECT_EQ(*p->statements[0].sid, "FirstStatement"); + EXPECT_TRUE(p->statements[0].princ.empty()); + EXPECT_TRUE(p->statements[0].noprinc.empty()); + EXPECT_EQ(p->statements[0].effect, Effect::Allow); + EXPECT_EQ(p->statements[0].action, s3PutBucketPolicy); + EXPECT_EQ(p->statements[0].notaction, s3None); + ASSERT_FALSE(p->statements[0].resource.empty()); + ASSERT_EQ(p->statements[0].resource.size(), 1U); + EXPECT_EQ(p->statements[0].resource.begin()->partition, Partition::wildcard); + EXPECT_EQ(p->statements[0].resource.begin()->service, Service::wildcard); + EXPECT_EQ(p->statements[0].resource.begin()->region, "*"); + EXPECT_EQ(p->statements[0].resource.begin()->account, arbitrary_tenant); + EXPECT_EQ(p->statements[0].resource.begin()->resource, "*"); + EXPECT_TRUE(p->statements[0].notresource.empty()); + EXPECT_TRUE(p->statements[0].conditions.empty()); + + EXPECT_EQ(*p->statements[1].sid, "SecondStatement"); + EXPECT_TRUE(p->statements[1].princ.empty()); + EXPECT_TRUE(p->statements[1].noprinc.empty()); + EXPECT_EQ(p->statements[1].effect, Effect::Allow); + EXPECT_EQ(p->statements[1].action, s3ListAllMyBuckets); + EXPECT_EQ(p->statements[1].notaction, s3None); + ASSERT_FALSE(p->statements[1].resource.empty()); + ASSERT_EQ(p->statements[1].resource.size(), 1U); + EXPECT_EQ(p->statements[1].resource.begin()->partition, Partition::wildcard); + EXPECT_EQ(p->statements[1].resource.begin()->service, Service::wildcard); + EXPECT_EQ(p->statements[1].resource.begin()->region, "*"); + EXPECT_EQ(p->statements[1].resource.begin()->account, arbitrary_tenant); + EXPECT_EQ(p->statements[1].resource.begin()->resource, "*"); + EXPECT_TRUE(p->statements[1].notresource.empty()); + EXPECT_TRUE(p->statements[1].conditions.empty()); + + EXPECT_EQ(*p->statements[2].sid, "ThirdStatement"); + EXPECT_TRUE(p->statements[2].princ.empty()); + EXPECT_TRUE(p->statements[2].noprinc.empty()); + EXPECT_EQ(p->statements[2].effect, Effect::Allow); + EXPECT_EQ(p->statements[2].action, (s3ListMultipartUploadParts | + s3ListBucket | s3ListBucketVersions | + s3ListAllMyBuckets | + s3ListBucketMultiPartUploads | + s3GetObject | s3GetObjectVersion | + s3GetObjectAcl | s3GetObjectVersionAcl | + s3GetObjectTorrent | + s3GetObjectVersionTorrent | + s3GetAccelerateConfiguration | + s3GetBucketAcl | s3GetBucketCORS | + s3GetBucketVersioning | + s3GetBucketRequestPayment | + s3GetBucketLocation | + s3GetBucketPolicy | + s3GetBucketNotification | + s3GetBucketLogging | + s3GetBucketTagging | + s3GetBucketWebsite | + s3GetLifecycleConfiguration | + s3GetReplicationConfiguration)); + EXPECT_EQ(p->statements[2].notaction, s3None); + ASSERT_FALSE(p->statements[2].resource.empty()); + ASSERT_EQ(p->statements[2].resource.size(), 2U); + EXPECT_EQ(p->statements[2].resource.begin()->partition, Partition::aws); + EXPECT_EQ(p->statements[2].resource.begin()->service, Service::s3); + EXPECT_TRUE(p->statements[2].resource.begin()->region.empty()); + EXPECT_EQ(p->statements[2].resource.begin()->account, arbitrary_tenant); + EXPECT_EQ(p->statements[2].resource.begin()->resource, "confidential-data"); + EXPECT_EQ((p->statements[2].resource.begin() + 1)->partition, + Partition::aws); + EXPECT_EQ((p->statements[2].resource.begin() + 1)->service, Service::s3); + EXPECT_TRUE((p->statements[2].resource.begin() + 1)->region.empty()); + EXPECT_EQ((p->statements[2].resource.begin() + 1)->account, + arbitrary_tenant); + EXPECT_EQ((p->statements[2].resource.begin() + 1)->resource, + "confidential-data/*"); + EXPECT_TRUE(p->statements[2].notresource.empty()); + ASSERT_FALSE(p->statements[2].conditions.empty()); + ASSERT_EQ(p->statements[2].conditions.size(), 1U); + EXPECT_EQ(p->statements[2].conditions[0].op, TokenID::Bool); + EXPECT_EQ(p->statements[2].conditions[0].key, "aws:MultiFactorAuthPresent"); + EXPECT_FALSE(p->statements[2].conditions[0].ifexists); + ASSERT_FALSE(p->statements[2].conditions[0].vals.empty()); + EXPECT_EQ(p->statements[2].conditions[0].vals.size(), 1U); + EXPECT_EQ(p->statements[2].conditions[0].vals[0], "true"); +} + +TEST_F(PolicyTest, Eval3) { + auto p = Policy(cct.get(), arbitrary_tenant, + bufferlist::static_from_string(example3)); + Environment em; + Environment tr = { { "aws:MultiFactorAuthPresent", "true" } }; + Environment fa = { { "aws:MultiFactorAuthPresent", "false" } }; + + auto s3allow = (s3ListMultipartUploadParts | s3ListBucket | + s3ListBucketVersions | s3ListAllMyBuckets | + s3ListBucketMultiPartUploads | s3GetObject | + s3GetObjectVersion | s3GetObjectAcl | s3GetObjectVersionAcl | + s3GetObjectTorrent | s3GetObjectVersionTorrent | + s3GetAccelerateConfiguration | s3GetBucketAcl | + s3GetBucketCORS | s3GetBucketVersioning | + s3GetBucketRequestPayment | s3GetBucketLocation | + s3GetBucketPolicy | s3GetBucketNotification | + s3GetBucketLogging | s3GetBucketTagging | + s3GetBucketWebsite | s3GetLifecycleConfiguration | + s3GetReplicationConfiguration); + + EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket")), + Effect::Allow); + + EXPECT_EQ(p.eval(em, none, s3PutBucketPolicy, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "mybucket")), + Effect::Allow); + + + for (auto i = 0ULL; i < s3Count; ++i) { + auto op = 1ULL << i; + if ((op == s3ListAllMyBuckets) || (op == s3PutBucketPolicy)) { + continue; + } + + EXPECT_EQ(p.eval(em, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data")), + Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data")), + op & s3allow ? Effect::Allow : Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data")), + Effect::Pass); + + EXPECT_EQ(p.eval(em, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data/moo")), + Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data/moo")), + op & s3allow ? Effect::Allow : Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "confidential-data/moo")), + Effect::Pass); + + EXPECT_EQ(p.eval(em, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "really-confidential-data")), + Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "really-confidential-data")), + Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, "really-confidential-data")), + Effect::Pass); + + EXPECT_EQ(p.eval(em, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, + "really-confidential-data/moo")), Effect::Pass); + EXPECT_EQ(p.eval(tr, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, + "really-confidential-data/moo")), Effect::Pass); + EXPECT_EQ(p.eval(fa, none, op, + ARN(Partition::aws, Service::s3, + "", arbitrary_tenant, + "really-confidential-data/moo")), Effect::Pass); + + } +} + +const string PolicyTest::arbitrary_tenant = "arbitrary_tenant"; +string PolicyTest::example1 = R"( +{ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::example_bucket" + } +} +)"; + +string PolicyTest::example2 = R"( +{ + "Version": "2012-10-17", + "Id": "S3-Account-Permissions", + "Statement": [{ + "Sid": "1", + "Effect": "Allow", + "Principal": {"AWS": ["arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:root"]}, + "Action": "s3:*", + "Resource": [ + "arn:aws:s3:::mybucket", + "arn:aws:s3:::mybucket/*" + ] + }] +} +)"; + +string PolicyTest::example3 = R"( +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "FirstStatement", + "Effect": "Allow", + "Action": ["s3:PutBucketPolicy"], + "Resource": "*" + }, + { + "Sid": "SecondStatement", + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "*" + }, + { + "Sid": "ThirdStatement", + "Effect": "Allow", + "Action": [ + "s3:List*", + "s3:Get*" + ], + "Resource": [ + "arn:aws:s3:::confidential-data", + "arn:aws:s3:::confidential-data/*" + ], + "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}} + } + ] +} +)";