]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Add basic support for IAM policies
authorAdam C. Emerson <aemerson@redhat.com>
Tue, 9 Aug 2016 16:28:07 +0000 (12:28 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Wed, 17 May 2017 18:22:18 +0000 (14:22 -0400)
This support is currently incomplete but should provide a starting
point.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
15 files changed:
ceph.spec.in
debian/control
install-deps.sh
src/CMakeLists.txt
src/rgw/CMakeLists.txt
src/rgw/rgw_basic_types.cc
src/rgw/rgw_basic_types.h
src/rgw/rgw_common.cc
src/rgw/rgw_common.h
src/rgw/rgw_iam_policy.cc [new file with mode: 0644]
src/rgw/rgw_iam_policy.h [new file with mode: 0644]
src/rgw/rgw_iam_policy_keywords.gperf [new file with mode: 0644]
src/rgw/rgw_iam_policy_keywords.h [new file with mode: 0644]
src/test/rgw/CMakeLists.txt
src/test/rgw/test_rgw_iam_policy.cc [new file with mode: 0644]

index 339adc627bf5e02492b96b12ab573102b89f6ede..40704d68b53b7f3d4dac0daa75f97d05659368f0 100644 (file)
@@ -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
index eb91c40b65f0bb656899dacb5752f61f5f9e869f..43b582b4e0dcb6a525ef6132f08f5cb3fe0255a0 100644 (file)
@@ -9,6 +9,7 @@ Uploaders: Ken Dreyer <kdreyer@redhat.com>,
            Alfredo Deza <adeza@redhat.com>
 Build-Depends: bc,
                btrfs-tools,
+              gperf,
               cmake,
                cpio,
               cryptsetup-bin | cryptsetup,
index 90347622e29ec47d7d8afd77fd641b5a6caa2717..42c34e68f434c43d3d313c44a198e5d2a18bfc4c 100755 (executable)
@@ -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 \
index 01f3de06eeec03b20e996186ff95a50720bd3f3d..387a7d30731bc0047260d64b85797b5ca96efc2e 100644 (file)
@@ -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")
index ede95a5bd8e7fafde8e238d00ab775a68d1a42be..53583652acdfec563e5018776268bbc98c86eb9c 100644 (file)
@@ -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
index 5ebf1cfe4460934b38eda6436f4f36cc24003500..c16d920f21b0be67ce9873900987ead3a6602757 100644 (file)
@@ -1,6 +1,13 @@
+#include <iostream>
+#include <sstream>
+#include <string>
+
 #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();
+}
+}
+}
index 4e76fa8c07e05e0b13f147aa4fe939a05cfd2a53..31e9d3a32acb4d4aac93aac5cadd993f44468e70 100644 (file)
@@ -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);
 }
 }
 
index cb9040d64ab7861469bd8ba6ae6a2b32202a3fdd..f0a34a5e22bb953bce5d423a22219551612bada2 100644 (file)
@@ -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;
index c615008e27134a64797adfe667140ceb91449e74..3fb45393892268b779ec39f12fe97f0973e47721 100644 (file)
@@ -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 (file)
index 0000000..c12e243
--- /dev/null
@@ -0,0 +1,1514 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+
+#include <cstring>
+#include <regex>
+#include <sstream>
+#include <stack>
+#include <utility>
+
+#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<Partition> 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<Service> to_service(const smatch::value_type& s,
+                            bool wildcards) {
+  static const unordered_map<string, Service> 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> 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<Service, string> 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]{"<Top>", TokenKind::pseudo, TokenID::Top, 0, false,
+    false};
+const Keyword cond_key[1]{"<Condition Key>", 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<UTF8<>, PolicyParser> {
+  keyword_hash tokens;
+  std::vector<ParseState> 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<TokenID> l) {
+    for (auto in : l) {
+      seen |= dex(in);
+    }
+  }
+  void reset(TokenID in) {
+    seen &= ~dex(in);
+  }
+  void reset(std::initializer_list<TokenID> 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<Principal> 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<Version>(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<Effect>(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<std::string>(), s, vals);
+
+  case TokenID::StringNotEquals:
+    return orrible(std::not2(std::equal_to<std::string>()),
+                  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<std::string>(), s, vals);
+  case TokenID::StringNotLike:
+    return orrible(std::not2(std::equal_to<std::string>()),
+                  s, vals);
+
+    // Numeric
+  case TokenID::NumericEquals:
+    return shortible(std::equal_to<double>(), as_number, s, vals);
+
+  case TokenID::NumericNotEquals:
+    return shortible(std::not2(std::equal_to<double>()),
+                    as_number, s, vals);
+
+
+  case TokenID::NumericLessThan:
+    return shortible(std::less<double>(), as_number, s, vals);
+
+
+  case TokenID::NumericLessThanEquals:
+    return shortible(std::less_equal<double>(), as_number, s, vals);
+
+  case TokenID::NumericGreaterThan:
+    return shortible(std::greater<double>(), as_number, s, vals);
+
+  case TokenID::NumericGreaterThanEquals:
+    return shortible(std::greater_equal<double>(), as_number, s, vals);
+
+    // Date!
+  case TokenID::DateEquals:
+    return shortible(std::equal_to<ceph::real_time>(), as_date, s, vals);
+
+  case TokenID::DateNotEquals:
+    return shortible(std::not2(std::equal_to<ceph::real_time>()),
+                    as_date, s, vals);
+
+  case TokenID::DateLessThan:
+    return shortible(std::less<ceph::real_time>(), as_date, s, vals);
+
+
+  case TokenID::DateLessThanEquals:
+    return shortible(std::less_equal<ceph::real_time>(), as_date, s, vals);
+
+  case TokenID::DateGreaterThan:
+    return shortible(std::greater<ceph::real_time>(), as_date, s, vals);
+
+  case TokenID::DateGreaterThanEquals:
+    return shortible(std::greater_equal<ceph::real_time>(), as_date, s,
+                    vals);
+
+    // Bool!
+  case TokenID::Bool:
+    return shortible(std::equal_to<bool>(), as_bool, s, vals);
+
+    // Binary!
+  case TokenID::BinaryEquals:
+    return shortible(std::equal_to<ceph::bufferlist>(), as_binary, s,
+                    vals);
+
+    // IP Address!
+  case TokenID::IpAddress:
+    return shortible(std::equal_to<MaskedIP>(), as_network, s, vals);
+
+  case TokenID::NotIpAddress:
+    return shortible(std::not2(std::equal_to<MaskedIP>()), 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<MaskedIP> 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<void*>(&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<void*>(&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<typename Iterator>
+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<const rgw::auth::Identity&> 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<kParseNumbersAsStringsFlag |
+                          kParseCommentsFlag>(ss, pp);
+  if (!pr) {
+    throw PolicyParseException(std::move(pr));
+  }
+}
+
+Effect Policy::eval(const Environment& e,
+                   optional<const rgw::auth::Identity&> 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 (file)
index 0000000..4429a57
--- /dev/null
@@ -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 <bitset>
+#include <chrono>
+#include <cstdint>
+#include <iostream>
+#include <string>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <boost/optional.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <boost/utility/string_ref.hpp>
+#include <boost/variant.hpp>
+
+#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<std::string, std::string>;
+
+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<ARN> 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<std::string> vals;
+
+  Condition() = default;
+  Condition(TokenID op, const char* s, std::size_t len) : op(op) {
+    static constexpr char ifexistr[] = "IfExists";
+    auto l = static_cast<const char*>(memmem(static_cast<const void*>(s), len,
+                                            static_cast<const void*>(ifexistr),
+                                            sizeof(ifexistr) -1));
+    if (l && ((l + sizeof(ifexistr) - 1 == (s + len)))) {
+      ifexists = true;
+      key.assign(s, static_cast<const char*>(l) - s);
+    } else {
+      key.assign(s, len);
+    }
+  }
+
+  bool eval(const Environment& e) const;
+
+  static boost::optional<double> 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<ceph::real_time> 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<uint64_t>(d)) +
+         std::chrono::nanoseconds(
+           static_cast<uint64_t>((d - static_cast<uint64_t>(d))
+                                 * 1000000000)));
+      }
+
+      return from_iso_8601(boost::string_ref(s), false);
+    } catch (const std::logic_error& e) {
+      return boost::none;
+    }
+  }
+
+  static boost::optional<bool> 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<ceph::bufferlist> 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<char*>(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<MaskedIP> as_network(const std::string& s);
+
+
+  struct ci_equal_to : public std::binary_function<const std::string,
+                                                  const std::string,
+                                                  bool> {
+    bool operator ()(const std::string& s1,
+                    const std::string& s2) const {
+      return boost::iequals(s1, s2);
+    }
+  };
+
+
+  template<typename F>
+  static bool orrible(F&& f, const std::string& c,
+                     const std::vector<std::string>& v) {
+    for (const auto& d : v) {
+      if (std::forward<F>(f)(c, d)) {
+       return true;
+      }
+    }
+    return false;
+  }
+
+  template<typename F, typename X>
+  static bool shortible(F&& f, X& x, const std::string& c,
+                       const std::vector<std::string>& v) {
+    auto xc = std::forward<X>(x)(c);
+    if (!xc) {
+      return false;
+    }
+
+    for (const auto& d : v) {
+      auto xd = std::forward<X>(x)(d);
+      if (!xd) {
+       continue;
+      }
+
+      if (std::forward<F>(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<std::string> sid = boost::none;
+
+  boost::container::flat_set<rgw::auth::Principal> princ;
+  boost::container::flat_set<rgw::auth::Principal> 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<ARN> resource;
+  boost::container::flat_set<ARN> notresource;
+
+  std::vector<Condition> conditions;
+
+  Effect eval(const Environment& e,
+             boost::optional<const rgw::auth::Identity&> 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<std::string> id = boost::none;
+
+  std::vector<Statement> statements;
+
+  Policy(CephContext* cct, const std::string& tenant,
+        const bufferlist& text);
+
+  Effect eval(const Environment& e,
+             boost::optional<const rgw::auth::Identity&> 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<int>()(static_cast<int>(s));
+  }
+};
+}
+
+#endif
diff --git a/src/rgw/rgw_iam_policy_keywords.gperf b/src/rgw/rgw_iam_policy_keywords.gperf
new file mode 100644 (file)
index 0000000..d37fa6a
--- /dev/null
@@ -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 (file)
index 0000000..a0cd34b
--- /dev/null
@@ -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
index b5a03e0baadf602c74690af92b9adabc8e006e18..c85bda788265cc51690d4e7d8309568cbbdbd07b 100644 (file)
@@ -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 (file)
index 0000000..cc512e0
--- /dev/null
@@ -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 <string>
+
+#include <boost/intrusive_ptr.hpp>
+#include <boost/optional.hpp>
+
+#include <gtest/gtest.h>
+
+#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<Principal>& ids) const override {
+    return ids.find(id) != ids.end();
+  }
+};
+
+class PolicyTest : public ::testing::Test {
+protected:
+  intrusive_ptr<CephContext> 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<Policy> 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<Policy> 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<Policy> 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"}}
+    }
+  ]
+}
+)";